fedipod 0.13.0 → 0.14.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.
@@ -28,7 +28,7 @@ const MODERATION = $('moderation');
28
28
  const STATUS = $('status-ctl');
29
29
  const STATUS_PICK = $('status-pick'); // held: getElementById can't see it mid-render
30
30
  const BSKY_CTL = $('bsky-ctl');
31
- const BSKY_CONNECT_CTL = $('bsky-connect-ctl');
31
+ const IDENT_CTL = $('ident-ctl');
32
32
  const FOLLOWS_CTL = $('follows-ctl');
33
33
  const FOLLOWS_PICK = $('follows-pick');
34
34
  const UPDATE_CTL = $('update-ctl');
@@ -113,6 +113,49 @@ document.getElementById('win').addEventListener('win:closed', () => {
113
113
  });
114
114
 
115
115
 
116
+ // One row per identity connected elsewhere, indented under the row that adds
117
+ // them. Each carries its own way out: disconnecting one has nothing to do with
118
+ // the others.
119
+ function identityRows() {
120
+ const out = [];
121
+ const row = (label, ...nodes) => {
122
+ const dt = document.createElement('dt');
123
+ dt.className = 'under';
124
+ dt.textContent = label;
125
+ const dd = document.createElement('dd');
126
+ dd.append(...nodes);
127
+ out.push([dt, dd]);
128
+ };
129
+ if (config.atproto?.connected) {
130
+ const a = document.createElement('a');
131
+ a.href = `https://bsky.app/profile/${config.atproto.handle}`;
132
+ a.textContent = `@${config.atproto.handle}`;
133
+ a.target = '_blank';
134
+ a.rel = 'noopener';
135
+ a.title = ` Open this account's Bluesky page in a new tab`;
136
+ BSKY_CTL.hidden = false;
137
+ BSKY_CROSSPOST.value = config.atproto.crossPost ? 'on' : 'off';
138
+ row('Bluesky', a, ' ', BSKY_CTL);
139
+ } else {
140
+ BSKY_CTL.hidden = true;
141
+ }
142
+ for (const acct of config.fediAccounts || []) {
143
+ const name = document.createElement('span');
144
+ name.textContent = acct.needsReconnect ? `${acct.handle} — sign in again` : acct.handle;
145
+ if (acct.needsReconnect) name.className = 'warn';
146
+ const off = document.createElement('button');
147
+ off.type = 'button';
148
+ off.className = 'inline danger';
149
+ off.textContent = 'Disconnect';
150
+ off.title = ` Stop reading ${acct.handle} and remove its stored token`;
151
+ off.addEventListener('click', async () => {
152
+ if (await write('/fediacct/disconnect', { id: acct.id }, `${acct.handle} disconnected`)) await load();
153
+ });
154
+ row(acct.host, name, ' ', off);
155
+ }
156
+ return out;
157
+ }
158
+
116
159
  function render() {
117
160
  // The bar names the actor, the same way on every page. Only the tab title is
118
161
  // this page's own business.
@@ -134,7 +177,7 @@ function render() {
134
177
  ['Fediverse identity', config.address || `@${config.handle} — no resolvable address`,
135
178
  config.accountId && config.address ? { href: `/admin/client/#/a/${config.accountId}` } : null],
136
179
  ['Solid identity', config.webId, config.remotePod ? { href: config.remotePod, blank: true } : null],
137
- ['Bluesky identity', 'ctl'],
180
+ ['Other identities', 'ctl'],
138
181
  ['local store', config.home],
139
182
  // The address you actually open, not the bare number — the named origin when
140
183
  // there is one, since that is what the client and the OAuth redirect use.
@@ -148,6 +191,8 @@ function render() {
148
191
  // A person gates followers here; a group's gate is the joins control on its
149
192
  // kind row, so the row would be a second switch for the same thing.
150
193
  if (config.kind !== 'group') rows.splice(2, 0, ['new followers', 'ctl']);
194
+ // Rows that belong under the one being written, appended after it.
195
+ let kids = null;
151
196
  for (const [k, v, link] of rows) {
152
197
  if (!v) continue;
153
198
  const dt = document.createElement('dt');
@@ -207,27 +252,19 @@ function render() {
207
252
  UPDATE_WORD.textContent = words.join('; ');
208
253
  UPDATE_GO.hidden = !u?.available;
209
254
  }
210
- // Connected: the account itself, then the way out. Not: a form on the page.
211
- if (k === 'Bluesky identity') {
255
+ // The way to add one. Each account already connected is a row of its own
256
+ // underneath, so this row stays the action and never becomes a list.
257
+ if (k === 'Other identities') {
212
258
  dd.textContent = '';
213
- if (config.atproto?.connected) {
214
- const a = document.createElement('a');
215
- a.href = `https://bsky.app/profile/${config.atproto.handle}`;
216
- a.textContent = `@${config.atproto.handle}`;
217
- a.target = '_blank';
218
- a.rel = 'noopener';
219
- a.title = ` Open this account's Bluesky page in a new tab`;
220
- dd.append(a, ' ', BSKY_CTL);
221
- BSKY_CTL.hidden = false;
222
- BSKY_CONNECT_CTL.hidden = true;
223
- BSKY_CROSSPOST.value = config.atproto.crossPost ? 'on' : 'off';
224
- } else {
225
- dd.append(BSKY_CONNECT_CTL);
226
- BSKY_CONNECT_CTL.hidden = false;
227
- BSKY_CTL.hidden = true;
228
- }
259
+ dd.append(IDENT_CTL);
260
+ IDENT_CTL.hidden = false;
261
+ kids = identityRows();
229
262
  }
230
263
  facts.append(dt, dd);
264
+ if (kids) {
265
+ for (const [ckt, ckd] of kids) facts.append(ckt, ckd);
266
+ kids = null;
267
+ }
231
268
  }
232
269
  if (!config.address) {
233
270
  const p = document.createElement('p');
@@ -420,9 +457,17 @@ function setRail(closed) {
420
457
  $('rail-toggle').addEventListener('click', () => setRail(!$('rail').classList.contains('closed')));
421
458
  if (matchMedia('(max-width: 47rem)').matches) setRail(true);
422
459
 
423
- $('bsky-open').addEventListener('click', () => {
460
+ // One way in for both networks, because from the record they are the same
461
+ // thing: an account elsewhere whose timeline joins the one you read here.
462
+ $('ident-open').addEventListener('click', () => {
424
463
  closePanels();
425
- solWindow.show('bsky-form', 'Connect Bluesky account');
464
+ solWindow.show('ident-form', 'Connect an identity');
465
+ });
466
+ $('ident-pick-fedi').addEventListener('click', () => {
467
+ solWindow.show('fediacct-form', 'Connect a Fediverse account');
468
+ });
469
+ $('ident-pick-bsky').addEventListener('click', () => {
470
+ solWindow.show('bsky-form', 'Connect a Bluesky account');
426
471
  });
427
472
 
428
473
  let bskyBusy = false;
@@ -443,6 +488,20 @@ $('bsky-form').addEventListener('submit', async (ev) => {
443
488
  }
444
489
  } finally { bskyBusy = false; }
445
490
  });
491
+ // The sign-in happens at the other server, so this hands the browser over
492
+ // rather than taking a password. What comes back lands on /fediacct/callback,
493
+ // which this machine answers and nowhere else does.
494
+ let fediBusy = false;
495
+ $('fediacct-form').addEventListener('submit', async (ev) => {
496
+ ev.preventDefault();
497
+ if (fediBusy) return;
498
+ fediBusy = true;
499
+ try {
500
+ const r = await write('/fediacct/connect', { host: $('fediacct-host').value.trim() }, '');
501
+ if (r?.authorize) location.href = r.authorize;
502
+ } finally { fediBusy = false; }
503
+ });
504
+
446
505
  $('bsky-disconnect').addEventListener('click', async () => {
447
506
  if (bskyBusy) return;
448
507
  bskyBusy = true;
@@ -133,6 +133,9 @@ dl { margin: .5rem 0; } /* grid only above 34rem — see the media query
133
133
  gap: .6rem .9rem; align-items: baseline; }
134
134
  }
135
135
  dt { color: var(--muted); }
136
+ /* An identity connected under "Other identities" belongs to the row above it,
137
+ and the indent is what says so. */
138
+ dt.under { padding-left: 1.25rem; }
136
139
  dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
137
140
  .rows { list-style: none; padding: 0; margin: .3rem 0; }
138
141
  .rows li { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center;
@@ -216,16 +219,16 @@ pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
216
219
  <label class="choice"><input type="radio" name="newKind" value="person" checked>
217
220
  A <strong>person</strong> — an account you read and post from</label>
218
221
  <label class="choice"><input type="radio" name="newKind" value="group">
219
- A <strong>group</strong> of fediverse users you manage</label>
222
+ A <strong>group</strong> of Fediverse users you manage</label>
220
223
  </fieldset>
221
224
 
222
225
  <fieldset>
223
226
  <legend>Actor's Handle</legend>
224
227
  <label for="new-handle">Handle</label>
225
228
  <input type="text" id="new-handle" autocomplete="off"
226
- placeholder="handle part of fediverse address: @HANDLE@server"
229
+ placeholder="handle part of Fediverse address: @HANDLE@server"
227
230
  aria-describedby="new-handle-hint">
228
- <p class="hint" id="new-handle-hint">The name in your fediverse address. This is permanent.
231
+ <p class="hint" id="new-handle-hint">The name in your Fediverse address. This is permanent.
229
232
  Letters, digits, hyphens, and underscores only.</p>
230
233
  </fieldset>
231
234
 
@@ -303,8 +306,8 @@ pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
303
306
  <option value="auto">accepted automatically</option>
304
307
  </select>
305
308
  </span>
306
- <span id="bsky-connect-ctl" hidden>
307
- <button type="button" id="bsky-open" class="inline" title=" Connect a Bluesky account this identity also posts as">Connect Bluesky account</button>
309
+ <span id="ident-ctl" hidden>
310
+ <button type="button" id="ident-open" class="inline" title=" Connect an account you hold on another network, so its timeline joins the one you read here">Connect an identity</button>
308
311
  </span>
309
312
  <span id="bsky-ctl" hidden>
310
313
  <select id="bsky-crosspost" aria-label="Bluesky crossposting" title=" Whether your public posts also go to the Bluesky account">
@@ -344,7 +347,7 @@ pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
344
347
  on arrival. Shown only when something is actually waiting. -->
345
348
  <div id="block-modqueue" hidden>
346
349
  <h3>Moderation requests <span class="muted" id="modqueue-count"></span></h3>
347
- <p class="hint">Asked for by a moderator over the fediverse. A delivery cannot prove who
350
+ <p class="hint">Asked for by a moderator over the Fediverse. A delivery cannot prove who
348
351
  sent it, so nothing here has been acted on.</p>
349
352
  <ul class="rows" role="list" id="modqueue"></ul>
350
353
  </div>
@@ -423,20 +426,46 @@ pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
423
426
  <button id="bsky-connect" type="submit" class="primary" title=" Sign in to the Bluesky account with the app password">Connect</button>
424
427
  </p>
425
428
  </form>
429
+ <form id="ident-form" hidden>
430
+ <p class="hint">Either kind brings that account's timeline and notifications
431
+ into the feed you read here, and acts as that account when you reply to,
432
+ favourite or boost one of its posts.</p>
433
+ <p>
434
+ <button type="button" id="ident-pick-fedi" class="primary" title=" Sign in at a Mastodon-API server: Mastodon, GoToSocial, Pleroma, Akkoma, Pixelfed or Friendica">A Fediverse account</button>
435
+ <button type="button" id="ident-pick-bsky" title=" Sign in to Bluesky or another ATProto server with an app password">A Bluesky account</button>
436
+ </p>
437
+ </form>
438
+ <form id="fediacct-form" hidden>
439
+ <ol class="hint">
440
+ <li>Name the server your other account is on.</li>
441
+ <li>That server asks you to sign in and to allow FediPod. It is the only
442
+ place your password is typed; this agent never sees or stores one.</li>
443
+ <li>Its timeline and notifications then join the ones you already read here,
444
+ and a like or a reply on one of its posts goes out as that account.</li>
445
+ <li>What arrives is held on this machine for reading. None of it is written
446
+ to your pod.</li>
447
+ </ol>
448
+ <label for="fediacct-host">server</label>
449
+ <input type="text" id="fediacct-host" placeholder="mastodon.social" autocomplete="off"
450
+ title=" The server your other account lives on — Mastodon, GoToSocial, Pleroma, Akkoma, Pixelfed and Friendica all speak the API this needs">
451
+ <p>
452
+ <button id="fediacct-connect" type="submit" class="primary" title=" Go to that server to sign in and allow FediPod">Continue to that server</button>
453
+ </p>
454
+ </form>
426
455
  <!-- Every consequence is written here rather than assembled in script: the
427
456
  page asking "are you sure" has to say what it is sure about, and this is
428
457
  the only record of it. Script picks which block to show, nothing else. -->
429
458
  <!-- The gateway popups: attach proves the pod with this agent's own credential;
430
459
  detach republishes the pod's own inbox. -->
431
460
  <form id="gateway-attach-form" hidden>
432
- <p class="hint">A gateway filters your fediverse mail before it reaches your pod: each
461
+ <p class="hint">A gateway filters your Fediverse mail before it reaches your pod: each
433
462
  delivery is verified at its door, spam is dropped, and the rest lands in your pod as
434
463
  before. Attaching proves the pod with this agent's own credential — no password
435
464
  leaves this machine.</p>
436
465
  <label for="gw-front">gateway</label>
437
466
  <input type="url" id="gw-front" placeholder="https://fedipod.net" autocomplete="off">
438
467
  <fieldset>
439
- <legend>Your fediverse handle</legend>
468
+ <legend>Your Fediverse handle</legend>
440
469
  <label class="choice"><input type="radio" name="gwShape" value="pod" checked>
441
470
  Keep my pod-based handle: <strong id="gw-pod-preview"></strong><br>
442
471
  <span class="hint">Your address stays tied to your own pod — you can drop the