source: Klonkt/src/views/pages/connect.ejs@ 31680c3

main
Last change on this file since 31680c3 was 31680c3, checked in by Robin <roboburr@…>, 8 hours ago

[Add to HUB] on the Connect page — the click is the owner's yes

Putting a klonkt on the Klonkt Hub means the hub sends a Follow. With the
owner gate on (the default) that Follow then waits for the owner — the same
person who just asked for it. Without this they would sign up on the hub and
then have to approve their own sign-up on /connect.

The button POSTs to /connect/add-to-hub, which records a one-day invitation
and redirects to the hub's sign-up form with the site's handle filled in
(?add=). A Follow from the hub's host, signed by the hub itself, passes the
owner gate while that invitation holds. It is not used up on the first
Follow: a hub that re-sends after a timeout would otherwise land in the queue
for something already decided.

Narrow on purpose: only the hub's host, only signed by it, only for a day.
Anyone else still waits for the owner, and a WARD's guardians still decide —
their gate sits before this one and this path never reaches it.

If a request from the hub is already waiting (someone signed the klonkt up
earlier), the click is exactly the yes it waits for: it is accepted on the
spot and the button goes to the channel on the hub instead of the form. Once
the hub follows, /connect shows a confirmation instead of the button. Hidden
after a move, where the outgoing side is locked.

KLONKT_HUB_URL lets a self-hoster point the button at another hub; default
​https://hub.klonkt.com.

Co-Authored-By: Claude Opus 5.5 <noreply@…>

  • Property mode set to 100644
File size: 14.2 KB
Line 
1<div class="tl-wrap">
2 <%- include('../partials/fedi-tabs', { active: 'connect' }) %>
3 <h1 class="tl-title"><%= t('tl.connect') %></h1>
4 <% if (typeof success !== 'undefined' && success) { %><div class="alert alert-success"><%= success %></div><% } %>
5 <% if (typeof error !== 'undefined' && error) { %><div class="alert alert-error"><%= error %></div><% } %>
6
7 <%# FEP-633c §2 + §3.6: who guards this account, and how available each of
8 them is. Connect is the "who am I connected to" page, and a guardian is
9 the one connection a ward should never have to hunt for. Availability in
10 buddy-list language on the responsibility axis; the labels are UI, the
11 wire keeps the spec terms. Never public: this page is the owner's. %>
12 <% if (typeof myGuardians !== 'undefined' && myGuardians.length) { %>
13 <section class="conn-guardians">
14 <h2 class="conn-gtitle">&#128737; <%= t('msg.guardians_label') %></h2>
15 <ul class="conn-glist">
16 <% myGuardians.forEach(function(g){
17 var _st = g.availability === 'away' ? 'away' : (g.availability === 'dormant' ? 'dormant' : 'active');
18 var _lbl = _st === 'away' ? t('msg.g_away', { date: g.awayUntil ? formatDate(g.awayUntil) : '?' })
19 : (_st === 'dormant' ? t('msg.g_offline') : t('msg.g_available')); %>
20 <li class="conn-guardian">
21 <span class="conn-gdot conn-gdot--<%= _st %>" aria-hidden="true"></span>
22 <span class="conn-ghandle"><%= g.handle %></span>
23 <span class="conn-gstate"><%= _lbl %></span>
24 </li>
25 <% }); %>
26 </ul>
27 </section>
28 <% } %>
29
30 <% var _moved = (typeof movedTo !== 'undefined' && movedTo) ? movedTo : null; %>
31 <% if (_moved) { %>
32 <%# De uitgaande kant staat op slot (FEP-7628). Dat hoort te blijken voordat
33 je op een knop drukt, niet daarna uit een foutmelding. Lezen blijft wel
34 gewoon werken, dus de lijst hieronder staat er nog. %>
35 <div class="alert alert-error tl-moved">
36 <strong><%= t('tl.moved_title') %></strong>
37 <p><%= t('tl.moved_lead') %> <a href="<%= _moved %>"><%= _moved %></a></p>
38 <p class="form-hint"><%= t('tl.moved_hint') %></p>
39 </div>
40 <% } else { %>
41 <form method="post" action="/news/follow" class="tl-follow">
42 <input type="text" name="handle" placeholder="@naam@server.social of site.com" autocomplete="off" spellcheck="false" required>
43 <button type="submit" class="btn btn-primary"><%= t('tl.follow_btn') %></button>
44 <label class="tl-follow-ab" title="<%= t('tl.autoboost_hint') %>"><input type="checkbox" name="auto_boost" value="1"> &#10024; <%= t('tl.autoboost_follow') %></label>
45 </form>
46 <% } %>
47
48 <%# Je volglijst meenemen staat sinds 14-8 op /admin/migrate, bij de rest van
49 de verhuizing (Robin: alle migratie-opties op de migrate-pagina). Hier
50 stond hij bovendien zonder enige opmaak: labels als losse tekst naast hun
51 invoerveld. Op die pagina erft hij de kaartopmaak. %>
52
53 <%# [Add to HUB] (Robin, 30-9): je klonkt op de Klonkt Hub zetten. De knop
54 gaat naar het aanmeldformulier van de hub, vooraf ingevuld, en keurt de
55 Follow die daarop volgt hier alvast goed -- anders moest je straks je
56 eigen aanmelding nog accepteren. Een nieuw tabblad, zodat deze pagina
57 blijft staan. Staat hij er al op, dan een bevestiging in plaats van de knop. %>
58 <% if (typeof hub !== 'undefined' && hub && !(typeof movedTo !== 'undefined' && movedTo)) { %>
59 <section class="conn-hub">
60 <h2 class="conn-h"><%= t('tl.hub_title') %></h2>
61 <% if (hub.onHub) { %>
62 <p class="conn-unreach-lead">&#10003; <%= t('tl.hub_on') %> <a href="<%= hub.url %>" target="_blank" rel="noopener"><%= t('tl.hub_open') %></a></p>
63 <% } else { %>
64 <p class="conn-unreach-lead"><%= t('tl.hub_lead') %></p>
65 <% if (hub.ward) { %><p class="conn-unreach-lead"><%= t('tl.hub_ward') %></p><% } %>
66 <form method="post" action="/connect/add-to-hub" target="_blank">
67 <button type="submit" class="btn btn-primary"><%= t('tl.hub_add') %></button>
68 </form>
69 <% } %>
70 </section>
71 <% } %>
72
73 <%# De eigenaarspoort (Robins wens, 18-8): volgverzoeken die wachten op
74 een beslissing van de eigenaar. Alleen buiten voogdij — een ward ziet
75 dit nooit, want daar beslissen de guardians. De poort zelf zit hier
76 ook: aan/uit op de plek waar de verzoeken staan. %>
77 <% if (!(myGuardians && myGuardians.length)) { %>
78 <form method="post" action="/connect/approve-followers" class="tl-foll-ab" title="<%= t('tl.approve_toggle_hint') %>">
79 <label><input type="checkbox" name="on" value="1" <%= (typeof approveFollowers !== 'undefined' && approveFollowers) ? 'checked' : '' %> data-autosubmit> &#128737; <%= t('tl.approve_toggle') %></label>
80 </form>
81 <% } %>
82 <% var folreqs = (typeof followRequests !== 'undefined' && followRequests) || []; %>
83 <% if (folreqs.length) { %>
84 <h2 class="conn-h"><%= t('tl.folreq_title') %></h2>
85 <p class="conn-unreach-lead"><%= t('tl.folreq_sub') %></p>
86 <ul class="tl-foll-list">
87 <% folreqs.forEach(function (f) { var disp = f.follower_name || f.follower_handle || f.follower_uri; %>
88 <li class="tl-foll">
89 <span class="tl-foll-av"><% if (f.follower_icon) { %><img src="<%= avatar(f.follower_icon, 96) %>" alt=""><% } else { %><%= String(disp || '?').replace(/^@/, '').charAt(0).toUpperCase() %><% } %></span>
90 <span class="tl-foll-meta">
91 <a href="<%= f.follower_uri %>" target="_blank" rel="nofollow noopener"><%= disp %></a>
92 <% if (f.follower_handle && f.follower_name) { %><span class="conn-sub"><%= f.follower_handle %></span><% } %>
93 </span>
94 <form method="post" action="/follow-requests/approve"><input type="hidden" name="id" value="<%= f.id %>"><button type="submit" class="btn btn-primary"><%= t('tl.folreq_accept') %></button></form>
95 <form method="post" action="/follow-requests/deny"><input type="hidden" name="id" value="<%= f.id %>"><button type="submit" class="tl-unfollow"><%= t('tl.folreq_deny') %></button></form>
96 </li>
97 <% }); %>
98 </ul>
99 <% } %>
100
101 <%
102 var conns = connections || [];
103 var reachable = conns.filter(function (c) { return !c.unreachable; });
104 var unreachable = conns.filter(function (c) { return c.unreachable; });
105 // Display fields for one connection: name/handle (fallback: derive from actor_uri).
106 function connView(c) {
107 var host = '', user = '';
108 try { var u = new URL(c.actor_uri); host = u.hostname; user = (u.pathname.split('/').filter(Boolean).pop() || ''); } catch (e) {}
109 var handle = c.handle || (user ? ('@' + user + '@' + host) : c.actor_uri);
110 var display = c.name || handle;
111 return { handle: handle, display: display,
112 initial: String(display || '?').replace(/^@/, '').charAt(0).toUpperCase(),
113 profileUrl: c.url || c.actor_uri };
114 }
115 function arrow(dir) { return dir === 'mutual' ? '↔' : (dir === 'following' ? '→' : '←'); }
116 %>
117
118 <% if (reachable.length) { %>
119 <ul class="tl-foll-list">
120 <% reachable.forEach(function (c) { var v = connView(c); %>
121 <li class="tl-foll">
122 <span class="tl-foll-av"><% if (c.icon) { %><img src="<%= avatar(c.icon, 96) %>" alt=""><% } else { %><%= v.initial %><% } %></span>
123 <span class="tl-foll-meta">
124 <a href="<%= v.profileUrl %>" target="_blank" rel="nofollow noopener"><%= v.display %></a>
125 <span class="conn-sub">
126 <span class="conn-dir conn-dir--<%= c.direction %>"><%= arrow(c.direction) %> <%= t('tl.dir_' + c.direction) %></span>
127 <% if (c.follower && c.last_delivery_at) { %><span class="conn-deliv">&middot; <%= t('tl.last_delivery') %>: <%= formatDateTime(c.last_delivery_at) %></span><% } %>
128 </span>
129 </span>
130 <% if (c.following) { %>
131 <form method="post" action="/news/autoboost" class="tl-foll-ab" title="<%= t('tl.autoboost_hint') %>">
132 <input type="hidden" name="actor_uri" value="<%= c.actor_uri %>">
133 <label><input type="checkbox" name="auto_boost" value="1" <%= c.auto_boost ? 'checked' : '' %> data-autosubmit> &#10024; <%= t('tl.autoboost') %></label>
134 </form>
135 <form method="post" action="/news/unfollow"><input type="hidden" name="actor_uri" value="<%= c.actor_uri %>"><button type="submit" class="tl-unfollow"><%= t('tl.unfollow') %></button></form>
136 <% } %>
137 </li>
138 <% }); %>
139 </ul>
140 <% } else { %>
141 <p class="tl-empty"><%= t('tl.connect_empty') %></p>
142 <% } %>
143
144 <% if (unreachable.length) { %>
145 <details class="conn-unreach">
146 <summary>&#9888; <%= t('tl.unreachable') %> (<%= unreachable.length %>)</summary>
147 <p class="conn-unreach-lead"><%= t('tl.unreachable_lead') %></p>
148 <ul class="tl-foll-list">
149 <% unreachable.forEach(function (c) { var v = connView(c); %>
150 <li class="tl-foll is-stale">
151 <span class="tl-foll-av"><% if (c.icon) { %><img src="<%= avatar(c.icon, 96) %>" alt=""><% } else { %><%= v.initial %><% } %></span>
152 <span class="tl-foll-meta">
153 <a href="<%= v.profileUrl %>" target="_blank" rel="nofollow noopener"><%= v.display %></a>
154 <span class="conn-sub">
155 <span class="conn-dir conn-dir--<%= c.direction %>"><%= arrow(c.direction) %> <%= t('tl.dir_' + c.direction) %></span>
156 <% if (c.last_delivery_at) { %><span class="conn-deliv">&middot; <%= t('tl.last_delivery') %>: <%= formatDateTime(c.last_delivery_at) %></span><% } else { %><span class="conn-never">&middot; <%= t('tl.never_delivered') %></span><% } %>
157 <% if (c.last_error_at) { %><span class="conn-err">&middot; <%= t('tl.delivery_failed') %>: <%= formatDateTime(c.last_error_at) %></span><% } %>
158 </span>
159 </span>
160 <% if (c.follower_id) { %>
161 <form method="post" action="/followers/<%= c.follower_id %>/remove" onsubmit="return confirm('<%= t('tl.remove_confirm') %>')">
162 <button type="submit" class="tl-unfollow"><%= t('tl.remove_follower') %></button>
163 </form>
164 <% } %>
165 </li>
166 <% }); %>
167 </ul>
168 </details>
169 <% } %>
170</div>
171
172<style>
173 /* Guardians (FEP-633c 3.6): the buddy-list dot on the responsibility axis.
174 Green available, yellow declared away with an end, grey observed dormant
175 (one answer restores). Set apart from the follow list on purpose: these
176 are not people you chose to follow, they are the ones looking after you. */
177 .conn-guardians { margin: 0 0 1.25rem; padding: .8rem 1rem; border-radius: 12px;
178 border: 1px solid color-mix(in srgb, var(--ink, #000) 10%, transparent);
179 background: color-mix(in srgb, var(--ink, #000) 2.5%, transparent);
180 border-left: 3px solid #35a2ff; }
181 .conn-gtitle { margin: 0 0 .5rem; font-size: .95rem; }
182 .conn-glist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .35rem; }
183 .conn-guardian { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
184 .conn-gdot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
185 .conn-gdot--active { background: #4caf7d; }
186 .conn-gdot--away { background: #e0a83a; }
187 .conn-gdot--dormant { background: #8a93a3; }
188 .conn-ghandle { font-weight: 600; }
189 .conn-gstate { color: var(--ink-soft, #888); font-size: .82rem; }
190 .tl-wrap { max-width: 640px; margin: 1rem auto; padding: 0 1rem; }
191 .tl-title { margin: 0 0 .75rem; }
192 .tl-empty { color: var(--ink-soft, #888); }
193 .tl-follow { display: flex; gap: .5rem; margin: 0 0 1.25rem; flex-wrap: wrap; align-items: center; }
194 .tl-follow input[type="text"] { flex: 1; min-width: 200px; height: 2.4rem; padding: 0 .9rem; border-radius: 999px; font: inherit;
195 border: 1px solid color-mix(in srgb, var(--ink, #000) 18%, transparent); background: color-mix(in srgb, var(--ink, #000) 4%, transparent); color: var(--ink, #000); }
196 .tl-follow-ab { font-size: .85rem; color: var(--ink-soft, #888); display: inline-flex; align-items: center; gap: .3rem; cursor: pointer; }
197 .tl-foll-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .4rem; }
198 .tl-foll { display: flex; align-items: center; gap: .6rem; padding: .45rem .6rem; border-radius: 12px; background: color-mix(in srgb, var(--ink, #000) 4%, transparent); }
199 .tl-foll.is-stale { background: color-mix(in srgb, #c0392b 10%, transparent); }
200 .tl-foll-av { flex: 0 0 36px; width: 36px; height: 36px; border-radius: 50%; overflow: hidden; display: inline-flex; align-items: center; justify-content: center; font-weight: 700; background: color-mix(in srgb, var(--accent, #888) 20%, transparent); color: var(--accent, #555); }
201 .tl-foll-av img { width: 100%; height: 100%; object-fit: cover; background: #fff; }
202 .tl-foll-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; line-height: 1.3; }
203 .tl-foll-meta a { color: var(--ink, inherit); text-decoration: none; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
204 .tl-foll-meta a:hover { text-decoration: underline; }
205 .conn-sub { font-size: .78rem; color: var(--ink-soft, #888); display: inline-flex; flex-wrap: wrap; gap: .35rem; align-items: baseline; }
206 .conn-dir { font-weight: 600; }
207 .conn-dir--mutual { color: var(--accent, #06c); }
208 .conn-dir--following { color: color-mix(in srgb, var(--ink, #000) 62%, transparent); }
209 .conn-dir--follower { color: color-mix(in srgb, var(--ink, #000) 62%, transparent); }
210 .conn-never, .conn-err { color: #c0392b; }
211 .tl-foll-ab { margin: 0; font-size: .8rem; color: var(--ink-soft, #888); cursor: pointer; }
212 .tl-foll-ab label { display: inline-flex; align-items: center; gap: .25rem; cursor: pointer; }
213 .tl-unfollow { background: none; border: 1px solid color-mix(in srgb, var(--ink, #000) 18%, transparent); border-radius: 8px; padding: .25rem .6rem; font-size: .8rem; color: var(--ink-soft, #888); cursor: pointer; white-space: nowrap; }
214 .tl-unfollow:hover { color: var(--ink, #000); }
215 .conn-unreach { margin: 1.25rem 0 0; border-top: 1px solid color-mix(in srgb, var(--ink, #000) 10%, transparent); padding-top: .75rem; }
216 .conn-unreach summary { cursor: pointer; font-weight: 600; color: #c0392b; }
217 .conn-unreach-lead { font-size: .8rem; color: var(--ink-soft, #888); margin: .5rem 0 .75rem; }
218</style>
Note: See TracBrowser for help on using the repository browser.