astro-atmosphere-comments 0.7.0 → 0.8.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 +12 -0
- package/README.md +35 -5
- package/components/AtmosphereComments.astro +19 -65
- package/components/AtmosphereFollow.astro +63 -0
- package/components/AtmosphereReactions.astro +89 -28
- package/components/AtmosphereSubscribe.astro +135 -0
- package/components/Composer.astro +7 -17
- package/components/PostPreview.astro +24 -0
- package/components/SignInForm.astro +20 -6
- package/components/atmosphere.css +93 -0
- package/lib/client/composer.ts +52 -35
- package/lib/client/done.ts +54 -0
- package/lib/resolve.ts +2 -1
- package/locales/de.json +13 -6
- package/locales/en.json +12 -6
- package/locales/es.json +13 -6
- package/locales/fr.json +13 -6
- package/locales/it.json +12 -5
- package/locales/ja.json +13 -6
- package/locales/nl.json +12 -5
- package/locales/pt.json +13 -6
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0 (8 November 2026)
|
|
4
|
+
|
|
5
|
+
- **New `AtmosphereFollow` component:** "Follow @handle on Bluesky →", a link to the account's profile that becomes a Follow / ✓ Following button for signed-in readers (with `AtmosphereReactions` in the layout). Signed out it stays a link, so nobody is asked to sign in just to follow. Props, all optional: `account` (handle or DID; default the `standardSite` publication's account), `label`, `class`. The handle and DID are looked up when the page is built, with a warning if the account can't be found or isn't in `allowedAuthors`. The Subscribe menu's "Follow on Bluesky" uses it. New key `follow.link`.
|
|
6
|
+
- **New `AtmosphereSubscribe` component:** the Subscribe button and its ⌄ menu, to place anywhere, such as at the top of a blog's list of posts (with `AtmosphereReactions` in the layout, which works the button and offers signing in). Props, all optional: `align` (`"end"` opens the menu leftwards, for a button near the right-hand edge), `copyUrl` (what Copy link copies), `links` (extra menu items, e.g. "Add to Feedly") and `bluesky` (adds "View on Bluesky"; under a post, it's the post's thread). Shows nothing without `standardSite`. `AtmosphereComments` uses it for the one under each post, so the two can't drift apart; the menu's script moved into it.
|
|
7
|
+
- **"View on Leaflet"** in the menu (`https://leaflet.pub/lish/<did>/<rkey>`), which shows any Standard.site publication, not only ones made with Leaflet. New key `subscribe.leaflet`.
|
|
8
|
+
- **The menu says what Subscribe is for, and offers following.** Its note now reads "Subscribe to read new blog posts in apps such as Leaflet or Standard Reader, or follow the feed in any feed reader. To see everything @handle posts, follow it on Bluesky." (the feed part only with `feed`; the handle is the publication's account, looked up when the page is built). Under it, "Follow on Bluesky →" links to that account's profile, and `AtmosphereReactions` turns it into a Follow / Following button for signed-in readers. Subscribing still never follows. Keys: `subscribe.panelText` changed; new `subscribe.panelTextFeed`, `subscribe.followNote`, `subscribe.follow`. The note's line height is 1.5.
|
|
9
|
+
- **Several Follow buttons for one account stay in step** (say, one in a toolbar and one in the Subscribe menu): before, only the first link for an account became a button.
|
|
10
|
+
- **A clear "Liked" or "Reposted" after signing in.** When a reader presses Like or Repost while signed out, it's finished once they're back from signing in. Now a small bubble pops up over the button, with its icon enlarged and the word, then fades, so it's clear it went through. It appears only once the account host has accepted it, and readers who ask for less motion get a plain fade. In both components, for posts and comments. New `lib/client/done.ts`; styles `.atmo-done`; buttons are marked `data-busy` while their request runs.
|
|
11
|
+
- **Replying from `AtmosphereReactions` shows the post being replied to** above the box, as the Quote preview does, because the reply won't appear on that page. The post comes through the site's `post` route (as the Quote preview's does), and also gives "Replying to" its author's handle. The preview's markup is now a shared `PostPreview` component (`.atmo__parent` for this one).
|
|
12
|
+
- **"Replying to" never shows a DID.** In `AtmosphereReactions`, a post whose `data-atmosphere-url` names its author by DID (`https://bsky.app/profile/did:plc:…/post/…`) used to show "Replying to @did:plc:…". It now shows "Replying to someone" until the post being replied to has loaded (see above), then its author's `@handle`. If that fails, it stays "someone". Addresses with a handle are shown straight away, as before.
|
|
13
|
+
- **Clearer sign-in overlay wording.** The link for readers who'd rather not sign in now says "Respond on Bluesky →" (was "Reply on Bluesky instead →", even when they'd pressed Like or Repost), after "Rather not sign in here?". "How does signing in work?" is now one short paragraph, and no longer says the site "can't post for you", which overstated it: it says the sign-in is kept in the browser and that the site (named by its address, from `site`) can use it only for what the reader approved. "No account? Create one for free" has a line of its own. The overlay's small text has its own line height (1.7 for the two lines about accounts, 1.5 for the paragraph) rather than the site's body text's. Keys: new `signIn.noAccount` and `signIn.thisSite`; `signIn.access` removed; `signIn.anyAccount`, `signIn.privacy`, `elsewhere.text` and `elsewhere.link` changed; all eight languages.
|
|
14
|
+
|
|
3
15
|
## 0.7.0 (8 November 2026)
|
|
4
16
|
|
|
5
17
|
**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".
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ See it on [saltedlolly.com](https://saltedlolly.com/2005/07/13/summer-jam-party-
|
|
|
25
25
|
3. **The sign-in stays in the reader's browser.** Your server never sees it: comments, likes and reposts go from the reader's browser straight to their own account host. So your server has no way to post for anyone, a copy of your server's files holds nothing about your readers, and when a reader signs out, the sign-in is cancelled at their host and deleted from their browser. That's it: nothing is left anywhere.
|
|
26
26
|
4. Your site asks, in one approval, only for what readers can do on it: to create posts, to like and repost, to delete their comments and to follow your accounts (and, when one of your own accounts signs in, to hide comments; with the `standardSite` option, to subscribe and recommend). Each is used only on your posts, the comments under them and your accounts. Readers can see and cancel it all at their account host.
|
|
27
27
|
|
|
28
|
-
Everyone sees Like, Repost, Quote and a box to write a comment. Pressing any of them signed out opens an overlay to sign in, and what they pressed is finished once they're back. Readers who'd rather not sign in can use the **
|
|
28
|
+
Everyone sees Like, Repost, Quote and a box to write a comment. Pressing any of them signed out opens an overlay to sign in, and what they pressed is finished once they're back. Readers who'd rather not sign in can use the **Respond on Bluesky** link in the overlay.
|
|
29
29
|
|
|
30
30
|
Images (avatars, link-card pictures), account searches (the suggestions under the sign-in box and for @mentions) and account lookups when signing in go through your server, so readers' browsers contact only their own account host, never Bluesky's other servers.
|
|
31
31
|
|
|
@@ -101,9 +101,21 @@ import AtmosphereReactions from "astro-atmosphere-comments/AtmosphereReactions.a
|
|
|
101
101
|
<AtmosphereReactions />
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
Give `data-atmosphere-url` with the author's handle, as above. An address with a DID instead (`/profile/did:plc:…/post/…`) works too: the reply box then shows "Replying to someone" until the post has loaded through your site, then the handle, and never the DID.
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
Each marked post gets a row of icons: Reply, Repost, Quote and Like, with counts for Repost and Like. Readers already signed in on your site like and repost straight away, and Reply and Quote open the comment box in an overlay (Reply with the post being replied to above the box, Quote with the quote preview); what they post goes to Bluesky, not your page, and the overlay says so with a link to it. Anyone else is offered the sign-in form in an overlay when they press Like or Repost (their like or repost is done when they come back, with a brief "Liked" or "Reposted" over the button), and Reply and Quote open the post on Bluesky. It works with posts from loaders such as [`@ascorbic/bluesky-loader`](https://www.npmjs.com/package/@ascorbic/bluesky-loader) or embeds such as [`@astro-community/astro-embed-bluesky`](https://www.npmjs.com/package/@astro-community/astro-embed-bluesky) (wrap the embed in the marked element). Only posts by your `allowedAuthors` can be liked or reposted through your site.
|
|
107
|
+
|
|
108
|
+
A **Follow** button: `AtmosphereFollow` shows "Follow @you.bsky.social on Bluesky →", a link to the account's Bluesky profile, and for signed-in readers it becomes a Follow button, showing "Following" once they do (a second press unfollows, as on Bluesky). Readers who aren't signed in just follow on Bluesky: they're never asked to sign in here for it.
|
|
109
|
+
|
|
110
|
+
```astro
|
|
111
|
+
---
|
|
112
|
+
import AtmosphereFollow from "astro-atmosphere-comments/AtmosphereFollow.astro";
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
<AtmosphereFollow account="you.bsky.social" />
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`account` takes a handle or a DID (default: your `standardSite` publication's account), `label` replaces the wording and `class` styles the link; the handle and DID are looked up when the page is built. Only your `allowedAuthors` can be followed through your site (others stay a link, with a warning in the build), and the account's owner, who can't follow themselves, keeps the plain link. It needs `AtmosphereReactions` in your layout. A link of your own works too, marked with the account's DID and handle:
|
|
107
119
|
|
|
108
120
|
```astro
|
|
109
121
|
<a href="https://bsky.app/profile/you.bsky.social"
|
|
@@ -112,7 +124,25 @@ A "follow me" link can become a **Follow** button for signed-in readers: mark it
|
|
|
112
124
|
|
|
113
125
|
**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
126
|
|
|
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
|
|
127
|
+
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, and a ⌄ menu of more ways to follow your site: a note on what subscribing does with a Follow on Bluesky link, Copy link, your feed (the option's `feed`), View on Bluesky (the post's thread), View on Standard Reader and View on Leaflet.
|
|
128
|
+
|
|
129
|
+
The same button and menu can go anywhere, such as at the top of your blog's list of posts, with `AtmosphereSubscribe` (and `AtmosphereReactions` in your layout, which works the button and offers signing in):
|
|
130
|
+
|
|
131
|
+
```astro
|
|
132
|
+
---
|
|
133
|
+
import AtmosphereSubscribe from "astro-atmosphere-comments/AtmosphereSubscribe.astro";
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
<AtmosphereSubscribe
|
|
137
|
+
align="end"
|
|
138
|
+
copyUrl="https://example.com/blog/"
|
|
139
|
+
links={[{ label: "Add to Feedly", href: "https://feedly.com/i/subscription/feed%2Fhttps%3A%2F%2Fexample.com%2Ffeed%2F" }]}
|
|
140
|
+
/>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
All its props are optional. `align="end"` lines the menu up with the button's right-hand edge, for a button near the right of the page (the default, `"start"`, lines it up on the left). `copyUrl` is what Copy link copies (default: your site's address). `links` adds items to the end of the menu. `bluesky` adds a View on Bluesky item going there (under each post it's the post's thread). The menu's note also suggests following your publication's account on Bluesky, with a link to its profile that becomes a Follow / Following button for signed-in readers; subscribing itself never follows. It shows nothing without the `standardSite` option, and needs JavaScript.
|
|
144
|
+
|
|
145
|
+
For a button of your own design instead (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.
|
|
116
146
|
|
|
117
147
|
```astro
|
|
118
148
|
<button type="button" data-atmosphere-subscribe hidden>
|
|
@@ -159,7 +189,7 @@ Locally the integration uses the protocol's development mode, so it works withou
|
|
|
159
189
|
| `post` | The announcement: bsky.app address or AT URI. Optional with Standard.site (above). |
|
|
160
190
|
| `label` | The sign-in overlay's heading when commenting. Default "Sign in to leave a comment". |
|
|
161
191
|
| `authorLabel` | The badge on your own comments. Default "Author". |
|
|
162
|
-
| `blueskyLink` | "Rather not sign in here?
|
|
192
|
+
| `blueskyLink` | "Rather not sign in here? Respond on Bluesky →" in the sign-in overlay, linking to the announcement (with `u-syndication`, so microformats readers learn it's the post's copy on Bluesky). Default `true`; `blueskyLink={false}` hides it. |
|
|
163
193
|
| `locale`, `messages` | The same as the integration's options, for this one component. |
|
|
164
194
|
| `locale`, `messages` (also on `AtmosphereReactions`) | See Languages. |
|
|
165
195
|
| `credit` | A small "Powered by Atmosphere Comments for Astro" link at the end. Default `true`; set `credit={false}` to hide it. |
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Props:
|
|
8
8
|
// post the announcement's AT URI (at://did/app.bsky.feed.post/rkey).
|
|
9
9
|
// label the sign-in overlay's heading when commenting (default "Sign in to leave a comment").
|
|
10
|
-
// blueskyLink "Rather not sign in here?
|
|
10
|
+
// blueskyLink "Rather not sign in here? Respond on Bluesky →" in the
|
|
11
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).
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
import settings from "../lib/settings";
|
|
39
39
|
import SignInForm from "./SignInForm.astro";
|
|
40
40
|
import Composer from "./Composer.astro";
|
|
41
|
+
import AtmosphereSubscribe from "./AtmosphereSubscribe.astro";
|
|
41
42
|
import "./atmosphere.css";
|
|
42
43
|
import { messagesFor, pickLocale, translate, translateHtml } from "../lib/i18n";
|
|
43
44
|
import { escapeHtml } from "../lib/translate";
|
|
@@ -56,7 +57,7 @@ interface Props {
|
|
|
56
57
|
authorLabel?: string;
|
|
57
58
|
/** A small "Powered by Atmosphere Comments for Astro" link under the comments. Default true. */
|
|
58
59
|
credit?: boolean;
|
|
59
|
-
/** "Rather not sign in here?
|
|
60
|
+
/** "Rather not sign in here? Respond on Bluesky →" in the sign-in overlay. Default true. */
|
|
60
61
|
blueskyLink?: boolean;
|
|
61
62
|
/** The language of the wording, e.g. "fr". Overrides the integration's `locale`. */
|
|
62
63
|
locale?: string;
|
|
@@ -110,31 +111,8 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
110
111
|
<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>
|
|
111
112
|
<span>{t("actions.quote")}</span>
|
|
112
113
|
</button>
|
|
113
|
-
{/* With the `standardSite` option. */}
|
|
114
|
-
|
|
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
|
-
)}
|
|
114
|
+
{/* With the `standardSite` option: Subscribe and its ⌄ menu. "View on Bluesky" is this post's thread. */}
|
|
115
|
+
<AtmosphereSubscribe bluesky={blueskyUrl || undefined} locale={lang} messages={messages} />
|
|
138
116
|
</div>
|
|
139
117
|
|
|
140
118
|
{/* A sign-in from before posting was asked for: one more permission. */}
|
|
@@ -195,6 +173,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
195
173
|
import { translate } from "../lib/translate";
|
|
196
174
|
import { enhanceHandleBox } from "../lib/client/handle-suggestions";
|
|
197
175
|
import { MAX, quotePreview, setUpComposer, type ComposerBehaviour, type Posted } from "../lib/client/composer";
|
|
176
|
+
import { showDoneWhenPressed } from "../lib/client/done";
|
|
198
177
|
import { handleSignInForm, me as whoIsSignedIn, send, signIn, signOut, state, withheldPosts } from "../lib/client/account";
|
|
199
178
|
import publicSettings from "../lib/public-settings";
|
|
200
179
|
import { segments } from "../lib/richtext";
|
|
@@ -669,43 +648,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
669
648
|
const askToSubscribe = (problem?: string) => openSignIn(t("subscribe.signIn"), "#atmo-subscribe", problem);
|
|
670
649
|
if (subscribePanel) addSubscribeButtons([...subscribePanel.querySelectorAll<HTMLButtonElement>("[data-atmosphere-subscribe]")], t, askToSubscribe);
|
|
671
650
|
|
|
672
|
-
// The ⌄ menu beside Subscribe
|
|
673
|
-
const moreButton = subscribePanel?.querySelector<HTMLButtonElement>("[data-subscribe-more]");
|
|
674
|
-
const moreMenu = subscribePanel?.querySelector<HTMLElement>("[data-subscribe-menu]");
|
|
675
|
-
if (subscribePanel && moreButton && moreMenu) {
|
|
676
|
-
const setMenu = (open: boolean) => {
|
|
677
|
-
moreMenu.hidden = !open;
|
|
678
|
-
moreButton.setAttribute("aria-expanded", String(open));
|
|
679
|
-
};
|
|
680
|
-
moreButton.addEventListener("click", () => {
|
|
681
|
-
setMenu(moreMenu.hidden);
|
|
682
|
-
if (!moreMenu.hidden) moreMenu.querySelector<HTMLElement>("[data-subscribe-copy]")?.focus();
|
|
683
|
-
});
|
|
684
|
-
document.addEventListener("click", (event) => {
|
|
685
|
-
if (!subscribePanel.contains(event.target as Node)) setMenu(false);
|
|
686
|
-
});
|
|
687
|
-
subscribePanel.addEventListener("keydown", (event) => {
|
|
688
|
-
if (event.key === "Escape" && !moreMenu.hidden) {
|
|
689
|
-
setMenu(false);
|
|
690
|
-
moreButton.focus();
|
|
691
|
-
}
|
|
692
|
-
});
|
|
693
|
-
const copy = moreMenu.querySelector<HTMLButtonElement>("[data-subscribe-copy]")!;
|
|
694
|
-
const copyText = copy.querySelector<HTMLElement>("[data-copy-label]")!;
|
|
695
|
-
const copyLabel = copyText.textContent;
|
|
696
|
-
copy.addEventListener("click", async () => {
|
|
697
|
-
try {
|
|
698
|
-
await navigator.clipboard.writeText(copy.dataset.subscribeCopy!);
|
|
699
|
-
copyText.textContent = copy.dataset.copied!;
|
|
700
|
-
window.setTimeout(() => {
|
|
701
|
-
copyText.textContent = copyLabel;
|
|
702
|
-
setMenu(false);
|
|
703
|
-
}, 1200);
|
|
704
|
-
} catch {
|
|
705
|
-
// Clipboard refused: the button stays as it was.
|
|
706
|
-
}
|
|
707
|
-
});
|
|
708
|
-
}
|
|
651
|
+
// The ⌄ menu beside Subscribe is worked by AtmosphereSubscribe itself.
|
|
709
652
|
|
|
710
653
|
// What the reader has liked or reposted, by post (filled in by loadState).
|
|
711
654
|
let known: Record<string, { liked: boolean; reposted: boolean }> = {};
|
|
@@ -728,6 +671,8 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
728
671
|
if (!isSignedIn) return askToSignIn(kind === "like" ? "comment-like" : "comment-repost", comment.dataset.atmosphereUri);
|
|
729
672
|
const on = button.getAttribute("aria-pressed") !== "true";
|
|
730
673
|
const before = Number(button.dataset.count || 0);
|
|
674
|
+
// Busy until the account host answers (lib/client/done.ts waits for it).
|
|
675
|
+
button.dataset.busy = "";
|
|
731
676
|
button.setAttribute("aria-pressed", String(on));
|
|
732
677
|
setCount(Math.max(0, before + (on ? 1 : -1)));
|
|
733
678
|
try {
|
|
@@ -738,6 +683,8 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
738
683
|
setCount(before);
|
|
739
684
|
const { error, permission } = (problem ?? {}) as { error?: string; permission?: string };
|
|
740
685
|
handleError(error ?? "error", say, permission);
|
|
686
|
+
} finally {
|
|
687
|
+
delete button.dataset.busy;
|
|
741
688
|
}
|
|
742
689
|
});
|
|
743
690
|
return button;
|
|
@@ -943,8 +890,11 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
943
890
|
return;
|
|
944
891
|
}
|
|
945
892
|
const comment = uri ? document.querySelector<HTMLElement>(`[data-atmosphere-uri="${CSS.escape(uri)}"]`) : null;
|
|
893
|
+
// Pressed for them, with a brief "Liked" or "Reposted" once it's gone through.
|
|
946
894
|
const press = (button: HTMLElement | null | undefined) => {
|
|
947
|
-
if (button
|
|
895
|
+
if (!button || button.getAttribute("aria-pressed") === "true") return;
|
|
896
|
+
showDoneWhenPressed(button, t(kind.endsWith("like") ? "actions.liked" : "actions.reposted"));
|
|
897
|
+
button.click();
|
|
948
898
|
};
|
|
949
899
|
(comment ?? this).scrollIntoView({ block: "center" });
|
|
950
900
|
if (kind === "like" || kind === "repost") press(reactButton(kind));
|
|
@@ -1101,6 +1051,8 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
1101
1051
|
reactButton(kind).addEventListener("click", async () => {
|
|
1102
1052
|
if (!isSignedIn) return askToSignIn(kind);
|
|
1103
1053
|
const on = reactButton(kind).getAttribute("aria-pressed") !== "true";
|
|
1054
|
+
// Busy until the account host answers (lib/client/done.ts waits for it).
|
|
1055
|
+
reactButton(kind).dataset.busy = "";
|
|
1104
1056
|
setPressed(kind, on);
|
|
1105
1057
|
try {
|
|
1106
1058
|
// Liking the post also recommends the page's Standard.site record, if it has one.
|
|
@@ -1122,6 +1074,8 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
1122
1074
|
} catch {
|
|
1123
1075
|
setPressed(kind, !on);
|
|
1124
1076
|
say(message("error"));
|
|
1077
|
+
} finally {
|
|
1078
|
+
delete reactButton(kind).dataset.busy;
|
|
1125
1079
|
}
|
|
1126
1080
|
});
|
|
1127
1081
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
// "Follow @handle on Bluesky →": a link to the account's Bluesky profile,
|
|
3
|
+
// which AtmosphereReactions (in the layout) turns into a Follow / ✓ Following
|
|
4
|
+
// button for signed-in readers. Signed out it stays a link, so readers follow
|
|
5
|
+
// on Bluesky itself: nobody is asked to sign in here just to follow.
|
|
6
|
+
//
|
|
7
|
+
// Props (all optional):
|
|
8
|
+
// account the account to follow, by DID or handle (default: the
|
|
9
|
+
// `standardSite` publication's account). Must be one of the
|
|
10
|
+
// integration's `allowedAuthors` for the button to work.
|
|
11
|
+
// label the link's text, instead of "Follow @handle on Bluesky"
|
|
12
|
+
// class classes for the link
|
|
13
|
+
// locale, messages as for AtmosphereComments
|
|
14
|
+
import settings from "../lib/settings";
|
|
15
|
+
import { messagesFor, pickLocale, translate } from "../lib/i18n";
|
|
16
|
+
import { profileFor } from "../lib/profile";
|
|
17
|
+
import { resolveHandle } from "../lib/resolve";
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
account?: string;
|
|
21
|
+
label?: string;
|
|
22
|
+
class?: string;
|
|
23
|
+
locale?: string;
|
|
24
|
+
messages?: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const lang = pickLocale(Astro.props.locale, settings.locale, Astro.currentLocale);
|
|
28
|
+
const messages = messagesFor(lang, { ...settings.messages, ...Astro.props.messages });
|
|
29
|
+
|
|
30
|
+
// The DID and handle, looked up when the page is built.
|
|
31
|
+
const given = (Astro.props.account ?? settings.standardSite?.publication?.split("/")[2] ?? "").replace(/^@/, "");
|
|
32
|
+
let did = "";
|
|
33
|
+
let handle = "";
|
|
34
|
+
if (given.startsWith("did:")) {
|
|
35
|
+
did = given;
|
|
36
|
+
const profile = await profileFor(did).catch(() => null);
|
|
37
|
+
if (profile && profile.handle !== did) handle = profile.handle;
|
|
38
|
+
} else if (given) {
|
|
39
|
+
handle = given;
|
|
40
|
+
did = await resolveHandle(given).catch(() => "");
|
|
41
|
+
}
|
|
42
|
+
if (given && !did) {
|
|
43
|
+
console.warn(`[atmosphere-comments] AtmosphereFollow: couldn't find the account ${given}, so it's a plain link.`);
|
|
44
|
+
} else if (did && !settings.allowedAuthors.includes(did)) {
|
|
45
|
+
console.warn(
|
|
46
|
+
`[atmosphere-comments] AtmosphereFollow: ${handle || did} isn't in allowedAuthors, so it stays a link for everyone.`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const label =
|
|
50
|
+
Astro.props.label ??
|
|
51
|
+
(handle ? translate(messages, lang, "follow.link", { handle: `@${handle}` }) : translate(messages, lang, "subscribe.follow"));
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
{(did || handle) && (
|
|
55
|
+
<a
|
|
56
|
+
class:list={["atmo-follow-link", Astro.props.class]}
|
|
57
|
+
href={`https://bsky.app/profile/${handle || did}`}
|
|
58
|
+
target="_blank"
|
|
59
|
+
rel="noopener"
|
|
60
|
+
data-atmosphere-follow={did && settings.allowedAuthors.includes(did) ? did : undefined}
|
|
61
|
+
data-atmosphere-follow-handle={handle || undefined}
|
|
62
|
+
>{label} →</a>
|
|
63
|
+
)}
|
|
@@ -92,7 +92,8 @@ const { base } = settings;
|
|
|
92
92
|
<script>
|
|
93
93
|
import { translate } from "../lib/translate";
|
|
94
94
|
import { enhanceHandleBox } from "../lib/client/handle-suggestions";
|
|
95
|
-
import { MAX, quotePreview, setUpComposer, type Posted } from "../lib/client/composer";
|
|
95
|
+
import { MAX, fetchPost, quotePreview, setUpComposer, showPost, type Posted, type ShownPost } from "../lib/client/composer";
|
|
96
|
+
import { showDone } from "../lib/client/done";
|
|
96
97
|
import { following, handleSignInForm, me as whoIsSignedIn, send as act, signIn, signOut as endSignIn, state } from "../lib/client/account";
|
|
97
98
|
import { addSubscribeButtons, finishPendingSubscribe, loadSubscribed } from "../lib/client/subscribe";
|
|
98
99
|
import type { Permission } from "../lib/permissions";
|
|
@@ -211,13 +212,45 @@ const { base } = settings;
|
|
|
211
212
|
const quote = quotePreview(composeForm, composer.textarea, composerContext);
|
|
212
213
|
$c<HTMLElement>("[data-cancel-context]").hidden = true;
|
|
213
214
|
|
|
215
|
+
// The post being replied to, shown above the box, since the reply won't
|
|
216
|
+
// appear on this page: fetched through the site once per post.
|
|
217
|
+
const parent = $c<HTMLElement>("[data-parent]");
|
|
218
|
+
const fetched = new Map<string, Promise<ShownPost | null>>();
|
|
219
|
+
const postFor = (uri: string) => {
|
|
220
|
+
if (!fetched.has(uri)) fetched.set(uri, fetchPost(base, uri));
|
|
221
|
+
return fetched.get(uri)!;
|
|
222
|
+
};
|
|
223
|
+
// A handle, never a DID: a post's address may name its author either way
|
|
224
|
+
// (Bluesky accepts both), and a lookup that fails gives back the DID.
|
|
225
|
+
const shownHandle = (handle?: string | null) =>
|
|
226
|
+
handle && !handle.startsWith("did:") && handle !== "handle.invalid" ? handle : null;
|
|
227
|
+
const replyingTo = (handle?: string | null) =>
|
|
228
|
+
t("composer.replyingTo", { name: shownHandle(handle) ? `@${handle}` : t("comment.someone") });
|
|
229
|
+
|
|
214
230
|
const openCompose = (post: HTMLElement, mode: "reply" | "quote") => {
|
|
215
231
|
composeRoot = post.dataset.atmospherePost!;
|
|
216
232
|
composeMode = mode;
|
|
217
|
-
|
|
233
|
+
// From the bsky.app address until the post itself arrives.
|
|
234
|
+
let author: string | undefined;
|
|
235
|
+
try {
|
|
236
|
+
author = decodeURIComponent(post.dataset.atmosphereUrl?.match(/\/profile\/([^/]+)\//)?.[1] ?? "");
|
|
237
|
+
} catch {
|
|
238
|
+
author = undefined;
|
|
239
|
+
}
|
|
240
|
+
const context = $c<HTMLElement>("[data-context-text]");
|
|
218
241
|
$c<HTMLElement>("[data-context]").hidden = false;
|
|
219
|
-
|
|
220
|
-
|
|
242
|
+
parent.hidden = true;
|
|
243
|
+
if (mode === "quote") context.textContent = t("composer.quoting");
|
|
244
|
+
else {
|
|
245
|
+
context.textContent = replyingTo(author);
|
|
246
|
+
const root = composeRoot;
|
|
247
|
+
void postFor(root).then((shown) => {
|
|
248
|
+
if (!shown || composeRoot !== root || composeMode !== "reply") return;
|
|
249
|
+
showPost(parent, shown, composerContext);
|
|
250
|
+
parent.hidden = false;
|
|
251
|
+
if (shownHandle(shown.author.handle)) context.textContent = replyingTo(shown.author.handle);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
221
254
|
$c<HTMLElement>("[data-verb]").textContent = t(mode === "quote" ? "composer.quote" : "comment.reply");
|
|
222
255
|
$c<HTMLElement>("[data-submit]").textContent = t(mode === "quote" ? "composer.postQuote" : "composer.postReply");
|
|
223
256
|
$c<HTMLElement>("[data-preview]").hidden = mode !== "quote";
|
|
@@ -407,7 +440,16 @@ const { base } = settings;
|
|
|
407
440
|
// Follow buttons in place of links marked data-atmosphere-follow, for
|
|
408
441
|
// signed-in readers: "Follow @handle", then "Following" (a second press
|
|
409
442
|
// unfollows, as on Bluesky, without inviting it).
|
|
410
|
-
|
|
443
|
+
// One entry per account, however many links for it the page has (say,
|
|
444
|
+
// one in a toolbar and one in the Subscribe menu): they all agree.
|
|
445
|
+
type FollowState = {
|
|
446
|
+
following: boolean;
|
|
447
|
+
buttons: HTMLButtonElement[];
|
|
448
|
+
renders: (() => void)[];
|
|
449
|
+
set: (on: boolean) => void;
|
|
450
|
+
press: () => Promise<void>;
|
|
451
|
+
};
|
|
452
|
+
const followButtons = new Map<string, FollowState>();
|
|
411
453
|
// Following isn't part of signing in: the first follow asks the
|
|
412
454
|
// reader's account host for it, then carries on when they're back.
|
|
413
455
|
const askFollowPermission = (subject: string, who: string) =>
|
|
@@ -415,19 +457,47 @@ const { base } = settings;
|
|
|
415
457
|
const setUpFollow = async () => {
|
|
416
458
|
for (const link of document.querySelectorAll<HTMLElement>("[data-atmosphere-follow]")) {
|
|
417
459
|
const subject = link.dataset.atmosphereFollow!;
|
|
418
|
-
if (
|
|
460
|
+
if (link.nextElementSibling?.matches(".atmo-follow")) continue;
|
|
419
461
|
// Nobody can follow themselves: the account's owner keeps the link.
|
|
420
462
|
if (subject === me.did) continue;
|
|
421
463
|
const who = link.dataset.atmosphereFollowHandle ? `@${link.dataset.atmosphereFollowHandle}` : t("comment.someone");
|
|
464
|
+
let state = followButtons.get(subject);
|
|
465
|
+
const isNew = !state;
|
|
466
|
+
if (!state) {
|
|
467
|
+
const created: FollowState = {
|
|
468
|
+
following: false,
|
|
469
|
+
buttons: [],
|
|
470
|
+
renders: [],
|
|
471
|
+
set: (on) => {
|
|
472
|
+
created.following = on;
|
|
473
|
+
created.renders.forEach((render) => render());
|
|
474
|
+
},
|
|
475
|
+
press: async () => {
|
|
476
|
+
created.buttons.forEach((each) => (each.disabled = true));
|
|
477
|
+
try {
|
|
478
|
+
const response = await act("follow", { subject, on: !created.following });
|
|
479
|
+
const data = (await response.json().catch(() => ({}))) as { following?: boolean; error?: string };
|
|
480
|
+
if (response.ok) return created.set(!!data.following);
|
|
481
|
+
if (data.error === "needs-permission") return void askFollowPermission(subject, who);
|
|
482
|
+
if (data.error === "signed-out") location.reload();
|
|
483
|
+
} finally {
|
|
484
|
+
created.buttons.forEach((each) => (each.disabled = false));
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
state = created;
|
|
489
|
+
followButtons.set(subject, state);
|
|
490
|
+
}
|
|
422
491
|
const button = make("button", "atmo-follow");
|
|
423
492
|
button.type = "button";
|
|
424
|
-
let isFollowing = false;
|
|
425
493
|
// The account's avatar, through the site (its own accounts' are always served).
|
|
426
494
|
const avatar = make("img", "atmo-follow__avatar");
|
|
427
495
|
avatar.src = `${base}/media?avatar=${encodeURIComponent(subject)}`;
|
|
428
496
|
avatar.alt = "";
|
|
429
497
|
avatar.onerror = () => avatar.remove();
|
|
498
|
+
const current = state;
|
|
430
499
|
const render = () => {
|
|
500
|
+
const isFollowing = current.following;
|
|
431
501
|
button.setAttribute("aria-pressed", String(isFollowing));
|
|
432
502
|
// The butterfly, then "Follow" with the account's avatar beside its
|
|
433
503
|
// handle (wherever the language puts the handle), or the avatar
|
|
@@ -444,32 +514,19 @@ const { base } = settings;
|
|
|
444
514
|
button.title = isFollowing ? t("follow.followingLabel", { handle: who }) : t("follow.followAs", { handle: who, me: handle });
|
|
445
515
|
button.setAttribute("aria-label", button.title);
|
|
446
516
|
};
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
};
|
|
451
|
-
const press = async () => {
|
|
452
|
-
button.disabled = true;
|
|
453
|
-
try {
|
|
454
|
-
const response = await act("follow", { subject, on: !isFollowing });
|
|
455
|
-
const data = (await response.json().catch(() => ({}))) as { following?: boolean; error?: string };
|
|
456
|
-
if (response.ok) return set(!!data.following);
|
|
457
|
-
if (data.error === "needs-permission") return void askFollowPermission(subject, who);
|
|
458
|
-
if (data.error === "signed-out") location.reload();
|
|
459
|
-
} finally {
|
|
460
|
-
button.disabled = false;
|
|
461
|
-
}
|
|
462
|
-
};
|
|
463
|
-
button.addEventListener("click", () => void press());
|
|
464
|
-
followButtons.set(subject, { get following() { return isFollowing; }, set, press });
|
|
517
|
+
button.addEventListener("click", () => void current.press());
|
|
518
|
+
current.buttons.push(button);
|
|
519
|
+
current.renders.push(render);
|
|
465
520
|
render();
|
|
466
521
|
// The button takes the link's place. (Inline, as a site's own style
|
|
467
522
|
// for the link could otherwise keep it showing.)
|
|
468
523
|
link.hidden = true;
|
|
469
524
|
link.style.display = "none";
|
|
470
525
|
link.after(button);
|
|
471
|
-
|
|
472
|
-
|
|
526
|
+
if (isNew) {
|
|
527
|
+
const known = await following(subject);
|
|
528
|
+
if (known !== null) current.set(known);
|
|
529
|
+
}
|
|
473
530
|
}
|
|
474
531
|
};
|
|
475
532
|
|
|
@@ -516,7 +573,11 @@ const { base } = settings;
|
|
|
516
573
|
return;
|
|
517
574
|
}
|
|
518
575
|
const button = post.querySelector<HTMLButtonElement>(`.atmo-post-action[data-kind='${kind}']`);
|
|
519
|
-
if (button && button.getAttribute("aria-pressed") !== "true")
|
|
576
|
+
if (button && button.getAttribute("aria-pressed") !== "true") {
|
|
577
|
+
await react(post, button, kind as Kind, true);
|
|
578
|
+
// A brief "Liked" or "Reposted", so it's clear it went through.
|
|
579
|
+
if (button.getAttribute("aria-pressed") === "true") showDone(button, t(kind === "like" ? "actions.liked" : "actions.reposted"));
|
|
580
|
+
}
|
|
520
581
|
};
|
|
521
582
|
|
|
522
583
|
// Back from signing in or allowing posting: open the Reply or Quote box they pressed.
|