astro-atmosphere-comments 0.4.1 → 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 +25 -0
- package/README.md +21 -23
- package/components/AtmosphereComments.astro +186 -83
- package/components/AtmosphereReactions.astro +86 -51
- package/components/SignInForm.astro +17 -4
- 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
|
@@ -71,8 +71,9 @@ const messages = messagesFor(lang, {
|
|
|
71
71
|
});
|
|
72
72
|
const t = (key: string, vars?: Record<string, string | number>) => translate(messages, lang, key, vars);
|
|
73
73
|
const th = (key: string, html: Record<string, string>) => translateHtml(messages, lang, key, html);
|
|
74
|
+
// Links lead off the page, so they open in a new tab.
|
|
74
75
|
const link = (href: string | undefined, text: string, attributes = "") =>
|
|
75
|
-
`<a${href ? ` href="${escapeHtml(href)}"` : ""}${attributes}>${escapeHtml(text)}</a>`;
|
|
76
|
+
`<a${href ? ` href="${escapeHtml(href)}"` : ""} target="_blank" rel="noopener"${attributes}>${escapeHtml(text)}</a>`;
|
|
76
77
|
// The announcement on bsky.app, when the page is given it (otherwise the
|
|
77
78
|
// script fills it in once the thread has loaded).
|
|
78
79
|
const atUri = post.match(/^at:\/\/([^/]+)\/app\.bsky\.feed\.post\/([^/]+)$/);
|
|
@@ -120,6 +121,16 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
120
121
|
</button>
|
|
121
122
|
</div>
|
|
122
123
|
|
|
124
|
+
{/* A sign-in from before posting was asked for: one more permission. */}
|
|
125
|
+
<div class="atmo__permission" data-permission hidden>
|
|
126
|
+
<p data-permission-text>{t("composer.needsPermission")}</p>
|
|
127
|
+
<p class="atmo__hint">{t("permission.approve")}</p>
|
|
128
|
+
<div class="atmo__row">
|
|
129
|
+
<button type="button" class="atmo__button" data-permission-continue>{t("comment.continue")}</button>
|
|
130
|
+
<button type="button" class="atmo__link-button" data-permission-sign-out>{t("composer.signOut")}</button>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
123
134
|
<Composer id="atmo" messages={messages} lang={lang} signOut />
|
|
124
135
|
</div>
|
|
125
136
|
|
|
@@ -142,6 +153,9 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
142
153
|
import { translate } from "../lib/translate";
|
|
143
154
|
import { enhanceHandleBox } from "../lib/client/handle-suggestions";
|
|
144
155
|
import { MAX, quotePreview, setUpComposer, type ComposerBehaviour, type Posted } from "../lib/client/composer";
|
|
156
|
+
import { handleSignInForm, me as whoIsSignedIn, send, signIn, signOut, state } from "../lib/client/account";
|
|
157
|
+
import type { Permission } from "../lib/permissions";
|
|
158
|
+
import { authorOf } from "../lib/uris";
|
|
145
159
|
|
|
146
160
|
|
|
147
161
|
// The error and status codes the routes and the sign-in round trip send,
|
|
@@ -154,7 +168,6 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
154
168
|
"signed-out": "messages.signedOut",
|
|
155
169
|
"needs-permission": "messages.needsPermission",
|
|
156
170
|
"not-allowed": "messages.notAllowed",
|
|
157
|
-
"new-permissions": "messages.newPermissions",
|
|
158
171
|
"too-long": "messages.tooLong",
|
|
159
172
|
"too-many": "messages.tooMany",
|
|
160
173
|
error: "messages.error",
|
|
@@ -176,23 +189,32 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
176
189
|
const signedOut = $<HTMLElement>("[data-view='signed-out']");
|
|
177
190
|
const signedIn = $<HTMLElement>("[data-view='signed-in']");
|
|
178
191
|
const composer = $<HTMLFormElement>("[data-composer]");
|
|
192
|
+
const permissionPanel = $<HTMLElement>("[data-permission]");
|
|
179
193
|
// A clean copy of the comment box, for reply boxes under comments.
|
|
180
194
|
const template = composer.cloneNode(true) as HTMLFormElement;
|
|
181
195
|
const loginForm = $<HTMLFormElement>("[data-login-form]");
|
|
182
196
|
const status = $<HTMLElement>("[data-status]");
|
|
183
197
|
let isSignedIn = false;
|
|
184
|
-
let me: { did?: string; handle?: string; displayName?: string; avatar?: string } = {};
|
|
198
|
+
let me: { did?: string; handle?: string; displayName?: string; avatar?: string; can?: string[] } = {};
|
|
199
|
+
const can = (permission: Permission) => !!me.can?.includes(permission);
|
|
185
200
|
// What the main box is doing: a comment on the post, or a quote post.
|
|
186
201
|
let mainMode: "comment" | "quote" = "comment";
|
|
187
202
|
|
|
188
203
|
const say = (message: string) => (status.textContent = message);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
204
|
+
// Sends the reader to their account host for one more permission,
|
|
205
|
+
// coming back to `hash` on this page (to finish what they asked).
|
|
206
|
+
const askPermission = (permission: Permission, hash = "#atmosphere-comments") =>
|
|
207
|
+
signIn(me.did!, { permissions: [permission], returnTo: `${location.pathname}${location.search}${hash}`, lang });
|
|
208
|
+
// Says what the permission is for before going (the account host may
|
|
209
|
+
// ask the reader to sign in again first, which is puzzling unexplained),
|
|
210
|
+
// with Continue to approve. Delete and Hide do the same in their panels.
|
|
211
|
+
const explainPermission = (permission: "post" | "react") => {
|
|
212
|
+
const go = Object.assign(make("button", "atmo__link-button", t("comment.continue")), { type: "button" });
|
|
213
|
+
go.addEventListener("click", () => void askPermission(permission));
|
|
214
|
+
status.replaceChildren(`${t(`permission.${permission}`)} ${t("permission.approve")} `, go);
|
|
215
|
+
status.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
216
|
+
go.focus({ preventScroll: true });
|
|
217
|
+
};
|
|
196
218
|
// Elements this script creates get the component's marker attribute, so
|
|
197
219
|
// Astro's scoped styles still reach them.
|
|
198
220
|
const scoped = <T extends HTMLElement>(element: T): T => {
|
|
@@ -215,6 +237,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
215
237
|
depth: number;
|
|
216
238
|
likeCount: number;
|
|
217
239
|
repostCount: number;
|
|
240
|
+
withheld?: true;
|
|
218
241
|
};
|
|
219
242
|
type Thread = { uri: string; url: string; likeCount: number; repostCount: number; likes: Person[]; reposts: Person[]; replies: Reply[] };
|
|
220
243
|
let thread: Thread | null = null;
|
|
@@ -254,7 +277,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
254
277
|
group.append(make("p", "atmo-reactions__count", t(kind === "like" ? "reactions.likes" : "reactions.reposts", { count })));
|
|
255
278
|
const faces = make("ul", "atmo-facepile");
|
|
256
279
|
for (const person of people) {
|
|
257
|
-
const link = Object.assign(make("a"), { href: person.url, title: nameOf(person), rel: "ugc nofollow noopener" });
|
|
280
|
+
const link = Object.assign(make("a"), { href: person.url, title: nameOf(person), rel: "ugc nofollow noopener", target: "_blank" });
|
|
258
281
|
link.setAttribute("aria-label", nameOf(person));
|
|
259
282
|
link.append(avatarOf(person, "atmo-facepile__avatar"));
|
|
260
283
|
const item = make("li");
|
|
@@ -271,18 +294,29 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
271
294
|
const replyElement = (reply: Reply) => {
|
|
272
295
|
const item = make("li", `atmo-comment${reply.byAuthor ? " atmo-comment--author" : ""}${reply.depth ? " atmo-comment--nested" : ""}`);
|
|
273
296
|
if (reply.depth) item.style.setProperty("--depth", String(Math.min(reply.depth, 3)));
|
|
297
|
+
// Its author asks not to be shown to logged-out people: a placeholder,
|
|
298
|
+
// keeping the replies under it in place. (No AT URI, so no actions.)
|
|
299
|
+
if (reply.withheld) {
|
|
300
|
+
item.classList.add("atmo-comment--withheld");
|
|
301
|
+
const article = make("article");
|
|
302
|
+
article.append(make("p", "atmo-comment__withheld", t("comment.withheld")));
|
|
303
|
+
item.append(article);
|
|
304
|
+
return item;
|
|
305
|
+
}
|
|
274
306
|
item.dataset.atmosphereUri = reply.uri;
|
|
307
|
+
item.dataset.atmosphereUrl = reply.url;
|
|
275
308
|
item.dataset.atmosphereAuthor = nameOf(reply.author);
|
|
276
309
|
item.dataset.atmosphereLikes = String(reply.likeCount);
|
|
277
310
|
item.dataset.atmosphereReposts = String(reply.repostCount);
|
|
278
311
|
const author = Object.assign(make("a", "atmo-comment__author", nameOf(reply.author)), {
|
|
279
312
|
href: reply.author.url,
|
|
280
313
|
rel: "ugc nofollow noopener",
|
|
314
|
+
target: "_blank",
|
|
281
315
|
});
|
|
282
316
|
const who = make("div", "atmo-comment__who");
|
|
283
317
|
who.append(author);
|
|
284
318
|
if (reply.byAuthor) who.append(make("span", "atmo-comment__badge", authorLabel));
|
|
285
|
-
const date = Object.assign(make("a", "atmo-comment__date"), { href: reply.url, rel: "ugc nofollow noopener" });
|
|
319
|
+
const date = Object.assign(make("a", "atmo-comment__date"), { href: reply.url, rel: "ugc nofollow noopener", target: "_blank" });
|
|
286
320
|
const time = Object.assign(make("time"), { dateTime: reply.createdAt, textContent: reply.createdAt ? dates.format(new Date(reply.createdAt)) : "" });
|
|
287
321
|
date.append(time, make("span", "", ` · ${t("comment.viewOnBluesky")}`));
|
|
288
322
|
const names = make("div");
|
|
@@ -293,7 +327,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
293
327
|
const paragraph = make("p");
|
|
294
328
|
for (const piece of reply.text) {
|
|
295
329
|
paragraph.append(
|
|
296
|
-
piece.href ? Object.assign(make("a", "", piece.text), { href: piece.href, rel: "ugc nofollow noopener" }) : piece.text,
|
|
330
|
+
piece.href ? Object.assign(make("a", "", piece.text), { href: piece.href, rel: "ugc nofollow noopener", target: "_blank" }) : piece.text,
|
|
297
331
|
);
|
|
298
332
|
}
|
|
299
333
|
body.append(paragraph);
|
|
@@ -383,6 +417,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
383
417
|
const openLogin = () => loginForm.querySelector("input")?.focus({ preventScroll: true });
|
|
384
418
|
|
|
385
419
|
enhanceHandleBox(loginForm, base);
|
|
420
|
+
handleSignInForm(loginForm, lang);
|
|
386
421
|
|
|
387
422
|
// An avatar picture, or the name's first letter if there isn't one.
|
|
388
423
|
const setAvatar = (slot: HTMLElement, name: string, src?: string) => {
|
|
@@ -393,15 +428,17 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
393
428
|
img.onload = () => slot.replaceChildren(img);
|
|
394
429
|
};
|
|
395
430
|
|
|
396
|
-
const handleError = (error: string, tell: (message: string) => void = say) => {
|
|
397
|
-
if (error === "signed-out"
|
|
431
|
+
const handleError = (error: string, tell: (message: string) => void = say, permission?: string) => {
|
|
432
|
+
if (error === "signed-out") void show(false);
|
|
433
|
+
// The page checks before it starts, so this is rare: ask now.
|
|
434
|
+
if (error === "needs-permission" && (permission === "post" || permission === "react")) return explainPermission(permission);
|
|
398
435
|
tell(message(error));
|
|
399
436
|
};
|
|
400
437
|
|
|
401
438
|
const postedMessage = (where: HTMLElement, data: Posted) =>
|
|
402
439
|
where.replaceChildren(
|
|
403
440
|
`${t(data.quote ? "posted.quote" : "posted.comment")} `,
|
|
404
|
-
Object.assign(document.createElement("a"), { href: data.url, textContent: t("posted.see") }),
|
|
441
|
+
Object.assign(document.createElement("a"), { href: data.url, textContent: t("posted.see"), target: "_blank", rel: "noopener noreferrer" }),
|
|
405
442
|
);
|
|
406
443
|
|
|
407
444
|
// The comment box (lib/client/composer.ts): posts as a reply to the
|
|
@@ -451,6 +488,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
451
488
|
main.textarea.focus();
|
|
452
489
|
});
|
|
453
490
|
$<HTMLButtonElement>("[data-quote]").addEventListener("click", () => {
|
|
491
|
+
if (!can("post")) return explainPermission("post");
|
|
454
492
|
setMainMode("quote");
|
|
455
493
|
main.textarea.focus();
|
|
456
494
|
});
|
|
@@ -535,7 +573,8 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
535
573
|
} catch (problem) {
|
|
536
574
|
button.setAttribute("aria-pressed", String(!on));
|
|
537
575
|
setCount(before);
|
|
538
|
-
|
|
576
|
+
const { error, permission } = (problem ?? {}) as { error?: string; permission?: string };
|
|
577
|
+
handleError(error ?? "error", say, permission);
|
|
539
578
|
}
|
|
540
579
|
});
|
|
541
580
|
return button;
|
|
@@ -557,6 +596,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
557
596
|
reply.setAttribute("aria-label", t("comment.replyTo", { name: author }));
|
|
558
597
|
reply.addEventListener("click", () => {
|
|
559
598
|
if (!isSignedIn) return signInFirst(t("signInFirst.reply"));
|
|
599
|
+
if (!can("post")) return explainPermission("post");
|
|
560
600
|
openReply(comment, author);
|
|
561
601
|
});
|
|
562
602
|
actions.append(reply, commentButton("like", comment), commentButton("repost", comment));
|
|
@@ -581,7 +621,8 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
581
621
|
this.dispatchEvent(new CustomEvent("atmosphere:deleted", { detail: { uri }, bubbles: true }));
|
|
582
622
|
return;
|
|
583
623
|
}
|
|
584
|
-
|
|
624
|
+
// The page checks first, so this is rare: ask now.
|
|
625
|
+
if (data.error === "needs-permission") return void askPermission("delete", `#atmo-delete=${encodeURIComponent(uri)}`);
|
|
585
626
|
panel.remove();
|
|
586
627
|
handleError(data.error ?? "error");
|
|
587
628
|
} catch {
|
|
@@ -590,58 +631,112 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
590
631
|
}
|
|
591
632
|
};
|
|
592
633
|
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
)
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
634
|
+
// Hide, for the announcement's own author: hides a comment here and on
|
|
635
|
+
// Bluesky. Showing it again is done in the Bluesky app.
|
|
636
|
+
const isOwner = () => !!me.did && !!post && authorOf(post) === me.did;
|
|
637
|
+
|
|
638
|
+
const hideComment = async (comment: HTMLElement, panel: HTMLElement) => {
|
|
639
|
+
const uri = comment.dataset.atmosphereUri!;
|
|
640
|
+
panel.replaceChildren(make("span", "atmo-comment-delete__text", t("comment.hiding")));
|
|
641
|
+
try {
|
|
642
|
+
const response = await send("hide", { post: uri, root: post });
|
|
643
|
+
const data = (await response.json()) as { error?: string };
|
|
644
|
+
if (response.ok) {
|
|
645
|
+
comment.remove();
|
|
646
|
+
say(t("comment.hidden"));
|
|
647
|
+
this.dispatchEvent(new CustomEvent("atmosphere:hidden", { detail: { uri }, bubbles: true }));
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (data.error === "needs-permission") return void askPermission("moderate", `#atmo-hide=${encodeURIComponent(uri)}`);
|
|
651
|
+
panel.remove();
|
|
652
|
+
handleError(data.error ?? "error");
|
|
653
|
+
} catch {
|
|
654
|
+
panel.remove();
|
|
655
|
+
say(message("error"));
|
|
656
|
+
}
|
|
615
657
|
};
|
|
616
658
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
659
|
+
// Delete and Hide each need one more permission than signing in gives.
|
|
660
|
+
// Readers who haven't given it yet are asked for it first; once they
|
|
661
|
+
// have (straight away, or back from approving), they confirm.
|
|
662
|
+
const STEPS = {
|
|
663
|
+
delete: {
|
|
664
|
+
permission: "delete",
|
|
665
|
+
action: "comment.delete",
|
|
666
|
+
label: "comment.deleteLabel",
|
|
667
|
+
confirm: "comment.deleteConfirm",
|
|
668
|
+
needs: "comment.deletePermission",
|
|
669
|
+
// "{{link}}" in the wording: the comment on Bluesky, to delete it there instead.
|
|
670
|
+
elsewhere: "comment.deleteElsewhere",
|
|
671
|
+
run: deleteComment,
|
|
672
|
+
},
|
|
673
|
+
hide: {
|
|
674
|
+
permission: "moderate",
|
|
675
|
+
action: "comment.hide",
|
|
676
|
+
label: "comment.hideLabel",
|
|
677
|
+
confirm: "comment.hideConfirm",
|
|
678
|
+
needs: "comment.hidePermission",
|
|
679
|
+
elsewhere: "",
|
|
680
|
+
run: hideComment,
|
|
681
|
+
},
|
|
682
|
+
} as const;
|
|
683
|
+
|
|
684
|
+
const openPanel = (comment: HTMLElement, kind: keyof typeof STEPS) => {
|
|
685
|
+
const actions = comment.querySelector<HTMLElement>(":scope .atmo-comment-actions");
|
|
686
|
+
if (!actions || actions.nextElementSibling?.matches(".atmo-comment-delete")) return;
|
|
687
|
+
const step = STEPS[kind];
|
|
688
|
+
const panel = make("div", "atmo-comment-delete");
|
|
689
|
+
panel.setAttribute("role", "group");
|
|
690
|
+
panel.setAttribute("aria-label", t(step.label));
|
|
691
|
+
const button = (text: string, danger = false) =>
|
|
692
|
+
Object.assign(make("button", `atmo-comment-action${danger ? " atmo-comment-action--danger" : ""}`, text), { type: "button" });
|
|
693
|
+
const no = button(t("comment.cancel"));
|
|
694
|
+
no.addEventListener("click", () => {
|
|
695
|
+
panel.remove();
|
|
696
|
+
actions.querySelector<HTMLElement>(`.atmo-comment-action--${kind}`)?.focus();
|
|
697
|
+
});
|
|
698
|
+
if (can(step.permission)) {
|
|
699
|
+
const yes = button(t(step.action), true);
|
|
700
|
+
yes.addEventListener("click", () => step.run(comment, panel));
|
|
701
|
+
panel.append(make("span", "atmo-comment-delete__text", t(step.confirm)), yes, no);
|
|
634
702
|
actions.after(panel);
|
|
635
703
|
no.focus();
|
|
636
|
-
}
|
|
704
|
+
} else {
|
|
705
|
+
const go = button(t("comment.continue"), true);
|
|
706
|
+
go.addEventListener("click", () =>
|
|
707
|
+
void askPermission(step.permission, `#atmo-${kind}=${encodeURIComponent(comment.dataset.atmosphereUri!)}`),
|
|
708
|
+
);
|
|
709
|
+
const text = make("span", "atmo-comment-delete__text");
|
|
710
|
+
const [before, after = ""] = t(step.needs, { link: "\u0000" }).split("\u0000");
|
|
711
|
+
text.append(before);
|
|
712
|
+
if (step.elsewhere) {
|
|
713
|
+
const url = comment.dataset.atmosphereUrl;
|
|
714
|
+
text.append(
|
|
715
|
+
url
|
|
716
|
+
? Object.assign(make("a", "", t(step.elsewhere)), { href: url, target: "_blank", rel: "noopener noreferrer" })
|
|
717
|
+
: t(step.elsewhere),
|
|
718
|
+
after,
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
panel.append(text, go, no);
|
|
722
|
+
actions.after(panel);
|
|
723
|
+
go.focus();
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
const panelButton = (comment: HTMLElement, kind: keyof typeof STEPS) => {
|
|
728
|
+
const button = Object.assign(make("button", `atmo-comment-action atmo-comment-action--${kind}`, t(STEPS[kind].action)), { type: "button" });
|
|
729
|
+
button.setAttribute("aria-label", t(STEPS[kind].label));
|
|
730
|
+
button.addEventListener("click", () => openPanel(comment, kind));
|
|
637
731
|
return button;
|
|
638
732
|
};
|
|
639
733
|
|
|
640
734
|
const addDeleteButtons = () => {
|
|
641
735
|
document.querySelectorAll<HTMLElement>("[data-atmosphere-uri]").forEach((comment) => {
|
|
642
736
|
const actions = comment.querySelector<HTMLElement>(":scope .atmo-comment-actions");
|
|
643
|
-
if (!actions || actions.querySelector(".atmo-comment-action--delete")) return;
|
|
644
|
-
if (isSignedIn && isMine(comment)) actions.append(
|
|
737
|
+
if (!actions || actions.querySelector(".atmo-comment-action--delete, .atmo-comment-action--hide")) return;
|
|
738
|
+
if (isSignedIn && isMine(comment)) actions.append(panelButton(comment, "delete"));
|
|
739
|
+
else if (isSignedIn && isOwner()) actions.append(panelButton(comment, "hide"));
|
|
645
740
|
});
|
|
646
741
|
};
|
|
647
742
|
|
|
@@ -651,14 +746,13 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
651
746
|
const comments = [...document.querySelectorAll<HTMLElement>("[data-atmosphere-uri]")];
|
|
652
747
|
const uris = [post, ...comments.map((comment) => comment.dataset.atmosphereUri!)];
|
|
653
748
|
try {
|
|
654
|
-
const
|
|
655
|
-
const
|
|
656
|
-
const state = (await response.json()) as {
|
|
749
|
+
const response = await state(uris);
|
|
750
|
+
const found = (await response.json()) as {
|
|
657
751
|
error?: string;
|
|
658
752
|
posts?: Record<string, { liked: boolean; reposted: boolean }>;
|
|
659
753
|
};
|
|
660
|
-
if (!response.ok) return handleError(
|
|
661
|
-
const posts =
|
|
754
|
+
if (!response.ok) return handleError(found.error ?? "error");
|
|
755
|
+
const posts = found.posts ?? {};
|
|
662
756
|
known = { ...known, ...posts };
|
|
663
757
|
setPressed("like", !!posts[post]?.liked);
|
|
664
758
|
setPressed("repost", !!posts[post]?.reposted);
|
|
@@ -687,23 +781,28 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
687
781
|
signedOut.hidden = signed;
|
|
688
782
|
signedIn.hidden = !signed;
|
|
689
783
|
if (!signed) {
|
|
690
|
-
document.querySelectorAll(".atmo-comment-action--delete, .atmo-comment-delete").forEach((part) => part.remove());
|
|
784
|
+
document.querySelectorAll(".atmo-comment-action--delete, .atmo-comment-action--hide, .atmo-comment-delete").forEach((part) => part.remove());
|
|
691
785
|
return closeReplies();
|
|
692
786
|
}
|
|
787
|
+
// A sign-in without posting (from an older version, which asked for
|
|
788
|
+
// less): one more permission, asked for before they type.
|
|
789
|
+
composer.hidden = !can("post");
|
|
790
|
+
permissionPanel.hidden = can("post");
|
|
791
|
+
// Which account, as they may have more than one.
|
|
792
|
+
$<HTMLElement>("[data-permission-text]").textContent = t("composer.needsPermission", { handle: handle ? `@${handle}` : "" });
|
|
693
793
|
addCommentActions();
|
|
794
|
+
addDeleteButtons();
|
|
694
795
|
await loadState();
|
|
695
|
-
// Back from approving the delete permission:
|
|
696
|
-
|
|
796
|
+
// Back from approving the delete or hide permission: ask them to
|
|
797
|
+
// confirm the delete or hide they started.
|
|
798
|
+
const pending = location.hash.match(/^#atmo-(delete|hide)=(.+)$/);
|
|
697
799
|
if (pending) {
|
|
698
800
|
history.replaceState(history.state, "", `${location.pathname}${location.search}#atmosphere-comments`);
|
|
699
|
-
const
|
|
700
|
-
const comment = document.querySelector<HTMLElement>(`[data-atmosphere-uri="${CSS.escape(
|
|
701
|
-
|
|
702
|
-
if (comment && actions && isMine(comment)) {
|
|
703
|
-
const panel = make("div", "atmo-comment-delete");
|
|
704
|
-
actions.after(panel);
|
|
801
|
+
const kind = pending[1] as keyof typeof STEPS;
|
|
802
|
+
const comment = document.querySelector<HTMLElement>(`[data-atmosphere-uri="${CSS.escape(decodeURIComponent(pending[2]))}"]`);
|
|
803
|
+
if (comment && (kind === "delete" ? isMine(comment) : isOwner())) {
|
|
705
804
|
comment.scrollIntoView({ block: "center" });
|
|
706
|
-
|
|
805
|
+
openPanel(comment, kind);
|
|
707
806
|
}
|
|
708
807
|
}
|
|
709
808
|
};
|
|
@@ -718,11 +817,10 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
718
817
|
}
|
|
719
818
|
|
|
720
819
|
this.classList.add("is-loading");
|
|
721
|
-
|
|
722
|
-
.then((
|
|
723
|
-
.then((found: { signedIn: boolean; did?: string; handle?: string; displayName?: string; avatar?: string }) => {
|
|
820
|
+
whoIsSignedIn()
|
|
821
|
+
.then((found) => {
|
|
724
822
|
me = found;
|
|
725
|
-
return
|
|
823
|
+
return found.signedIn ? show(true, found.handle, found.displayName, found.avatar) : show(false);
|
|
726
824
|
})
|
|
727
825
|
.catch(() => show(false))
|
|
728
826
|
.finally(() => {
|
|
@@ -749,7 +847,8 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
749
847
|
const response = await send("react", { post, kind, on });
|
|
750
848
|
if (!response.ok) {
|
|
751
849
|
setPressed(kind, !on);
|
|
752
|
-
|
|
850
|
+
const { error, permission } = (await response.json()) as { error?: string; permission?: string };
|
|
851
|
+
return handleError(error ?? "error", say, permission);
|
|
753
852
|
}
|
|
754
853
|
say(t(on ? (kind === "like" ? "status.liked" : "status.reposted") : kind === "like" ? "status.likeRemoved" : "status.repostRemoved"));
|
|
755
854
|
showReaction(kind, on);
|
|
@@ -766,12 +865,16 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
766
865
|
});
|
|
767
866
|
}
|
|
768
867
|
|
|
769
|
-
|
|
770
|
-
await
|
|
868
|
+
const signOutHere = async () => {
|
|
869
|
+
await signOut();
|
|
870
|
+
me = {};
|
|
771
871
|
void show(false);
|
|
772
872
|
setMainMode("comment");
|
|
773
873
|
say(t("status.signedOut"));
|
|
774
|
-
}
|
|
874
|
+
};
|
|
875
|
+
$<HTMLButtonElement>("[data-sign-out]").addEventListener("click", signOutHere);
|
|
876
|
+
$<HTMLButtonElement>("[data-permission-sign-out]").addEventListener("click", signOutHere);
|
|
877
|
+
$<HTMLButtonElement>("[data-permission-continue]").addEventListener("click", () => void askPermission("post"));
|
|
775
878
|
}
|
|
776
879
|
}
|
|
777
880
|
|