astro-atmosphere-comments 0.4.2 → 0.6.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 +40 -0
- package/README.md +36 -27
- package/components/AtmosphereComments.astro +378 -117
- package/components/AtmosphereReactions.astro +145 -98
- package/components/Composer.astro +5 -2
- package/components/SignInForm.astro +65 -7
- package/components/atmosphere.css +267 -2
- package/index.d.ts +11 -0
- package/index.mjs +53 -32
- package/lib/client/account.ts +661 -0
- package/lib/client/composer.ts +23 -10
- package/lib/client/subscribe.ts +106 -0
- package/lib/http.ts +28 -0
- package/lib/permissions.ts +132 -0
- package/lib/public-settings.ts +20 -0
- package/lib/quoted.ts +4 -0
- package/lib/settings.ts +2 -0
- package/lib/thread.ts +36 -6
- package/lib/uris.ts +39 -0
- package/locales/de.json +20 -4
- package/locales/en.json +48 -13
- 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 +2 -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
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Under a post: Like, Repost and Quote buttons and a box to write a comment (a
|
|
3
|
+
// reply to the post's announcement, or to one of its comments). Signed out,
|
|
4
|
+
// they open an overlay to sign in with an Atmosphere account (Bluesky or any
|
|
5
|
+
// other), and what the reader pressed is finished once they're back.
|
|
5
6
|
//
|
|
6
7
|
// Props:
|
|
7
8
|
// post the announcement's AT URI (at://did/app.bsky.feed.post/rkey).
|
|
8
|
-
// label the sign-in
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// sign-in form (default true; false hides it).
|
|
9
|
+
// label the sign-in overlay's heading when commenting (default "Sign in to leave a comment").
|
|
10
|
+
// blueskyLink "Rather not sign in here? Reply on Bluesky instead →" in the
|
|
11
|
+
// sign-in overlay (default true; false hides it).
|
|
12
12
|
// credit a small "Powered by Atmosphere Comments for Astro" link under the
|
|
13
13
|
// comments (default true; false hides it).
|
|
14
14
|
//
|
|
@@ -31,12 +31,17 @@
|
|
|
31
31
|
// text, author, createdAt, quote, parent }), and after a like or repost (or
|
|
32
32
|
// undoing one) `atmosphere:reacted` (detail: { kind, on, author }), so a site
|
|
33
33
|
// can show it straight away.
|
|
34
|
+
//
|
|
35
|
+
// With the integration's `standardSite` option, Like on the post also
|
|
36
|
+
// recommends the page's Standard.site record (<link rel="site.standard.document">),
|
|
37
|
+
// and un-liking removes the recommend. Likes on comments stay Bluesky likes.
|
|
34
38
|
import settings from "../lib/settings";
|
|
35
39
|
import SignInForm from "./SignInForm.astro";
|
|
36
40
|
import Composer from "./Composer.astro";
|
|
37
41
|
import "./atmosphere.css";
|
|
38
42
|
import { messagesFor, pickLocale, translate, translateHtml } from "../lib/i18n";
|
|
39
43
|
import { escapeHtml } from "../lib/translate";
|
|
44
|
+
import logo from "../assets/union.svg?url";
|
|
40
45
|
|
|
41
46
|
interface Props {
|
|
42
47
|
/**
|
|
@@ -45,13 +50,13 @@ interface Props {
|
|
|
45
50
|
* is used to find it.
|
|
46
51
|
*/
|
|
47
52
|
post?: string;
|
|
48
|
-
/** The sign-in
|
|
53
|
+
/** The sign-in overlay's heading when commenting. */
|
|
49
54
|
label?: string;
|
|
50
55
|
/** The badge on comments by the site's own accounts. */
|
|
51
56
|
authorLabel?: string;
|
|
52
57
|
/** A small "Powered by Atmosphere Comments for Astro" link under the comments. Default true. */
|
|
53
58
|
credit?: boolean;
|
|
54
|
-
/** "Rather not sign in here? Reply on Bluesky instead →"
|
|
59
|
+
/** "Rather not sign in here? Reply on Bluesky instead →" in the sign-in overlay. Default true. */
|
|
55
60
|
blueskyLink?: boolean;
|
|
56
61
|
/** The language of the wording, e.g. "fr". Overrides the integration's `locale`. */
|
|
57
62
|
locale?: string;
|
|
@@ -90,22 +95,8 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
90
95
|
<atmosphere-comments class="atmo" id="atmosphere-comments" data-post={post} data-base={base} data-locale={lang} data-messages={JSON.stringify(messages)}>
|
|
91
96
|
<div class="atmo-reactions" data-reactions data-pagefind-ignore hidden></div>
|
|
92
97
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
{blueskyLink && (
|
|
96
|
-
<p
|
|
97
|
-
class="atmo__elsewhere"
|
|
98
|
-
data-elsewhere
|
|
99
|
-
hidden={!blueskyUrl}
|
|
100
|
-
set:html={th("elsewhere.text", {
|
|
101
|
-
// u-syndication: microformats readers learn this is the post's copy on Bluesky.
|
|
102
|
-
link: link(blueskyUrl || undefined, t("elsewhere.link"), ' class="atmo__elsewhere-link u-syndication" data-elsewhere-link'),
|
|
103
|
-
})}
|
|
104
|
-
/>
|
|
105
|
-
)}
|
|
106
|
-
</div>
|
|
107
|
-
|
|
108
|
-
<div class="atmo__signed-in" data-view="signed-in" data-pagefind-ignore hidden>
|
|
98
|
+
{/* For everyone: signed out, these open the sign-in overlay below, and what was pressed is finished on coming back. */}
|
|
99
|
+
<div class="atmo__signed-in" data-view="main" data-pagefind-ignore hidden>
|
|
109
100
|
<div class="atmo__actions" role="group" aria-label={t("actions.label")}>
|
|
110
101
|
<button type="button" class="atmo__action" data-react="like" aria-pressed="false">
|
|
111
102
|
<svg viewBox="0 0 24 24" aria-hidden="true"><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" /></svg>
|
|
@@ -119,11 +110,72 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
119
110
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 18h5v-5H6.5A4.5 4.5 0 0 1 11 8.5V7a6 6 0 0 0-6 6Zm9 0h5v-5h-3.5A4.5 4.5 0 0 1 20 8.5V7a6 6 0 0 0-6 6Z" /></svg>
|
|
120
111
|
<span>{t("actions.quote")}</span>
|
|
121
112
|
</button>
|
|
113
|
+
{/* With the `standardSite` option. */}
|
|
114
|
+
{settings.standardSite && (
|
|
115
|
+
<div class="atmo-subscribe" data-subscribe-panel data-pagefind-ignore hidden>
|
|
116
|
+
<button type="button" class="atmo__action atmo-subscribe__button" data-atmosphere-subscribe hidden>
|
|
117
|
+
<span class="atmo-subscribe__logo" style={`--atmo-logo: url("${logo}")`} aria-hidden="true"></span>
|
|
118
|
+
<span data-atmosphere-subscribe-label>{t("subscribe.subscribe")}</span>
|
|
119
|
+
</button>
|
|
120
|
+
<button type="button" class="atmo__action atmo-subscribe__more" aria-label={t("subscribe.more")} title={t("subscribe.more")} aria-expanded="false" data-subscribe-more>
|
|
121
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
|
122
|
+
</button>
|
|
123
|
+
<div class="atmo-subscribe__menu" data-subscribe-menu hidden>
|
|
124
|
+
<p
|
|
125
|
+
class="atmo-subscribe__note"
|
|
126
|
+
set:html={th("subscribe.panelText", {
|
|
127
|
+
leaflet: link("https://leaflet.pub", "Leaflet"),
|
|
128
|
+
standardReader: link("https://standard-reader.app", "Standard Reader"),
|
|
129
|
+
})}
|
|
130
|
+
/>
|
|
131
|
+
<button type="button" data-subscribe-copy={settings.site} data-copied={t("subscribe.copied")}><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1" /></svg><span data-copy-label>{t("subscribe.copyLink")}</span></button>
|
|
132
|
+
{settings.standardSite.feed && <a href={settings.standardSite.feed}><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 11a8 8 0 0 1 8 8M5 5a14 14 0 0 1 14 14" /><circle cx="6" cy="18" r="1" /></svg><span>{t("subscribe.feed")}</span></a>}
|
|
133
|
+
<a href={blueskyUrl || undefined} hidden={!blueskyUrl} data-subscribe-bluesky target="_blank" rel="noopener"><svg class="atmo-subscribe__filled" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z" /></svg><span>{t("subscribe.viewBluesky")}</span></a>
|
|
134
|
+
<a href={`https://standard-reader.app/p/${settings.standardSite.publication.split("/")[2]}/${settings.standardSite.publication.split("/")[4]}`} target="_blank" rel="noopener"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-8 8M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4" /></svg><span>{t("subscribe.standardReader")}</span></a>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
{/* A sign-in from before posting was asked for: one more permission. */}
|
|
141
|
+
<div class="atmo__permission" data-permission hidden>
|
|
142
|
+
<p data-permission-text>{t("composer.needsPermission")}</p>
|
|
143
|
+
<p class="atmo__hint">{t("permission.approve")}</p>
|
|
144
|
+
<div class="atmo__row">
|
|
145
|
+
<button type="button" class="atmo__button" data-permission-continue>{t("comment.continue")}</button>
|
|
146
|
+
<button type="button" class="atmo__link-button" data-permission-sign-out>{t("composer.signOut")}</button>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
{/* Signed out: looks like the comment box, and opens the sign-in overlay. */}
|
|
151
|
+
<div class="atmo__prompt-box" data-comment-prompt hidden>
|
|
152
|
+
<button type="button" class="atmo__prompt" data-comment-prompt-open>{t("comment.prompt")}</button>
|
|
153
|
+
<div class="atmo__row atmo__row--end">
|
|
154
|
+
<button type="button" class="atmo__link-button" data-comment-prompt-open>{t("comment.signIn")}</button>
|
|
155
|
+
<button type="button" class="atmo__button" disabled>{t("composer.postComment")}</button>
|
|
156
|
+
</div>
|
|
122
157
|
</div>
|
|
123
158
|
|
|
124
159
|
<Composer id="atmo" messages={messages} lang={lang} signOut />
|
|
125
160
|
</div>
|
|
126
161
|
|
|
162
|
+
<dialog class="atmo atmo-signin" aria-label={t("signIn.heading")} data-signin-dialog data-pagefind-ignore>
|
|
163
|
+
<button type="button" class="atmo-signin__close" aria-label={t("dialog.close")} data-close>×</button>
|
|
164
|
+
<SignInForm id="atmo" returnTo={returnTo} messages={messages} lang={lang} />
|
|
165
|
+
{blueskyLink && (
|
|
166
|
+
<p
|
|
167
|
+
class="atmo__elsewhere"
|
|
168
|
+
data-elsewhere
|
|
169
|
+
hidden={!blueskyUrl}
|
|
170
|
+
set:html={th("elsewhere.text", {
|
|
171
|
+
// u-syndication: microformats readers learn this is the post's copy on Bluesky.
|
|
172
|
+
link: link(blueskyUrl || undefined, t("elsewhere.link"), ' class="atmo__elsewhere-link u-syndication" data-elsewhere-link'),
|
|
173
|
+
})}
|
|
174
|
+
/>
|
|
175
|
+
)}
|
|
176
|
+
<p class="atmo-signin__status" role="status" data-dialog-status></p>
|
|
177
|
+
</dialog>
|
|
178
|
+
|
|
127
179
|
<p class="atmo__status" data-status role="status" data-pagefind-ignore></p>
|
|
128
180
|
|
|
129
181
|
{/* Anything the site wants between the comment box and the comments. */}
|
|
@@ -143,6 +195,10 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
143
195
|
import { translate } from "../lib/translate";
|
|
144
196
|
import { enhanceHandleBox } from "../lib/client/handle-suggestions";
|
|
145
197
|
import { MAX, quotePreview, setUpComposer, type ComposerBehaviour, type Posted } from "../lib/client/composer";
|
|
198
|
+
import { handleSignInForm, me as whoIsSignedIn, send, signIn, signOut, state } from "../lib/client/account";
|
|
199
|
+
import { addSubscribeButtons, finishPendingSubscribe, forgetSubscribed, loadSubscribed } from "../lib/client/subscribe";
|
|
200
|
+
import type { Permission } from "../lib/permissions";
|
|
201
|
+
import { authorOf } from "../lib/uris";
|
|
146
202
|
|
|
147
203
|
|
|
148
204
|
// The error and status codes the routes and the sign-in round trip send,
|
|
@@ -155,7 +211,6 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
155
211
|
"signed-out": "messages.signedOut",
|
|
156
212
|
"needs-permission": "messages.needsPermission",
|
|
157
213
|
"not-allowed": "messages.notAllowed",
|
|
158
|
-
"new-permissions": "messages.newPermissions",
|
|
159
214
|
"too-long": "messages.tooLong",
|
|
160
215
|
"too-many": "messages.tooMany",
|
|
161
216
|
error: "messages.error",
|
|
@@ -174,26 +229,34 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
174
229
|
// Standard.site record is turned into one when the thread loads.
|
|
175
230
|
let post = this.dataset.post?.startsWith("at://") ? this.dataset.post : "";
|
|
176
231
|
const $ = <T extends Element>(selector: string) => this.querySelector<T>(selector)!;
|
|
177
|
-
const
|
|
178
|
-
const signedIn = $<HTMLElement>("[data-view='signed-in']");
|
|
232
|
+
const commentPrompt = $<HTMLElement>("[data-comment-prompt]");
|
|
179
233
|
const composer = $<HTMLFormElement>("[data-composer]");
|
|
234
|
+
const permissionPanel = $<HTMLElement>("[data-permission]");
|
|
180
235
|
// A clean copy of the comment box, for reply boxes under comments.
|
|
181
236
|
const template = composer.cloneNode(true) as HTMLFormElement;
|
|
182
237
|
const loginForm = $<HTMLFormElement>("[data-login-form]");
|
|
183
238
|
const status = $<HTMLElement>("[data-status]");
|
|
184
239
|
let isSignedIn = false;
|
|
185
|
-
let me: { did?: string; handle?: string; displayName?: string; avatar?: string } = {};
|
|
240
|
+
let me: { did?: string; handle?: string; displayName?: string; avatar?: string; can?: string[] } = {};
|
|
241
|
+
const can = (permission: Permission) => !!me.can?.includes(permission);
|
|
186
242
|
// What the main box is doing: a comment on the post, or a quote post.
|
|
187
243
|
let mainMode: "comment" | "quote" = "comment";
|
|
188
244
|
|
|
189
245
|
const say = (message: string) => (status.textContent = message);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
246
|
+
// Sends the reader to their account host for one more permission,
|
|
247
|
+
// coming back to `hash` on this page (to finish what they asked).
|
|
248
|
+
const askPermission = (permission: Permission, hash = "#atmosphere-comments") =>
|
|
249
|
+
signIn(me.did!, { permissions: [permission], returnTo: `${location.pathname}${location.search}${hash}`, lang });
|
|
250
|
+
// Says what the permission is for before going (the account host may
|
|
251
|
+
// ask the reader to sign in again first, which is puzzling unexplained),
|
|
252
|
+
// with Continue to approve. Delete and Hide do the same in their panels.
|
|
253
|
+
const explainPermission = (permission: "post" | "react") => {
|
|
254
|
+
const go = Object.assign(make("button", "atmo__link-button", t("comment.continue")), { type: "button" });
|
|
255
|
+
go.addEventListener("click", () => void askPermission(permission));
|
|
256
|
+
status.replaceChildren(`${t(`permission.${permission}`)} ${t("permission.approve")} `, go);
|
|
257
|
+
status.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
258
|
+
go.focus({ preventScroll: true });
|
|
259
|
+
};
|
|
197
260
|
// Elements this script creates get the component's marker attribute, so
|
|
198
261
|
// Astro's scoped styles still reach them.
|
|
199
262
|
const scoped = <T extends HTMLElement>(element: T): T => {
|
|
@@ -216,6 +279,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
216
279
|
depth: number;
|
|
217
280
|
likeCount: number;
|
|
218
281
|
repostCount: number;
|
|
282
|
+
withheld?: true;
|
|
219
283
|
};
|
|
220
284
|
type Thread = { uri: string; url: string; likeCount: number; repostCount: number; likes: Person[]; reposts: Person[]; replies: Reply[] };
|
|
221
285
|
let thread: Thread | null = null;
|
|
@@ -272,7 +336,17 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
272
336
|
const replyElement = (reply: Reply) => {
|
|
273
337
|
const item = make("li", `atmo-comment${reply.byAuthor ? " atmo-comment--author" : ""}${reply.depth ? " atmo-comment--nested" : ""}`);
|
|
274
338
|
if (reply.depth) item.style.setProperty("--depth", String(Math.min(reply.depth, 3)));
|
|
339
|
+
// Its author asks not to be shown to logged-out people: a placeholder,
|
|
340
|
+
// keeping the replies under it in place. (No AT URI, so no actions.)
|
|
341
|
+
if (reply.withheld) {
|
|
342
|
+
item.classList.add("atmo-comment--withheld");
|
|
343
|
+
const article = make("article");
|
|
344
|
+
article.append(make("p", "atmo-comment__withheld", t("comment.withheld")));
|
|
345
|
+
item.append(article);
|
|
346
|
+
return item;
|
|
347
|
+
}
|
|
275
348
|
item.dataset.atmosphereUri = reply.uri;
|
|
349
|
+
item.dataset.atmosphereUrl = reply.url;
|
|
276
350
|
item.dataset.atmosphereAuthor = nameOf(reply.author);
|
|
277
351
|
item.dataset.atmosphereLikes = String(reply.likeCount);
|
|
278
352
|
item.dataset.atmosphereReposts = String(reply.repostCount);
|
|
@@ -310,7 +384,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
310
384
|
// Nothing to respond to: hide the box and likes, but keep whatever the
|
|
311
385
|
// site put in the slots.
|
|
312
386
|
const deactivate = () => {
|
|
313
|
-
for (const part of this.querySelectorAll<HTMLElement>("[data-view], [data-reactions], [data-replies], [data-credit]")) {
|
|
387
|
+
for (const part of this.querySelectorAll<HTMLElement>("[data-view], [data-reactions], [data-replies], [data-credit], [data-subscribe-panel]")) {
|
|
314
388
|
part.hidden = true;
|
|
315
389
|
}
|
|
316
390
|
this.classList.add("is-inactive");
|
|
@@ -332,6 +406,11 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
332
406
|
}
|
|
333
407
|
thread = data.thread;
|
|
334
408
|
post = thread.uri;
|
|
409
|
+
const viewOnBluesky = this.querySelector<HTMLAnchorElement>("[data-subscribe-bluesky]");
|
|
410
|
+
if (viewOnBluesky && thread.url) {
|
|
411
|
+
viewOnBluesky.href = thread.url;
|
|
412
|
+
viewOnBluesky.hidden = false;
|
|
413
|
+
}
|
|
335
414
|
const elsewhere = this.querySelector<HTMLElement>("[data-elsewhere]");
|
|
336
415
|
if (elsewhere && thread.url) {
|
|
337
416
|
elsewhere.querySelector("a")!.href = thread.url;
|
|
@@ -382,9 +461,39 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
382
461
|
renderReactions();
|
|
383
462
|
};
|
|
384
463
|
|
|
385
|
-
|
|
464
|
+
// The sign-in overlay: for whatever a signed-out reader pressed. They come
|
|
465
|
+
// back to `hash` on this page, and it's finished then (resumePending).
|
|
466
|
+
const signInDialog = $<HTMLDialogElement>("[data-signin-dialog]");
|
|
467
|
+
const dialogHeading = loginForm.querySelector("label")!;
|
|
468
|
+
const dialogStatus = $<HTMLElement>("[data-dialog-status]");
|
|
469
|
+
const openSignIn = (heading: string, hash: string, problem = "") => {
|
|
470
|
+
dialogHeading.textContent = heading;
|
|
471
|
+
signInDialog.setAttribute("aria-label", heading);
|
|
472
|
+
loginForm.querySelector<HTMLInputElement>("[data-return-to]")!.value = `${location.pathname}${location.search}${hash}`;
|
|
473
|
+
dialogStatus.textContent = problem;
|
|
474
|
+
if (!signInDialog.open) signInDialog.showModal();
|
|
475
|
+
loginForm.querySelector<HTMLInputElement>('input[name="handle"]')!.focus();
|
|
476
|
+
};
|
|
477
|
+
signInDialog.querySelector("[data-close]")!.addEventListener("click", () => signInDialog.close());
|
|
478
|
+
// A click on the dimmed page around the overlay closes it.
|
|
479
|
+
signInDialog.addEventListener("click", (event) => {
|
|
480
|
+
if (event.target === signInDialog) signInDialog.close();
|
|
481
|
+
});
|
|
482
|
+
const SIGN_IN_HEADINGS: Record<string, string> = {
|
|
483
|
+
like: "signInFirst.likePost",
|
|
484
|
+
repost: "signInFirst.repostPost",
|
|
485
|
+
quote: "signInFirst.quotePost",
|
|
486
|
+
comment: "signIn.heading",
|
|
487
|
+
reply: "signInFirst.reply",
|
|
488
|
+
"comment-like": "signInFirst.like",
|
|
489
|
+
"comment-repost": "signInFirst.repost",
|
|
490
|
+
};
|
|
491
|
+
const askToSignIn = (kind: string, uri?: string, problem?: string) =>
|
|
492
|
+
openSignIn(t(SIGN_IN_HEADINGS[kind]), `#atmo-do=${kind}${uri ? `:${encodeURIComponent(uri)}` : ""}`, problem);
|
|
493
|
+
commentPrompt.querySelectorAll("[data-comment-prompt-open]").forEach((opener) => opener.addEventListener("click", () => askToSignIn("comment")));
|
|
386
494
|
|
|
387
495
|
enhanceHandleBox(loginForm, base);
|
|
496
|
+
handleSignInForm(loginForm, lang);
|
|
388
497
|
|
|
389
498
|
// An avatar picture, or the name's first letter if there isn't one.
|
|
390
499
|
const setAvatar = (slot: HTMLElement, name: string, src?: string) => {
|
|
@@ -395,8 +504,10 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
395
504
|
img.onload = () => slot.replaceChildren(img);
|
|
396
505
|
};
|
|
397
506
|
|
|
398
|
-
const handleError = (error: string, tell: (message: string) => void = say) => {
|
|
399
|
-
if (error === "signed-out"
|
|
507
|
+
const handleError = (error: string, tell: (message: string) => void = say, permission?: string) => {
|
|
508
|
+
if (error === "signed-out") void show(false);
|
|
509
|
+
// The page checks before it starts, so this is rare: ask now.
|
|
510
|
+
if (error === "needs-permission" && (permission === "post" || permission === "react")) return explainPermission(permission);
|
|
400
511
|
tell(message(error));
|
|
401
512
|
};
|
|
402
513
|
|
|
@@ -453,6 +564,8 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
453
564
|
main.textarea.focus();
|
|
454
565
|
});
|
|
455
566
|
$<HTMLButtonElement>("[data-quote]").addEventListener("click", () => {
|
|
567
|
+
if (!isSignedIn) return askToSignIn("quote");
|
|
568
|
+
if (!can("post")) return explainPermission("post");
|
|
456
569
|
setMainMode("quote");
|
|
457
570
|
main.textarea.focus();
|
|
458
571
|
});
|
|
@@ -502,11 +615,49 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
502
615
|
// and Repost with their counts, as in the Bluesky app.
|
|
503
616
|
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" />';
|
|
504
617
|
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" />';
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
618
|
+
|
|
619
|
+
// Subscribe: not signed in, the overlay, coming back to subscribe.
|
|
620
|
+
const subscribePanel = this.querySelector<HTMLElement>("[data-subscribe-panel]");
|
|
621
|
+
const askToSubscribe = (problem?: string) => openSignIn(t("subscribe.signIn"), "#atmo-subscribe", problem);
|
|
622
|
+
if (subscribePanel) addSubscribeButtons([...subscribePanel.querySelectorAll<HTMLButtonElement>("[data-atmosphere-subscribe]")], t, askToSubscribe);
|
|
623
|
+
|
|
624
|
+
// The ⌄ menu beside Subscribe: more ways to follow the site.
|
|
625
|
+
const moreButton = subscribePanel?.querySelector<HTMLButtonElement>("[data-subscribe-more]");
|
|
626
|
+
const moreMenu = subscribePanel?.querySelector<HTMLElement>("[data-subscribe-menu]");
|
|
627
|
+
if (subscribePanel && moreButton && moreMenu) {
|
|
628
|
+
const setMenu = (open: boolean) => {
|
|
629
|
+
moreMenu.hidden = !open;
|
|
630
|
+
moreButton.setAttribute("aria-expanded", String(open));
|
|
631
|
+
};
|
|
632
|
+
moreButton.addEventListener("click", () => {
|
|
633
|
+
setMenu(moreMenu.hidden);
|
|
634
|
+
if (!moreMenu.hidden) moreMenu.querySelector<HTMLElement>("[data-subscribe-copy]")?.focus();
|
|
635
|
+
});
|
|
636
|
+
document.addEventListener("click", (event) => {
|
|
637
|
+
if (!subscribePanel.contains(event.target as Node)) setMenu(false);
|
|
638
|
+
});
|
|
639
|
+
subscribePanel.addEventListener("keydown", (event) => {
|
|
640
|
+
if (event.key === "Escape" && !moreMenu.hidden) {
|
|
641
|
+
setMenu(false);
|
|
642
|
+
moreButton.focus();
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
const copy = moreMenu.querySelector<HTMLButtonElement>("[data-subscribe-copy]")!;
|
|
646
|
+
const copyText = copy.querySelector<HTMLElement>("[data-copy-label]")!;
|
|
647
|
+
const copyLabel = copyText.textContent;
|
|
648
|
+
copy.addEventListener("click", async () => {
|
|
649
|
+
try {
|
|
650
|
+
await navigator.clipboard.writeText(copy.dataset.subscribeCopy!);
|
|
651
|
+
copyText.textContent = copy.dataset.copied!;
|
|
652
|
+
window.setTimeout(() => {
|
|
653
|
+
copyText.textContent = copyLabel;
|
|
654
|
+
setMenu(false);
|
|
655
|
+
}, 1200);
|
|
656
|
+
} catch {
|
|
657
|
+
// Clipboard refused: the button stays as it was.
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
}
|
|
510
661
|
|
|
511
662
|
// What the reader has liked or reposted, by post (filled in by loadState).
|
|
512
663
|
let known: Record<string, { liked: boolean; reposted: boolean }> = {};
|
|
@@ -526,7 +677,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
526
677
|
};
|
|
527
678
|
setCount(count);
|
|
528
679
|
button.addEventListener("click", async () => {
|
|
529
|
-
if (!isSignedIn) return
|
|
680
|
+
if (!isSignedIn) return askToSignIn(kind === "like" ? "comment-like" : "comment-repost", comment.dataset.atmosphereUri);
|
|
530
681
|
const on = button.getAttribute("aria-pressed") !== "true";
|
|
531
682
|
const before = Number(button.dataset.count || 0);
|
|
532
683
|
button.setAttribute("aria-pressed", String(on));
|
|
@@ -537,7 +688,8 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
537
688
|
} catch (problem) {
|
|
538
689
|
button.setAttribute("aria-pressed", String(!on));
|
|
539
690
|
setCount(before);
|
|
540
|
-
|
|
691
|
+
const { error, permission } = (problem ?? {}) as { error?: string; permission?: string };
|
|
692
|
+
handleError(error ?? "error", say, permission);
|
|
541
693
|
}
|
|
542
694
|
});
|
|
543
695
|
return button;
|
|
@@ -558,7 +710,8 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
558
710
|
});
|
|
559
711
|
reply.setAttribute("aria-label", t("comment.replyTo", { name: author }));
|
|
560
712
|
reply.addEventListener("click", () => {
|
|
561
|
-
if (!isSignedIn) return
|
|
713
|
+
if (!isSignedIn) return askToSignIn("reply", comment.dataset.atmosphereUri);
|
|
714
|
+
if (!can("post")) return explainPermission("post");
|
|
562
715
|
openReply(comment, author);
|
|
563
716
|
});
|
|
564
717
|
actions.append(reply, commentButton("like", comment), commentButton("repost", comment));
|
|
@@ -583,7 +736,8 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
583
736
|
this.dispatchEvent(new CustomEvent("atmosphere:deleted", { detail: { uri }, bubbles: true }));
|
|
584
737
|
return;
|
|
585
738
|
}
|
|
586
|
-
|
|
739
|
+
// The page checks first, so this is rare: ask now.
|
|
740
|
+
if (data.error === "needs-permission") return void askPermission("delete", `#atmo-delete=${encodeURIComponent(uri)}`);
|
|
587
741
|
panel.remove();
|
|
588
742
|
handleError(data.error ?? "error");
|
|
589
743
|
} catch {
|
|
@@ -592,58 +746,112 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
592
746
|
}
|
|
593
747
|
};
|
|
594
748
|
|
|
595
|
-
//
|
|
596
|
-
//
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
)
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
749
|
+
// Hide, for the announcement's own author: hides a comment here and on
|
|
750
|
+
// Bluesky. Showing it again is done in the Bluesky app.
|
|
751
|
+
const isOwner = () => !!me.did && !!post && authorOf(post) === me.did;
|
|
752
|
+
|
|
753
|
+
const hideComment = async (comment: HTMLElement, panel: HTMLElement) => {
|
|
754
|
+
const uri = comment.dataset.atmosphereUri!;
|
|
755
|
+
panel.replaceChildren(make("span", "atmo-comment-delete__text", t("comment.hiding")));
|
|
756
|
+
try {
|
|
757
|
+
const response = await send("hide", { post: uri, root: post });
|
|
758
|
+
const data = (await response.json()) as { error?: string };
|
|
759
|
+
if (response.ok) {
|
|
760
|
+
comment.remove();
|
|
761
|
+
say(t("comment.hidden"));
|
|
762
|
+
this.dispatchEvent(new CustomEvent("atmosphere:hidden", { detail: { uri }, bubbles: true }));
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (data.error === "needs-permission") return void askPermission("moderate", `#atmo-hide=${encodeURIComponent(uri)}`);
|
|
766
|
+
panel.remove();
|
|
767
|
+
handleError(data.error ?? "error");
|
|
768
|
+
} catch {
|
|
769
|
+
panel.remove();
|
|
770
|
+
say(message("error"));
|
|
771
|
+
}
|
|
617
772
|
};
|
|
618
773
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
774
|
+
// Delete and Hide each need one more permission than signing in gives.
|
|
775
|
+
// Readers who haven't given it yet are asked for it first; once they
|
|
776
|
+
// have (straight away, or back from approving), they confirm.
|
|
777
|
+
const STEPS = {
|
|
778
|
+
delete: {
|
|
779
|
+
permission: "delete",
|
|
780
|
+
action: "comment.delete",
|
|
781
|
+
label: "comment.deleteLabel",
|
|
782
|
+
confirm: "comment.deleteConfirm",
|
|
783
|
+
needs: "comment.deletePermission",
|
|
784
|
+
// "{{link}}" in the wording: the comment on Bluesky, to delete it there instead.
|
|
785
|
+
elsewhere: "comment.deleteElsewhere",
|
|
786
|
+
run: deleteComment,
|
|
787
|
+
},
|
|
788
|
+
hide: {
|
|
789
|
+
permission: "moderate",
|
|
790
|
+
action: "comment.hide",
|
|
791
|
+
label: "comment.hideLabel",
|
|
792
|
+
confirm: "comment.hideConfirm",
|
|
793
|
+
needs: "comment.hidePermission",
|
|
794
|
+
elsewhere: "",
|
|
795
|
+
run: hideComment,
|
|
796
|
+
},
|
|
797
|
+
} as const;
|
|
798
|
+
|
|
799
|
+
const openPanel = (comment: HTMLElement, kind: keyof typeof STEPS) => {
|
|
800
|
+
const actions = comment.querySelector<HTMLElement>(":scope .atmo-comment-actions");
|
|
801
|
+
if (!actions || actions.nextElementSibling?.matches(".atmo-comment-delete")) return;
|
|
802
|
+
const step = STEPS[kind];
|
|
803
|
+
const panel = make("div", "atmo-comment-delete");
|
|
804
|
+
panel.setAttribute("role", "group");
|
|
805
|
+
panel.setAttribute("aria-label", t(step.label));
|
|
806
|
+
const button = (text: string, danger = false) =>
|
|
807
|
+
Object.assign(make("button", `atmo-comment-action${danger ? " atmo-comment-action--danger" : ""}`, text), { type: "button" });
|
|
808
|
+
const no = button(t("comment.cancel"));
|
|
809
|
+
no.addEventListener("click", () => {
|
|
810
|
+
panel.remove();
|
|
811
|
+
actions.querySelector<HTMLElement>(`.atmo-comment-action--${kind}`)?.focus();
|
|
812
|
+
});
|
|
813
|
+
if (can(step.permission)) {
|
|
814
|
+
const yes = button(t(step.action), true);
|
|
815
|
+
yes.addEventListener("click", () => step.run(comment, panel));
|
|
816
|
+
panel.append(make("span", "atmo-comment-delete__text", t(step.confirm)), yes, no);
|
|
636
817
|
actions.after(panel);
|
|
637
818
|
no.focus();
|
|
638
|
-
}
|
|
819
|
+
} else {
|
|
820
|
+
const go = button(t("comment.continue"), true);
|
|
821
|
+
go.addEventListener("click", () =>
|
|
822
|
+
void askPermission(step.permission, `#atmo-${kind}=${encodeURIComponent(comment.dataset.atmosphereUri!)}`),
|
|
823
|
+
);
|
|
824
|
+
const text = make("span", "atmo-comment-delete__text");
|
|
825
|
+
const [before, after = ""] = t(step.needs, { link: "\u0000" }).split("\u0000");
|
|
826
|
+
text.append(before);
|
|
827
|
+
if (step.elsewhere) {
|
|
828
|
+
const url = comment.dataset.atmosphereUrl;
|
|
829
|
+
text.append(
|
|
830
|
+
url
|
|
831
|
+
? Object.assign(make("a", "", t(step.elsewhere)), { href: url, target: "_blank", rel: "noopener noreferrer" })
|
|
832
|
+
: t(step.elsewhere),
|
|
833
|
+
after,
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
panel.append(text, go, no);
|
|
837
|
+
actions.after(panel);
|
|
838
|
+
go.focus();
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
const panelButton = (comment: HTMLElement, kind: keyof typeof STEPS) => {
|
|
843
|
+
const button = Object.assign(make("button", `atmo-comment-action atmo-comment-action--${kind}`, t(STEPS[kind].action)), { type: "button" });
|
|
844
|
+
button.setAttribute("aria-label", t(STEPS[kind].label));
|
|
845
|
+
button.addEventListener("click", () => openPanel(comment, kind));
|
|
639
846
|
return button;
|
|
640
847
|
};
|
|
641
848
|
|
|
642
849
|
const addDeleteButtons = () => {
|
|
643
850
|
document.querySelectorAll<HTMLElement>("[data-atmosphere-uri]").forEach((comment) => {
|
|
644
851
|
const actions = comment.querySelector<HTMLElement>(":scope .atmo-comment-actions");
|
|
645
|
-
if (!actions || actions.querySelector(".atmo-comment-action--delete")) return;
|
|
646
|
-
if (isSignedIn && isMine(comment)) actions.append(
|
|
852
|
+
if (!actions || actions.querySelector(".atmo-comment-action--delete, .atmo-comment-action--hide")) return;
|
|
853
|
+
if (isSignedIn && isMine(comment)) actions.append(panelButton(comment, "delete"));
|
|
854
|
+
else if (isSignedIn && isOwner()) actions.append(panelButton(comment, "hide"));
|
|
647
855
|
});
|
|
648
856
|
};
|
|
649
857
|
|
|
@@ -653,14 +861,13 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
653
861
|
const comments = [...document.querySelectorAll<HTMLElement>("[data-atmosphere-uri]")];
|
|
654
862
|
const uris = [post, ...comments.map((comment) => comment.dataset.atmosphereUri!)];
|
|
655
863
|
try {
|
|
656
|
-
const
|
|
657
|
-
const
|
|
658
|
-
const state = (await response.json()) as {
|
|
864
|
+
const response = await state(uris);
|
|
865
|
+
const found = (await response.json()) as {
|
|
659
866
|
error?: string;
|
|
660
867
|
posts?: Record<string, { liked: boolean; reposted: boolean }>;
|
|
661
868
|
};
|
|
662
|
-
if (!response.ok) return handleError(
|
|
663
|
-
const posts =
|
|
869
|
+
if (!response.ok) return handleError(found.error ?? "error");
|
|
870
|
+
const posts = found.posts ?? {};
|
|
664
871
|
known = { ...known, ...posts };
|
|
665
872
|
setPressed("like", !!posts[post]?.liked);
|
|
666
873
|
setPressed("repost", !!posts[post]?.reposted);
|
|
@@ -675,6 +882,31 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
675
882
|
}
|
|
676
883
|
};
|
|
677
884
|
|
|
885
|
+
// Back from the sign-in overlay: finishes what the reader pressed. If
|
|
886
|
+
// signing in failed, the overlay opens again to say so.
|
|
887
|
+
const resumePending = (signed: boolean) => {
|
|
888
|
+
const pending = location.hash.match(/^#atmo-do=([a-z-]+)(?::(.+))?$/);
|
|
889
|
+
if (!pending) return;
|
|
890
|
+
const [, kind, encoded] = pending;
|
|
891
|
+
const uri = encoded ? decodeURIComponent(encoded) : undefined;
|
|
892
|
+
history.replaceState(history.state, "", `${location.pathname}${location.search}#atmosphere-comments`);
|
|
893
|
+
if (!signed) {
|
|
894
|
+
if (result && result !== "signed-in" && SIGN_IN_HEADINGS[kind]) askToSignIn(kind, uri, message(result));
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
const comment = uri ? document.querySelector<HTMLElement>(`[data-atmosphere-uri="${CSS.escape(uri)}"]`) : null;
|
|
898
|
+
const press = (button: HTMLElement | null | undefined) => {
|
|
899
|
+
if (button?.getAttribute("aria-pressed") !== "true") button?.click();
|
|
900
|
+
};
|
|
901
|
+
(comment ?? this).scrollIntoView({ block: "center" });
|
|
902
|
+
if (kind === "like" || kind === "repost") press(reactButton(kind));
|
|
903
|
+
else if (kind === "quote") $<HTMLButtonElement>("[data-quote]").click();
|
|
904
|
+
else if (kind === "comment") main.textarea.focus({ preventScroll: true });
|
|
905
|
+
else if (kind === "reply" && comment) openReply(comment, comment.dataset.atmosphereAuthor || t("comment.someone"));
|
|
906
|
+
else if (kind === "comment-like" && comment) press(comment.querySelector<HTMLElement>(".atmo-comment-action[data-kind='like']"));
|
|
907
|
+
else if (kind === "comment-repost" && comment) press(comment.querySelector<HTMLElement>(".atmo-comment-action[data-kind='repost']"));
|
|
908
|
+
};
|
|
909
|
+
|
|
678
910
|
const show = async (signed: boolean, handle?: string, displayName?: string, avatar?: string) => {
|
|
679
911
|
if (handle) {
|
|
680
912
|
$<HTMLElement>("[data-preview-name]").textContent = displayName || handle;
|
|
@@ -686,26 +918,42 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
686
918
|
await threadReady;
|
|
687
919
|
if (this.classList.contains("is-inactive")) return;
|
|
688
920
|
isSignedIn = signed;
|
|
689
|
-
|
|
690
|
-
|
|
921
|
+
commentPrompt.hidden = signed;
|
|
922
|
+
if (subscribePanel) {
|
|
923
|
+
subscribePanel.hidden = false;
|
|
924
|
+
if (signed) void loadSubscribed();
|
|
925
|
+
else forgetSubscribed();
|
|
926
|
+
await finishPendingSubscribe(t, askToSubscribe);
|
|
927
|
+
}
|
|
691
928
|
if (!signed) {
|
|
692
|
-
document.querySelectorAll(".atmo-comment-action--delete, .atmo-comment-delete").forEach((part) => part.remove());
|
|
929
|
+
document.querySelectorAll(".atmo-comment-action--delete, .atmo-comment-action--hide, .atmo-comment-delete").forEach((part) => part.remove());
|
|
930
|
+
composer.hidden = true;
|
|
931
|
+
permissionPanel.hidden = true;
|
|
932
|
+
setPressed("like", false);
|
|
933
|
+
setPressed("repost", false);
|
|
934
|
+
resumePending(false);
|
|
693
935
|
return closeReplies();
|
|
694
936
|
}
|
|
937
|
+
// A sign-in without posting (from an older version, which asked for
|
|
938
|
+
// less): one more permission, asked for before they type.
|
|
939
|
+
composer.hidden = !can("post");
|
|
940
|
+
permissionPanel.hidden = can("post");
|
|
941
|
+
// Which account, as they may have more than one.
|
|
942
|
+
$<HTMLElement>("[data-permission-text]").textContent = t("composer.needsPermission", { handle: handle ? `@${handle}` : "" });
|
|
695
943
|
addCommentActions();
|
|
944
|
+
addDeleteButtons();
|
|
696
945
|
await loadState();
|
|
697
|
-
|
|
698
|
-
|
|
946
|
+
resumePending(true);
|
|
947
|
+
// Back from approving the delete or hide permission: ask them to
|
|
948
|
+
// confirm the delete or hide they started.
|
|
949
|
+
const pending = location.hash.match(/^#atmo-(delete|hide)=(.+)$/);
|
|
699
950
|
if (pending) {
|
|
700
951
|
history.replaceState(history.state, "", `${location.pathname}${location.search}#atmosphere-comments`);
|
|
701
|
-
const
|
|
702
|
-
const comment = document.querySelector<HTMLElement>(`[data-atmosphere-uri="${CSS.escape(
|
|
703
|
-
|
|
704
|
-
if (comment && actions && isMine(comment)) {
|
|
705
|
-
const panel = make("div", "atmo-comment-delete");
|
|
706
|
-
actions.after(panel);
|
|
952
|
+
const kind = pending[1] as keyof typeof STEPS;
|
|
953
|
+
const comment = document.querySelector<HTMLElement>(`[data-atmosphere-uri="${CSS.escape(decodeURIComponent(pending[2]))}"]`);
|
|
954
|
+
if (comment && (kind === "delete" ? isMine(comment) : isOwner())) {
|
|
707
955
|
comment.scrollIntoView({ block: "center" });
|
|
708
|
-
|
|
956
|
+
openPanel(comment, kind);
|
|
709
957
|
}
|
|
710
958
|
}
|
|
711
959
|
};
|
|
@@ -719,12 +967,13 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
719
967
|
history.replaceState(history.state, "", url);
|
|
720
968
|
}
|
|
721
969
|
|
|
970
|
+
// These need this script, so they're hidden without it.
|
|
722
971
|
this.classList.add("is-loading");
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
.then((found
|
|
972
|
+
$<HTMLElement>("[data-view='main']").hidden = false;
|
|
973
|
+
whoIsSignedIn()
|
|
974
|
+
.then((found) => {
|
|
726
975
|
me = found;
|
|
727
|
-
return
|
|
976
|
+
return found.signedIn ? show(true, found.handle, found.displayName, found.avatar) : show(false);
|
|
728
977
|
})
|
|
729
978
|
.catch(() => show(false))
|
|
730
979
|
.finally(() => {
|
|
@@ -743,15 +992,23 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
743
992
|
// Like and Repost: shown as done straight away, put back if it fails.
|
|
744
993
|
const reactButton = (kind: string) => $<HTMLButtonElement>(`[data-react='${kind}']`);
|
|
745
994
|
const setPressed = (kind: string, on: boolean) => reactButton(kind).setAttribute("aria-pressed", String(on));
|
|
995
|
+
// Liking also recommends the page's Standard.site record: says so.
|
|
996
|
+
if (subscribePanel && document.querySelector('link[rel="site.standard.document"]')) {
|
|
997
|
+
reactButton("like").title = t("actions.likeAndRecommend");
|
|
998
|
+
}
|
|
746
999
|
for (const kind of ["like", "repost"]) {
|
|
747
1000
|
reactButton(kind).addEventListener("click", async () => {
|
|
1001
|
+
if (!isSignedIn) return askToSignIn(kind);
|
|
748
1002
|
const on = reactButton(kind).getAttribute("aria-pressed") !== "true";
|
|
749
1003
|
setPressed(kind, on);
|
|
750
1004
|
try {
|
|
751
|
-
|
|
1005
|
+
// Liking the post also recommends the page's Standard.site record, if it has one.
|
|
1006
|
+
const document_ = kind === "like" ? document.querySelector('link[rel="site.standard.document"]')?.getAttribute("href") : undefined;
|
|
1007
|
+
const response = await send("react", { post, kind, on, document: document_ ?? undefined });
|
|
752
1008
|
if (!response.ok) {
|
|
753
1009
|
setPressed(kind, !on);
|
|
754
|
-
|
|
1010
|
+
const { error, permission } = (await response.json()) as { error?: string; permission?: string };
|
|
1011
|
+
return handleError(error ?? "error", say, permission);
|
|
755
1012
|
}
|
|
756
1013
|
say(t(on ? (kind === "like" ? "status.liked" : "status.reposted") : kind === "like" ? "status.likeRemoved" : "status.repostRemoved"));
|
|
757
1014
|
showReaction(kind, on);
|
|
@@ -768,12 +1025,16 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
768
1025
|
});
|
|
769
1026
|
}
|
|
770
1027
|
|
|
771
|
-
|
|
772
|
-
await
|
|
1028
|
+
const signOutHere = async () => {
|
|
1029
|
+
await signOut();
|
|
1030
|
+
me = {};
|
|
773
1031
|
void show(false);
|
|
774
1032
|
setMainMode("comment");
|
|
775
1033
|
say(t("status.signedOut"));
|
|
776
|
-
}
|
|
1034
|
+
};
|
|
1035
|
+
$<HTMLButtonElement>("[data-sign-out]").addEventListener("click", signOutHere);
|
|
1036
|
+
$<HTMLButtonElement>("[data-permission-sign-out]").addEventListener("click", signOutHere);
|
|
1037
|
+
$<HTMLButtonElement>("[data-permission-continue]").addEventListener("click", () => void askPermission("post"));
|
|
777
1038
|
}
|
|
778
1039
|
}
|
|
779
1040
|
|