| … | … | @@ -1530,77 +1530,6 @@ export function themeSettings (current) { |
| 1530 | 1530 | * ⚠ A plain form. Every other control here works without JavaScript and so does this one: tick, |
| 1531 | 1531 | * submit, reload. A live toggle would be the first thing on this page that needed a script. |
| 1532 | 1532 | */ |
| 1533 | | export function reachSettings (reach, did = null) { |
| 1534 | | if (!reach) return '' |
| 1535 | | const on = new Set(Array.isArray(reach.networks) ? reach.networks : []) |
| 1536 | | // ⛔ ONE FIELD PER NETWORK. `body()` in the router keeps the last value for a repeated name, so |
| 1537 | | // two checkboxes sharing one name would collapse into a single answer and silently drop whichever |
| 1538 | | // the person ticked first. |
| 1539 | | const box = (name, label, why) => |
| 1540 | | '<p><label><input type="checkbox" name="reach-' + esc(name) + '" value="on"' + |
| 1541 | | (on.has(name) ? ' checked' : '') + '> ' + esc(label) + '</label>' + |
| 1542 | | '<br><span class="who">' + esc(why) + '</span></p>' |
| 1543 | | |
| 1544 | | // ⛔ identity.md §5.1 — with no designated credential the projection is WITHHELD rather than |
| 1545 | | // published under a guess: "Several without a designation → refuse to project. Silently picking |
| 1546 | | // one would publish under an identity the author did not choose." The page has to say so, or an |
| 1547 | | // enabled network looks like it works. |
| 1548 | | // |
| 1549 | | // ⛔ AND IT HAS TO BE SETTABLE HERE. This said "Link one on the machine running the daemon" and |
| 1550 | | // that mechanism did not exist: `setReach` accepts a credential, this rendered one, and NOTHING |
| 1551 | | // anywhere passed one — no route, no CLI verb. An instruction pointing at a door nobody built is |
| 1552 | | // worse than no instruction, because it reads like operator error. Same shape as `ota.js` being |
| 1553 | | // copied and never imported. |
| 1554 | | // |
| 1555 | | // ⚠ It is the ATPROTO credential specifically (§5.1: "designated: 'atproto'"), which is why it |
| 1556 | | // says DID rather than "identifier". The fediverse equivalent is not a credential at all — §6 |
| 1557 | | // makes a bridge a RESOURCE you hold a grant on, obtained by registering with one. |
| 1558 | | // ⚠ DEMOTED WHEN THIS NODE HAS ITS OWN. The identity section below can now create one and elect |
| 1559 | | // it with a button, so leading with a paste box would put two controls for one decision on one |
| 1560 | | // page — which is the confusion this whole flow set out to remove. The field stays, because |
| 1561 | | // designating a DID held somewhere else is a real thing to want and this is the only way to say it. |
| 1562 | | const ownIdentity = did && did.did |
| 1563 | | const cred = '<p><label>' + (ownIdentity ? 'Publish under a different identity' : 'Bluesky identity (DID)') + '<br>' + |
| 1564 | | '<input type="text" name="credential" value="' + esc(reach.credential || '') + '" ' + |
| 1565 | | 'placeholder="did:plc:…" autocomplete="off"></label></p>' + |
| 1566 | | (reach.credential |
| 1567 | | ? '<p class="who">Publishing to Bluesky as <code>' + esc(reach.credential) + '</code></p>' |
| 1568 | | : ownIdentity |
| 1569 | | ? '<p class="who">Leave this empty to use the identity below. Nothing is published to Bluesky ' + |
| 1570 | | 'until one is chosen.</p>' |
| 1571 | | : '<p class="warn">No Bluesky identity is designated, so nothing is published there even ' + |
| 1572 | | 'with the box ticked. Paste a DID this node holds, or leave it empty to keep it off.</p>') |
| 1573 | | |
| 1574 | | // ⛔ A NAME YOU HOLD, RECORDED — not a claim performed here. Asking a bridge for a name means |
| 1575 | | // reaching for a projection, which `test/boundary.test.js` forbids this package outright ("the |
| 1576 | | // surface holds a socket and nothing else"). Recording the answer is just data, and the daemon |
| 1577 | | // already stores it beside `networks`. |
| 1578 | | // |
| 1579 | | // ⚠ It is deliberately NOT beside the DID field. §5.1's `credential` is the designated ATPROTO one |
| 1580 | | // and the two networks are not symmetric: on bsky you hold the key, on the fediverse a bridge |
| 1581 | | // holds it and you hold a grant. One input each, labelled for what it actually is. |
| 1582 | | // ⚠ A NOTE, NOT A CONTROL — and it says so now. This box asked you to TYPE a name you had already |
| 1583 | | // obtained somewhere else, and there was no somewhere else: the values a bridge needs (an identity |
| 1584 | | // root and a feed key) are not shown anywhere in this UI, so from a handset it could not be done at |
| 1585 | | // all. Asking a bridge is now its own section below; this stays only to record a name held on a |
| 1586 | | // bridge this node cannot reach. |
| 1587 | | const fedi = reach.fediverse |
| 1588 | | ? '<p class="who">Publishing to the fediverse as <code>' + esc(reach.fediverse) + '</code></p>' |
| 1589 | | : '' |
| 1590 | | |
| 1591 | | return '<h2>Publishing</h2>' + |
| 1592 | | (on.size === 0 |
| 1593 | | ? '<p class="who">Nothing you write leaves favela.</p>' |
| 1594 | | : '<p class="who">Posts you write in a public room are also sent to the networks ticked below.</p>') + |
| 1595 | | '<form method="post" action="/reach">' + |
| 1596 | | box('bsky', 'Bluesky', 'Plain text only, up to 300 characters. Longer posts are withheld, not shortened.') + |
| 1597 | | box('fediverse', 'Mastodon and the fediverse', 'Keeps headings, lists and links. No length limit.') + |
| 1598 | | cred + fedi + |
| 1599 | | '<p class="who">⛔ A post sent to either network cannot be recalled from it. Turning a network ' + |
| 1600 | | 'off stops future posts; it does not retract the ones already sent.</p>' + |
| 1601 | | '<button>save</button></form>' |
| 1602 | | } |
| 1603 | | |
| 1604 | 1533 | |
| 1605 | 1534 | // ── app updates ────────────────────────────────────────────────────────────────────────────────── |
| 1606 | 1535 | // |
| … | … | @@ -1680,7 +1609,7 @@ export function settings (profile, message, handle = null, namespace = null, tok |
| 1680 | 1609 | const shownBanner = p.banner |
| 1681 | 1610 | ? '<p><img class="banner-lg" src="/avatar/' + esc(p.banner) + '" alt="your banner"></p>' |
| 1682 | 1611 | : '' |
| 1683 | | return layout('settings', note + updateSettings(update) + reachSettings(reach, did) + didSettings(did, reach) + bridgeSettings(reach) + |
| 1612 | return layout('settings', note + updateSettings(update) + blueskySection(did, reach) + fediverseSection(reach) + |
| 1684 | 1613 | '<h1>Settings</h1>' + shown + |
| 1685 | 1614 | '<form method="post" action="/settings" enctype="multipart/form-data">' + |
| 1686 | 1615 | '<p><label>Display name<br><input type="text" name="name" value="' + esc(p.name || '') + |
| … | … | @@ -1943,83 +1872,6 @@ export function passphraseFields (name = 'passphrase', { required = false, hint |
| 1943 | 1872 | (hint ? '<p class="who">' + esc(hint) + '</p>' : '') |
| 1944 | 1873 | } |
| 1945 | 1874 | |
| 1946 | | // ── Flow 1 — the atproto identity ──────────────────────────────────────────────────────────────── |
| 1947 | | // |
| 1948 | | // ⛔ THREE STATES, AND IT NAMES THE ONE IT IS IN. What stood here was a text box you pasted a DID |
| 1949 | | // into, which recorded a string and performed nothing — hyper-social #69 §10.7 filed it as "wired a |
| 1950 | | // field nothing reads". Minting exists, publishing exists; neither had a route to it. |
| 1951 | | // |
| 1952 | | // ⛔ AND IT REPORTS STATE, NOT A VERDICT (#69 §3, earned four times): the daemon says what is on |
| 1953 | | // disk — a DID, a sealed vault, a submission — and this decides what to show. A page that printed a |
| 1954 | | // conclusion could not be used to discover the conclusion was wrong. |
| 1955 | | // |
| 1956 | | // ⚠ ABSENT, not empty, when the daemon cannot answer. A surface pointed at a plain favela-peer gets |
| 1957 | | // no `did-status`, and rendering "you have no identity" there would be a claim this page cannot |
| 1958 | | // support — the same tolerance `reachSettings` already applies to a daemon with no `reach`. |
| 1959 | | export function didSettings (did, reach = null) { |
| 1960 | | if (!did) return '' |
| 1961 | | |
| 1962 | | if (!did.did) { |
| 1963 | | return '<h2>Bluesky identity</h2>' + |
| 1964 | | '<p class="who">You do not have one. Creating it is free and reaches no outside service — ' + |
| 1965 | | 'publishing to the Bluesky network is a separate step you can take later, or never.</p>' + |
| 1966 | | '<p><a href="/did/create">Create an identity</a></p>' |
| 1967 | | } |
| 1968 | | |
| 1969 | | const vault = did.sealed |
| 1970 | | ? '<p class="who">Your recovery phrase is sealed in a vault on this device.</p>' |
| 1971 | | : '<p class="warn">No recovery vault is on this device. If you did not write the phrase down, ' + |
| 1972 | | 'this identity cannot be recovered.</p>' |
| 1973 | | |
| 1974 | | // ⛔ DESIGNATION IS OFFERED, NEVER SILENT. identity.md §5.1: a user may hold several DIDs and the |
| 1975 | | // signer needs exactly one at sign time, so "several without a designation → refuse to project. |
| 1976 | | // Silently picking one would publish under an identity the author did not choose." Minting one |
| 1977 | | // does not elect it; this is the election, and it is one button with a name on it. |
| 1978 | | // |
| 1979 | | // ⚠ Absent when `reach` could not be read — the same tolerance the rest of settings applies — and |
| 1980 | | // absent when this identity is ALREADY the designated one, because an action that changes nothing |
| 1981 | | // reads as an action that did not work. |
| 1982 | | const designated = reach && reach.credential === did.did |
| 1983 | | const designate = !reach |
| 1984 | | ? '' |
| 1985 | | : designated |
| 1986 | | ? '<p class="who">Posts you send to Bluesky are published under this identity.</p>' |
| 1987 | | : '<form method="post" action="/did/designate">' + |
| 1988 | | '<button>publish under this identity</button></form>' + |
| 1989 | | '<p class="who">Nothing is sent to Bluesky until you also switch the network on above.</p>' |
| 1990 | | |
| 1991 | | if (did.submitted) { |
| 1992 | | // ⛔ PUBLISHED IS NOT REACHABLE, and conflating them is why this looked finished when it was not. |
| 1993 | | // A mint is P2P-only: the genesis carries `alsoKnownAs: []` and `services: {}`, so a published |
| 1994 | | // DID has no server for Bluesky to fetch a repo from and resolves for nobody. A bridge grants the |
| 1995 | | // handle and the endpoint, and the peer puts them on-chain. |
| 1996 | | const carried = did.handle |
| 1997 | | ? '<p>Reachable as <code>' + esc(did.handle) + '</code>' + |
| 1998 | | (did.pdsUrl ? ' <span class="who">via ' + esc(did.pdsUrl) + '</span>' : '') + '</p>' |
| 1999 | | : '<p class="warn">Published, but nothing can resolve it yet: this identity names no handle and ' + |
| 2000 | | 'no server. Ask a bridge to carry it.</p>' + |
| 2001 | | '<p><a href="/did/carry">Get carried by a bridge</a></p>' |
| 2002 | | return '<h2>Bluesky identity</h2>' + |
| 2003 | | '<p>Published as <code>' + esc(did.did) + '</code></p>' + carried + vault + designate |
| 2004 | | } |
| 2005 | | |
| 2006 | | return '<h2>Bluesky identity</h2>' + |
| 2007 | | '<p><code>' + esc(did.did) + '</code></p>' + |
| 2008 | | '<p class="who">This works between favela peers. The Bluesky network cannot see it yet.</p>' + |
| 2009 | | vault + |
| 2010 | | (did.publishable |
| 2011 | | ? '<p><a href="/did/publish">Publish to the Bluesky network</a></p>' |
| 2012 | | : '<p class="warn">The genesis operation for this identity is not on this device, so it cannot ' + |
| 2013 | | 'be published from here. It can be re-derived from your recovery phrase.</p>') + |
| 2014 | | designate |
| 2015 | | } |
| 2016 | | |
| 2017 | | /** |
| 2018 | | * The confirmation in front of minting. |
| 2019 | | * |
| 2020 | | * ⛔ A SEPARATE SCREEN, because the next one shows a secret exactly once and somebody who arrived by |
| 2021 | | * mis-tapping a button is not ready to write down 24 words. |
| 2022 | | */ |
| 2023 | 1875 | export function didCreate (message = null) { |
| 2024 | 1876 | const warn = message ? '<p class="warn">' + esc(message) + '</p>' : '' |
| 2025 | 1877 | return layout('create an identity', warn + |
| … | … | @@ -2097,53 +1949,6 @@ export function didPublish (did, message = null) { |
| 2097 | 1949 | '<p><a href="/settings">not now</a></p>') |
| 2098 | 1950 | } |
| 2099 | 1951 | |
| 2100 | | // ── Flow 2 — a name on a bridge ────────────────────────────────────────────────────────────────── |
| 2101 | | // |
| 2102 | | // ⛔ WHAT WAS HERE WAS A BOX THAT ASKED YOU TO TYPE THE ANSWER. Recording `name@bridge` performed |
| 2103 | | // nothing, and the two values a bridge actually requires — an identity root and a feed key — appear |
| 2104 | | // nowhere in this UI, so from a handset there was no way to obtain one at all. The only route was a |
| 2105 | | // hand-written HTTP call using values read off a desktop CLI. |
| 2106 | | // |
| 2107 | | // ⛔ THE PEER ASKS, NOT THIS PAGE. `bridge-claim` fetches a challenge, signs bytes that bind this |
| 2108 | | // name and this feed, and sends both; the daemon holds the credential and this page never sees one. |
| 2109 | | // It is also deliberately generic — the daemon is not told what a fediverse is, so the same ceremony |
| 2110 | | // serves any bridge. |
| 2111 | | // |
| 2112 | | // ⛔ BY KEY, NOT HOSTNAME. A topic derived from a name is squattable, and while a squatter cannot |
| 2113 | | // steal a claim — the root and feed key are public and the proof is bound to a single-use nonce — it |
| 2114 | | // CAN answer "registered!" and leave this node recording a name that does not exist. |
| 2115 | | export function bridgeSettings (reach) { |
| 2116 | | if (!reach) return '' |
| 2117 | | const held = reach.fediverse |
| 2118 | | |
| 2119 | | if (held) { |
| 2120 | | return '<h2>Bridge</h2>' + |
| 2121 | | '<p>Your feed is carried as <code>' + esc(held) + '</code></p>' + |
| 2122 | | '<p class="who">A bridge holds the signing key for that network and serves your feed under this ' + |
| 2123 | | 'name. You hold the name; it holds the delivery.</p>' + |
| 2124 | | '<p><a href="/bridge/release">Give up this name</a></p>' |
| 2125 | | } |
| 2126 | | |
| 2127 | | return '<h2>Bridge</h2>' + |
| 2128 | | '<p class="who">A bridge carries your posts onto a network favela does not speak. Ask one for a ' + |
| 2129 | | 'name and it will hold your feed under it.</p>' + |
| 2130 | | '<form method="post" action="/bridge/claim">' + |
| 2131 | | '<p><label>Bridge key<br><input type="text" name="bridge" placeholder="64 hex characters" ' + |
| 2132 | | 'autocomplete="off"></label></p>' + |
| 2133 | | '<p class="who">The bridge prints this when it starts. A key rather than an address, so the ' + |
| 2134 | | 'answer can only come from the bridge you meant.</p>' + |
| 2135 | | '<p><label>Name you want<br><input type="text" name="name" placeholder="alice" ' + |
| 2136 | | 'autocomplete="off"></label></p>' + |
| 2137 | | '<p><label>Invite (only if the bridge is closed)<br><input type="text" name="invite" ' + |
| 2138 | | 'placeholder="leave empty on an open bridge" autocomplete="off"></label></p>' + |
| 2139 | | '<button>ask for this name</button></form>' |
| 2140 | | } |
| 2141 | | |
| 2142 | | /** |
| 2143 | | * ⛔ ITS OWN SCREEN, because a release cannot be undone. The bridge burns the name: the next holder |
| 2144 | | * gets a NEW identity on purpose, so the followers of the old one do not transfer — which is what |
| 2145 | | * stops name reuse becoming impersonation, and also means there is no way back for you. |
| 2146 | | */ |
| 2147 | 1952 | export function bridgeRelease (name, message = null) { |
| 2148 | 1953 | const warn = message ? '<p class="warn">' + esc(message) + '</p>' : '' |
| 2149 | 1954 | return layout('give up this name', warn + |
| … | … | @@ -2191,3 +1996,121 @@ export function didCarry (did, message = null) { |
| 2191 | 1996 | '<button>ask, and publish</button></form>' + |
| 2192 | 1997 | '<p><a href="/settings">cancel</a></p>') |
| 2193 | 1998 | } |
| 1999 | |
| 2000 | // ── one section per network ────────────────────────────────────────────────────────────────────── |
| 2001 | // |
| 2002 | // ⛔ SPLIT BY NETWORK, NOT BY MECHANISM. This page used to have "Publishing" (two checkboxes and a |
| 2003 | // DID box), "Bluesky identity", and "Bridge" — where "Bridge" was the FEDIVERSE one and Bluesky's |
| 2004 | // bridge was a link inside a different card. Two sections offered "a bridge key" and only one named |
| 2005 | // its network, so the obvious thing to do was paste the Bluesky key into the fediverse form. Somebody |
| 2006 | // did exactly that, got an error, retried, and got a second error about channels. |
| 2007 | // |
| 2008 | // ⛔ AND IN DEPENDENCY ORDER, because the order is the thing that was invisible. A switch does |
| 2009 | // nothing without an identity; an identity reaches nobody without a bridge. Each section now reads |
| 2010 | // top to bottom as the sequence it actually is, and each step says what the one below it still needs. |
| 2011 | // |
| 2012 | // ⚠ ONE FORM PER NETWORK, which forces the toggle to be per-network too. The old single form read |
| 2013 | // both checkboxes at once, so two forms would have meant saving one network silently switching the |
| 2014 | // other off — `POST /reach` now takes one network and merges. |
| 2015 | |
| 2016 | const switchFor = (network, label, on, blocked) => |
| 2017 | '<form method="post" action="/reach">' + |
| 2018 | '<input type="hidden" name="network" value="' + esc(network) + '">' + |
| 2019 | '<p><label><input type="checkbox" name="on" value="on"' + (on ? ' checked' : '') + '> ' + esc(label) + |
| 2020 | '</label></p>' + |
| 2021 | (blocked |
| 2022 | ? '<p class="warn">' + esc(blocked) + '</p>' |
| 2023 | : '') + |
| 2024 | '<button>save</button></form>' |
| 2025 | |
| 2026 | /** |
| 2027 | * Bluesky, end to end: who you are, who carries you, and whether anything is sent. |
| 2028 | * |
| 2029 | * ⚠ Absent when the daemon cannot answer — a surface talking to a plain favela-peer has no atproto |
| 2030 | * verbs, and offering what cannot be done is worse than offering nothing. |
| 2031 | */ |
| 2032 | export function blueskySection (did, reach) { |
| 2033 | if (!did) return '' |
| 2034 | const on = reach && Array.isArray(reach.networks) && reach.networks.includes('bsky') |
| 2035 | |
| 2036 | if (!did.did) { |
| 2037 | return '<h2>Bluesky</h2>' + |
| 2038 | '<p class="who">Nothing is sent to Bluesky yet. It needs an identity, and a bridge to carry it.</p>' + |
| 2039 | '<p><a href="/did/create">Create a Bluesky identity</a></p>' + |
| 2040 | '<p class="who">Free, and it reaches no outside service until you publish it.</p>' |
| 2041 | } |
| 2042 | |
| 2043 | // ⛔ THE THREE STEPS, always all three, always in this order. A page that showed only the step you |
| 2044 | // were on could not tell you why the one above it had not taken effect. |
| 2045 | const identity = '<p><strong>1. Identity</strong><br><code>' + esc(did.did) + '</code></p>' + |
| 2046 | (did.sealed |
| 2047 | ? '<p class="who">Your recovery phrase is sealed in a vault on this device.</p>' |
| 2048 | : '<p class="warn">No recovery vault on this device. If you did not write the phrase down, this ' + |
| 2049 | 'identity cannot be recovered.</p>') |
| 2050 | |
| 2051 | const carried = did.handle |
| 2052 | ? '<p><strong>2. Carried by</strong><br><code>' + esc(did.handle) + '</code>' + |
| 2053 | (did.pdsUrl ? ' <span class="who">via ' + esc(did.pdsUrl) + '</span>' : '') + '</p>' |
| 2054 | : '<p><strong>2. Carried by</strong> — nobody yet</p>' + |
| 2055 | '<p class="who">Until a bridge carries it, this identity names no handle and no server, so ' + |
| 2056 | 'Bluesky cannot resolve it however it was published.</p>' + |
| 2057 | '<p><a href="/did/carry">Get carried by a bridge</a></p>' |
| 2058 | |
| 2059 | const designated = reach && reach.credential === did.did |
| 2060 | const designate = !reach |
| 2061 | ? '' |
| 2062 | : designated |
| 2063 | ? '<p class="who">Posts go out under this identity.</p>' |
| 2064 | : '<form method="post" action="/did/designate"><button>publish under this identity</button></form>' |
| 2065 | |
| 2066 | // ⚠ KEPT, and secondary. Designating a DID held somewhere else is a real thing to want, and the |
| 2067 | // button above covers the common case — but removing the field entirely would take away the only |
| 2068 | // way to say it. It names its own network now, which the old shared box did not. |
| 2069 | const other = '<details><summary class="who">Use a different Bluesky identity</summary>' + |
| 2070 | '<form method="post" action="/reach">' + |
| 2071 | '<p><label>DID to publish under<br><input type="text" name="credential" ' + |
| 2072 | 'value="' + esc((reach && reach.credential) || '') + '" placeholder="did:plc:…" autocomplete="off">' + |
| 2073 | '</label></p>' + |
| 2074 | '<p class="who">Leave empty to publish under the identity above. Nothing is sent to Bluesky until ' + |
| 2075 | 'one is chosen.</p><button>save</button></form></details>' |
| 2076 | |
| 2077 | return '<h2>Bluesky</h2>' + identity + carried + |
| 2078 | '<p><strong>3. Send posts there</strong></p>' + designate + other + |
| 2079 | switchFor('bsky', 'Send my public posts to Bluesky', on, |
| 2080 | !did.handle ? 'No bridge carries this identity yet, so nothing will be sent even with this on.' : null) + |
| 2081 | '<p class="who">Plain text only, up to 300 characters — longer posts are withheld, not shortened. ' + |
| 2082 | 'A post sent there cannot be recalled.</p>' |
| 2083 | } |
| 2084 | |
| 2085 | /** |
| 2086 | * Mastodon and the fediverse, end to end: the name a bridge granted, and whether anything is sent. |
| 2087 | */ |
| 2088 | export function fediverseSection (reach) { |
| 2089 | if (!reach) return '' |
| 2090 | const on = Array.isArray(reach.networks) && reach.networks.includes('fediverse') |
| 2091 | const held = reach.fediverse |
| 2092 | |
| 2093 | const name = held |
| 2094 | ? '<p><strong>1. Your name</strong><br><code>' + esc(held) + '</code></p>' + |
| 2095 | '<p class="who">A bridge holds the signing key for that network and serves your feed under this ' + |
| 2096 | 'name. You hold the name; it holds the delivery.</p>' + |
| 2097 | '<p><a href="/bridge/release">Give up this name</a></p>' |
| 2098 | : '<p><strong>1. Your name</strong> — none yet</p>' + |
| 2099 | '<form method="post" action="/bridge/claim">' + |
| 2100 | '<p><label>Fediverse bridge key<br><input type="text" name="bridge" placeholder="64 hex characters" ' + |
| 2101 | 'autocomplete="off"></label></p>' + |
| 2102 | '<p class="who">⚠ The key of a FEDIVERSE bridge. A Bluesky bridge has its own, and they are not ' + |
| 2103 | 'interchangeable — pasting one into the other fails in a way that does not say so.</p>' + |
| 2104 | '<p><label>Name you want<br><input type="text" name="name" placeholder="alice" ' + |
| 2105 | 'autocomplete="off"></label></p>' + |
| 2106 | '<p><label>Invite (only if that bridge is closed)<br><input type="text" name="invite" ' + |
| 2107 | 'placeholder="leave empty on an open bridge" autocomplete="off"></label></p>' + |
| 2108 | '<button>ask for this name</button></form>' |
| 2109 | |
| 2110 | return '<h2>Mastodon and the fediverse</h2>' + name + |
| 2111 | '<p><strong>2. Send posts there</strong></p>' + |
| 2112 | switchFor('fediverse', 'Send my public posts to the fediverse', on, |
| 2113 | !held ? 'No bridge carries you yet, so nothing will be sent even with this on.' : null) + |
| 2114 | '<p class="who">Keeps headings, lists and links. No length limit. A post sent there cannot be ' + |
| 2115 | 'recalled.</p>' |
| 2116 | } |