tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
289
fork
atom
a tool for shared writing and social publishing
289
fork
atom
overview
issues
27
pulls
pipelines
clean up leaflet options and fix links
awarm.space
3 months ago
65d57620
7955fabe
+289
-344
8 changed files
expand all
collapse all
unified
split
app
(home-pages)
home
HomeLayout.tsx
LeafletList
LeafletInfo.tsx
LeafletListItem.tsx
LeafletOptions.tsx
LeafletPreview.tsx
lish
[did]
[publication]
dashboard
PublishedPostsLists.tsx
createPub
getPublicationURL.ts
components
PageSWRDataProvider.tsx
-19
app/(home-pages)/home/HomeLayout.tsx
···
235
235
<LeafletListItem
236
236
title={props?.titles?.[leaflet.root_entity]}
237
237
archived={archived}
238
238
-
token={leaflet}
239
239
-
draftInPublication={
240
240
-
leaflet.leaflets_in_publications?.[0]?.publication
241
241
-
}
242
242
-
published={
243
243
-
!!leaflet.leaflets_in_publications?.find((l) => l.doc) ||
244
244
-
!!leaflet.leaflets_to_documents?.find((l) => !!l.documents)
245
245
-
}
246
246
-
publishedAt={
247
247
-
leaflet.leaflets_in_publications?.find((l) => l.doc)?.documents
248
248
-
?.indexed_at ||
249
249
-
leaflet.leaflets_to_documents?.find((l) => !!l.documents)
250
250
-
?.documents?.indexed_at
251
251
-
}
252
252
-
document_uri={
253
253
-
leaflet.leaflets_in_publications?.find((l) => l.doc)?.documents
254
254
-
?.uri
255
255
-
}
256
256
-
leaflet_id={leaflet.root_entity}
257
238
loggedIn={!!identity}
258
239
display={display}
259
240
added_at={added_at}
+13
-24
app/(home-pages)/home/LeafletList/LeafletInfo.tsx
···
1
1
"use client";
2
2
-
import { PermissionToken, useEntity } from "src/replicache";
2
2
+
import { useEntity } from "src/replicache";
3
3
import { LeafletOptions } from "./LeafletOptions";
4
4
-
import { useState } from "react";
5
4
import { timeAgo } from "src/utils/timeAgo";
6
5
import { usePageTitle } from "components/utils/UpdateLeafletTitle";
6
6
+
import { useLeafletPublicationStatus } from "components/PageSWRDataProvider";
7
7
8
8
export const LeafletInfo = (props: {
9
9
title?: string;
10
10
-
draftInPublication?: string;
11
11
-
published?: boolean;
12
12
-
token: PermissionToken;
13
13
-
leaflet_id: string;
14
14
-
loggedIn: boolean;
15
10
className?: string;
16
11
display: "grid" | "list";
17
12
added_at: string;
18
18
-
publishedAt?: string;
19
19
-
document_uri?: string;
20
13
archived?: boolean | null;
14
14
+
loggedIn: boolean;
21
15
}) => {
22
22
-
let [prefetch, setPrefetch] = useState(false);
16
16
+
const pubStatus = useLeafletPublicationStatus();
23
17
let prettyCreatedAt = props.added_at ? timeAgo(props.added_at) : "";
24
24
-
let prettyPublishedAt = props.publishedAt ? timeAgo(props.publishedAt) : "";
18
18
+
let prettyPublishedAt = pubStatus?.publishedAt
19
19
+
? timeAgo(pubStatus.publishedAt)
20
20
+
: "";
25
21
26
22
// Look up root page first, like UpdateLeafletTitle does
27
27
-
let firstPage = useEntity(props.leaflet_id, "root/page")[0];
28
28
-
let entityID = firstPage?.data.value || props.leaflet_id;
23
23
+
let firstPage = useEntity(pubStatus?.leafletId ?? "", "root/page")[0];
24
24
+
let entityID = firstPage?.data.value || pubStatus?.leafletId || "";
29
25
let titleFromDb = usePageTitle(entityID);
30
26
31
27
let title = props.title ?? titleFromDb ?? "Untitled";
···
39
35
{title}
40
36
</h3>
41
37
<div className="flex gap-1 shrink-0">
42
42
-
<LeafletOptions
43
43
-
leaflet={props.token}
44
44
-
draftInPublication={props.draftInPublication}
45
45
-
document_uri={props.document_uri}
46
46
-
shareLink={`${props.token.id}`}
47
47
-
archived={props.archived}
48
48
-
loggedIn={props.loggedIn}
49
49
-
/>
38
38
+
<LeafletOptions archived={props.archived} loggedIn={props.loggedIn} />
50
39
</div>
51
40
</div>
52
41
<div className="flex gap-2 items-center">
53
42
{props.archived ? (
54
43
<div className="text-xs text-tertiary truncate">Archived</div>
55
55
-
) : props.draftInPublication || props.published ? (
44
44
+
) : pubStatus?.draftInPublication || pubStatus?.isPublished ? (
56
45
<div
57
57
-
className={`text-xs w-max grow truncate ${props.published ? "font-bold text-tertiary" : "text-tertiary"}`}
46
46
+
className={`text-xs w-max grow truncate ${pubStatus?.isPublished ? "font-bold text-tertiary" : "text-tertiary"}`}
58
47
>
59
59
-
{props.published
48
48
+
{pubStatus?.isPublished
60
49
? `Published ${prettyPublishedAt}`
61
50
: `Draft ${prettyCreatedAt}`}
62
51
</div>
+20
-15
app/(home-pages)/home/LeafletList/LeafletListItem.tsx
···
1
1
"use client";
2
2
-
import { PermissionToken } from "src/replicache";
3
2
import { LeafletListPreview, LeafletGridPreview } from "./LeafletPreview";
4
3
import { LeafletInfo } from "./LeafletInfo";
5
4
import { useState, useRef, useEffect } from "react";
6
5
import { SpeedyLink } from "components/SpeedyLink";
6
6
+
import { useLeafletPublicationStatus } from "components/PageSWRDataProvider";
7
7
8
8
export const LeafletListItem = (props: {
9
9
-
token: PermissionToken;
10
9
archived?: boolean | null;
11
11
-
leaflet_id: string;
12
10
loggedIn: boolean;
13
11
display: "list" | "grid";
14
12
cardBorderHidden: boolean;
15
13
added_at: string;
16
14
title?: string;
17
17
-
draftInPublication?: string;
18
18
-
published?: boolean;
19
19
-
publishedAt?: string;
20
20
-
document_uri?: string;
21
15
index: number;
22
16
isHidden: boolean;
23
17
showPreview?: boolean;
24
18
}) => {
19
19
+
const pubStatus = useLeafletPublicationStatus();
25
20
let [isOnScreen, setIsOnScreen] = useState(props.index < 16 ? true : false);
26
21
let previewRef = useRef<HTMLDivElement | null>(null);
27
22
···
43
38
return () => observer.disconnect();
44
39
}, [previewRef]);
45
40
41
41
+
const tokenId = pubStatus?.shareLink ?? "";
42
42
+
46
43
if (props.display === "list")
47
44
return (
48
45
<>
···
58
55
}}
59
56
>
60
57
<SpeedyLink
61
61
-
href={`/${props.token.id}`}
58
58
+
href={`/${tokenId}`}
62
59
className={`absolute w-full h-full top-0 left-0 no-underline hover:no-underline! text-primary`}
63
60
/>
64
64
-
{props.showPreview && (
65
65
-
<LeafletListPreview isVisible={isOnScreen} {...props} />
66
66
-
)}
67
67
-
<LeafletInfo {...props} />
61
61
+
{props.showPreview && <LeafletListPreview isVisible={isOnScreen} />}
62
62
+
<LeafletInfo
63
63
+
title={props.title}
64
64
+
display={props.display}
65
65
+
added_at={props.added_at}
66
66
+
archived={props.archived}
67
67
+
loggedIn={props.loggedIn}
68
68
+
/>
68
69
</div>
69
70
{props.cardBorderHidden && (
70
71
<hr
···
92
93
}}
93
94
>
94
95
<SpeedyLink
95
95
-
href={`/${props.token.id}`}
96
96
+
href={`/${tokenId}`}
96
97
className={`absolute w-full h-full top-0 left-0 no-underline hover:no-underline! text-primary`}
97
98
/>
98
99
<div className="grow">
99
99
-
<LeafletGridPreview {...props} isVisible={isOnScreen} />
100
100
+
<LeafletGridPreview isVisible={isOnScreen} />
100
101
</div>
101
102
<LeafletInfo
102
103
className="px-1 pb-0.5 shrink-0"
103
103
-
{...props}
104
104
+
title={props.title}
105
105
+
display={props.display}
106
106
+
added_at={props.added_at}
107
107
+
archived={props.archived}
108
108
+
loggedIn={props.loggedIn}
104
109
/>
105
110
</div>
106
111
);
+136
-173
app/(home-pages)/home/LeafletList/LeafletOptions.tsx
···
21
21
import { HideSmall } from "components/Icons/HideSmall";
22
22
import { hideDoc } from "../storage";
23
23
24
24
-
import { PermissionToken } from "src/replicache";
25
24
import {
26
25
useIdentityData,
27
26
mutateIdentityData,
···
31
30
mutatePublicationData,
32
31
} from "app/lish/[did]/[publication]/dashboard/PublicationSWRProvider";
33
32
import { ShareButton } from "app/[leaflet_id]/actions/ShareOptions";
33
33
+
import { useLeafletPublicationStatus } from "components/PageSWRDataProvider";
34
34
35
35
export const LeafletOptions = (props: {
36
36
-
leaflet: PermissionToken;
37
37
-
draftInPublication?: string;
38
38
-
document_uri?: string;
39
39
-
shareLink: string;
40
36
archived?: boolean | null;
41
37
loggedIn?: boolean;
42
38
}) => {
39
39
+
const pubStatus = useLeafletPublicationStatus();
43
40
let [state, setState] = useState<"normal" | "areYouSure">("normal");
44
41
let [open, setOpen] = useState(false);
45
42
let { identity } = useIdentityData();
46
43
let isPublicationOwner =
47
47
-
!!identity?.atp_did && !!props.document_uri?.includes(identity.atp_did);
44
44
+
!!identity?.atp_did && !!pubStatus?.documentUri?.includes(identity.atp_did);
48
45
return (
49
46
<>
50
47
<Menu
···
68
65
>
69
66
{state === "normal" ? (
70
67
!props.loggedIn ? (
71
71
-
<LoggedOutOptions
72
72
-
leaflet={props.leaflet}
73
73
-
setState={setState}
74
74
-
shareLink={props.shareLink}
75
75
-
/>
76
76
-
) : props.document_uri && isPublicationOwner ? (
77
77
-
<PublishedPostOptions
78
78
-
setState={setState}
79
79
-
document_uri={props.document_uri}
80
80
-
{...props}
81
81
-
/>
68
68
+
<LoggedOutOptions setState={setState} />
69
69
+
) : pubStatus?.documentUri && isPublicationOwner ? (
70
70
+
<PublishedPostOptions setState={setState} />
82
71
) : (
83
83
-
<DefaultOptions setState={setState} {...props} />
72
72
+
<DefaultOptions setState={setState} archived={props.archived} />
84
73
)
85
74
) : state === "areYouSure" ? (
86
86
-
<DeleteAreYouSureForm
87
87
-
backToMenu={() => setState("normal")}
88
88
-
leaflet={props.leaflet}
89
89
-
document_uri={props.document_uri}
90
90
-
draft={!!props.draftInPublication}
91
91
-
/>
75
75
+
<DeleteAreYouSureForm backToMenu={() => setState("normal")} />
92
76
) : null}
93
77
</Menu>
94
78
</>
···
97
81
98
82
const DefaultOptions = (props: {
99
83
setState: (s: "areYouSure") => void;
100
100
-
draftInPublication?: string;
101
101
-
leaflet: PermissionToken;
102
102
-
shareLink: string;
103
84
archived?: boolean | null;
104
85
}) => {
105
105
-
let toaster = useToaster();
106
106
-
let { mutate: mutatePub } = usePublicationData();
107
107
-
let { mutate: mutateIdentity } = useIdentityData();
86
86
+
const pubStatus = useLeafletPublicationStatus();
87
87
+
const toaster = useToaster();
88
88
+
const { setArchived } = useArchiveMutations();
89
89
+
const tokenId = pubStatus?.token.id;
90
90
+
const itemType = pubStatus?.draftInPublication ? "Draft" : "Leaflet";
91
91
+
108
92
return (
109
93
<>
110
110
-
<ShareButton
111
111
-
text={
112
112
-
<div className="flex gap-2">
113
113
-
<ShareSmall />
114
114
-
Copy Edit Link
115
115
-
</div>
116
116
-
}
117
117
-
subtext=""
118
118
-
smokerText="Link copied!"
119
119
-
id="get-link"
120
120
-
link={props.shareLink}
121
121
-
/>
94
94
+
<EditLinkShareButton link={pubStatus?.shareLink ?? ""} />
122
95
<hr className="border-border-light" />
123
96
<MenuItem
124
97
onSelect={async () => {
98
98
+
if (!tokenId) return;
99
99
+
setArchived(tokenId, !props.archived);
100
100
+
125
101
if (!props.archived) {
126
126
-
mutateIdentityData(mutateIdentity, (data) => {
127
127
-
let item = data.permission_token_on_homepage.find(
128
128
-
(p) => p.permission_tokens?.id === props.leaflet.id,
129
129
-
);
130
130
-
if (item) item.archived = true;
131
131
-
});
132
132
-
mutatePublicationData(mutatePub, (data) => {
133
133
-
let item = data.publication?.leaflets_in_publications.find(
134
134
-
(l) => l.permission_tokens?.id === props.leaflet.id,
135
135
-
);
136
136
-
if (item) item.archived = true;
137
137
-
});
138
138
-
await archivePost(props.leaflet.id);
102
102
+
await archivePost(tokenId);
139
103
toaster({
140
104
content: (
141
105
<div className="font-bold flex gap-2">
142
142
-
Archived{props.draftInPublication ? " Draft" : " Leaflet"}!
106
106
+
Archived {itemType}!
143
107
<ButtonTertiary
144
108
className="underline text-accent-2!"
145
109
onClick={async () => {
146
146
-
mutateIdentityData(mutateIdentity, (data) => {
147
147
-
let item = data.permission_token_on_homepage.find(
148
148
-
(p) => p.permission_tokens?.id === props.leaflet.id,
149
149
-
);
150
150
-
if (item) item.archived = false;
151
151
-
});
152
152
-
mutatePublicationData(mutatePub, (data) => {
153
153
-
let item =
154
154
-
data.publication?.leaflets_in_publications.find(
155
155
-
(l) => l.permission_tokens?.id === props.leaflet.id,
156
156
-
);
157
157
-
if (item) item.archived = false;
158
158
-
});
159
159
-
await unarchivePost(props.leaflet.id);
110
110
+
setArchived(tokenId, false);
111
111
+
await unarchivePost(tokenId);
160
112
toaster({
161
161
-
content: (
162
162
-
<div className="font-bold flex gap-2">
163
163
-
Unarchived!
164
164
-
</div>
165
165
-
),
113
113
+
content: <div className="font-bold">Unarchived!</div>,
166
114
type: "success",
167
115
});
168
116
}}
···
174
122
type: "success",
175
123
});
176
124
} else {
177
177
-
mutateIdentityData(mutateIdentity, (data) => {
178
178
-
let item = data.permission_token_on_homepage.find(
179
179
-
(p) => p.permission_tokens?.id === props.leaflet.id,
180
180
-
);
181
181
-
if (item) item.archived = false;
182
182
-
});
183
183
-
mutatePublicationData(mutatePub, (data) => {
184
184
-
let item = data.publication?.leaflets_in_publications.find(
185
185
-
(l) => l.permission_tokens?.id === props.leaflet.id,
186
186
-
);
187
187
-
if (item) item.archived = false;
188
188
-
});
189
189
-
await unarchivePost(props.leaflet.id);
125
125
+
await unarchivePost(tokenId);
190
126
toaster({
191
127
content: <div className="font-bold">Unarchived!</div>,
192
128
type: "success",
···
195
131
}}
196
132
>
197
133
<ArchiveSmall />
198
198
-
{!props.archived ? " Archive" : "Unarchive"}
199
199
-
{props.draftInPublication ? " Draft" : " Leaflet"}
134
134
+
{!props.archived ? " Archive" : "Unarchive"} {itemType}
200
135
</MenuItem>
201
201
-
<MenuItem
136
136
+
<DeleteForeverMenuItem
202
137
onSelect={(e) => {
203
138
e.preventDefault();
204
139
props.setState("areYouSure");
205
140
}}
206
206
-
>
207
207
-
<DeleteSmall />
208
208
-
Delete Forever
209
209
-
</MenuItem>
141
141
+
/>
210
142
</>
211
143
);
212
144
};
213
145
214
214
-
const LoggedOutOptions = (props: {
215
215
-
leaflet: PermissionToken;
216
216
-
setState: (s: "areYouSure") => void;
217
217
-
shareLink: string;
218
218
-
}) => {
219
219
-
let toaster = useToaster();
146
146
+
const LoggedOutOptions = (props: { setState: (s: "areYouSure") => void }) => {
147
147
+
const pubStatus = useLeafletPublicationStatus();
148
148
+
const toaster = useToaster();
149
149
+
220
150
return (
221
151
<>
222
222
-
<ShareButton
223
223
-
text={
224
224
-
<div className="flex gap-2">
225
225
-
<ShareSmall />
226
226
-
Copy Edit Link
227
227
-
</div>
228
228
-
}
229
229
-
subtext=""
230
230
-
smokerText="Link copied!"
231
231
-
id="get-link"
232
232
-
link={`/${props.shareLink}`}
233
233
-
/>
152
152
+
<EditLinkShareButton link={`/${pubStatus?.shareLink ?? ""}`} />
234
153
<hr className="border-border-light" />
235
154
<MenuItem
236
155
onSelect={() => {
237
237
-
hideDoc(props.leaflet);
156
156
+
if (pubStatus?.token) hideDoc(pubStatus.token);
238
157
toaster({
239
158
content: <div className="font-bold">Removed from Home!</div>,
240
159
type: "success",
···
244
163
<HideSmall />
245
164
Remove from Home
246
165
</MenuItem>
247
247
-
<MenuItem
166
166
+
<DeleteForeverMenuItem
248
167
onSelect={(e) => {
249
168
e.preventDefault();
250
169
props.setState("areYouSure");
251
170
}}
252
252
-
>
253
253
-
<DeleteSmall />
254
254
-
Delete Forever
255
255
-
</MenuItem>
171
171
+
/>
256
172
</>
257
173
);
258
174
};
259
175
260
176
const PublishedPostOptions = (props: {
261
177
setState: (s: "areYouSure") => void;
262
262
-
document_uri: string;
263
263
-
leaflet: PermissionToken;
264
264
-
shareLink: string;
265
178
}) => {
266
266
-
let toaster = useToaster();
179
179
+
const pubStatus = useLeafletPublicationStatus();
180
180
+
const toaster = useToaster();
181
181
+
const postLink = pubStatus?.postShareLink ?? "";
182
182
+
const isFullUrl = postLink.includes("http");
183
183
+
267
184
return (
268
185
<>
269
186
<ShareButton
···
275
192
}
276
193
smokerText="Link copied!"
277
194
id="get-link"
278
278
-
link=""
279
279
-
fullLink={props.shareLink}
195
195
+
link={postLink}
196
196
+
fullLink={isFullUrl ? postLink : undefined}
280
197
/>
281
281
-
282
198
<hr className="border-border-light" />
283
199
<MenuItem
284
200
onSelect={async () => {
285
285
-
if (props.document_uri) {
286
286
-
await unpublishPost(props.document_uri);
201
201
+
if (pubStatus?.documentUri) {
202
202
+
await unpublishPost(pubStatus.documentUri);
287
203
}
288
204
toaster({
289
205
content: <div className="font-bold">Unpublished Post!</div>,
···
299
215
</div>
300
216
</div>
301
217
</MenuItem>
302
302
-
<MenuItem
218
218
+
<DeleteForeverMenuItem
303
219
onSelect={(e) => {
304
220
e.preventDefault();
305
221
props.setState("areYouSure");
306
222
}}
307
307
-
>
308
308
-
<DeleteSmall />
309
309
-
<div className="flex flex-col">
310
310
-
Delete Post
311
311
-
<div className="text-tertiary text-sm font-normal!">
312
312
-
Unpublish AND delete
313
313
-
</div>
314
314
-
</div>
315
315
-
</MenuItem>
223
223
+
subtext="Post"
224
224
+
/>
316
225
</>
317
226
);
318
227
};
319
228
320
320
-
const DeleteAreYouSureForm = (props: {
321
321
-
backToMenu: () => void;
322
322
-
document_uri?: string;
323
323
-
leaflet: PermissionToken;
324
324
-
draft?: boolean;
325
325
-
}) => {
326
326
-
let toaster = useToaster();
327
327
-
let { mutate: mutatePub } = usePublicationData();
328
328
-
let { mutate: mutateIdentity } = useIdentityData();
229
229
+
const DeleteAreYouSureForm = (props: { backToMenu: () => void }) => {
230
230
+
const pubStatus = useLeafletPublicationStatus();
231
231
+
const toaster = useToaster();
232
232
+
const { removeFromLists } = useArchiveMutations();
233
233
+
const tokenId = pubStatus?.token.id;
234
234
+
235
235
+
const itemType = pubStatus?.documentUri
236
236
+
? "Post"
237
237
+
: pubStatus?.draftInPublication
238
238
+
? "Draft"
239
239
+
: "Leaflet";
329
240
330
241
return (
331
242
<div className="flex flex-col justify-center p-2 text-center">
···
339
250
</ButtonTertiary>
340
251
<ButtonPrimary
341
252
onClick={async () => {
342
342
-
mutateIdentityData(mutateIdentity, (data) => {
343
343
-
data.permission_token_on_homepage =
344
344
-
data.permission_token_on_homepage.filter(
345
345
-
(p) => p.permission_tokens?.id !== props.leaflet.id,
346
346
-
);
347
347
-
});
348
348
-
mutatePublicationData(mutatePub, (data) => {
349
349
-
if (!data.publication) return;
350
350
-
data.publication.leaflets_in_publications =
351
351
-
data.publication.leaflets_in_publications.filter(
352
352
-
(l) => l.permission_tokens?.id !== props.leaflet.id,
353
353
-
);
354
354
-
});
355
355
-
if (props.document_uri) {
356
356
-
await deletePost(props.document_uri);
253
253
+
if (tokenId) removeFromLists(tokenId);
254
254
+
if (pubStatus?.documentUri) {
255
255
+
await deletePost(pubStatus.documentUri);
357
256
}
358
358
-
deleteLeaflet(props.leaflet);
257
257
+
if (pubStatus?.token) deleteLeaflet(pubStatus.token);
359
258
360
259
toaster({
361
361
-
content: (
362
362
-
<div className="font-bold">
363
363
-
Deleted{" "}
364
364
-
{props.document_uri
365
365
-
? "Post!"
366
366
-
: props.draft
367
367
-
? "Draft"
368
368
-
: "Leaflet!"}
369
369
-
</div>
370
370
-
),
260
260
+
content: <div className="font-bold">Deleted {itemType}!</div>,
371
261
type: "success",
372
262
});
373
263
}}
···
378
268
</div>
379
269
);
380
270
};
271
271
+
272
272
+
// Shared menu items
273
273
+
const EditLinkShareButton = (props: { link: string }) => (
274
274
+
<ShareButton
275
275
+
text={
276
276
+
<div className="flex gap-2">
277
277
+
<ShareSmall />
278
278
+
Copy Edit Link
279
279
+
</div>
280
280
+
}
281
281
+
subtext=""
282
282
+
smokerText="Link copied!"
283
283
+
id="get-link"
284
284
+
link={props.link}
285
285
+
/>
286
286
+
);
287
287
+
288
288
+
const DeleteForeverMenuItem = (props: {
289
289
+
onSelect: (e: Event) => void;
290
290
+
subtext?: string;
291
291
+
}) => (
292
292
+
<MenuItem onSelect={props.onSelect}>
293
293
+
<DeleteSmall />
294
294
+
{props.subtext ? (
295
295
+
<div className="flex flex-col">
296
296
+
Delete {props.subtext}
297
297
+
<div className="text-tertiary text-sm font-normal!">
298
298
+
Unpublish AND delete
299
299
+
</div>
300
300
+
</div>
301
301
+
) : (
302
302
+
"Delete Forever"
303
303
+
)}
304
304
+
</MenuItem>
305
305
+
);
306
306
+
307
307
+
// Helper to update archived state in both identity and publication data
308
308
+
function useArchiveMutations() {
309
309
+
const { mutate: mutatePub } = usePublicationData();
310
310
+
const { mutate: mutateIdentity } = useIdentityData();
311
311
+
312
312
+
return {
313
313
+
setArchived: (tokenId: string, archived: boolean) => {
314
314
+
mutateIdentityData(mutateIdentity, (data) => {
315
315
+
const item = data.permission_token_on_homepage.find(
316
316
+
(p) => p.permission_tokens?.id === tokenId,
317
317
+
);
318
318
+
if (item) item.archived = archived;
319
319
+
});
320
320
+
mutatePublicationData(mutatePub, (data) => {
321
321
+
const item = data.publication?.leaflets_in_publications.find(
322
322
+
(l) => l.permission_tokens?.id === tokenId,
323
323
+
);
324
324
+
if (item) item.archived = archived;
325
325
+
});
326
326
+
},
327
327
+
removeFromLists: (tokenId: string) => {
328
328
+
mutateIdentityData(mutateIdentity, (data) => {
329
329
+
data.permission_token_on_homepage =
330
330
+
data.permission_token_on_homepage.filter(
331
331
+
(p) => p.permission_tokens?.id !== tokenId,
332
332
+
);
333
333
+
});
334
334
+
mutatePublicationData(mutatePub, (data) => {
335
335
+
if (!data.publication) return;
336
336
+
data.publication.leaflets_in_publications =
337
337
+
data.publication.leaflets_in_publications.filter(
338
338
+
(l) => l.permission_tokens?.id !== tokenId,
339
339
+
);
340
340
+
});
341
341
+
},
342
342
+
};
343
343
+
}
+46
-95
app/(home-pages)/home/LeafletList/LeafletPreview.tsx
···
3
3
ThemeBackgroundProvider,
4
4
ThemeProvider,
5
5
} from "components/ThemeManager/ThemeProvider";
6
6
-
import {
7
7
-
PermissionToken,
8
8
-
useEntity,
9
9
-
useReferenceToEntity,
10
10
-
} from "src/replicache";
6
6
+
import { useEntity, useReferenceToEntity } from "src/replicache";
11
7
import { useCardBorderHidden } from "components/Pages/useCardBorderHidden";
12
8
import { LeafletContent } from "./LeafletContent";
13
9
import { Tooltip } from "components/Tooltip";
10
10
+
import { useLeafletPublicationStatus } from "components/PageSWRDataProvider";
11
11
+
import { CSSProperties } from "react";
14
12
15
15
-
export const LeafletListPreview = (props: {
16
16
-
draft?: boolean;
17
17
-
published?: boolean;
18
18
-
isVisible: boolean;
19
19
-
token: PermissionToken;
20
20
-
leaflet_id: string;
21
21
-
loggedIn: boolean;
22
22
-
}) => {
23
23
-
let root =
24
24
-
useReferenceToEntity("root/page", props.leaflet_id)[0]?.entity ||
25
25
-
props.leaflet_id;
26
26
-
let firstPage = useEntity(root, "root/page")[0];
27
27
-
let page = firstPage?.data.value || root;
13
13
+
function useLeafletPreviewData() {
14
14
+
const pubStatus = useLeafletPublicationStatus();
15
15
+
const leafletId = pubStatus?.leafletId ?? "";
16
16
+
const root =
17
17
+
useReferenceToEntity("root/page", leafletId)[0]?.entity || leafletId;
18
18
+
const firstPage = useEntity(root, "root/page")[0];
19
19
+
const page = firstPage?.data.value || root;
28
20
29
29
-
let cardBorderHidden = useCardBorderHidden(root);
30
30
-
let rootBackgroundImage = useEntity(root, "theme/card-background-image");
31
31
-
let rootBackgroundRepeat = useEntity(
21
21
+
const cardBorderHidden = useCardBorderHidden(root);
22
22
+
const rootBackgroundImage = useEntity(root, "theme/card-background-image");
23
23
+
const rootBackgroundRepeat = useEntity(
32
24
root,
33
25
"theme/card-background-image-repeat",
34
26
);
35
35
-
let rootBackgroundOpacity = useEntity(
27
27
+
const rootBackgroundOpacity = useEntity(
36
28
root,
37
29
"theme/card-background-image-opacity",
38
30
);
39
31
32
32
+
const contentWrapperStyle: CSSProperties = cardBorderHidden
33
33
+
? {}
34
34
+
: {
35
35
+
backgroundImage: rootBackgroundImage
36
36
+
? `url(${rootBackgroundImage.data.src}), url(${rootBackgroundImage.data.fallback})`
37
37
+
: undefined,
38
38
+
backgroundRepeat: rootBackgroundRepeat ? "repeat" : "no-repeat",
39
39
+
backgroundPosition: "center",
40
40
+
backgroundSize: !rootBackgroundRepeat
41
41
+
? "cover"
42
42
+
: rootBackgroundRepeat?.data.value / 3,
43
43
+
opacity:
44
44
+
rootBackgroundImage?.data.src && rootBackgroundOpacity
45
45
+
? rootBackgroundOpacity.data.value
46
46
+
: 1,
47
47
+
backgroundColor: "rgba(var(--bg-page), var(--bg-page-alpha))",
48
48
+
};
49
49
+
50
50
+
const contentWrapperClass = `leafletContentWrapper h-full sm:w-48 w-40 mx-auto overflow-clip ${!cardBorderHidden && "border border-border-light border-b-0 rounded-t-md"}`;
51
51
+
52
52
+
return { root, page, cardBorderHidden, contentWrapperStyle, contentWrapperClass };
53
53
+
}
54
54
+
55
55
+
export const LeafletListPreview = (props: { isVisible: boolean }) => {
56
56
+
const { root, page, cardBorderHidden, contentWrapperStyle, contentWrapperClass } =
57
57
+
useLeafletPreviewData();
58
58
+
40
59
return (
41
60
<Tooltip
42
61
open={true}
···
73
92
<ThemeProvider local entityID={root} className="rounded-sm">
74
93
<ThemeBackgroundProvider entityID={root}>
75
94
<div className="leafletPreview grow shrink-0 h-44 w-64 px-2 pt-2 sm:px-3 sm:pt-3 flex items-end pointer-events-none rounded-[2px] ">
76
76
-
<div
77
77
-
className={`leafletContentWrapper h-full sm:w-48 w-40 mx-auto overflow-clip ${!cardBorderHidden && "border border-border-light border-b-0 rounded-t-md"}`}
78
78
-
style={
79
79
-
cardBorderHidden
80
80
-
? {}
81
81
-
: {
82
82
-
backgroundImage: rootBackgroundImage
83
83
-
? `url(${rootBackgroundImage.data.src}), url(${rootBackgroundImage.data.fallback})`
84
84
-
: undefined,
85
85
-
backgroundRepeat: rootBackgroundRepeat
86
86
-
? "repeat"
87
87
-
: "no-repeat",
88
88
-
backgroundPosition: "center",
89
89
-
backgroundSize: !rootBackgroundRepeat
90
90
-
? "cover"
91
91
-
: rootBackgroundRepeat?.data.value / 3,
92
92
-
opacity:
93
93
-
rootBackgroundImage?.data.src && rootBackgroundOpacity
94
94
-
? rootBackgroundOpacity.data.value
95
95
-
: 1,
96
96
-
backgroundColor:
97
97
-
"rgba(var(--bg-page), var(--bg-page-alpha))",
98
98
-
}
99
99
-
}
100
100
-
>
95
95
+
<div className={contentWrapperClass} style={contentWrapperStyle}>
101
96
<LeafletContent entityID={page} isOnScreen={props.isVisible} />
102
97
</div>
103
98
</div>
···
107
102
);
108
103
};
109
104
110
110
-
export const LeafletGridPreview = (props: {
111
111
-
draft?: boolean;
112
112
-
published?: boolean;
113
113
-
token: PermissionToken;
114
114
-
leaflet_id: string;
115
115
-
loggedIn: boolean;
116
116
-
isVisible: boolean;
117
117
-
}) => {
118
118
-
let root =
119
119
-
useReferenceToEntity("root/page", props.leaflet_id)[0]?.entity ||
120
120
-
props.leaflet_id;
121
121
-
let firstPage = useEntity(root, "root/page")[0];
122
122
-
let page = firstPage?.data.value || root;
105
105
+
export const LeafletGridPreview = (props: { isVisible: boolean }) => {
106
106
+
const { root, page, contentWrapperStyle, contentWrapperClass } =
107
107
+
useLeafletPreviewData();
123
108
124
124
-
let cardBorderHidden = useCardBorderHidden(root);
125
125
-
let rootBackgroundImage = useEntity(root, "theme/card-background-image");
126
126
-
let rootBackgroundRepeat = useEntity(
127
127
-
root,
128
128
-
"theme/card-background-image-repeat",
129
129
-
);
130
130
-
let rootBackgroundOpacity = useEntity(
131
131
-
root,
132
132
-
"theme/card-background-image-opacity",
133
133
-
);
134
109
return (
135
110
<ThemeProvider local entityID={root} className="w-full!">
136
111
<div className="border border-border-light rounded-md w-full h-full overflow-hidden ">
···
140
115
inert
141
116
className="leafletPreview grow shrink-0 h-full w-full px-2 pt-2 sm:px-3 sm:pt-3 flex items-end pointer-events-none"
142
117
>
143
143
-
<div
144
144
-
className={`leafletContentWrapper h-full sm:w-48 w-40 mx-auto overflow-clip ${!cardBorderHidden && "border border-border-light border-b-0 rounded-t-md"}`}
145
145
-
style={
146
146
-
cardBorderHidden
147
147
-
? {}
148
148
-
: {
149
149
-
backgroundImage: rootBackgroundImage
150
150
-
? `url(${rootBackgroundImage.data.src}), url(${rootBackgroundImage.data.fallback})`
151
151
-
: undefined,
152
152
-
backgroundRepeat: rootBackgroundRepeat
153
153
-
? "repeat"
154
154
-
: "no-repeat",
155
155
-
backgroundPosition: "center",
156
156
-
backgroundSize: !rootBackgroundRepeat
157
157
-
? "cover"
158
158
-
: rootBackgroundRepeat?.data.value / 3,
159
159
-
opacity:
160
160
-
rootBackgroundImage?.data.src && rootBackgroundOpacity
161
161
-
? rootBackgroundOpacity.data.value
162
162
-
: 1,
163
163
-
backgroundColor:
164
164
-
"rgba(var(--bg-page), var(--bg-page-alpha))",
165
165
-
}
166
166
-
}
167
167
-
>
118
118
+
<div className={contentWrapperClass} style={contentWrapperStyle}>
168
119
<LeafletContent entityID={page} isOnScreen={props.isVisible} />
169
120
</div>
170
121
</div>
+25
-7
app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx
···
19
19
import { CommentTiny } from "components/Icons/CommentTiny";
20
20
import { useLocalizedDate } from "src/hooks/useLocalizedDate";
21
21
import { LeafletOptions } from "app/(home-pages)/home/LeafletList/LeafletOptions";
22
22
+
import { StaticLeafletDataContext } from "components/PageSWRDataProvider";
22
23
23
24
export function PublishedPostsList(props: {
24
25
searchValue: string;
···
84
85
</h3>
85
86
</a>
86
87
<div className="flex justify-start align-top flex-row gap-1">
87
87
-
{leaflet && (
88
88
+
{leaflet && leaflet.permission_tokens && (
88
89
<>
89
90
<SpeedyLink
90
91
className="pt-[6px]"
···
93
94
<EditTiny />
94
95
</SpeedyLink>
95
96
96
96
-
<LeafletOptions
97
97
-
leaflet={leaflet?.permission_tokens!}
98
98
-
document_uri={doc.documents.uri}
99
99
-
shareLink={postLink}
100
100
-
loggedIn={true}
101
101
-
/>
97
97
+
<StaticLeafletDataContext
98
98
+
value={{
99
99
+
...leaflet.permission_tokens,
100
100
+
leaflets_in_publications: [
101
101
+
{
102
102
+
...leaflet,
103
103
+
publications: publication,
104
104
+
documents: doc.documents
105
105
+
? {
106
106
+
uri: doc.documents.uri,
107
107
+
indexed_at: doc.documents.indexed_at,
108
108
+
data: doc.documents.data,
109
109
+
}
110
110
+
: null,
111
111
+
},
112
112
+
],
113
113
+
leaflets_to_documents: [],
114
114
+
blocked_by_admin: null,
115
115
+
custom_domain_routes: [],
116
116
+
}}
117
117
+
>
118
118
+
<LeafletOptions loggedIn={true} />
119
119
+
</StaticLeafletDataContext>
102
120
</>
103
121
)}
104
122
</div>
+3
-11
app/lish/createPub/getPublicationURL.ts
···
3
3
import { isProductionDomain } from "src/utils/isProductionDeployment";
4
4
import { Json } from "supabase/database.types";
5
5
6
6
-
export function getPublicationURL(pub: {
7
7
-
uri: string;
8
8
-
name: string;
9
9
-
record: Json;
10
10
-
}) {
6
6
+
export function getPublicationURL(pub: { uri: string; record: Json }) {
11
7
let record = pub.record as PubLeafletPublication.Record;
12
8
if (isProductionDomain() && record?.base_path)
13
9
return `https://${record.base_path}`;
14
10
else return getBasePublicationURL(pub);
15
11
}
16
12
17
17
-
export function getBasePublicationURL(pub: {
18
18
-
uri: string;
19
19
-
name: string;
20
20
-
record: Json;
21
21
-
}) {
13
13
+
export function getBasePublicationURL(pub: { uri: string; record: Json }) {
22
14
let record = pub.record as PubLeafletPublication.Record;
23
15
let aturi = new AtUri(pub.uri);
24
24
-
return `/lish/${aturi.host}/${encodeURIComponent(aturi.rkey || record?.name || pub.name)}`;
16
16
+
return `/lish/${aturi.host}/${encodeURIComponent(aturi.rkey || record?.name)}`;
25
17
}
+46
components/PageSWRDataProvider.tsx
···
8
8
import type { GetLeafletDataReturnType } from "app/api/rpc/[command]/get_leaflet_data";
9
9
import { createContext, useContext } from "react";
10
10
import { getPublicationMetadataFromLeafletData } from "src/utils/getPublicationMetadataFromLeafletData";
11
11
+
import { getPublicationURL } from "app/lish/createPub/getPublicationURL";
12
12
+
import { AtUri } from "@atproto/syntax";
11
13
12
14
export const StaticLeafletDataContext = createContext<
13
15
null | GetLeafletDataReturnType["result"]["data"]
···
80
82
let { data, mutate } = useLeafletData();
81
83
return { data: data?.custom_domain_routes, mutate: mutate };
82
84
}
85
85
+
86
86
+
export function useLeafletPublicationStatus() {
87
87
+
const data = useContext(StaticLeafletDataContext);
88
88
+
if (!data) return null;
89
89
+
90
90
+
const publishedInPublication = data.leaflets_in_publications?.find(
91
91
+
(l) => l.doc,
92
92
+
);
93
93
+
const publishedStandalone = data.leaflets_to_documents?.find(
94
94
+
(l) => !!l.documents,
95
95
+
);
96
96
+
97
97
+
const documentUri =
98
98
+
publishedInPublication?.documents?.uri ?? publishedStandalone?.document;
99
99
+
100
100
+
// Compute the full post URL for sharing
101
101
+
let postShareLink: string | undefined;
102
102
+
if (publishedInPublication?.publications && publishedInPublication.documents) {
103
103
+
// Published in a publication - use publication URL + document rkey
104
104
+
const docUri = new AtUri(publishedInPublication.documents.uri);
105
105
+
postShareLink = `${getPublicationURL(publishedInPublication.publications)}/${docUri.rkey}`;
106
106
+
} else if (publishedStandalone?.document) {
107
107
+
// Standalone published post - use /p/{did}/{rkey} format
108
108
+
const docUri = new AtUri(publishedStandalone.document);
109
109
+
postShareLink = `/p/${docUri.host}/${docUri.rkey}`;
110
110
+
}
111
111
+
112
112
+
return {
113
113
+
token: data,
114
114
+
leafletId: data.root_entity,
115
115
+
shareLink: data.id,
116
116
+
// Draft state - in a publication but not yet published
117
117
+
draftInPublication:
118
118
+
data.leaflets_in_publications?.[0]?.publication ?? undefined,
119
119
+
// Published state
120
120
+
isPublished: !!(publishedInPublication || publishedStandalone),
121
121
+
publishedAt:
122
122
+
publishedInPublication?.documents?.indexed_at ??
123
123
+
publishedStandalone?.documents?.indexed_at,
124
124
+
documentUri,
125
125
+
// Full URL for sharing published posts
126
126
+
postShareLink,
127
127
+
};
128
128
+
}