astro-atmosphere-comments 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +21 -23
- package/components/AtmosphereComments.astro +179 -78
- package/components/AtmosphereReactions.astro +86 -51
- package/components/SignInForm.astro +14 -3
- package/components/atmosphere.css +21 -0
- package/index.mjs +42 -32
- package/lib/client/account.ts +553 -0
- package/lib/client/composer.ts +7 -10
- package/lib/http.ts +28 -0
- package/lib/permissions.ts +108 -0
- package/lib/public-settings.ts +18 -0
- package/lib/thread.ts +36 -6
- package/lib/uris.ts +31 -0
- package/locales/de.json +20 -4
- package/locales/en.json +20 -4
- package/locales/es.json +20 -4
- package/locales/fr.json +20 -4
- package/locales/it.json +20 -4
- package/locales/ja.json +20 -4
- package/locales/nl.json +20 -4
- package/locales/pt.json +20 -4
- package/package.json +2 -7
- package/routes/callback.astro +42 -0
- package/routes/client-metadata.json.ts +3 -2
- package/routes/following.ts +28 -0
- package/routes/identity.ts +69 -0
- package/routes/media.ts +7 -10
- package/routes/post.ts +1 -1
- package/routes/profile.ts +20 -0
- package/routes/richtext.ts +30 -0
- package/routes/thread.ts +23 -3
- package/routes/typeahead.ts +7 -23
- package/virtual.d.ts +5 -0
- package/bin/cli.mjs +0 -31
- package/lib/oauth.ts +0 -110
- package/lib/reader.ts +0 -130
- package/lib/store.ts +0 -46
- package/lib/visitor.ts +0 -42
- package/routes/callback.ts +0 -30
- package/routes/comment.ts +0 -97
- package/routes/delete.ts +0 -41
- package/routes/follow.ts +0 -71
- package/routes/jwks.json.ts +0 -13
- package/routes/login.ts +0 -58
- package/routes/logout.ts +0 -20
- package/routes/me.ts +0 -12
- package/routes/react.ts +0 -48
- package/routes/state.ts +0 -24
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// A link marked `data-atmosphere-follow="<DID>"` (and
|
|
16
16
|
// `data-atmosphere-follow-handle="you.bsky.social"`), such as "Follow me on
|
|
17
17
|
// Bluesky", becomes a Follow / Following button for signed-in readers; a
|
|
18
|
-
// second press unfollows.
|
|
18
|
+
// second press unfollows. Only
|
|
19
19
|
// `allowedAuthors` can be followed through your site.
|
|
20
20
|
//
|
|
21
21
|
// Signed-in readers like and repost from your page. Anyone else who presses
|
|
@@ -49,6 +49,17 @@ const { base } = settings;
|
|
|
49
49
|
<SignInForm id="atmo-dialog" returnTo={Astro.url.pathname} messages={messages} lang={lang} heading={t("dialog.heading")} />
|
|
50
50
|
<p class="atmo-signin__status" role="status" data-dialog-status></p>
|
|
51
51
|
</dialog>
|
|
52
|
+
{/* Before asking the reader's account host for one more permission: what it's for. */}
|
|
53
|
+
<dialog class="atmo atmo-signin atmo-permission" aria-labelledby="atmo-permission-heading" data-permission-dialog>
|
|
54
|
+
<button type="button" class="atmo-signin__close" aria-label={t("dialog.close")} data-close>×</button>
|
|
55
|
+
<p class="atmo-permission__heading" id="atmo-permission-heading">{t("permission.heading")}</p>
|
|
56
|
+
<p data-permission-text></p>
|
|
57
|
+
<p class="atmo__hint">{t("permission.approve")}</p>
|
|
58
|
+
<div class="atmo__row">
|
|
59
|
+
<button type="button" class="atmo__button" data-permission-continue>{t("comment.continue")}</button>
|
|
60
|
+
<button type="button" class="atmo__link-button" data-close>{t("comment.cancel")}</button>
|
|
61
|
+
</div>
|
|
62
|
+
</dialog>
|
|
52
63
|
{/* Reply or Quote, for signed-in readers: the comment box in an overlay. */}
|
|
53
64
|
<dialog class="atmo atmo-signin atmo-compose" aria-label={t("postActions.replyLabel")} data-compose-dialog>
|
|
54
65
|
<button type="button" class="atmo-signin__close" aria-label={t("dialog.close")} data-close>×</button>
|
|
@@ -65,6 +76,8 @@ const { base } = settings;
|
|
|
65
76
|
import { translate } from "../lib/translate";
|
|
66
77
|
import { enhanceHandleBox } from "../lib/client/handle-suggestions";
|
|
67
78
|
import { MAX, quotePreview, setUpComposer, type Posted } from "../lib/client/composer";
|
|
79
|
+
import { following, handleSignInForm, me as whoIsSignedIn, send as act, signIn, signOut as endSignIn, state } from "../lib/client/account";
|
|
80
|
+
import type { Permission } from "../lib/permissions";
|
|
68
81
|
|
|
69
82
|
const HEART = '<path d="M12 20.5s-7.5-4.4-7.5-10.1A4.4 4.4 0 0 1 12 7.6a4.4 4.4 0 0 1 7.5 2.8c0 5.7-7.5 10.1-7.5 10.1Z" />';
|
|
70
83
|
const ARROWS = '<path d="M7 8h10l-3-3M17 16H7l3 3M5 13V9.5A1.5 1.5 0 0 1 6.5 8M19 11v3.5a1.5 1.5 0 0 1-1.5 1.5" />';
|
|
@@ -83,7 +96,6 @@ const { base } = settings;
|
|
|
83
96
|
// Error codes from the routes, and their wording (as in the comments).
|
|
84
97
|
const MESSAGE_KEYS: Record<string, string> = {
|
|
85
98
|
"signed-out": "messages.signedOut",
|
|
86
|
-
"new-permissions": "messages.newPermissions",
|
|
87
99
|
"not-allowed": "messages.notAllowed",
|
|
88
100
|
"too-long": "messages.tooLong",
|
|
89
101
|
"too-many": "messages.tooMany",
|
|
@@ -101,11 +113,30 @@ const { base } = settings;
|
|
|
101
113
|
const status = dialog.querySelector<HTMLElement>("[data-dialog-status]")!;
|
|
102
114
|
let signedIn = false;
|
|
103
115
|
let handle = "";
|
|
104
|
-
let me: { did?: string; handle?: string; displayName?: string; avatar?: string } = {};
|
|
116
|
+
let me: { did?: string; handle?: string; displayName?: string; avatar?: string; can?: string[] } = {};
|
|
117
|
+
const can = (permission: Permission) => !!me.can?.includes(permission);
|
|
118
|
+
// One more permission from the reader's account host, then back to
|
|
119
|
+
// `hash` on this page to finish what they pressed. An overlay says
|
|
120
|
+
// what it's for first: the host may ask them to sign in again before
|
|
121
|
+
// it shows what's asked for, which is puzzling without warning.
|
|
122
|
+
const permissionDialog = this.querySelector<HTMLDialogElement>("[data-permission-dialog]")!;
|
|
123
|
+
let pendingPermission: { permission: Permission; hash: string } | null = null;
|
|
124
|
+
const askPermission = (permission: Permission, hash: string, vars: Record<string, string> = {}) => {
|
|
125
|
+
pendingPermission = { permission, hash };
|
|
126
|
+
permissionDialog.querySelector("[data-permission-text]")!.textContent = t(`permission.${permission}`, vars);
|
|
127
|
+
if (!permissionDialog.open) permissionDialog.showModal();
|
|
128
|
+
permissionDialog.querySelector<HTMLButtonElement>("[data-permission-continue]")!.focus();
|
|
129
|
+
};
|
|
130
|
+
permissionDialog.querySelector("[data-permission-continue]")!.addEventListener("click", () => {
|
|
131
|
+
if (!pendingPermission) return;
|
|
132
|
+
const { permission, hash } = pendingPermission;
|
|
133
|
+
void signIn(me.did!, { permissions: [permission], returnTo: `${location.pathname}${location.search}${hash}`, lang });
|
|
134
|
+
});
|
|
105
135
|
// What the reader has liked or reposted, by post.
|
|
106
136
|
const known: Record<string, { liked: boolean; reposted: boolean }> = {};
|
|
107
137
|
|
|
108
138
|
enhanceHandleBox(form, base);
|
|
139
|
+
handleSignInForm(form, lang);
|
|
109
140
|
for (const overlay of this.querySelectorAll<HTMLDialogElement>("dialog")) {
|
|
110
141
|
overlay.querySelectorAll("[data-close]").forEach((button) => button.addEventListener("click", () => overlay.close()));
|
|
111
142
|
// A click on the dimmed page around the overlay closes it.
|
|
@@ -150,7 +181,11 @@ const { base } = settings;
|
|
|
150
181
|
this.dispatchEvent(new CustomEvent("atmosphere:commented", { detail: data, bubbles: true }));
|
|
151
182
|
},
|
|
152
183
|
onError: (code, tell) => {
|
|
153
|
-
if (code === "signed-out"
|
|
184
|
+
if (code === "signed-out") signedIn = false;
|
|
185
|
+
if (code === "needs-permission") {
|
|
186
|
+
compose.close();
|
|
187
|
+
return void askPermission("post", `#atmo-compose=${composeMode}:${encodeURIComponent(composeRoot)}`);
|
|
188
|
+
}
|
|
154
189
|
tell(message(code));
|
|
155
190
|
},
|
|
156
191
|
onInput: () => quote.update(),
|
|
@@ -206,13 +241,6 @@ const { base } = settings;
|
|
|
206
241
|
form.querySelector<HTMLInputElement>('input[name="handle"]')!.focus();
|
|
207
242
|
};
|
|
208
243
|
|
|
209
|
-
const send = (body: unknown) =>
|
|
210
|
-
fetch(`${base}/react`, {
|
|
211
|
-
method: "POST",
|
|
212
|
-
credentials: "same-origin",
|
|
213
|
-
headers: { "content-type": "application/json" },
|
|
214
|
-
body: JSON.stringify(body),
|
|
215
|
-
});
|
|
216
244
|
|
|
217
245
|
const icon = (paths: string) => `<svg viewBox="0 0 24 24" aria-hidden="true">${paths}</svg>`;
|
|
218
246
|
|
|
@@ -228,7 +256,7 @@ const { base } = settings;
|
|
|
228
256
|
button.setAttribute("aria-pressed", String(on));
|
|
229
257
|
setCount(button, Math.max(0, before + (on ? 1 : -1)));
|
|
230
258
|
try {
|
|
231
|
-
const response = await
|
|
259
|
+
const response = await act("react", { post: uri, kind, on });
|
|
232
260
|
if (response.ok) {
|
|
233
261
|
known[uri] = { ...(known[uri] ?? { liked: false, reposted: false }), [kind === "like" ? "liked" : "reposted"]: on };
|
|
234
262
|
return;
|
|
@@ -236,10 +264,12 @@ const { base } = settings;
|
|
|
236
264
|
const data = (await response.json().catch(() => ({}))) as { error?: string };
|
|
237
265
|
button.setAttribute("aria-pressed", String(!on));
|
|
238
266
|
setCount(button, before);
|
|
239
|
-
|
|
267
|
+
// Signed in to comment before likes and reposts were asked for.
|
|
268
|
+
if (data.error === "needs-permission") return void askPermission("react", `#atmo-react=${kind}:${encodeURIComponent(uri)}`);
|
|
269
|
+
if (data.error === "signed-out") {
|
|
240
270
|
signedIn = false;
|
|
241
271
|
labelButton(button, kind);
|
|
242
|
-
askToSignIn(kind, uri, t(
|
|
272
|
+
askToSignIn(kind, uri, t("messages.signedOut"));
|
|
243
273
|
}
|
|
244
274
|
} catch {
|
|
245
275
|
button.setAttribute("aria-pressed", String(!on));
|
|
@@ -293,6 +323,8 @@ const { base } = settings;
|
|
|
293
323
|
link.addEventListener("click", (event) => {
|
|
294
324
|
if (!signedIn || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
295
325
|
event.preventDefault();
|
|
326
|
+
// A sign-in without posting (from an older version): asked for first.
|
|
327
|
+
if (!can("post")) return void askPermission("post", `#atmo-compose=${mode}:${encodeURIComponent(post.dataset.atmospherePost!)}`);
|
|
296
328
|
openCompose(post, mode);
|
|
297
329
|
});
|
|
298
330
|
return link;
|
|
@@ -323,8 +355,7 @@ const { base } = settings;
|
|
|
323
355
|
const uris = posts().map((post) => post.dataset.atmospherePost!);
|
|
324
356
|
if (!signedIn || !uris.length) return;
|
|
325
357
|
try {
|
|
326
|
-
const
|
|
327
|
-
const response = await fetch(`${base}/state?${query}`, { credentials: "same-origin" });
|
|
358
|
+
const response = await state(uris);
|
|
328
359
|
if (!response.ok) return;
|
|
329
360
|
const state = ((await response.json()) as { posts?: typeof known }).posts ?? {};
|
|
330
361
|
Object.assign(known, state);
|
|
@@ -345,17 +376,8 @@ const { base } = settings;
|
|
|
345
376
|
const followButtons = new Map<string, { following: boolean; set: (on: boolean) => void; press: () => Promise<void> }>();
|
|
346
377
|
// Following isn't part of signing in: the first follow asks the
|
|
347
378
|
// reader's account host for it, then carries on when they're back.
|
|
348
|
-
const askFollowPermission = (subject: string) =>
|
|
349
|
-
|
|
350
|
-
permit.hidden = true;
|
|
351
|
-
const field = (name: string, value: string) =>
|
|
352
|
-
permit.append(Object.assign(document.createElement("input"), { type: "hidden", name, value }));
|
|
353
|
-
field("handle", me.did || me.handle || "");
|
|
354
|
-
field("permission", "follow");
|
|
355
|
-
field("returnTo", `${location.pathname}${location.search}#atmo-follow=${encodeURIComponent(subject)}`);
|
|
356
|
-
document.body.append(permit);
|
|
357
|
-
permit.submit();
|
|
358
|
-
};
|
|
379
|
+
const askFollowPermission = (subject: string, who: string) =>
|
|
380
|
+
askPermission("follow", `#atmo-follow=${encodeURIComponent(subject)}`, { handle: who });
|
|
359
381
|
const setUpFollow = async () => {
|
|
360
382
|
for (const link of document.querySelectorAll<HTMLElement>("[data-atmosphere-follow]")) {
|
|
361
383
|
const subject = link.dataset.atmosphereFollow!;
|
|
@@ -365,19 +387,19 @@ const { base } = settings;
|
|
|
365
387
|
const who = link.dataset.atmosphereFollowHandle ? `@${link.dataset.atmosphereFollowHandle}` : t("comment.someone");
|
|
366
388
|
const button = make("button", "atmo-follow");
|
|
367
389
|
button.type = "button";
|
|
368
|
-
let
|
|
390
|
+
let isFollowing = false;
|
|
369
391
|
// The account's avatar, through the site (its own accounts' are always served).
|
|
370
392
|
const avatar = make("img", "atmo-follow__avatar");
|
|
371
393
|
avatar.src = `${base}/media?avatar=${encodeURIComponent(subject)}`;
|
|
372
394
|
avatar.alt = "";
|
|
373
395
|
avatar.onerror = () => avatar.remove();
|
|
374
396
|
const render = () => {
|
|
375
|
-
button.setAttribute("aria-pressed", String(
|
|
397
|
+
button.setAttribute("aria-pressed", String(isFollowing));
|
|
376
398
|
// The butterfly, then "Follow" with the account's avatar beside its
|
|
377
399
|
// handle (wherever the language puts the handle), or the avatar
|
|
378
400
|
// with "Following".
|
|
379
401
|
button.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true">${BUTTERFLY}</svg>`;
|
|
380
|
-
if (
|
|
402
|
+
if (isFollowing) {
|
|
381
403
|
button.append(avatar, make("span", "", `✓ ${t("follow.following")}`));
|
|
382
404
|
} else {
|
|
383
405
|
const [before, after = ""] = t("follow.follow").split("{{handle}}");
|
|
@@ -385,44 +407,35 @@ const { base } = settings;
|
|
|
385
407
|
button.append(avatar, make("span", "", who));
|
|
386
408
|
if (after.trim()) button.append(make("span", "", after.trim()));
|
|
387
409
|
}
|
|
388
|
-
button.title =
|
|
410
|
+
button.title = isFollowing ? t("follow.followingLabel", { handle: who }) : t("follow.followAs", { handle: who, me: handle });
|
|
389
411
|
button.setAttribute("aria-label", button.title);
|
|
390
412
|
};
|
|
391
413
|
const set = (on: boolean) => {
|
|
392
|
-
|
|
414
|
+
isFollowing = on;
|
|
393
415
|
render();
|
|
394
416
|
};
|
|
395
417
|
const press = async () => {
|
|
396
418
|
button.disabled = true;
|
|
397
419
|
try {
|
|
398
|
-
const response = await
|
|
399
|
-
method: "POST",
|
|
400
|
-
credentials: "same-origin",
|
|
401
|
-
headers: { "content-type": "application/json" },
|
|
402
|
-
body: JSON.stringify({ subject, on: !following }),
|
|
403
|
-
});
|
|
420
|
+
const response = await act("follow", { subject, on: !isFollowing });
|
|
404
421
|
const data = (await response.json().catch(() => ({}))) as { following?: boolean; error?: string };
|
|
405
422
|
if (response.ok) return set(!!data.following);
|
|
406
|
-
if (data.error === "needs-permission") return askFollowPermission(subject);
|
|
407
|
-
if (data.error === "signed-out"
|
|
423
|
+
if (data.error === "needs-permission") return void askFollowPermission(subject, who);
|
|
424
|
+
if (data.error === "signed-out") location.reload();
|
|
408
425
|
} finally {
|
|
409
426
|
button.disabled = false;
|
|
410
427
|
}
|
|
411
428
|
};
|
|
412
429
|
button.addEventListener("click", () => void press());
|
|
413
|
-
followButtons.set(subject, { get following() { return
|
|
430
|
+
followButtons.set(subject, { get following() { return isFollowing; }, set, press });
|
|
414
431
|
render();
|
|
415
432
|
// The button takes the link's place. (Inline, as a site's own style
|
|
416
433
|
// for the link could otherwise keep it showing.)
|
|
417
434
|
link.hidden = true;
|
|
418
435
|
link.style.display = "none";
|
|
419
436
|
link.after(button);
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
if (response.ok) set(!!((await response.json()) as { following?: boolean }).following);
|
|
423
|
-
} catch {
|
|
424
|
-
// Shown as Follow; pressing it still works.
|
|
425
|
-
}
|
|
437
|
+
const known = await following(subject);
|
|
438
|
+
if (known !== null) set(known);
|
|
426
439
|
}
|
|
427
440
|
};
|
|
428
441
|
|
|
@@ -464,6 +477,18 @@ const { base } = settings;
|
|
|
464
477
|
if (button && button.getAttribute("aria-pressed") !== "true") await react(post, button, kind as Kind, true);
|
|
465
478
|
};
|
|
466
479
|
|
|
480
|
+
// Back from allowing posting: open the Reply or Quote box they pressed.
|
|
481
|
+
const finishPendingCompose = () => {
|
|
482
|
+
const pending = location.hash.match(/^#atmo-compose=(reply|quote):(.+)$/);
|
|
483
|
+
if (!pending) return;
|
|
484
|
+
const url = new URL(location.href);
|
|
485
|
+
if (!document.querySelector("atmosphere-comments")) url.searchParams.delete("atmosphere");
|
|
486
|
+
history.replaceState(history.state, "", `${url.pathname}${url.search}`);
|
|
487
|
+
const uri = decodeURIComponent(pending[2]);
|
|
488
|
+
const post = posts().find((candidate) => candidate.dataset.atmospherePost === uri);
|
|
489
|
+
if (post && signedIn && can("post")) openCompose(post, pending[1] as "reply" | "quote");
|
|
490
|
+
};
|
|
491
|
+
|
|
467
492
|
// The reader's avatar wherever the page marks data-atmosphere-account,
|
|
468
493
|
// opening a menu with who they're signed in as and Sign out.
|
|
469
494
|
const showAccount = (me: { handle?: string; displayName?: string; avatar?: string }) => {
|
|
@@ -492,7 +517,7 @@ const { base } = settings;
|
|
|
492
517
|
signOut.type = "button";
|
|
493
518
|
signOut.addEventListener("click", async () => {
|
|
494
519
|
signOut.disabled = true;
|
|
495
|
-
await
|
|
520
|
+
await endSignIn();
|
|
496
521
|
location.reload();
|
|
497
522
|
});
|
|
498
523
|
menu.append(who, signOut);
|
|
@@ -521,9 +546,8 @@ const { base } = settings;
|
|
|
521
546
|
}
|
|
522
547
|
};
|
|
523
548
|
|
|
524
|
-
|
|
525
|
-
.then((
|
|
526
|
-
.then((found: { signedIn?: boolean; handle?: string; displayName?: string; avatar?: string }) => {
|
|
549
|
+
whoIsSignedIn()
|
|
550
|
+
.then((found) => {
|
|
527
551
|
me = found;
|
|
528
552
|
signedIn = !!found.signedIn;
|
|
529
553
|
handle = found.handle ? `@${found.handle}` : "";
|
|
@@ -542,6 +566,7 @@ const { base } = settings;
|
|
|
542
566
|
addActions();
|
|
543
567
|
await loadState();
|
|
544
568
|
await finishPending();
|
|
569
|
+
finishPendingCompose();
|
|
545
570
|
if (signedIn) {
|
|
546
571
|
await setUpFollow();
|
|
547
572
|
await finishPendingFollow();
|
|
@@ -574,6 +599,16 @@ const { base } = settings;
|
|
|
574
599
|
box-shadow: 0 24px 60px rgb(0 0 0 / 0.35);
|
|
575
600
|
}
|
|
576
601
|
|
|
602
|
+
.atmo-permission p {
|
|
603
|
+
margin: 0 0 10px;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.atmo-permission__heading {
|
|
607
|
+
/* Clear of the close button. */
|
|
608
|
+
padding-right: 32px;
|
|
609
|
+
font-weight: 700;
|
|
610
|
+
}
|
|
611
|
+
|
|
577
612
|
.atmo-signin::backdrop {
|
|
578
613
|
background: rgb(0 0 0 / 0.55);
|
|
579
614
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
// The sign-in form: a handle box (which suggests matching accounts, see
|
|
3
3
|
// lib/client/handle-suggestions.ts) and a "Sign in with Atmosphere" button.
|
|
4
|
+
// Signing in happens in the browser (handleSignInForm in
|
|
5
|
+
// lib/client/account.ts), so the form needs JavaScript.
|
|
4
6
|
// Used under posts by AtmosphereComments and in the sign-in overlay of
|
|
5
7
|
// AtmosphereReactions; not meant to be used on its own.
|
|
6
8
|
//
|
|
@@ -9,7 +11,10 @@
|
|
|
9
11
|
// returnTo where the reader comes back to after signing in
|
|
10
12
|
// messages, lang the wording (see lib/i18n.ts)
|
|
11
13
|
// heading the form's heading, instead of "signIn.heading"
|
|
14
|
+
// permissions more to ask for (groups in lib/permissions.ts) on top of
|
|
15
|
+
// everything readers can do here (DEFAULT_PERMISSIONS)
|
|
12
16
|
import logo from "../assets/union.svg?url";
|
|
17
|
+
import type { Permission } from "../lib/permissions";
|
|
13
18
|
import settings from "../lib/settings";
|
|
14
19
|
import { translate, translateHtml } from "../lib/i18n";
|
|
15
20
|
import { escapeHtml } from "../lib/translate";
|
|
@@ -20,10 +25,11 @@ interface Props {
|
|
|
20
25
|
messages: Record<string, string>;
|
|
21
26
|
lang: string;
|
|
22
27
|
heading?: string;
|
|
28
|
+
permissions?: Permission[];
|
|
23
29
|
}
|
|
24
30
|
|
|
25
|
-
const { id, returnTo, messages, lang, heading } = Astro.props;
|
|
26
|
-
const {
|
|
31
|
+
const { id, returnTo, messages, lang, heading, permissions = [] } = Astro.props;
|
|
32
|
+
const { policyUri } = settings;
|
|
27
33
|
const t = (key: string) => translate(messages, lang, key);
|
|
28
34
|
const th = (key: string, html: Record<string, string>) => translateHtml(messages, lang, key, html);
|
|
29
35
|
// Links lead off the page, so they open in a new tab.
|
|
@@ -31,7 +37,7 @@ const link = (href: string, text: string) =>
|
|
|
31
37
|
`<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml(text)}</a>`;
|
|
32
38
|
---
|
|
33
39
|
|
|
34
|
-
<form class="atmo__login" id={`${id}-login`}
|
|
40
|
+
<form class="atmo__login" id={`${id}-login`} data-login-form>
|
|
35
41
|
<label for={`${id}-handle`}>{heading ?? t("signIn.heading")}</label>
|
|
36
42
|
<p
|
|
37
43
|
class="atmo__hint"
|
|
@@ -67,6 +73,7 @@ const link = (href: string, text: string) =>
|
|
|
67
73
|
></ul>
|
|
68
74
|
</span>
|
|
69
75
|
<input type="hidden" name="returnTo" value={returnTo} data-return-to />
|
|
76
|
+
{permissions.map((permission) => <input type="hidden" name="permission" value={permission} />)}
|
|
70
77
|
<button type="submit" class="atmo__button atmo__button--sign-in">
|
|
71
78
|
<span class="atmo__logo" style={`--atmo-logo: url("${logo}")`} aria-hidden="true"></span>
|
|
72
79
|
{t("signIn.button")}
|
|
@@ -76,6 +83,10 @@ const link = (href: string, text: string) =>
|
|
|
76
83
|
class="atmo__hint"
|
|
77
84
|
set:html={th("signIn.privacy", { createAccount: link("https://atmosphereaccount.com", t("signIn.createAccount")) })}
|
|
78
85
|
/>
|
|
86
|
+
<p class="atmo__hint">
|
|
87
|
+
{t("signIn.access")}
|
|
88
|
+
{policyUri && <Fragment> <a href={policyUri} target="_blank" rel="noopener">{t("signIn.policy")}</a></Fragment>}
|
|
89
|
+
</p>
|
|
79
90
|
</form>
|
|
80
91
|
|
|
81
92
|
<style>
|
|
@@ -610,3 +610,24 @@ dialog.atmo:not([open]) {
|
|
|
610
610
|
.atmo-comment-action[data-kind="like"][aria-pressed="true"] svg {
|
|
611
611
|
fill: currentColor;
|
|
612
612
|
}
|
|
613
|
+
|
|
614
|
+
/* "One more permission" under posts, for readers who signed in only to like
|
|
615
|
+
and repost. */
|
|
616
|
+
.atmo__permission {
|
|
617
|
+
display: grid;
|
|
618
|
+
gap: 8px;
|
|
619
|
+
max-width: 560px;
|
|
620
|
+
margin-top: 14px;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
.atmo__permission p {
|
|
624
|
+
margin: 0;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/* In place of a comment whose author asks not to be shown to logged-out people. */
|
|
628
|
+
:where(.atmo-comment__withheld) {
|
|
629
|
+
margin: 0;
|
|
630
|
+
color: var(--atmo-muted, inherit);
|
|
631
|
+
font-size: 0.9em;
|
|
632
|
+
font-style: italic;
|
|
633
|
+
}
|
package/index.mjs
CHANGED
|
@@ -4,44 +4,39 @@
|
|
|
4
4
|
// replies to the post's announcement, written to the reader's own account, so
|
|
5
5
|
// they can delete them there at any time.
|
|
6
6
|
//
|
|
7
|
-
// Sign-in uses AT Protocol OAuth straight to the reader's own account host
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// SCOPE and DELETE_SCOPE in lib/oauth.ts).
|
|
7
|
+
// Sign-in uses AT Protocol OAuth straight to the reader's own account host,
|
|
8
|
+
// as a browser client: the sign-in is kept only in the reader's browser, and
|
|
9
|
+
// the site's server never sees it, so the server can't act for anyone.
|
|
10
|
+
// Signing out cancels it at the reader's host. See lib/client/account.ts.
|
|
12
11
|
//
|
|
13
12
|
// It adds /oauth-client-metadata.json at the site's root (what account hosts
|
|
14
|
-
// read to know the site), and these routes under `base` (default /atmosphere)
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// and a component, AtmosphereComments.astro, to put under each post.
|
|
13
|
+
// read to know the site), and these routes under `base` (default /atmosphere),
|
|
14
|
+
// none of which act for a reader:
|
|
15
|
+
// callback the page readers come back to after approving
|
|
16
|
+
// identity looks up an account for signing in
|
|
17
|
+
// profile a reader's public profile, for the comment box
|
|
18
|
+
// richtext links, @mentions and #hashtags in a comment, before it's posted
|
|
19
|
+
// following whether an account follows one of the site's accounts
|
|
20
|
+
// post the announcement, for the Quote preview
|
|
21
|
+
// media avatars and link-card pictures, via this site
|
|
22
|
+
// typeahead account suggestions as the reader types
|
|
23
|
+
// thread the comments, likes and reposts to show
|
|
24
|
+
// and two components: AtmosphereComments.astro under each post, and
|
|
25
|
+
// AtmosphereReactions.astro once in the layout.
|
|
28
26
|
//
|
|
29
27
|
// It needs an on-demand (server) adapter such as @astrojs/node.
|
|
30
28
|
import { fileURLToPath } from "node:url";
|
|
31
29
|
|
|
32
30
|
const VIRTUAL_ID = "virtual:atmosphere-comments/settings";
|
|
31
|
+
// The settings that are safe for readers' browsers (lib/public-settings.ts).
|
|
32
|
+
const PUBLIC_ID = "virtual:atmosphere-comments/public-settings";
|
|
33
33
|
|
|
34
34
|
const ROUTES = {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
comment: "comment.ts",
|
|
41
|
-
state: "state.ts",
|
|
42
|
-
react: "react.ts",
|
|
43
|
-
delete: "delete.ts",
|
|
44
|
-
follow: "follow.ts",
|
|
35
|
+
callback: "callback.astro",
|
|
36
|
+
identity: "identity.ts",
|
|
37
|
+
profile: "profile.ts",
|
|
38
|
+
richtext: "richtext.ts",
|
|
39
|
+
following: "following.ts",
|
|
45
40
|
post: "post.ts",
|
|
46
41
|
media: "media.ts",
|
|
47
42
|
typeahead: "typeahead.ts",
|
|
@@ -102,14 +97,29 @@ export default function atmosphereComments(options) {
|
|
|
102
97
|
});
|
|
103
98
|
}
|
|
104
99
|
|
|
105
|
-
|
|
100
|
+
const publicSettings = {
|
|
101
|
+
clientName: settings.clientName,
|
|
102
|
+
allowedAuthors,
|
|
103
|
+
base,
|
|
104
|
+
site: settings.site,
|
|
105
|
+
logoUri: settings.logoUri,
|
|
106
|
+
policyUri: settings.policyUri,
|
|
107
|
+
tosUri: settings.tosUri,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// The routes and components read the settings from virtual modules.
|
|
106
111
|
updateConfig({
|
|
107
112
|
vite: {
|
|
108
113
|
plugins: [
|
|
109
114
|
{
|
|
110
115
|
name: "atmosphere-comments-settings",
|
|
111
|
-
resolveId: (id) => (id === VIRTUAL_ID ? `\0${
|
|
112
|
-
load: (id) =>
|
|
116
|
+
resolveId: (id) => (id === VIRTUAL_ID || id === PUBLIC_ID ? `\0${id}` : undefined),
|
|
117
|
+
load: (id) =>
|
|
118
|
+
id === `\0${VIRTUAL_ID}`
|
|
119
|
+
? `export default ${JSON.stringify(settings)};`
|
|
120
|
+
: id === `\0${PUBLIC_ID}`
|
|
121
|
+
? `export default ${JSON.stringify(publicSettings)};`
|
|
122
|
+
: undefined,
|
|
113
123
|
},
|
|
114
124
|
],
|
|
115
125
|
},
|