astro-atmosphere-comments 0.6.0 → 0.7.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 +15 -0
- package/README.md +3 -1
- package/components/AtmosphereComments.astro +107 -6
- package/components/atmosphere.css +73 -4
- package/lib/client/account.ts +39 -0
- package/lib/thread.ts +27 -9
- package/locales/de.json +5 -1
- package/locales/en.json +5 -1
- package/locales/es.json +5 -1
- package/locales/fr.json +5 -1
- package/locales/it.json +5 -1
- package/locales/ja.json +5 -1
- package/locales/nl.json +5 -1
- package/locales/pt.json +5 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0 (8 November 2026)
|
|
4
|
+
|
|
5
|
+
**Comments from people who show their posts only to signed-in readers.** Those accounts use Bluesky's "Discourage apps from showing my account to logged-out users".
|
|
6
|
+
|
|
7
|
+
- **Signed-in readers see them:** the reader's browser fetches those comments through the reader's own sign-in (`app.bsky.feed.getPosts`, already part of the approval, so nobody has to sign in again). They then show as normal comments, with Reply, Like and Repost. Signing out puts the placeholders back. The server still never sends their names, pictures or words; the page holds only each comment's address (its AT URI).
|
|
8
|
+
- **Signed-out readers get a clearer placeholder:** a blurred stand-in made of grey bars (never the real comment), with "Comment hidden" and a line saying to sign in to see it. Clicking it opens the sign-in overlay, and the page scrolls to the comment once they're back. The replies under it stay, as before. Styles: `.atmo-comment--withheld`, `.atmo-withheld__ghost`, `.atmo-withheld__cover`; `.atmo-comment__withheld` is gone.
|
|
9
|
+
- **Like and repost counts match the faces:** those accounts are no longer left out of the faces. Signed out, they show as a faceless silhouette (`.atmo-avatar-anonymous`) with no name or link; signed in, their real face, looked up through the site's `profile` route. The thread data gives only their DID (`withheld: true`).
|
|
10
|
+
- A broken avatar picture now falls back to the person's initial.
|
|
11
|
+
- New wording keys `comment.withheldTitle`, `comment.withheldSignedIn`, `signInFirst.view`, `reactions.hiddenPerson`; `comment.withheld` changed. Draft translations in all eight languages.
|
|
12
|
+
|
|
13
|
+
## 0.6.1 (7 November 2026)
|
|
14
|
+
|
|
15
|
+
- Uses the current AT Protocol library (`@atproto/api` 0.30, was 0.25), the same version as Atmosphere Publishing for Astro, so a site using both installs it once.
|
|
16
|
+
- README: using it with [Atmosphere Publishing for Astro](https://github.com/saltedlolly/astro-atmosphere-publishing), and with other Standard.site publishers such as Sequoia.
|
|
17
|
+
|
|
3
18
|
## 0.6.0 (7 November 2026)
|
|
4
19
|
|
|
5
20
|
**Buttons for everyone, and Standard.site support for readers.** The buttons and sign-in change applies to every site; Subscribe and recommending are optional.
|
package/README.md
CHANGED
|
@@ -110,6 +110,8 @@ A "follow me" link can become a **Follow** button for signed-in readers: mark it
|
|
|
110
110
|
data-atmosphere-follow="did:plc:…" data-atmosphere-follow-handle="you.bsky.social">Follow me on Bluesky</a>
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
+
**Publishing to Standard.site:** [Atmosphere Publishing for Astro](https://github.com/saltedlolly/astro-atmosphere-publishing) is the companion integration made to work with this one. It publishes your posts' Standard.site records, announces new posts on Bluesky and adds each page's `<link rel="site.standard.document">` tag, so the comment box finds each post's announcement without a `post` prop. Its `publicationFromState()` helper gives the `standardSite` option your publication (below). Other publishers work too, as long as they add that tag and set the record's `bskyPostRef`, as [Sequoia](https://sequoia.pub) does.
|
|
114
|
+
|
|
113
115
|
If you publish [Standard.site](https://standard.site) records, set the integration's `standardSite` option. Readers can then **subscribe** to your publication from your site, and read new posts in apps such as Leaflet or Standard Reader. `AtmosphereComments` shows a Subscribe button under each post, with Like and Repost. To put a button anywhere else (with `AtmosphereReactions` in your layout), mark it as below; it stays hidden without the option or JavaScript. Anyone not signed in is offered the sign-in form, and subscribed once they're back; a second press unsubscribes.
|
|
114
116
|
|
|
115
117
|
```astro
|
|
@@ -221,7 +223,7 @@ The component fires (bubbling):
|
|
|
221
223
|
- **What's checked before anything is sent** (by the browser code, `lib/client/account.ts`): only your announcements and the comments under them can be replied to, liked, reposted or quoted; only a reader's own replies under your announcements can be deleted; only your `standardSite` publication can be subscribed to, and only documents in its account recommended; only your `allowedAuthors` can be followed; only an announcement's author can hide comments under it.
|
|
222
224
|
- **What your server does:** it shows the threads, looks up accounts and suggestions, finds the links and mentions in a comment before it's posted, and passes on avatars and link pictures it has just shown. None of it needs to know who a reader is. Each route that calls Bluesky is rate-limited per address, with an overall cap; requests that aren't simple reads must come from your own pages (checked by `Origin`). Readers' addresses are used only for those limits, never stored.
|
|
223
225
|
- **Trade-off:** account hosts trust browser sign-ins less than ones kept on a server, so they last less long (Bluesky's currently last about two weeks), and there's no way for a site to sign everyone out at once. For a reader who'd rather not trust your server, that's the point.
|
|
224
|
-
- **People who'd rather not be shown publicly:** Bluesky's setting "Discourage apps from showing my account to logged-out users" is respected, as your page is public.
|
|
226
|
+
- **People who'd rather not be shown publicly:** Bluesky's setting "Discourage apps from showing my account to logged-out users" is respected, as your page is public. For readers who aren't signed in, a comment from such an account shows as a blurred "Comment hidden" placeholder (made-up grey bars: no name, picture or words reach the page, only the comment's address), the replies under it stay, and the account appears in the faces of who liked and reposted as a faceless silhouette, so the counts still add up. Clicking the placeholder offers sign-in. Signed-in readers see these comments and faces as normal: their own browser fetches them with their own sign-in.
|
|
225
227
|
- **Moderation:** comments appear automatically. Hide one with Hide under it (signed in as the account that posted the announcement) or in the Bluesky app; it disappears here too. Show it again in the Bluesky app. You can also block the account, or add it to `hidden`.
|
|
226
228
|
|
|
227
229
|
## Limitations (for now)
|
|
@@ -195,7 +195,9 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
195
195
|
import { translate } from "../lib/translate";
|
|
196
196
|
import { enhanceHandleBox } from "../lib/client/handle-suggestions";
|
|
197
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";
|
|
198
|
+
import { handleSignInForm, me as whoIsSignedIn, send, signIn, signOut, state, withheldPosts } from "../lib/client/account";
|
|
199
|
+
import publicSettings from "../lib/public-settings";
|
|
200
|
+
import { segments } from "../lib/richtext";
|
|
199
201
|
import { addSubscribeButtons, finishPendingSubscribe, forgetSubscribed, loadSubscribed } from "../lib/client/subscribe";
|
|
200
202
|
import type { Permission } from "../lib/permissions";
|
|
201
203
|
import { authorOf } from "../lib/uris";
|
|
@@ -268,7 +270,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
268
270
|
|
|
269
271
|
// The thread: likes and reposts above the comment box, and the comments
|
|
270
272
|
// below it, read through this site (routes/thread.ts).
|
|
271
|
-
type Person = { did: string; handle: string; displayName?: string; avatar?: string; url: string };
|
|
273
|
+
type Person = { did: string; handle: string; displayName?: string; avatar?: string; url: string; withheld?: true };
|
|
272
274
|
type Reply = {
|
|
273
275
|
uri: string;
|
|
274
276
|
url: string;
|
|
@@ -283,6 +285,9 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
283
285
|
};
|
|
284
286
|
type Thread = { uri: string; url: string; likeCount: number; repostCount: number; likes: Person[]; reposts: Person[]; replies: Reply[] };
|
|
285
287
|
let thread: Thread | null = null;
|
|
288
|
+
// Withheld comments and faces, as a signed-in reader sees them (by AT URI and DID).
|
|
289
|
+
const revealedReplies = new Map<string, Reply>();
|
|
290
|
+
const revealedPeople = new Map<string, Person>();
|
|
286
291
|
const reactionsBox = $<HTMLElement>("[data-reactions]");
|
|
287
292
|
const repliesList = $<HTMLOListElement>("[data-replies]");
|
|
288
293
|
const authorLabel = t("comment.author");
|
|
@@ -298,12 +303,24 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
298
303
|
if (text !== undefined) element.textContent = text;
|
|
299
304
|
return element;
|
|
300
305
|
};
|
|
301
|
-
const
|
|
302
|
-
if (person.avatar) return Object.assign(make("img", className), { src: person.avatar, alt: "", loading: "lazy" as const });
|
|
306
|
+
const initialOf = (person: Pick<Person, "handle" | "displayName">, className: string) => {
|
|
303
307
|
const initial = make("span", `${className} atmo-avatar-initial`, nameOf(person).replace("@", "").charAt(0).toUpperCase());
|
|
304
308
|
initial.setAttribute("aria-hidden", "true");
|
|
305
309
|
return initial;
|
|
306
310
|
};
|
|
311
|
+
// A faceless avatar, for someone who asks not to be shown to people who aren't signed in.
|
|
312
|
+
const anonymousAvatar = (className: string) => {
|
|
313
|
+
const face = make("span", `${className} atmo-avatar-anonymous`);
|
|
314
|
+
face.setAttribute("aria-hidden", "true");
|
|
315
|
+
return face;
|
|
316
|
+
};
|
|
317
|
+
const avatarOf = (person: Pick<Person, "handle" | "displayName" | "avatar"> & { withheld?: true }, className: string) => {
|
|
318
|
+
if (person.withheld) return anonymousAvatar(className);
|
|
319
|
+
if (!person.avatar) return initialOf(person, className);
|
|
320
|
+
const img = Object.assign(make("img", className), { src: person.avatar, alt: "", loading: "lazy" as const });
|
|
321
|
+
img.addEventListener("error", () => img.replaceWith(initialOf(person, className)), { once: true });
|
|
322
|
+
return img;
|
|
323
|
+
};
|
|
307
324
|
|
|
308
325
|
const renderReactions = () => {
|
|
309
326
|
if (!thread) return;
|
|
@@ -318,7 +335,16 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
318
335
|
group.dataset.reaction = kind;
|
|
319
336
|
group.append(make("p", "atmo-reactions__count", t(kind === "like" ? "reactions.likes" : "reactions.reposts", { count })));
|
|
320
337
|
const faces = make("ul", "atmo-facepile");
|
|
321
|
-
for (const
|
|
338
|
+
for (const shown of people) {
|
|
339
|
+
const person = (shown.withheld && revealedPeople.get(shown.did)) || shown;
|
|
340
|
+
if (person.withheld) {
|
|
341
|
+
// Counted, but not identified: signed out, or not yet looked up.
|
|
342
|
+
const item = make("li");
|
|
343
|
+
item.title = t("reactions.hiddenPerson");
|
|
344
|
+
item.append(anonymousAvatar("atmo-facepile__avatar"));
|
|
345
|
+
faces.append(item);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
322
348
|
const link = Object.assign(make("a"), { href: person.url, title: nameOf(person), rel: "ugc nofollow noopener", target: "_blank" });
|
|
323
349
|
link.setAttribute("aria-label", nameOf(person));
|
|
324
350
|
link.append(avatarOf(person, "atmo-facepile__avatar"));
|
|
@@ -339,9 +365,30 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
339
365
|
// Its author asks not to be shown to logged-out people: a placeholder,
|
|
340
366
|
// keeping the replies under it in place. (No AT URI, so no actions.)
|
|
341
367
|
if (reply.withheld) {
|
|
368
|
+
const revealed = revealedReplies.get(reply.uri);
|
|
369
|
+
if (revealed) return replyElement({ ...revealed, depth: reply.depth });
|
|
342
370
|
item.classList.add("atmo-comment--withheld");
|
|
371
|
+
if (reply.uri) item.dataset.withheldUri = reply.uri;
|
|
372
|
+
// A blurred, made-up comment (never the real one) under the explanation.
|
|
343
373
|
const article = make("article");
|
|
344
|
-
|
|
374
|
+
const ghost = make("div", "atmo-withheld__ghost");
|
|
375
|
+
ghost.setAttribute("aria-hidden", "true");
|
|
376
|
+
const ghostHeader = make("div", "atmo-comment__header");
|
|
377
|
+
const bar = (width: string) => Object.assign(make("span", "atmo-withheld__bar"), { style: `width: ${width}` });
|
|
378
|
+
const ghostNames = make("div", "atmo-withheld__names");
|
|
379
|
+
ghostNames.append(bar("9em"), bar("13em"));
|
|
380
|
+
ghostHeader.append(anonymousAvatar("atmo-comment__avatar"), ghostNames);
|
|
381
|
+
const ghostText = make("div", "atmo-withheld__text");
|
|
382
|
+
ghostText.append(bar("100%"), bar("92%"), bar("60%"));
|
|
383
|
+
ghost.append(ghostHeader, ghostText);
|
|
384
|
+
const cover = make("button", "atmo-withheld__cover");
|
|
385
|
+
cover.type = "button";
|
|
386
|
+
cover.append(make("strong", "", t("comment.withheldTitle")), make("span", "", t(isSignedIn ? "comment.withheldSignedIn" : "comment.withheld")));
|
|
387
|
+
cover.disabled = isSignedIn;
|
|
388
|
+
cover.addEventListener("click", () => {
|
|
389
|
+
if (!isSignedIn) askToSignIn("view", reply.uri || undefined);
|
|
390
|
+
});
|
|
391
|
+
article.append(ghost, cover);
|
|
345
392
|
item.append(article);
|
|
346
393
|
return item;
|
|
347
394
|
}
|
|
@@ -487,6 +534,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
487
534
|
reply: "signInFirst.reply",
|
|
488
535
|
"comment-like": "signInFirst.like",
|
|
489
536
|
"comment-repost": "signInFirst.repost",
|
|
537
|
+
view: "signInFirst.view",
|
|
490
538
|
};
|
|
491
539
|
const askToSignIn = (kind: string, uri?: string, problem?: string) =>
|
|
492
540
|
openSignIn(t(SIGN_IN_HEADINGS[kind]), `#atmo-do=${kind}${uri ? `:${encodeURIComponent(uri)}` : ""}`, problem);
|
|
@@ -907,6 +955,53 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
907
955
|
else if (kind === "comment-repost" && comment) press(comment.querySelector<HTMLElement>(".atmo-comment-action[data-kind='repost']"));
|
|
908
956
|
};
|
|
909
957
|
|
|
958
|
+
// Draws the comments and faces again (after revealing, or signing out).
|
|
959
|
+
const redrawThread = () => {
|
|
960
|
+
if (!thread) return;
|
|
961
|
+
renderReactions();
|
|
962
|
+
repliesList.replaceChildren(...thread.replies.map(replyElement));
|
|
963
|
+
};
|
|
964
|
+
// Signed in: the comments and faces of people who ask not to be shown to
|
|
965
|
+
// people who aren't signed in, fetched as the reader and shown.
|
|
966
|
+
const revealWithheld = async () => {
|
|
967
|
+
if (!thread) return;
|
|
968
|
+
const uris = thread.replies.filter((reply) => reply.withheld && reply.uri && !revealedReplies.has(reply.uri)).map((reply) => reply.uri);
|
|
969
|
+
const dids = [...new Set([...thread.likes, ...thread.reposts].filter((person) => person.withheld && person.did && !revealedPeople.has(person.did)).map((person) => person.did))].slice(0, 20);
|
|
970
|
+
if (!uris.length && !dids.length) return;
|
|
971
|
+
const [posts, people] = await Promise.all([
|
|
972
|
+
withheldPosts(uris),
|
|
973
|
+
Promise.all(
|
|
974
|
+
dids.map((did) =>
|
|
975
|
+
fetch(`${base}/profile?did=${encodeURIComponent(did)}`)
|
|
976
|
+
.then((response) => (response.ok ? (response.json() as Promise<Pick<Person, "did" | "handle" | "displayName" | "avatar">>) : null))
|
|
977
|
+
.catch(() => null),
|
|
978
|
+
),
|
|
979
|
+
),
|
|
980
|
+
]);
|
|
981
|
+
if (!isSignedIn) return;
|
|
982
|
+
for (const found of posts) {
|
|
983
|
+
const handle = found.author.handle;
|
|
984
|
+
const profileUrl = `https://bsky.app/profile/${encodeURIComponent(handle)}`;
|
|
985
|
+
revealedReplies.set(found.uri, {
|
|
986
|
+
uri: found.uri,
|
|
987
|
+
url: `${profileUrl}/post/${found.uri.split("/").pop()}`,
|
|
988
|
+
author: { did: found.author.did, handle, displayName: found.author.displayName, avatar: `${base}/media?avatar=${encodeURIComponent(found.author.did)}`, url: profileUrl },
|
|
989
|
+
byAuthor: publicSettings.allowedAuthors.includes(found.author.did),
|
|
990
|
+
createdAt: found.record.createdAt ?? "",
|
|
991
|
+
text: segments(found.record.text ?? "", found.record.facets),
|
|
992
|
+
depth: 0,
|
|
993
|
+
likeCount: found.likeCount,
|
|
994
|
+
repostCount: found.repostCount,
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
for (const found of people) {
|
|
998
|
+
if (found?.did && found.handle && found.handle !== found.did) {
|
|
999
|
+
revealedPeople.set(found.did, { ...found, url: `https://bsky.app/profile/${encodeURIComponent(found.handle)}` });
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
redrawThread();
|
|
1003
|
+
};
|
|
1004
|
+
|
|
910
1005
|
const show = async (signed: boolean, handle?: string, displayName?: string, avatar?: string) => {
|
|
911
1006
|
if (handle) {
|
|
912
1007
|
$<HTMLElement>("[data-preview-name]").textContent = displayName || handle;
|
|
@@ -931,6 +1026,11 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
931
1026
|
permissionPanel.hidden = true;
|
|
932
1027
|
setPressed("like", false);
|
|
933
1028
|
setPressed("repost", false);
|
|
1029
|
+
if (revealedReplies.size || revealedPeople.size) {
|
|
1030
|
+
revealedReplies.clear();
|
|
1031
|
+
revealedPeople.clear();
|
|
1032
|
+
redrawThread();
|
|
1033
|
+
}
|
|
934
1034
|
resumePending(false);
|
|
935
1035
|
return closeReplies();
|
|
936
1036
|
}
|
|
@@ -940,6 +1040,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
940
1040
|
permissionPanel.hidden = can("post");
|
|
941
1041
|
// Which account, as they may have more than one.
|
|
942
1042
|
$<HTMLElement>("[data-permission-text]").textContent = t("composer.needsPermission", { handle: handle ? `@${handle}` : "" });
|
|
1043
|
+
await revealWithheld().catch(() => undefined);
|
|
943
1044
|
addCommentActions();
|
|
944
1045
|
addDeleteButtons();
|
|
945
1046
|
await loadState();
|
|
@@ -868,10 +868,79 @@ dialog.atmo.atmo-signin:has(> .atmo__login) {
|
|
|
868
868
|
margin: 0;
|
|
869
869
|
}
|
|
870
870
|
|
|
871
|
-
/* In place of a comment whose author asks not to be shown to logged-out people
|
|
872
|
-
|
|
873
|
-
|
|
871
|
+
/* In place of a comment whose author asks not to be shown to logged-out people:
|
|
872
|
+
a blurred, made-up comment (grey bars, never the real one) under an explanation. */
|
|
873
|
+
:where(.atmo-comment--withheld article) {
|
|
874
|
+
position: relative;
|
|
875
|
+
overflow: hidden;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
.atmo-withheld__ghost {
|
|
879
|
+
filter: blur(5px);
|
|
880
|
+
opacity: 0.7;
|
|
881
|
+
pointer-events: none;
|
|
882
|
+
user-select: none;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
.atmo-withheld__names,
|
|
886
|
+
.atmo-withheld__text {
|
|
887
|
+
display: grid;
|
|
888
|
+
gap: 8px;
|
|
889
|
+
align-content: center;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
.atmo-withheld__text {
|
|
893
|
+
margin-top: 14px;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
.atmo-withheld__bar {
|
|
897
|
+
display: block;
|
|
898
|
+
max-width: 100%;
|
|
899
|
+
height: 0.75em;
|
|
900
|
+
border-radius: 999px;
|
|
901
|
+
background: color-mix(in srgb, var(--atmo-muted, currentColor) 45%, transparent);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
.atmo-withheld__cover {
|
|
905
|
+
position: absolute;
|
|
906
|
+
inset: 0;
|
|
907
|
+
display: grid;
|
|
908
|
+
place-content: center;
|
|
909
|
+
gap: 4px;
|
|
910
|
+
padding: 12px 20px;
|
|
911
|
+
border: 0;
|
|
912
|
+
color: var(--atmo-text, inherit);
|
|
913
|
+
font: inherit;
|
|
914
|
+
text-align: center;
|
|
915
|
+
background: color-mix(in srgb, var(--atmo-card-bg, Canvas) 35%, transparent);
|
|
916
|
+
cursor: pointer;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
.atmo-withheld__cover:disabled {
|
|
920
|
+
cursor: default;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
.atmo-withheld__cover span {
|
|
924
|
+
max-width: 42ch;
|
|
874
925
|
color: var(--atmo-muted, inherit);
|
|
875
926
|
font-size: 0.9em;
|
|
876
|
-
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
.atmo-withheld__cover:not(:disabled):hover strong,
|
|
930
|
+
.atmo-withheld__cover:not(:disabled):focus-visible strong {
|
|
931
|
+
text-decoration: underline;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
.atmo-withheld__cover:focus-visible {
|
|
935
|
+
outline: 2px solid var(--atmo-accent, #1185fe);
|
|
936
|
+
outline-offset: -2px;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/* A faceless avatar: a head and shoulders. */
|
|
940
|
+
:where(.atmo-avatar-anonymous) {
|
|
941
|
+
display: inline-block;
|
|
942
|
+
background:
|
|
943
|
+
radial-gradient(circle at 50% 38%, color-mix(in srgb, var(--atmo-muted, #888) 70%, transparent) 0 18%, transparent 19%),
|
|
944
|
+
radial-gradient(ellipse 34% 26% at 50% 88%, color-mix(in srgb, var(--atmo-muted, #888) 70%, transparent) 0 99%, transparent 100%),
|
|
945
|
+
var(--atmo-field-bg, #8882);
|
|
877
946
|
}
|
package/lib/client/account.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
} from "../permissions";
|
|
36
36
|
import settings from "../public-settings";
|
|
37
37
|
import { authorOf, inSiteThread, isPostUri, isSiteDocument, isSitePost, rkeyOf, type PostView } from "../uris";
|
|
38
|
+
import type { Facet } from "../richtext";
|
|
38
39
|
|
|
39
40
|
type IdentityResolver = NonNullable<ConstructorParameters<typeof BrowserOAuthClient>[0]["identityResolver"]>;
|
|
40
41
|
|
|
@@ -538,6 +539,44 @@ export async function state(uris: string[]) {
|
|
|
538
539
|
}
|
|
539
540
|
}
|
|
540
541
|
|
|
542
|
+
export type WithheldPost = {
|
|
543
|
+
uri: string;
|
|
544
|
+
author: { did: string; handle: string; displayName?: string };
|
|
545
|
+
record: { text?: string; createdAt?: string; facets?: Facet[] };
|
|
546
|
+
likeCount: number;
|
|
547
|
+
repostCount: number;
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Comments whose authors ask not to be shown to people who aren't signed in,
|
|
552
|
+
* read as the signed-in reader (the site only gives their AT URIs). Only posts
|
|
553
|
+
* in the site's threads; nothing if no one is signed in.
|
|
554
|
+
*/
|
|
555
|
+
export async function withheldPosts(uris: string[]): Promise<WithheldPost[]> {
|
|
556
|
+
const signedIn = await reader();
|
|
557
|
+
if (signedIn instanceof Response || !uris.length) return [];
|
|
558
|
+
try {
|
|
559
|
+
const views = (await postsAsReader(signedIn, [...new Set(uris)].filter(isPostUri).slice(0, 100))) as (PostView & {
|
|
560
|
+
author?: { did: string; handle?: string; displayName?: string };
|
|
561
|
+
record?: { text?: string; createdAt?: string; facets?: Facet[] };
|
|
562
|
+
likeCount?: number;
|
|
563
|
+
repostCount?: number;
|
|
564
|
+
})[];
|
|
565
|
+
return views
|
|
566
|
+
.filter((view) => inSiteThread(view) && view.author?.did)
|
|
567
|
+
.map((view) => ({
|
|
568
|
+
uri: view.uri,
|
|
569
|
+
author: { did: view.author!.did, handle: view.author!.handle || view.author!.did, displayName: view.author!.displayName?.trim() || undefined },
|
|
570
|
+
record: { text: view.record?.text, createdAt: view.record?.createdAt, facets: view.record?.facets },
|
|
571
|
+
likeCount: view.likeCount ?? 0,
|
|
572
|
+
repostCount: view.repostCount ?? 0,
|
|
573
|
+
}));
|
|
574
|
+
} catch (error) {
|
|
575
|
+
failed(error, "withheld comments");
|
|
576
|
+
return [];
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
541
580
|
/** Signs out: cancels the sign-in at the reader's account host, and deletes it from this browser. */
|
|
542
581
|
export async function signOut() {
|
|
543
582
|
const current = await session().catch(() => null);
|
package/lib/thread.ts
CHANGED
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
// Accounts that ask apps not to show them to logged-out people (Bluesky's
|
|
10
10
|
// "Discourage apps from showing my account to logged-out users", the
|
|
11
11
|
// !no-unauthenticated label) are respected too, as the page is public:
|
|
12
|
-
// their comments become a bare placeholder (no name, picture,
|
|
13
|
-
//
|
|
14
|
-
//
|
|
12
|
+
// their comments become a bare placeholder (no name, picture or words, only
|
|
13
|
+
// the comment's AT URI), keeping the replies under them, which are other
|
|
14
|
+
// people's, and their faces among who liked and reposted become anonymous (only
|
|
15
|
+
// their DID), so the counts match. A signed-in reader's browser then fetches
|
|
16
|
+
// them with the reader's own sign-in and shows them (AtmosphereComments.astro).
|
|
15
17
|
// Cached for two minutes per announcement; if Bluesky can't be reached the
|
|
16
18
|
// last good copy is kept.
|
|
17
19
|
import { rememberAvatar } from "./avatars";
|
|
@@ -27,7 +29,15 @@ const MAX_FACES = 48;
|
|
|
27
29
|
const HIDING_LABELS = new Set(["!hide", "!takedown", "!suspend", "spam", "porn", "sexual", "nudity", "graphic-media", "gore"]);
|
|
28
30
|
const LOGGED_IN_ONLY = "!no-unauthenticated";
|
|
29
31
|
|
|
30
|
-
export type Person = {
|
|
32
|
+
export type Person = {
|
|
33
|
+
did: string;
|
|
34
|
+
handle: string;
|
|
35
|
+
displayName?: string;
|
|
36
|
+
avatar?: string;
|
|
37
|
+
url: string;
|
|
38
|
+
/** Asks not to be shown to logged-out people: only the DID is given (an anonymous face). */
|
|
39
|
+
withheld?: true;
|
|
40
|
+
};
|
|
31
41
|
|
|
32
42
|
export type Reply = {
|
|
33
43
|
uri: string;
|
|
@@ -42,8 +52,9 @@ export type Reply = {
|
|
|
42
52
|
likeCount: number;
|
|
43
53
|
repostCount: number;
|
|
44
54
|
/**
|
|
45
|
-
* Its author asks not to be shown to logged-out people: everything
|
|
46
|
-
* is left empty, and the page shows a placeholder in its
|
|
55
|
+
* Its author asks not to be shown to logged-out people: everything but
|
|
56
|
+
* `uri` and `depth` is left empty, and the page shows a placeholder in its
|
|
57
|
+
* place (and the real comment to signed-in readers).
|
|
47
58
|
*/
|
|
48
59
|
withheld?: true;
|
|
49
60
|
};
|
|
@@ -97,6 +108,13 @@ function person(author: Author): Person | null {
|
|
|
97
108
|
/** Whether an account asks apps not to show it to people who aren't signed in. */
|
|
98
109
|
const loggedInOnly = (author?: Author) => !!author?.labels?.some((label) => label.val === LOGGED_IN_ONLY);
|
|
99
110
|
|
|
111
|
+
/** Someone who liked or reposted: anonymous (their DID only) if they ask not to be shown to logged-out people. */
|
|
112
|
+
function face(actor: Author | undefined): Person[] {
|
|
113
|
+
const found = actor && person(actor);
|
|
114
|
+
if (!found) return [];
|
|
115
|
+
return [loggedInOnly(actor) ? { did: found.did, handle: "", url: "", withheld: true } : found];
|
|
116
|
+
}
|
|
117
|
+
|
|
100
118
|
function isHidden(post: ThreadPost, author: Person, url: string): boolean {
|
|
101
119
|
const labels = [...(post.labels ?? []), ...(post.author?.labels ?? [])];
|
|
102
120
|
if (labels.some((label) => label.val && HIDING_LABELS.has(label.val))) return true;
|
|
@@ -116,7 +134,7 @@ function collect(nodes: ThreadNode[] = [], depth: number, hiddenByAuthor: Set<st
|
|
|
116
134
|
if (isHidden(post, author, url)) continue;
|
|
117
135
|
if (loggedInOnly(post.author)) {
|
|
118
136
|
out.push({
|
|
119
|
-
uri:
|
|
137
|
+
uri: post.uri,
|
|
120
138
|
url: "",
|
|
121
139
|
author: { did: "", handle: "", url: "" },
|
|
122
140
|
byAuthor: false,
|
|
@@ -161,8 +179,8 @@ async function fetchThread(uri: string): Promise<Thread> {
|
|
|
161
179
|
url: rootAuthor ? `${rootAuthor.url}/post/${uri.split("/").pop()}` : "",
|
|
162
180
|
likeCount: root.likeCount ?? likes.length,
|
|
163
181
|
repostCount: root.repostCount ?? repostedBy.length,
|
|
164
|
-
likes: likes.flatMap(({ actor }) => (actor
|
|
165
|
-
reposts: repostedBy.
|
|
182
|
+
likes: likes.flatMap(({ actor }) => face(actor)),
|
|
183
|
+
reposts: repostedBy.flatMap(face),
|
|
166
184
|
replies,
|
|
167
185
|
};
|
|
168
186
|
}
|
package/locales/de.json
CHANGED
|
@@ -107,5 +107,9 @@
|
|
|
107
107
|
"permission.post": "Um von dieser Seite aus zu antworten und zu zitieren, braucht sie eine weitere Erlaubnis von deinem Atmosphere-Konto: Beiträge erstellen.",
|
|
108
108
|
"permission.react": "Um von dieser Seite aus zu liken und zu reposten, braucht sie eine weitere Erlaubnis von deinem Atmosphere-Konto: liken und reposten und das zurücknehmen.",
|
|
109
109
|
"permission.approve": "Die Seite deines Kontos (für die meisten die von Bluesky) bittet dich, das zu bestätigen, und vielleicht vorher, dich dort anzumelden. Danach kommst du direkt hierher zurück.",
|
|
110
|
-
"comment.withheld": "
|
|
110
|
+
"comment.withheld": "Die Person zeigt ihre Beiträge nur Leuten, die angemeldet sind. Melde dich an, um ihn zu sehen.",
|
|
111
|
+
"comment.withheldTitle": "Kommentar ausgeblendet",
|
|
112
|
+
"comment.withheldSignedIn": "Er konnte gerade nicht geladen werden. Lade die Seite neu.",
|
|
113
|
+
"signInFirst.view": "Melde dich an, um ausgeblendete Kommentare zu sehen",
|
|
114
|
+
"reactions.hiddenPerson": "Jemand, der sein Konto nur angemeldeten Leuten zeigt"
|
|
111
115
|
}
|
package/locales/en.json
CHANGED
|
@@ -126,5 +126,9 @@
|
|
|
126
126
|
"permission.post": "To reply and quote from this site, it needs one more permission from your Atmosphere account: to create posts.",
|
|
127
127
|
"permission.react": "To like and repost from this site, it needs one more permission from your Atmosphere account: to like and repost, and to undo that.",
|
|
128
128
|
"permission.approve": "Your account's own site (Bluesky's, for most people) will ask you to approve it, and may ask you to sign in there first. Then you'll come straight back here.",
|
|
129
|
-
"comment.withheld": "
|
|
129
|
+
"comment.withheld": "Its author shows their posts only to people who are signed in. Sign in to see it.",
|
|
130
|
+
"comment.withheldTitle": "Comment hidden",
|
|
131
|
+
"comment.withheldSignedIn": "It couldn't be loaded just now. Try reloading the page.",
|
|
132
|
+
"signInFirst.view": "Sign in to see hidden comments",
|
|
133
|
+
"reactions.hiddenPerson": "Someone who shows their account only to people who are signed in"
|
|
130
134
|
}
|
package/locales/es.json
CHANGED
|
@@ -110,5 +110,9 @@
|
|
|
110
110
|
"permission.post": "Para responder y citar desde este sitio, necesita un permiso más de tu cuenta de Atmosphere: crear publicaciones.",
|
|
111
111
|
"permission.react": "Para dar me gusta y republicar desde este sitio, necesita un permiso más de tu cuenta de Atmosphere: dar me gusta y republicar, y deshacerlo.",
|
|
112
112
|
"permission.approve": "El sitio de tu cuenta (el de Bluesky, para la mayoría) te pedirá que lo apruebes, y quizá que inicies sesión allí primero. Después volverás directamente aquí.",
|
|
113
|
-
"comment.withheld": "
|
|
113
|
+
"comment.withheld": "Su autor solo muestra sus publicaciones a quien haya iniciado sesión. Inicia sesión para verlo.",
|
|
114
|
+
"comment.withheldTitle": "Comentario oculto",
|
|
115
|
+
"comment.withheldSignedIn": "No se ha podido cargar. Prueba a recargar la página.",
|
|
116
|
+
"signInFirst.view": "Inicia sesión para ver los comentarios ocultos",
|
|
117
|
+
"reactions.hiddenPerson": "Alguien que solo muestra su cuenta a quien haya iniciado sesión"
|
|
114
118
|
}
|
package/locales/fr.json
CHANGED
|
@@ -110,5 +110,9 @@
|
|
|
110
110
|
"permission.post": "Pour répondre et citer depuis ce site, il a besoin d'une autorisation de plus de ton compte Atmosphere : créer des publications.",
|
|
111
111
|
"permission.react": "Pour aimer et republier depuis ce site, il a besoin d'une autorisation de plus de ton compte Atmosphere : aimer et republier, et l'annuler.",
|
|
112
112
|
"permission.approve": "Le site de ton compte (celui de Bluesky, pour la plupart des gens) te demandera de l'approuver, et peut-être de t'y connecter d'abord. Ensuite, tu reviendras directement ici.",
|
|
113
|
-
"comment.withheld": "
|
|
113
|
+
"comment.withheld": "Son auteur ne montre ses publications qu'aux personnes connectées. Connectez-vous pour le voir.",
|
|
114
|
+
"comment.withheldTitle": "Commentaire masqué",
|
|
115
|
+
"comment.withheldSignedIn": "Il n'a pas pu être chargé. Essayez de recharger la page.",
|
|
116
|
+
"signInFirst.view": "Connectez-vous pour voir les commentaires masqués",
|
|
117
|
+
"reactions.hiddenPerson": "Une personne qui ne montre son compte qu'aux personnes connectées"
|
|
114
118
|
}
|
package/locales/it.json
CHANGED
|
@@ -110,5 +110,9 @@
|
|
|
110
110
|
"permission.post": "Per rispondere e citare da questo sito, serve un'altra autorizzazione dal tuo account Atmosphere: creare post.",
|
|
111
111
|
"permission.react": "Per mettere mi piace e ripubblicare da questo sito, serve un'altra autorizzazione dal tuo account Atmosphere: mettere mi piace e ripubblicare, e annullarlo.",
|
|
112
112
|
"permission.approve": "Il sito del tuo account (quello di Bluesky, per la maggior parte delle persone) ti chiederà di approvare, e forse prima di accedere. Poi tornerai subito qui.",
|
|
113
|
-
"comment.withheld": "
|
|
113
|
+
"comment.withheld": "L'autore mostra i suoi post solo a chi ha effettuato l'accesso. Accedi per vederlo.",
|
|
114
|
+
"comment.withheldTitle": "Commento nascosto",
|
|
115
|
+
"comment.withheldSignedIn": "Non è stato possibile caricarlo. Prova a ricaricare la pagina.",
|
|
116
|
+
"signInFirst.view": "Accedi per vedere i commenti nascosti",
|
|
117
|
+
"reactions.hiddenPerson": "Qualcuno che mostra il proprio account solo a chi ha effettuato l'accesso"
|
|
114
118
|
}
|
package/locales/ja.json
CHANGED
|
@@ -104,5 +104,9 @@
|
|
|
104
104
|
"permission.post": "このサイトから返信や引用をするには、Atmosphereアカウントからの許可がもうひとつ必要です(投稿の作成)。",
|
|
105
105
|
"permission.react": "このサイトからいいねやリポストをするには、Atmosphereアカウントからの許可がもうひとつ必要です(いいねとリポスト、その取り消し)。",
|
|
106
106
|
"permission.approve": "アカウントのサイト(ほとんどの方はBluesky)で承認を求められます。先にそこでログインを求められることもあります。その後、すぐにここへ戻ります。",
|
|
107
|
-
"comment.withheld": "
|
|
107
|
+
"comment.withheld": "投稿者は、ログインしている人にだけ投稿を表示しています。表示するにはログインしてください。",
|
|
108
|
+
"comment.withheldTitle": "非表示のコメント",
|
|
109
|
+
"comment.withheldSignedIn": "読み込めませんでした。ページを再読み込みしてください。",
|
|
110
|
+
"signInFirst.view": "ログインすると非表示のコメントを表示できます",
|
|
111
|
+
"reactions.hiddenPerson": "ログインしている人にだけアカウントを表示している人"
|
|
108
112
|
}
|
package/locales/nl.json
CHANGED
|
@@ -107,5 +107,9 @@
|
|
|
107
107
|
"permission.post": "Om vanaf deze site te reageren en te citeren, is nog één toestemming van je Atmosphere-account nodig: berichten plaatsen.",
|
|
108
108
|
"permission.react": "Om vanaf deze site te liken en te reposten, is nog één toestemming van je Atmosphere-account nodig: liken en reposten, en dat ongedaan maken.",
|
|
109
109
|
"permission.approve": "De site van je account (voor de meeste mensen die van Bluesky) vraagt je dit goed te keuren, en misschien eerst daar in te loggen. Daarna kom je meteen hierheen terug.",
|
|
110
|
-
"comment.withheld": "
|
|
110
|
+
"comment.withheld": "De schrijver toont berichten alleen aan mensen die zijn ingelogd. Log in om de reactie te zien.",
|
|
111
|
+
"comment.withheldTitle": "Reactie verborgen",
|
|
112
|
+
"comment.withheldSignedIn": "De reactie kon nu niet worden geladen. Probeer de pagina opnieuw te laden.",
|
|
113
|
+
"signInFirst.view": "Log in om verborgen reacties te zien",
|
|
114
|
+
"reactions.hiddenPerson": "Iemand die zijn of haar account alleen toont aan mensen die zijn ingelogd"
|
|
111
115
|
}
|
package/locales/pt.json
CHANGED
|
@@ -110,5 +110,9 @@
|
|
|
110
110
|
"permission.post": "Para responder e citar por este site, ele precisa de mais uma permissão da sua conta Atmosphere: criar publicações.",
|
|
111
111
|
"permission.react": "Para curtir e repostar por este site, ele precisa de mais uma permissão da sua conta Atmosphere: curtir e repostar, e desfazer isso.",
|
|
112
112
|
"permission.approve": "O site da sua conta (o do Bluesky, para a maioria das pessoas) vai pedir que você aprove, e talvez que entre lá primeiro. Depois você volta direto para cá.",
|
|
113
|
-
"comment.withheld": "
|
|
113
|
+
"comment.withheld": "O autor mostra suas publicações apenas a quem está conectado. Entre para ver.",
|
|
114
|
+
"comment.withheldTitle": "Comentário oculto",
|
|
115
|
+
"comment.withheldSignedIn": "Não foi possível carregá-lo agora. Tente recarregar a página.",
|
|
116
|
+
"signInFirst.view": "Entre para ver os comentários ocultos",
|
|
117
|
+
"reactions.hiddenPerson": "Alguém que mostra sua conta apenas a quem está conectado"
|
|
114
118
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-atmosphere-comments",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Atmosphere Comments for Astro: Bluesky comments, likes, reposts and quote posts for Astro sites; readers sign in with their Atmosphere (AT Protocol) account and respond without leaving the page.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"astro": "^7.0.0"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@atproto/api": "^0.
|
|
57
|
+
"@atproto/api": "^0.30.3",
|
|
58
58
|
"@atproto/oauth-client-browser": "^0.5.11"
|
|
59
59
|
}
|
|
60
60
|
}
|