astro-atmosphere-comments 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +21 -23
  3. package/components/AtmosphereComments.astro +179 -78
  4. package/components/AtmosphereReactions.astro +86 -51
  5. package/components/SignInForm.astro +14 -3
  6. package/components/atmosphere.css +21 -0
  7. package/index.mjs +42 -32
  8. package/lib/client/account.ts +553 -0
  9. package/lib/client/composer.ts +7 -10
  10. package/lib/http.ts +28 -0
  11. package/lib/permissions.ts +108 -0
  12. package/lib/public-settings.ts +18 -0
  13. package/lib/thread.ts +36 -6
  14. package/lib/uris.ts +31 -0
  15. package/locales/de.json +20 -4
  16. package/locales/en.json +20 -4
  17. package/locales/es.json +20 -4
  18. package/locales/fr.json +20 -4
  19. package/locales/it.json +20 -4
  20. package/locales/ja.json +20 -4
  21. package/locales/nl.json +20 -4
  22. package/locales/pt.json +20 -4
  23. package/package.json +2 -7
  24. package/routes/callback.astro +42 -0
  25. package/routes/client-metadata.json.ts +3 -2
  26. package/routes/following.ts +28 -0
  27. package/routes/identity.ts +69 -0
  28. package/routes/media.ts +7 -10
  29. package/routes/post.ts +1 -1
  30. package/routes/profile.ts +20 -0
  31. package/routes/richtext.ts +30 -0
  32. package/routes/thread.ts +23 -3
  33. package/routes/typeahead.ts +7 -23
  34. package/virtual.d.ts +5 -0
  35. package/bin/cli.mjs +0 -31
  36. package/lib/oauth.ts +0 -110
  37. package/lib/reader.ts +0 -130
  38. package/lib/store.ts +0 -46
  39. package/lib/visitor.ts +0 -42
  40. package/routes/callback.ts +0 -30
  41. package/routes/comment.ts +0 -97
  42. package/routes/delete.ts +0 -41
  43. package/routes/follow.ts +0 -71
  44. package/routes/jwks.json.ts +0 -13
  45. package/routes/login.ts +0 -58
  46. package/routes/logout.ts +0 -20
  47. package/routes/me.ts +0 -12
  48. package/routes/react.ts +0 -48
  49. package/routes/state.ts +0 -24
@@ -0,0 +1,108 @@
1
+ // What a reader's account host is asked to allow, and the site's OAuth
2
+ // client metadata. Shared by the server (routes/client-metadata.json.ts) and
3
+ // the browser (lib/client/account.ts), so both always agree.
4
+ //
5
+ // The site is a browser ("public") client: a reader's sign-in is kept only in
6
+ // their own browser, and the site's server never sees it. Signing out cancels
7
+ // it at the reader's account host.
8
+
9
+ // What every signed-in reader grants: their identity, and reading posts as
10
+ // they see them (to know whether they've already liked or reposted),
11
+ // through Bluesky's AppView. Nothing here can change their account.
12
+ export const BASE_SCOPE = "atproto rpc:app.bsky.feed.getPosts?aud=did:web:api.bsky.app%23bsky_appview";
13
+
14
+ // Everything else comes in groups. Signing in asks for all that a reader can
15
+ // do here at once (DEFAULT_PERMISSIONS), so there's one approval rather than
16
+ // a trip to their account host for each new thing: as the sign-in stays in
17
+ // their browser, the server can't use any of it, and the page's own code has
18
+ // the same reach either way. (A permission can only name a kind of record,
19
+ // not particular posts, so lib/client/account.ts limits each one to the
20
+ // site's own threads.)
21
+ export const PERMISSIONS = {
22
+ // Comments, replies and quote posts.
23
+ post: "repo:app.bsky.feed.post?action=create",
24
+ // Likes and reposts, and taking them back.
25
+ react: "repo:app.bsky.feed.like?action=create&action=delete repo:app.bsky.feed.repost?action=create&action=delete",
26
+ // Deleting the reader's own comments.
27
+ delete: "repo:app.bsky.feed.post?action=delete",
28
+ // Following (and unfollowing) the site's own accounts.
29
+ follow: "repo:app.bsky.graph.follow?action=create&action=delete",
30
+ // For the site's own accounts: hiding comments under their announcements.
31
+ moderate: "repo:app.bsky.feed.threadgate?action=create&action=update",
32
+ } as const;
33
+
34
+ export type Permission = keyof typeof PERMISSIONS;
35
+
36
+ export const isPermission = (value: unknown): value is Permission =>
37
+ typeof value === "string" && Object.hasOwn(PERMISSIONS, value);
38
+
39
+ // What signing in asks for: everything readers can do here. "moderate" is
40
+ // added only for the site's own accounts (allowedAuthors), the only ones
41
+ // that can use it. Sign-ins from before a group was added are asked for it
42
+ // when it's first needed.
43
+ export const DEFAULT_PERMISSIONS: Permission[] = ["post", "react", "delete", "follow"];
44
+
45
+ /** The scope to ask for: the base, plus the groups named. */
46
+ export const scopeFor = (permissions: Iterable<Permission>) =>
47
+ [BASE_SCOPE, ...new Set([...permissions].map((name) => PERMISSIONS[name]))].join(" ");
48
+
49
+ // Everything the site may ask for, which its client metadata has to list.
50
+ export const ALL_SCOPES = scopeFor(Object.keys(PERMISSIONS) as Permission[]);
51
+
52
+ /**
53
+ * Whether `scopes` (a grant) allow `action` on records of `collection`: a
54
+ * repo permission for that collection (or all of them) listing the action,
55
+ * or listing no actions, which means all of them.
56
+ */
57
+ export function scopesAllow(scopes: string[], collection: string, action: string): boolean {
58
+ return scopes.some((scope) => {
59
+ const [name, query = ""] = scope.split("?");
60
+ if (name !== `repo:${collection}` && name !== "repo:*") return false;
61
+ const actions = new URLSearchParams(query).getAll("action");
62
+ return !actions.length || actions.includes(action);
63
+ });
64
+ }
65
+
66
+ /** The permission groups a grant covers in full. */
67
+ export function grantedPermissions(scopes: string[]): Permission[] {
68
+ return (Object.keys(PERMISSIONS) as Permission[]).filter((name) =>
69
+ PERMISSIONS[name].split(" ").every((part) => {
70
+ const [scope, query = ""] = part.split("?");
71
+ const collection = scope.replace(/^repo:/, "");
72
+ return new URLSearchParams(query).getAll("action").every((action) => scopesAllow(scopes, collection, action));
73
+ }),
74
+ );
75
+ }
76
+
77
+ /** Whether a grant has BASE_SCOPE (compared by name, as hosts may write the rest differently). */
78
+ export function hasBase(scopes: string[]): boolean {
79
+ const names = scopes.map((part) => part.split("?")[0]);
80
+ return BASE_SCOPE.split(" ").every((part) => names.includes(part.split("?")[0]));
81
+ }
82
+
83
+ type MetadataSettings = { site: string; base: string; clientName: string; logoUri?: string; policyUri?: string; tosUri?: string };
84
+
85
+ export const callbackPath = (base: string) => `${base}/callback`;
86
+
87
+ /** The client metadata, served at /oauth-client-metadata.json (the site's client ID). */
88
+ export function clientMetadata(settings: MetadataSettings) {
89
+ return {
90
+ // At the site's root, the address account hosts treat as the convention:
91
+ // Bluesky's approval page then shows just the site's address.
92
+ client_id: `${settings.site}/oauth-client-metadata.json`,
93
+ client_name: settings.clientName,
94
+ client_uri: settings.site,
95
+ ...(settings.logoUri && { logo_uri: settings.logoUri }),
96
+ ...(settings.policyUri && { policy_uri: settings.policyUri }),
97
+ ...(settings.tosUri && { tos_uri: settings.tosUri }),
98
+ redirect_uris: [`${settings.site}${callbackPath(settings.base)}`] as [`https://${string}`],
99
+ scope: ALL_SCOPES,
100
+ grant_types: ["authorization_code", "refresh_token"] as ["authorization_code", "refresh_token"],
101
+ response_types: ["code"] as ["code"],
102
+ application_type: "web" as const,
103
+ // A browser client: it has no secret to prove who it is, and its
104
+ // tokens only work together with a key kept in the reader's browser.
105
+ token_endpoint_auth_method: "none" as const,
106
+ dpop_bound_access_tokens: true,
107
+ };
108
+ }
@@ -0,0 +1,18 @@
1
+ // The integration's settings that are safe for readers' browsers: the
2
+ // site's address, the routes' base, its name and links, and its accounts.
3
+ // (The rest, such as `hidden`, stays on the server: lib/settings.ts.)
4
+ import settings from "virtual:atmosphere-comments/public-settings";
5
+
6
+ export type PublicSettings = {
7
+ clientName: string;
8
+ allowedAuthors: string[];
9
+ /** Where the routes live, e.g. "/atmosphere" (no trailing slash). */
10
+ base: string;
11
+ /** The site's address, e.g. "https://saltedlolly.com" (no trailing slash). */
12
+ site: string;
13
+ logoUri?: string;
14
+ policyUri?: string;
15
+ tosUri?: string;
16
+ };
17
+
18
+ export default settings as PublicSettings;
package/lib/thread.ts CHANGED
@@ -1,9 +1,17 @@
1
1
  // The Bluesky thread under an announcement, read from Bluesky's public API:
2
2
  // who liked and reposted it, and the replies (the comments), nested.
3
3
  //
4
- // Left out: replies the announcement's author hid (Bluesky's threadgate),
5
- // deleted or blocked posts, posts or accounts carrying hiding labels from
6
- // Bluesky's moderation (spam, porn…), and anything in the `hidden` option.
4
+ // Left out, with the replies under them: replies the announcement's author
5
+ // hid (Bluesky's threadgate), deleted or blocked posts, posts or accounts
6
+ // carrying hiding labels from Bluesky's moderation (spam, porn…), and
7
+ // anything in the `hidden` option.
8
+ //
9
+ // Accounts that ask apps not to show them to logged-out people (Bluesky's
10
+ // "Discourage apps from showing my account to logged-out users", the
11
+ // !no-unauthenticated label) are respected too, as the page is public:
12
+ // their comments become a bare placeholder (no name, picture, address or
13
+ // words), keeping the replies under them, which are other people's, and
14
+ // they're left out of the faces of who liked and reposted.
7
15
  // Cached for two minutes per announcement; if Bluesky can't be reached the
8
16
  // last good copy is kept.
9
17
  import { rememberAvatar } from "./avatars";
@@ -17,6 +25,7 @@ const MAX_CACHED = 200;
17
25
  const MAX_DEPTH = 6;
18
26
  const MAX_FACES = 48;
19
27
  const HIDING_LABELS = new Set(["!hide", "!takedown", "!suspend", "spam", "porn", "sexual", "nudity", "graphic-media", "gore"]);
28
+ const LOGGED_IN_ONLY = "!no-unauthenticated";
20
29
 
21
30
  export type Person = { did: string; handle: string; displayName?: string; avatar?: string; url: string };
22
31
 
@@ -32,6 +41,11 @@ export type Reply = {
32
41
  depth: number;
33
42
  likeCount: number;
34
43
  repostCount: number;
44
+ /**
45
+ * Its author asks not to be shown to logged-out people: everything else
46
+ * is left empty, and the page shows a placeholder in its place.
47
+ */
48
+ withheld?: true;
35
49
  };
36
50
 
37
51
  export type Thread = {
@@ -80,6 +94,9 @@ function person(author: Author): Person | null {
80
94
  };
81
95
  }
82
96
 
97
+ /** Whether an account asks apps not to show it to people who aren't signed in. */
98
+ const loggedInOnly = (author?: Author) => !!author?.labels?.some((label) => label.val === LOGGED_IN_ONLY);
99
+
83
100
  function isHidden(post: ThreadPost, author: Person, url: string): boolean {
84
101
  const labels = [...(post.labels ?? []), ...(post.author?.labels ?? [])];
85
102
  if (labels.some((label) => label.val && HIDING_LABELS.has(label.val))) return true;
@@ -97,7 +114,20 @@ function collect(nodes: ThreadNode[] = [], depth: number, hiddenByAuthor: Set<st
97
114
  if (!author || !post.uri || !key || hiddenByAuthor.has(post.uri)) continue;
98
115
  const url = `${author.url}/post/${key}`;
99
116
  if (isHidden(post, author, url)) continue;
100
- out.push({
117
+ if (loggedInOnly(post.author)) {
118
+ out.push({
119
+ uri: "",
120
+ url: "",
121
+ author: { did: "", handle: "", url: "" },
122
+ byAuthor: false,
123
+ createdAt: "",
124
+ text: [],
125
+ depth,
126
+ likeCount: 0,
127
+ repostCount: 0,
128
+ withheld: true,
129
+ });
130
+ } else out.push({
101
131
  uri: post.uri,
102
132
  url,
103
133
  author,
@@ -131,8 +161,8 @@ async function fetchThread(uri: string): Promise<Thread> {
131
161
  url: rootAuthor ? `${rootAuthor.url}/post/${uri.split("/").pop()}` : "",
132
162
  likeCount: root.likeCount ?? likes.length,
133
163
  repostCount: root.repostCount ?? repostedBy.length,
134
- likes: likes.flatMap(({ actor }) => (actor ? [person(actor)].filter((p): p is Person => !!p) : [])),
135
- reposts: repostedBy.map(person).filter((p): p is Person => !!p),
164
+ likes: likes.flatMap(({ actor }) => (actor && !loggedInOnly(actor) ? [person(actor)].filter((p): p is Person => !!p) : [])),
165
+ reposts: repostedBy.filter((actor) => !loggedInOnly(actor)).map(person).filter((p): p is Person => !!p),
136
166
  replies,
137
167
  };
138
168
  }
package/lib/uris.ts ADDED
@@ -0,0 +1,31 @@
1
+ // Telling the site's own posts and threads apart from everything else, for
2
+ // the routes and the browser alike. Nothing outside these can be replied to,
3
+ // liked, reposted or quoted through the site.
4
+ import settings from "./public-settings";
5
+
6
+ const POST_URI = /^at:\/\/(did:[a-z0-9]+:[a-zA-Z0-9._:%-]+)\/app\.bsky\.feed\.post\/([a-zA-Z0-9._~-]+)$/;
7
+
8
+ export const isPostUri = (uri: unknown): uri is string => typeof uri === "string" && POST_URI.test(uri);
9
+
10
+ /** The DID that wrote a post, from its AT URI. */
11
+ export const authorOf = (uri: string) => POST_URI.exec(uri)?.[1] ?? "";
12
+
13
+ /** The post's record key, the last part of its AT URI. */
14
+ export const rkeyOf = (uri: string) => POST_URI.exec(uri)?.[2] ?? "";
15
+
16
+ /** True for a post by one of the site's own accounts (an announcement). */
17
+ export function isSitePost(uri: unknown): uri is string {
18
+ return isPostUri(uri) && settings.allowedAuthors.includes(authorOf(uri));
19
+ }
20
+
21
+ export type PostView = {
22
+ uri: string;
23
+ cid: string;
24
+ author?: { did: string };
25
+ record?: { reply?: { root?: { uri?: string } } };
26
+ viewer?: { like?: string; repost?: string };
27
+ };
28
+
29
+ /** True for one of the site's announcements, or a comment somewhere under one. */
30
+ export const inSiteThread = (view: PostView): boolean =>
31
+ isSitePost(view.uri) || isSitePost(view.record?.reply?.root?.uri);
package/locales/de.json CHANGED
@@ -49,7 +49,7 @@
49
49
  "comment.deleteConfirm": "Diesen Kommentar von Bluesky löschen?",
50
50
  "comment.deleting": "Wird gelöscht…",
51
51
  "comment.deleted": "Dein Kommentar wurde gelöscht.",
52
- "comment.deletePermission": "Um hier Kommentare zu löschen, braucht diese Seite eine weitere Erlaubnis von deinem Konto. Du kannst ihn auch in jeder Bluesky-App löschen.",
52
+ "comment.deletePermission": "Um hier Kommentare zu löschen, braucht diese Seite eine weitere Erlaubnis von deinem Atmosphere-Konto. Du kannst ihn aber auch {{link}} löschen.",
53
53
  "comment.continue": "Weiter zur Bestätigung",
54
54
  "comment.cancel": "Abbrechen",
55
55
  "signInFirst.reply": "Melde dich an, um zu antworten.",
@@ -65,9 +65,8 @@
65
65
  "messages.accountNotFound": "Dieses Konto wurde nicht gefunden. Prüfe den Handle und versuche es noch einmal.",
66
66
  "messages.invalidHandle": "Das sieht nicht wie ein Handle aus. Meist ist es etwas wie alice.bsky.social.",
67
67
  "messages.signedOut": "Du wurdest abgemeldet. Bitte melde dich erneut an.",
68
- "messages.needsPermission": "Um hier Kommentare zu löschen, braucht diese Seite eine weitere Erlaubnis von deinem Konto.",
68
+ "messages.needsPermission": "Dafür braucht es eine weitere Erlaubnis von deinem Konto.",
69
69
  "messages.notAllowed": "Das ist hier nicht möglich.",
70
- "messages.newPermissions": "Diese Seite kann jetzt auch für dich liken und reposten und braucht deshalb erneut deine Erlaubnis. Bitte melde dich erneut an.",
71
70
  "messages.tooLong": "Beiträge können bis zu {{max}} Zeichen lang sein.",
72
71
  "messages.tooMany": "Du bist ziemlich schnell. Bitte warte eine Minute und versuche es noch einmal.",
73
72
  "messages.error": "Das hat nicht geklappt. Bitte versuche es noch einmal.",
@@ -91,5 +90,22 @@
91
90
  "follow.following": "Folge ich",
92
91
  "follow.followAs": "{{handle}} als {{me}} folgen",
93
92
  "follow.followingLabel": "Du folgst {{handle}}",
94
- "credit": "Bereitgestellt von {{link}}"
93
+ "credit": "Bereitgestellt von {{link}}",
94
+ "callback.signingIn": "Anmeldung läuft …",
95
+ "signIn.access": "Deine Anmeldung bleibt in diesem Browser: Der Server dieser Seite sieht sie nie und kann nicht für dich posten, und beim Abmelden wird sie aufgehoben.",
96
+ "signIn.policy": "Wie diese Seite mit deinen Daten umgeht",
97
+ "composer.needsPermission": "Um hier als {{handle}} zu kommentieren, braucht diese Seite eine weitere Erlaubnis von deinem Atmosphere-Konto.",
98
+ "comment.hide": "Ausblenden",
99
+ "comment.hideLabel": "Diesen Kommentar ausblenden",
100
+ "comment.hideConfirm": "Diesen Kommentar hier und auf Bluesky ausblenden? In der Bluesky-App kannst du ihn wieder einblenden.",
101
+ "comment.hiding": "Wird ausgeblendet …",
102
+ "comment.hidden": "Kommentar ausgeblendet.",
103
+ "comment.hidePermission": "Um hier Kommentare auszublenden, braucht diese Seite eine weitere Erlaubnis von deinem Konto. Du kannst sie auch in der Bluesky-App ausblenden.",
104
+ "comment.deleteElsewhere": "auf der Bluesky-Website",
105
+ "permission.heading": "Eine weitere Erlaubnis",
106
+ "permission.follow": "Um {{handle}} von dieser Seite aus zu folgen, braucht sie eine weitere Erlaubnis von deinem Atmosphere-Konto: Konten folgen und entfolgen.",
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
+ "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
+ "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": "Hier nicht angezeigt: Die Person bittet Apps, ihre Beiträge nicht Leuten zu zeigen, die nicht angemeldet sind."
95
111
  }
package/locales/en.json CHANGED
@@ -49,7 +49,7 @@
49
49
  "comment.deleteConfirm": "Delete this comment from Bluesky?",
50
50
  "comment.deleting": "Deleting…",
51
51
  "comment.deleted": "Your comment was deleted.",
52
- "comment.deletePermission": "To delete comments here, this site needs one more permission from your account. You can also delete it in any Bluesky app.",
52
+ "comment.deletePermission": "To delete comments here, this site needs one more permission from your Atmosphere account. Alternatively, you can delete it {{link}}.",
53
53
  "comment.continue": "Continue to approve",
54
54
  "comment.cancel": "Cancel",
55
55
  "signInFirst.reply": "Sign in to reply.",
@@ -65,9 +65,8 @@
65
65
  "messages.accountNotFound": "We couldn't find that account. Check the handle and try again.",
66
66
  "messages.invalidHandle": "That doesn't look like a handle. It's usually something like alice.bsky.social.",
67
67
  "messages.signedOut": "You've been signed out. Please sign in again.",
68
- "messages.needsPermission": "To delete comments here, this site needs one more permission from your account.",
68
+ "messages.needsPermission": "That needs one more permission from your account.",
69
69
  "messages.notAllowed": "That can't be done here.",
70
- "messages.newPermissions": "This site can now also like and repost for you, so it needs your permission again. Please sign in again.",
71
70
  "messages.tooLong": "Posts can be up to {{max}} characters.",
72
71
  "messages.tooMany": "You're going quickly. Please wait a minute and try again.",
73
72
  "messages.error": "That didn't work. Please try again.",
@@ -91,5 +90,22 @@
91
90
  "follow.following": "Following",
92
91
  "follow.followAs": "Follow {{handle}} as {{me}}",
93
92
  "follow.followingLabel": "You follow {{handle}}",
94
- "credit": "Powered by {{link}}"
93
+ "credit": "Powered by {{link}}",
94
+ "callback.signingIn": "Signing you in…",
95
+ "signIn.access": "Your sign-in stays in this browser: this site's server never sees it and can't post for you, and signing out cancels it.",
96
+ "signIn.policy": "How this site handles your data",
97
+ "composer.needsPermission": "To comment here as {{handle}}, this site needs one more permission from your Atmosphere account.",
98
+ "comment.hide": "Hide",
99
+ "comment.hideLabel": "Hide this comment",
100
+ "comment.hideConfirm": "Hide this comment here and on Bluesky? You can show it again in the Bluesky app.",
101
+ "comment.hiding": "Hiding…",
102
+ "comment.hidden": "Comment hidden.",
103
+ "comment.hidePermission": "To hide comments here, this site needs one more permission from your account. You can also hide them in the Bluesky app.",
104
+ "comment.deleteElsewhere": "via the Bluesky website",
105
+ "permission.heading": "One more permission",
106
+ "permission.follow": "To follow {{handle}} from this site, it needs one more permission from your Atmosphere account: to follow and unfollow accounts.",
107
+ "permission.post": "To reply and quote from this site, it needs one more permission from your Atmosphere account: to create posts.",
108
+ "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.",
109
+ "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.",
110
+ "comment.withheld": "Not shown here: its author asks apps not to show their posts to people who aren't signed in."
95
111
  }
package/locales/es.json CHANGED
@@ -52,7 +52,7 @@
52
52
  "comment.deleteConfirm": "¿Eliminar este comentario de Bluesky?",
53
53
  "comment.deleting": "Eliminando…",
54
54
  "comment.deleted": "Tu comentario se ha eliminado.",
55
- "comment.deletePermission": "Para eliminar comentarios aquí, este sitio necesita un permiso más de tu cuenta. También puedes eliminarlo en cualquier aplicación de Bluesky.",
55
+ "comment.deletePermission": "Para eliminar comentarios aquí, este sitio necesita un permiso más de tu cuenta de Atmosphere. También puedes eliminarlo {{link}}.",
56
56
  "comment.continue": "Continuar para aprobar",
57
57
  "comment.cancel": "Cancelar",
58
58
  "signInFirst.reply": "Inicia sesión para responder.",
@@ -68,9 +68,8 @@
68
68
  "messages.accountNotFound": "No encontramos esa cuenta. Revisa el identificador e inténtalo de nuevo.",
69
69
  "messages.invalidHandle": "Eso no parece un identificador. Suele ser algo como alice.bsky.social.",
70
70
  "messages.signedOut": "Se ha cerrado tu sesión. Vuelve a iniciar sesión.",
71
- "messages.needsPermission": "Para eliminar comentarios aquí, este sitio necesita un permiso más de tu cuenta.",
71
+ "messages.needsPermission": "Eso necesita un permiso más de tu cuenta.",
72
72
  "messages.notAllowed": "Eso no se puede hacer aquí.",
73
- "messages.newPermissions": "Este sitio ahora también puede dar me gusta y compartir por ti, así que necesita tu permiso de nuevo. Vuelve a iniciar sesión.",
74
73
  "messages.tooLong": "Las publicaciones pueden tener hasta {{max}} caracteres.",
75
74
  "messages.tooMany": "Vas muy rápido. Espera un minuto e inténtalo de nuevo.",
76
75
  "messages.error": "No ha funcionado. Inténtalo de nuevo.",
@@ -94,5 +93,22 @@
94
93
  "follow.following": "Siguiendo",
95
94
  "follow.followAs": "Seguir a {{handle}} como {{me}}",
96
95
  "follow.followingLabel": "Sigues a {{handle}}",
97
- "credit": "Funciona con {{link}}"
96
+ "credit": "Funciona con {{link}}",
97
+ "callback.signingIn": "Iniciando sesión…",
98
+ "signIn.access": "Tu sesión se queda en este navegador: el servidor de este sitio nunca la ve ni puede publicar por ti, y al cerrar sesión se cancela.",
99
+ "signIn.policy": "Cómo trata este sitio tus datos",
100
+ "composer.needsPermission": "Para comentar aquí como {{handle}}, este sitio necesita un permiso más de tu cuenta de Atmosphere.",
101
+ "comment.hide": "Ocultar",
102
+ "comment.hideLabel": "Ocultar este comentario",
103
+ "comment.hideConfirm": "¿Ocultar este comentario aquí y en Bluesky? Puedes volver a mostrarlo en la aplicación de Bluesky.",
104
+ "comment.hiding": "Ocultando…",
105
+ "comment.hidden": "Comentario oculto.",
106
+ "comment.hidePermission": "Para ocultar comentarios aquí, este sitio necesita un permiso más de tu cuenta. También puedes ocultarlos en la aplicación de Bluesky.",
107
+ "comment.deleteElsewhere": "en la web de Bluesky",
108
+ "permission.heading": "Un permiso más",
109
+ "permission.follow": "Para seguir a {{handle}} desde este sitio, necesita un permiso más de tu cuenta de Atmosphere: seguir y dejar de seguir cuentas.",
110
+ "permission.post": "Para responder y citar desde este sitio, necesita un permiso más de tu cuenta de Atmosphere: crear publicaciones.",
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
+ "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": "No se muestra aquí: su autor pide a las aplicaciones que no muestren sus publicaciones a quien no haya iniciado sesión."
98
114
  }
package/locales/fr.json CHANGED
@@ -52,7 +52,7 @@
52
52
  "comment.deleteConfirm": "Supprimer ce commentaire de Bluesky ?",
53
53
  "comment.deleting": "Suppression…",
54
54
  "comment.deleted": "Ton commentaire a été supprimé.",
55
- "comment.deletePermission": "Pour supprimer des commentaires ici, ce site a besoin d'une autorisation de plus de ton compte. Tu peux aussi le supprimer dans n'importe quelle application Bluesky.",
55
+ "comment.deletePermission": "Pour supprimer des commentaires ici, ce site a besoin d'une autorisation de plus de ton compte Atmosphere. Sinon, tu peux le supprimer {{link}}.",
56
56
  "comment.continue": "Continuer pour autoriser",
57
57
  "comment.cancel": "Annuler",
58
58
  "signInFirst.reply": "Connecte-toi pour répondre.",
@@ -68,9 +68,8 @@
68
68
  "messages.accountNotFound": "Ce compte est introuvable. Vérifie l'identifiant et réessaie.",
69
69
  "messages.invalidHandle": "Ça ne ressemble pas à un identifiant. C'est généralement quelque chose comme alice.bsky.social.",
70
70
  "messages.signedOut": "Tu as été déconnecté. Connecte-toi à nouveau.",
71
- "messages.needsPermission": "Pour supprimer des commentaires ici, ce site a besoin d'une autorisation de plus de ton compte.",
71
+ "messages.needsPermission": "Cela nécessite une autorisation de plus de ton compte.",
72
72
  "messages.notAllowed": "Ce n'est pas possible ici.",
73
- "messages.newPermissions": "Ce site peut désormais aussi aimer et republier pour toi ; il a donc de nouveau besoin de ton autorisation. Connecte-toi à nouveau.",
74
73
  "messages.tooLong": "Les publications peuvent contenir jusqu'à {{max}} caractères.",
75
74
  "messages.tooMany": "Tu vas un peu vite. Attends une minute et réessaie.",
76
75
  "messages.error": "Ça n'a pas marché. Réessaie.",
@@ -94,5 +93,22 @@
94
93
  "follow.following": "Abonné",
95
94
  "follow.followAs": "Suivre {{handle}} en tant que {{me}}",
96
95
  "follow.followingLabel": "Tu suis {{handle}}",
97
- "credit": "Propulsé par {{link}}"
96
+ "credit": "Propulsé par {{link}}",
97
+ "callback.signingIn": "Connexion en cours…",
98
+ "signIn.access": "Ta connexion reste dans ce navigateur : le serveur de ce site ne la voit jamais et ne peut pas publier à ta place, et te déconnecter l'annule.",
99
+ "signIn.policy": "Comment ce site traite tes données",
100
+ "composer.needsPermission": "Pour commenter ici en tant que {{handle}}, ce site a besoin d'une autorisation de plus de ton compte Atmosphere.",
101
+ "comment.hide": "Masquer",
102
+ "comment.hideLabel": "Masquer ce commentaire",
103
+ "comment.hideConfirm": "Masquer ce commentaire ici et sur Bluesky ? Tu pourras le réafficher dans l'application Bluesky.",
104
+ "comment.hiding": "Masquage…",
105
+ "comment.hidden": "Commentaire masqué.",
106
+ "comment.hidePermission": "Pour masquer des commentaires ici, ce site a besoin d'une autorisation de plus de ton compte. Tu peux aussi les masquer dans l'application Bluesky.",
107
+ "comment.deleteElsewhere": "sur le site de Bluesky",
108
+ "permission.heading": "Une autorisation de plus",
109
+ "permission.follow": "Pour suivre {{handle}} depuis ce site, il a besoin d'une autorisation de plus de ton compte Atmosphere : suivre des comptes et ne plus les suivre.",
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
+ "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
+ "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": "Non affiché ici : son auteur demande aux applications de ne pas montrer ses publications aux personnes non connectées."
98
114
  }
package/locales/it.json CHANGED
@@ -52,7 +52,7 @@
52
52
  "comment.deleteConfirm": "Eliminare questo commento da Bluesky?",
53
53
  "comment.deleting": "Eliminazione…",
54
54
  "comment.deleted": "Il tuo commento è stato eliminato.",
55
- "comment.deletePermission": "Per eliminare i commenti qui, questo sito ha bisogno di un'altra autorizzazione dal tuo account. Puoi anche eliminarlo in qualsiasi app di Bluesky.",
55
+ "comment.deletePermission": "Per eliminare i commenti qui, questo sito ha bisogno di un'altra autorizzazione dal tuo account Atmosphere. In alternativa, puoi eliminarlo {{link}}.",
56
56
  "comment.continue": "Continua per approvare",
57
57
  "comment.cancel": "Annulla",
58
58
  "signInFirst.reply": "Accedi per rispondere.",
@@ -68,9 +68,8 @@
68
68
  "messages.accountNotFound": "Non abbiamo trovato quell'account. Controlla l'handle e riprova.",
69
69
  "messages.invalidHandle": "Non sembra un handle. Di solito è qualcosa come alice.bsky.social.",
70
70
  "messages.signedOut": "Sei stato disconnesso. Accedi di nuovo.",
71
- "messages.needsPermission": "Per eliminare i commenti qui, questo sito ha bisogno di un'altra autorizzazione dal tuo account.",
71
+ "messages.needsPermission": "Serve un'altra autorizzazione dal tuo account.",
72
72
  "messages.notAllowed": "Qui non si può fare.",
73
- "messages.newPermissions": "Ora questo sito può anche mettere mi piace e ripubblicare per te, quindi ha di nuovo bisogno della tua autorizzazione. Accedi di nuovo.",
74
73
  "messages.tooLong": "I post possono avere fino a {{max}} caratteri.",
75
74
  "messages.tooMany": "Stai andando un po' veloce. Aspetta un minuto e riprova.",
76
75
  "messages.error": "Non ha funzionato. Riprova.",
@@ -94,5 +93,22 @@
94
93
  "follow.following": "Segui già",
95
94
  "follow.followAs": "Segui {{handle}} come {{me}}",
96
95
  "follow.followingLabel": "Segui {{handle}}",
97
- "credit": "Realizzato con {{link}}"
96
+ "credit": "Realizzato con {{link}}",
97
+ "callback.signingIn": "Accesso in corso…",
98
+ "signIn.access": "Il tuo accesso resta in questo browser: il server di questo sito non lo vede mai e non può pubblicare al posto tuo, e uscendo lo annulli.",
99
+ "signIn.policy": "Come questo sito tratta i tuoi dati",
100
+ "composer.needsPermission": "Per commentare qui come {{handle}}, questo sito ha bisogno di un'altra autorizzazione dal tuo account Atmosphere.",
101
+ "comment.hide": "Nascondi",
102
+ "comment.hideLabel": "Nascondi questo commento",
103
+ "comment.hideConfirm": "Nascondere questo commento qui e su Bluesky? Puoi mostrarlo di nuovo nell'app di Bluesky.",
104
+ "comment.hiding": "Nascondo…",
105
+ "comment.hidden": "Commento nascosto.",
106
+ "comment.hidePermission": "Per nascondere i commenti qui, questo sito ha bisogno di un'altra autorizzazione dal tuo account. Puoi anche nasconderli nell'app di Bluesky.",
107
+ "comment.deleteElsewhere": "dal sito di Bluesky",
108
+ "permission.heading": "Un'altra autorizzazione",
109
+ "permission.follow": "Per seguire {{handle}} da questo sito, serve un'altra autorizzazione dal tuo account Atmosphere: seguire account e smettere di seguirli.",
110
+ "permission.post": "Per rispondere e citare da questo sito, serve un'altra autorizzazione dal tuo account Atmosphere: creare post.",
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
+ "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": "Non mostrato qui: l'autore chiede alle app di non mostrare i suoi post a chi non ha effettuato l'accesso."
98
114
  }
package/locales/ja.json CHANGED
@@ -46,7 +46,7 @@
46
46
  "comment.deleteConfirm": "このコメントをBlueskyから削除しますか?",
47
47
  "comment.deleting": "削除中…",
48
48
  "comment.deleted": "コメントを削除しました。",
49
- "comment.deletePermission": "ここでコメントを削除するには、このサイトにアカウントからの許可がもうひとつ必要です。Blueskyのアプリから削除することもできます。",
49
+ "comment.deletePermission": "ここでコメントを削除するには、このサイトにAtmosphereアカウントからの許可がもうひとつ必要です。{{link}}削除することもできます。",
50
50
  "comment.continue": "許可に進む",
51
51
  "comment.cancel": "キャンセル",
52
52
  "signInFirst.reply": "返信するにはログインしてください。",
@@ -62,9 +62,8 @@
62
62
  "messages.accountNotFound": "そのアカウントが見つかりませんでした。ハンドルを確認して、もう一度お試しください。",
63
63
  "messages.invalidHandle": "ハンドルではないようです。通常は alice.bsky.social のような形式です。",
64
64
  "messages.signedOut": "ログアウトされました。もう一度ログインしてください。",
65
- "messages.needsPermission": "ここでコメントを削除するには、このサイトにアカウントからの許可がもうひとつ必要です。",
65
+ "messages.needsPermission": "それにはアカウントからの許可がもうひとつ必要です。",
66
66
  "messages.notAllowed": "ここではその操作はできません。",
67
- "messages.newPermissions": "このサイトはいいねとリポストもできるようになったため、改めて許可が必要です。もう一度ログインしてください。",
68
67
  "messages.tooLong": "投稿は{{max}}文字までです。",
69
68
  "messages.tooMany": "操作が早すぎます。1分待ってから、もう一度お試しください。",
70
69
  "messages.error": "うまくいきませんでした。もう一度お試しください。",
@@ -88,5 +87,22 @@
88
87
  "follow.following": "フォロー中",
89
88
  "follow.followAs": "{{me}}として{{handle}}をフォロー",
90
89
  "follow.followingLabel": "{{handle}}をフォローしています",
91
- "credit": "{{link}}で動作しています"
90
+ "credit": "{{link}}で動作しています",
91
+ "callback.signingIn": "ログインしています…",
92
+ "signIn.access": "ログイン情報はこのブラウザの中だけに保存されます。このサイトのサーバーがそれを見ることはなく、あなたの代わりに投稿することもできません。ログアウトすると取り消されます。",
93
+ "signIn.policy": "このサイトでのデータの取り扱い",
94
+ "composer.needsPermission": "ここで{{handle}}としてコメントするには、このサイトにAtmosphereアカウントからの許可がもうひとつ必要です。",
95
+ "comment.hide": "非表示",
96
+ "comment.hideLabel": "このコメントを非表示にする",
97
+ "comment.hideConfirm": "このコメントをこことBlueskyで非表示にしますか?Blueskyのアプリで再表示できます。",
98
+ "comment.hiding": "非表示にしています…",
99
+ "comment.hidden": "コメントを非表示にしました。",
100
+ "comment.hidePermission": "ここでコメントを非表示にするには、このサイトにアカウントからの許可がもうひとつ必要です。Blueskyのアプリでも非表示にできます。",
101
+ "comment.deleteElsewhere": "Blueskyのウェブサイトから",
102
+ "permission.heading": "もうひとつの許可",
103
+ "permission.follow": "このサイトから{{handle}}をフォローするには、Atmosphereアカウントからの許可がもうひとつ必要です(アカウントのフォローとフォロー解除)。",
104
+ "permission.post": "このサイトから返信や引用をするには、Atmosphereアカウントからの許可がもうひとつ必要です(投稿の作成)。",
105
+ "permission.react": "このサイトからいいねやリポストをするには、Atmosphereアカウントからの許可がもうひとつ必要です(いいねとリポスト、その取り消し)。",
106
+ "permission.approve": "アカウントのサイト(ほとんどの方はBluesky)で承認を求められます。先にそこでログインを求められることもあります。その後、すぐにここへ戻ります。",
107
+ "comment.withheld": "ここには表示されません:投稿者は、ログインしていない人に投稿を表示しないようアプリに求めています。"
92
108
  }
package/locales/nl.json CHANGED
@@ -49,7 +49,7 @@
49
49
  "comment.deleteConfirm": "Deze reactie van Bluesky verwijderen?",
50
50
  "comment.deleting": "Verwijderen…",
51
51
  "comment.deleted": "Je reactie is verwijderd.",
52
- "comment.deletePermission": "Om hier reacties te verwijderen, heeft deze site nog één toestemming van je account nodig. Je kunt hem ook in elke Bluesky-app verwijderen.",
52
+ "comment.deletePermission": "Om hier reacties te verwijderen, heeft deze site nog één toestemming van je Atmosphere-account nodig. Je kunt hem ook {{link}} verwijderen.",
53
53
  "comment.continue": "Doorgaan om toe te staan",
54
54
  "comment.cancel": "Annuleren",
55
55
  "signInFirst.reply": "Log in om te reageren.",
@@ -65,9 +65,8 @@
65
65
  "messages.accountNotFound": "We konden dat account niet vinden. Controleer de handle en probeer het opnieuw.",
66
66
  "messages.invalidHandle": "Dat lijkt geen handle. Meestal is het zoiets als alice.bsky.social.",
67
67
  "messages.signedOut": "Je bent uitgelogd. Log opnieuw in.",
68
- "messages.needsPermission": "Om hier reacties te verwijderen, heeft deze site nog één toestemming van je account nodig.",
68
+ "messages.needsPermission": "Daarvoor is nog één toestemming van je account nodig.",
69
69
  "messages.notAllowed": "Dat kan hier niet.",
70
- "messages.newPermissions": "Deze site kan nu ook voor je liken en reposten en heeft daarom opnieuw je toestemming nodig. Log opnieuw in.",
71
70
  "messages.tooLong": "Berichten mogen maximaal {{max}} tekens lang zijn.",
72
71
  "messages.tooMany": "Je gaat wel snel. Wacht een minuut en probeer het opnieuw.",
73
72
  "messages.error": "Dat is niet gelukt. Probeer het opnieuw.",
@@ -91,5 +90,22 @@
91
90
  "follow.following": "Volgend",
92
91
  "follow.followAs": "{{handle}} volgen als {{me}}",
93
92
  "follow.followingLabel": "Je volgt {{handle}}",
94
- "credit": "Mogelijk gemaakt door {{link}}"
93
+ "credit": "Mogelijk gemaakt door {{link}}",
94
+ "callback.signingIn": "Bezig met inloggen…",
95
+ "signIn.access": "Je login blijft in deze browser: de server van deze site ziet hem nooit en kan niet namens jou posten, en uitloggen trekt hem in.",
96
+ "signIn.policy": "Hoe deze site met je gegevens omgaat",
97
+ "composer.needsPermission": "Om hier als {{handle}} te reageren, heeft deze site nog één toestemming van je Atmosphere-account nodig.",
98
+ "comment.hide": "Verbergen",
99
+ "comment.hideLabel": "Deze reactie verbergen",
100
+ "comment.hideConfirm": "Deze reactie hier en op Bluesky verbergen? Je kunt hem in de Bluesky-app weer tonen.",
101
+ "comment.hiding": "Verbergen…",
102
+ "comment.hidden": "Reactie verborgen.",
103
+ "comment.hidePermission": "Om hier reacties te verbergen, heeft deze site nog één toestemming van je account nodig. Je kunt ze ook in de Bluesky-app verbergen.",
104
+ "comment.deleteElsewhere": "via de Bluesky-website",
105
+ "permission.heading": "Nog één toestemming",
106
+ "permission.follow": "Om {{handle}} vanaf deze site te volgen, is nog één toestemming van je Atmosphere-account nodig: accounts volgen en ontvolgen.",
107
+ "permission.post": "Om vanaf deze site te reageren en te citeren, is nog één toestemming van je Atmosphere-account nodig: berichten plaatsen.",
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
+ "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": "Hier niet getoond: de schrijver vraagt apps om zijn of haar berichten niet te tonen aan mensen die niet zijn ingelogd."
95
111
  }
package/locales/pt.json CHANGED
@@ -52,7 +52,7 @@
52
52
  "comment.deleteConfirm": "Excluir este comentário do Bluesky?",
53
53
  "comment.deleting": "Excluindo…",
54
54
  "comment.deleted": "Seu comentário foi excluído.",
55
- "comment.deletePermission": "Para excluir comentários aqui, este site precisa de mais uma permissão da sua conta. Você também pode excluí-lo em qualquer app do Bluesky.",
55
+ "comment.deletePermission": "Para excluir comentários aqui, este site precisa de mais uma permissão da sua conta Atmosphere. Ou você pode excluí-lo {{link}}.",
56
56
  "comment.continue": "Continuar para aprovar",
57
57
  "comment.cancel": "Cancelar",
58
58
  "signInFirst.reply": "Entre para responder.",
@@ -68,9 +68,8 @@
68
68
  "messages.accountNotFound": "Não encontramos essa conta. Confira o identificador e tente de novo.",
69
69
  "messages.invalidHandle": "Isso não parece um identificador. Normalmente é algo como alice.bsky.social.",
70
70
  "messages.signedOut": "Você foi desconectado. Entre de novo.",
71
- "messages.needsPermission": "Para excluir comentários aqui, este site precisa de mais uma permissão da sua conta.",
71
+ "messages.needsPermission": "Isso precisa de mais uma permissão da sua conta.",
72
72
  "messages.notAllowed": "Isso não pode ser feito aqui.",
73
- "messages.newPermissions": "Este site agora também pode curtir e repostar por você, então precisa da sua permissão de novo. Entre de novo.",
74
73
  "messages.tooLong": "As publicações podem ter até {{max}} caracteres.",
75
74
  "messages.tooMany": "Você está indo rápido demais. Espere um minuto e tente de novo.",
76
75
  "messages.error": "Não deu certo. Tente de novo.",
@@ -94,5 +93,22 @@
94
93
  "follow.following": "Seguindo",
95
94
  "follow.followAs": "Seguir {{handle}} como {{me}}",
96
95
  "follow.followingLabel": "Você segue {{handle}}",
97
- "credit": "Feito com {{link}}"
96
+ "credit": "Feito com {{link}}",
97
+ "callback.signingIn": "Entrando…",
98
+ "signIn.access": "Seu login fica neste navegador: o servidor deste site nunca o vê nem pode publicar por você, e sair o cancela.",
99
+ "signIn.policy": "Como este site trata seus dados",
100
+ "composer.needsPermission": "Para comentar aqui como {{handle}}, este site precisa de mais uma permissão da sua conta Atmosphere.",
101
+ "comment.hide": "Ocultar",
102
+ "comment.hideLabel": "Ocultar este comentário",
103
+ "comment.hideConfirm": "Ocultar este comentário aqui e no Bluesky? Você pode mostrá-lo de novo no app do Bluesky.",
104
+ "comment.hiding": "Ocultando…",
105
+ "comment.hidden": "Comentário ocultado.",
106
+ "comment.hidePermission": "Para ocultar comentários aqui, este site precisa de mais uma permissão da sua conta. Você também pode ocultá-los no app do Bluesky.",
107
+ "comment.deleteElsewhere": "no site do Bluesky",
108
+ "permission.heading": "Mais uma permissão",
109
+ "permission.follow": "Para seguir {{handle}} por este site, ele precisa de mais uma permissão da sua conta Atmosphere: seguir e deixar de seguir contas.",
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
+ "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
+ "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": "Não exibido aqui: o autor pede aos apps que não mostrem suas publicações a quem não está conectado."
98
114
  }