tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
297
fork
atom
a tool for shared writing and social publishing
297
fork
atom
overview
issues
31
pulls
pipelines
new subscribe flow in Pub Page
cozylittle.house
1 week ago
0ceea9fb
f3ee0f2c
+208
-173
8 changed files
expand all
collapse all
unified
split
app
lish
[did]
[publication]
PublicationAuthor.tsx
[rkey]
Interactions
Interactions.tsx
LinearDocumentPage.tsx
PostPubInfo.tsx
page.tsx
components
Subscribe
EmailSubscribe.tsx
HandleSubscribe.tsx
SubscribeButton.tsx
+1
-1
app/lish/[did]/[publication]/PublicationAuthor.tsx
reviewed
···
11
11
<ProfilePopover
12
12
didOrHandle={props.did}
13
13
trigger={
14
14
-
<span className="hover:underline">
14
14
+
<span className="hover:text-accent-contrast">
15
15
<strong>by {props.displayName}</strong> @{props.handle}
16
16
</span>
17
17
}
+1
-1
app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx
reviewed
···
15
15
import { type CommentOnDocument } from "contexts/DocumentContext";
16
16
import { prefetchQuotesData } from "./Quotes";
17
17
import { useIdentityData } from "components/IdentityProvider";
18
18
-
import { ManageSubscription, SubscribeWithBluesky } from "app/lish/Subscribe";
18
18
+
import { ManageSubscription } from "app/lish/Subscribe";
19
19
import { EditTiny } from "components/Icons/EditTiny";
20
20
import { getPublicationURL } from "app/lish/createPub/getPublicationURL";
21
21
import { RecommendButton } from "components/RecommendButton";
+5
-7
app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx
reviewed
···
1
1
"use client";
2
2
import { PubLeafletPagesLinearDocument } from "lexicons/api";
3
3
import { useLeafletContent } from "contexts/LeafletContentContext";
4
4
-
import { PostPageData } from "./getPostPageData";
5
5
-
import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
6
6
-
import { getPublicationURL } from "app/lish/createPub/getPublicationURL";
7
7
-
import { SubscribeWithBluesky } from "app/lish/Subscribe";
8
8
-
import { EditTiny } from "components/Icons/EditTiny";
9
4
import {
10
5
ExpandedInteractions,
11
6
getCommentCount,
···
74
69
pageOptions={pageOptions}
75
70
footnoteSideColumn={
76
71
!props.hasContentToRight ? (
77
77
-
<PublishedFootnoteSideColumn footnotes={footnotes} fullPageScroll={fullPageScroll} />
72
72
+
<PublishedFootnoteSideColumn
73
73
+
footnotes={footnotes}
74
74
+
fullPageScroll={fullPageScroll}
75
75
+
/>
78
76
) : undefined
79
77
}
80
78
>
···
96
94
footnoteIndexMap={footnoteIndexMap}
97
95
/>
98
96
<PublishedFootnoteSection footnotes={footnotes} />
99
99
-
<PostSubscribe />
97
97
+
{/*<PostSubscribe />*/}
100
98
<PostPrevNextButtons
101
99
showPrevNext={preferences.showPrevNext !== false && !isSubpage}
102
100
/>
+13
-7
app/lish/[did]/[publication]/[rkey]/PostPubInfo.tsx
reviewed
···
1
1
import { SubscribeButton } from "components/Subscribe/SubscribeButton";
2
2
3
3
-
export const PostPubInfo = () => {
4
4
-
let newsletterMode = true;
5
5
-
let user = {
3
3
+
export const dummy = {
4
4
+
newsletterMode: true,
5
5
+
user: {
6
6
loggedIn: false,
7
7
email: undefined,
8
8
handle: undefined,
9
9
-
subscribed: false,
10
10
-
};
9
9
+
subscribed: true,
10
10
+
},
11
11
+
};
12
12
+
13
13
+
export const PostPubInfo = () => {
11
14
return (
12
15
<div className="px-3 sm:px-4 w-full">
13
16
<div className="accent-container rounded-lg! w-full px-3 pt-3 pb-4 sm:px-4 sm:pt-4 sm:pb-5 text-center justify-center">
14
17
<h3 className="leading-snug text-secondary">Pub Title here</h3>
15
18
<div className="text-tertiary pb-3">this is the pubs description</div>
16
16
-
{user.subscribed ? (
19
19
+
{dummy.user.subscribed ? (
17
20
<div>Manage Sub</div>
18
21
) : (
19
19
-
<SubscribeButton newsletterMode={newsletterMode} user={user} />
22
22
+
<SubscribeButton
23
23
+
newsletterMode={dummy.newsletterMode}
24
24
+
user={dummy.user}
25
25
+
/>
20
26
)}
21
27
</div>
22
28
</div>
+135
-123
app/lish/[did]/[publication]/page.tsx
reviewed
···
1
1
import { supabaseServerClient } from "supabase/serverClient";
2
2
import { AtUri } from "@atproto/syntax";
3
3
-
import { getPublicationURL, getDocumentURL } from "app/lish/createPub/getPublicationURL";
3
3
+
import { getDocumentURL } from "app/lish/createPub/getPublicationURL";
4
4
import { BskyAgent } from "@atproto/api";
5
5
import { publicationNameOrUriFilter } from "src/utils/uriHelpers";
6
6
-
import { SubscribeWithBluesky } from "app/lish/Subscribe";
7
6
import React from "react";
8
7
import {
9
8
PublicationBackgroundProvider,
···
15
14
import { LocalizedDate } from "./LocalizedDate";
16
15
import { PublicationHomeLayout } from "./PublicationHomeLayout";
17
16
import { PublicationAuthor } from "./PublicationAuthor";
18
18
-
import { Separator } from "components/Layout";
19
17
import {
20
18
normalizePublicationRecord,
21
19
normalizeDocumentRecord,
22
20
} from "src/utils/normalizeRecords";
23
21
import { getFirstParagraph } from "src/utils/getFirstParagraph";
24
22
import { FontLoader } from "components/FontLoader";
23
23
+
import { SubscribeButton } from "components/Subscribe/SubscribeButton";
24
24
+
import { dummy } from "./[rkey]/PostPubInfo";
25
25
26
26
export default async function Publication(props: {
27
27
params: Promise<{ publication: string; did: string }>;
···
61
61
try {
62
62
return (
63
63
<>
64
64
-
<FontLoader headingFontId={record?.theme?.headingFont} bodyFontId={record?.theme?.bodyFont} />
65
65
-
<PublicationThemeProvider
66
66
-
theme={record?.theme}
67
67
-
pub_creator={publication.identity_did}
68
68
-
>
69
69
-
<PublicationBackgroundProvider
64
64
+
<FontLoader
65
65
+
headingFontId={record?.theme?.headingFont}
66
66
+
bodyFontId={record?.theme?.bodyFont}
67
67
+
/>
68
68
+
<PublicationThemeProvider
70
69
theme={record?.theme}
71
70
pub_creator={publication.identity_did}
72
71
>
73
73
-
<PublicationHomeLayout
74
74
-
uri={publication.uri}
75
75
-
showPageBackground={!!showPageBackground}
72
72
+
<PublicationBackgroundProvider
73
73
+
theme={record?.theme}
74
74
+
pub_creator={publication.identity_did}
76
75
>
77
77
-
<div className="pubHeader flex flex-col pb-8 w-full text-center justify-center ">
78
78
-
{record?.icon && (
79
79
-
<div
80
80
-
className="shrink-0 w-10 h-10 rounded-full mx-auto"
81
81
-
style={{
82
82
-
backgroundImage: `url(/api/atproto_images?did=${did}&cid=${(record.icon.ref as unknown as { $link: string })["$link"]})`,
83
83
-
backgroundRepeat: "no-repeat",
84
84
-
backgroundPosition: "center",
85
85
-
backgroundSize: "cover",
86
86
-
}}
87
87
-
/>
88
88
-
)}
89
89
-
<h2 className="text-accent-contrast sm:text-xl text-[22px] pt-1 ">
90
90
-
{publication.name}
91
91
-
</h2>
92
92
-
<p className="sm:text-lg text-secondary">
93
93
-
{record?.description}{" "}
94
94
-
</p>
95
95
-
{profile && (
96
96
-
<PublicationAuthor
97
97
-
did={profile.did}
98
98
-
displayName={profile.displayName}
99
99
-
handle={profile.handle}
100
100
-
/>
101
101
-
)}
102
102
-
<div className="sm:pt-4 pt-4">
76
76
+
<PublicationHomeLayout
77
77
+
uri={publication.uri}
78
78
+
showPageBackground={!!showPageBackground}
79
79
+
>
80
80
+
<div className="pubHeader flex flex-col pb-8 w-full text-center justify-center ">
81
81
+
{record?.icon && (
82
82
+
<div
83
83
+
className="shrink-0 w-10 h-10 rounded-full mx-auto"
84
84
+
style={{
85
85
+
backgroundImage: `url(/api/atproto_images?did=${did}&cid=${(record.icon.ref as unknown as { $link: string })["$link"]})`,
86
86
+
backgroundRepeat: "no-repeat",
87
87
+
backgroundPosition: "center",
88
88
+
backgroundSize: "cover",
89
89
+
}}
90
90
+
/>
91
91
+
)}
92
92
+
<h2 className="text-accent-contrast sm:text-xl text-[22px] pt-1 ">
93
93
+
{publication.name}
94
94
+
</h2>
95
95
+
<p className="sm:text-lg text-secondary">
96
96
+
{record?.description}{" "}
97
97
+
</p>
98
98
+
{profile && (
99
99
+
<PublicationAuthor
100
100
+
did={profile.did}
101
101
+
displayName={profile.displayName}
102
102
+
handle={profile.handle}
103
103
+
/>
104
104
+
)}
105
105
+
<div className="spacer h-4 w-full" />
106
106
+
<SubscribeButton {...dummy} />
107
107
+
{/*<div className="sm:pt-4 pt-4">
103
108
<SubscribeWithBluesky
104
109
base_url={getPublicationURL(publication)}
105
110
pubName={publication.name}
106
111
pub_uri={publication.uri}
107
112
subscribers={publication.publication_subscriptions}
108
113
/>
114
114
+
</div>*/}
109
115
</div>
110
110
-
</div>
111
111
-
<div className="publicationPostList w-full flex flex-col gap-4">
112
112
-
{publication.documents_in_publications
113
113
-
.filter((d) => !!d?.documents)
114
114
-
.sort((a, b) => {
115
115
-
const aRecord = normalizeDocumentRecord(a.documents?.data);
116
116
-
const bRecord = normalizeDocumentRecord(b.documents?.data);
117
117
-
const aDate = aRecord?.publishedAt
118
118
-
? new Date(aRecord.publishedAt)
119
119
-
: new Date(0);
120
120
-
const bDate = bRecord?.publishedAt
121
121
-
? new Date(bRecord.publishedAt)
122
122
-
: new Date(0);
123
123
-
return bDate.getTime() - aDate.getTime(); // Sort by most recent first
124
124
-
})
125
125
-
.map((doc) => {
126
126
-
if (!doc.documents) return null;
127
127
-
const doc_record = normalizeDocumentRecord(
128
128
-
doc.documents.data,
129
129
-
);
130
130
-
if (!doc_record) return null;
131
131
-
let uri = new AtUri(doc.documents.uri);
132
132
-
let quotes =
133
133
-
doc.documents.document_mentions_in_bsky[0].count || 0;
134
134
-
let comments =
135
135
-
record?.preferences?.showComments === false
136
136
-
? 0
137
137
-
: doc.documents.comments_on_documents[0].count || 0;
138
138
-
let recommends =
139
139
-
doc.documents.recommends_on_documents?.[0]?.count || 0;
140
140
-
let tags = doc_record.tags || [];
116
116
+
<div className="publicationPostList w-full flex flex-col gap-4">
117
117
+
{publication.documents_in_publications
118
118
+
.filter((d) => !!d?.documents)
119
119
+
.sort((a, b) => {
120
120
+
const aRecord = normalizeDocumentRecord(a.documents?.data);
121
121
+
const bRecord = normalizeDocumentRecord(b.documents?.data);
122
122
+
const aDate = aRecord?.publishedAt
123
123
+
? new Date(aRecord.publishedAt)
124
124
+
: new Date(0);
125
125
+
const bDate = bRecord?.publishedAt
126
126
+
? new Date(bRecord.publishedAt)
127
127
+
: new Date(0);
128
128
+
return bDate.getTime() - aDate.getTime(); // Sort by most recent first
129
129
+
})
130
130
+
.map((doc) => {
131
131
+
if (!doc.documents) return null;
132
132
+
const doc_record = normalizeDocumentRecord(
133
133
+
doc.documents.data,
134
134
+
);
135
135
+
if (!doc_record) return null;
136
136
+
let uri = new AtUri(doc.documents.uri);
137
137
+
let quotes =
138
138
+
doc.documents.document_mentions_in_bsky[0].count || 0;
139
139
+
let comments =
140
140
+
record?.preferences?.showComments === false
141
141
+
? 0
142
142
+
: doc.documents.comments_on_documents[0].count || 0;
143
143
+
let recommends =
144
144
+
doc.documents.recommends_on_documents?.[0]?.count || 0;
145
145
+
let tags = doc_record.tags || [];
141
146
142
142
-
const docUrl = getDocumentURL(doc_record, doc.documents.uri, publication);
143
143
-
return (
144
144
-
<React.Fragment key={doc.documents?.uri}>
145
145
-
<div className="flex w-full grow flex-col ">
146
146
-
<SpeedyLink
147
147
-
href={docUrl}
148
148
-
className="publishedPost hover:no-underline! flex flex-col"
149
149
-
>
150
150
-
{doc_record.title && (
151
151
-
<h3 className="text-primary">{doc_record.title}</h3>
152
152
-
)}
153
153
-
<p className="italic text-secondary line-clamp-3">
154
154
-
{doc_record.description || getFirstParagraph(doc_record)}
155
155
-
</p>
156
156
-
</SpeedyLink>
147
147
+
const docUrl = getDocumentURL(
148
148
+
doc_record,
149
149
+
doc.documents.uri,
150
150
+
publication,
151
151
+
);
152
152
+
return (
153
153
+
<React.Fragment key={doc.documents?.uri}>
154
154
+
<div className="flex w-full grow flex-col ">
155
155
+
<SpeedyLink
156
156
+
href={docUrl}
157
157
+
className="publishedPost hover:no-underline! flex flex-col"
158
158
+
>
159
159
+
{doc_record.title && (
160
160
+
<h3 className="text-primary">
161
161
+
{doc_record.title}
162
162
+
</h3>
163
163
+
)}
164
164
+
<p className="italic text-secondary line-clamp-3">
165
165
+
{doc_record.description ||
166
166
+
getFirstParagraph(doc_record)}
167
167
+
</p>
168
168
+
</SpeedyLink>
157
169
158
158
-
<div className="justify-between w-full text-sm text-tertiary flex gap-1 flex-wrap pt-2 items-center">
159
159
-
<p className="text-sm text-tertiary ">
160
160
-
{doc_record.publishedAt && (
161
161
-
<LocalizedDate
162
162
-
dateString={doc_record.publishedAt}
163
163
-
options={{
164
164
-
year: "numeric",
165
165
-
month: "long",
166
166
-
day: "2-digit",
167
167
-
}}
168
168
-
/>
169
169
-
)}{" "}
170
170
-
</p>
170
170
+
<div className="justify-between w-full text-sm text-tertiary flex gap-1 flex-wrap pt-2 items-center">
171
171
+
<p className="text-sm text-tertiary ">
172
172
+
{doc_record.publishedAt && (
173
173
+
<LocalizedDate
174
174
+
dateString={doc_record.publishedAt}
175
175
+
options={{
176
176
+
year: "numeric",
177
177
+
month: "long",
178
178
+
day: "2-digit",
179
179
+
}}
180
180
+
/>
181
181
+
)}{" "}
182
182
+
</p>
171
183
172
172
-
<InteractionPreview
173
173
-
quotesCount={quotes}
174
174
-
commentsCount={comments}
175
175
-
recommendsCount={recommends}
176
176
-
documentUri={doc.documents.uri}
177
177
-
tags={tags}
178
178
-
postUrl={docUrl}
179
179
-
showComments={
180
180
-
record?.preferences?.showComments !== false
181
181
-
}
182
182
-
showMentions={
183
183
-
record?.preferences?.showMentions !== false
184
184
-
}
185
185
-
showRecommends={
186
186
-
record?.preferences?.showRecommends !== false
187
187
-
}
188
188
-
/>
184
184
+
<InteractionPreview
185
185
+
quotesCount={quotes}
186
186
+
commentsCount={comments}
187
187
+
recommendsCount={recommends}
188
188
+
documentUri={doc.documents.uri}
189
189
+
tags={tags}
190
190
+
postUrl={docUrl}
191
191
+
showComments={
192
192
+
record?.preferences?.showComments !== false
193
193
+
}
194
194
+
showMentions={
195
195
+
record?.preferences?.showMentions !== false
196
196
+
}
197
197
+
showRecommends={
198
198
+
record?.preferences?.showRecommends !== false
199
199
+
}
200
200
+
/>
201
201
+
</div>
189
202
</div>
190
190
-
</div>
191
191
-
<hr className="last:hidden border-border-light" />
192
192
-
</React.Fragment>
193
193
-
);
194
194
-
})}
195
195
-
</div>
196
196
-
</PublicationHomeLayout>
197
197
-
</PublicationBackgroundProvider>
198
198
-
</PublicationThemeProvider>
203
203
+
<hr className="last:hidden border-border-light" />
204
204
+
</React.Fragment>
205
205
+
);
206
206
+
})}
207
207
+
</div>
208
208
+
</PublicationHomeLayout>
209
209
+
</PublicationBackgroundProvider>
210
210
+
</PublicationThemeProvider>
199
211
</>
200
212
);
201
213
} catch (e) {
+48
-30
components/Subscribe/AtSubscribe.tsx
components/Subscribe/HandleSubscribe.tsx
reviewed
···
1
1
-
import { ManageSubscription } from "app/lish/Subscribe";
1
1
+
"use client";
2
2
import { ButtonPrimary } from "components/Buttons";
3
3
import { BlueskyTiny } from "components/Icons/BlueskyTiny";
4
4
-
import { GoToArrow } from "components/Icons/GoToArrow";
5
4
import { Input } from "components/Input";
6
6
-
import { Modal } from "components/Modal";
7
5
import { Popover } from "components/Popover";
8
6
import Link from "next/link";
9
7
import { useState } from "react";
···
14
12
LogoLeaflet,
15
13
LogoTangled,
16
14
} from "./Logos";
15
15
+
import { GoToArrow } from "components/Icons/GoToArrow";
16
16
+
import { Separator } from "components/Layout";
17
17
18
18
-
export const AtSubscribe = (props: {
18
18
+
export const HandleSubscribe = (props: {
19
19
compact?: boolean;
20
20
user: {
21
21
loggedIn: boolean;
···
23
23
handle: string | undefined;
24
24
};
25
25
}) => {
26
26
-
if (props.user.loggedIn) {
26
26
+
if (props.user.loggedIn && props.user.handle) {
27
27
return (
28
28
<ButtonPrimary className="mx-auto max-w-full">
29
29
<span className="shrink-0">Subscribe as</span>
···
38
38
return (
39
39
<div className="max-w-sm mx-auto">
40
40
<HandleInput compact={props.compact} />
41
41
-
<div className="flex justify-between pt-0.5">
42
42
-
<UniversalHandleInfo />{" "}
41
41
+
<div className="flex gap-2 justify-center items-center mx-auto pt-0.5 ">
42
42
+
<UniversalHandleInfo />
43
43
+
<Separator classname="h-3! border-accent-contrast!" />
43
44
<div className="text-sm text-accent-contrast font-bold">Create</div>
44
45
</div>
45
46
</div>
···
49
50
export const HandleInput = (props: { compact?: boolean }) => {
50
51
let [handleValue, setHandleValue] = useState("");
51
52
return (
52
52
-
<div className="flex flex-col">
53
53
-
<div className="input-with-border pl-0! py-0! flex gap-0 ">
54
54
-
<div className="border-r border-border text-center w-7 mr-2">@</div>
55
55
-
<Input
56
56
-
className="appearance-none! outline-none! py-0.5 grow max-w-full"
57
57
-
placeholder="universal.handle"
58
58
-
size={30}
59
59
-
value={handleValue}
60
60
-
onChange={(e) => setHandleValue(e.target.value)}
61
61
-
/>
53
53
+
<div className="handleInput input-with-border relative pl-0! py-0! flex gap-0 ">
54
54
+
<div className="border-r border-border text-center w-7 shrink-0 mr-2">
55
55
+
@
62
56
</div>
63
63
-
{!props.compact && (
64
64
-
<>
65
65
-
<div className="w-full flex gap-2 items-center mt-2 mb-3 ">
66
66
-
<hr className="grow border-border-light" />
67
67
-
<div className="shrink-0 text-sm italix text-tertiary">
68
68
-
or link with
69
69
-
</div>
70
70
-
<hr className="grow border-border-light" />
71
71
-
</div>
72
72
-
<ButtonPrimary fullWidth>
73
73
-
<BlueskyTiny /> Bluesky
74
74
-
</ButtonPrimary>
75
75
-
</>
57
57
+
<Input
58
58
+
className={`appearance-none! outline-none! py-0.5 ${props.compact ? "pr-6" : "pr-14"} grow max-w-full`}
59
59
+
placeholder="universal.handle"
60
60
+
size={30}
61
61
+
value={handleValue}
62
62
+
onChange={(e) => setHandleValue(e.target.value)}
63
63
+
/>
64
64
+
65
65
+
{props.compact ? (
66
66
+
<button className="absolute text-sm py-0! right-[6px] top-[6px] leading-snug outline-none!">
67
67
+
<GoToArrow />
68
68
+
</button>
69
69
+
) : (
70
70
+
<ButtonPrimary
71
71
+
compact
72
72
+
className="absolute text-sm py-0! right-[3px] top-[3.5px] leading-snug outline-none!"
73
73
+
>
74
74
+
Subscribe
75
75
+
</ButtonPrimary>
76
76
)}
77
77
+
</div>
78
78
+
);
79
79
+
};
80
80
+
81
81
+
export const HandleInputandOAuth = () => {
82
82
+
return (
83
83
+
<div className="handleInputAndOAuth flex flex-col">
84
84
+
<HandleInput />
85
85
+
<div className="w-full flex gap-2 items-center mt-2 mb-3 ">
86
86
+
<hr className="grow border-border-light" />
87
87
+
<div className="shrink-0 text-sm italix text-tertiary">
88
88
+
or link with
89
89
+
</div>
90
90
+
<hr className="grow border-border-light" />
91
91
+
</div>
92
92
+
<ButtonPrimary fullWidth>
93
93
+
<BlueskyTiny /> Bluesky
94
94
+
</ButtonPrimary>
77
95
</div>
78
96
);
79
97
};
+3
-2
components/Subscribe/EmailSubscribe.tsx
reviewed
···
1
1
+
"use client";
1
2
import * as OneTimePasswordField from "@radix-ui/react-one-time-password-field";
2
3
import { ButtonPrimary } from "components/Buttons";
3
4
import { GoToArrow } from "components/Icons/GoToArrow";
4
5
import { Input } from "components/Input";
5
6
import { Modal } from "components/Modal";
6
7
import { useState } from "react";
7
7
-
import { HandleInput, UniversalHandleInfo } from "./AtSubscribe";
8
8
+
import { HandleInputandOAuth, UniversalHandleInfo } from "./HandleSubscribe";
8
9
9
10
export const EmailSubscribe = (props: {
10
11
compact?: boolean;
···
117
118
</div>
118
119
<UniversalHandleInfo />
119
120
</div>
120
120
-
<HandleInput />
121
121
+
<HandleInputandOAuth />
121
122
</div>
122
123
</>
123
124
)}
+2
-2
components/Subscribe/SubscribeButton.tsx
reviewed
···
1
1
-
import { AtSubscribe } from "./AtSubscribe";
1
1
+
import { HandleSubscribe } from "./HandleSubscribe";
2
2
import { EmailSubscribe } from "./EmailSubscribe";
3
3
4
4
export const SubscribeButton = (props: {
···
11
11
}) => {
12
12
if (props.newsletterMode) {
13
13
return <EmailSubscribe user={props.user} />;
14
14
-
} else return <AtSubscribe user={props.user} compact />;
14
14
+
} else return <HandleSubscribe user={props.user} />;
15
15
};