aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-30 11:49:56 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-30 11:49:56 +0200
commitd3ad243c4ae3a273f623bd5fc631e3266aa4d0e4 (patch)
tree95ff1252c80a71e93c5098822d31b835572d8b52 /packages/meshbay-hub/src/meshbay_hub/static
parent69554fac7eba6eef7eb8a1c0111c5b92e7f21256 (diff)
downloadmeshbay-d3ad243c4ae3a273f623bd5fc631e3266aa4d0e4.tar.gz
fix: only the owner decides who hosts a group, and nobody is made a member unasked
- hub: a node may host a group only if its account owns it or the owner approved that node (new `group_hosts`). Membership was the ceiling, and every member holds the group key, so any member's node could register as a host and be the one clients kept. A node claiming a group it may not host is recorded as a request; the owner is notified once and approves or refuses it (GET/POST/DELETE /v1/groups/{id}/hosts[/{node_id}]), which takes effect on a connected node at once. - hub: an owner adding a username creates an invitation (new `group_invitations`), accepted or declined by the invitee (/v1/groups/invitations, /{id}/invitation/accept|decline). Until then the group is not listed, not dialled, not searched and not in any token. Invitation links, open joins and group creation still make members directly: they are the account's own act. - hub: the MNP token names only the group it is minted for (group_id is now required), so a node operator no longer learns a member's other groups. - SPA: invitations on the home page; invited people and host requests in the group's settings; the transport sends group_id. Ten catalogues. - Browser probes for both screens, run in Chrome and Firefox. - Design §5.2, §7.2, §7.3, AV32, AV33; protocol §6.3; user guide. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js74
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-settings.js78
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js18
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js18
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js18
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js18
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js18
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js18
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js18
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js18
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js18
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js18
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js5
13 files changed, 335 insertions, 2 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js
index 875b1aa..3a85bf7 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js
@@ -528,7 +528,66 @@ function NotificationFeed({ notifications, onMarkRead, onPurge }) {
`;
}
-function HomePage({ groups, notifications, onMarkRead, onPurge, allowPublicGroups = true }) {
+/**
+ * Groups somebody added this account to, waiting for an answer.
+ *
+ * Being added is an invitation, not a membership (the hub's `group_invitations`):
+ * until it is accepted the group is not in the sidebar, not dialled, not in
+ * Search and not named in any token this browser hands a node. Accepting is
+ * what makes it a group of ours; declining forgets it.
+ */
+function PendingInvitations({ token, onAccepted }) {
+ const [items, setItems] = useState([]);
+ const [busy, setBusy] = useState('');
+ const [error, setError] = useState('');
+ const load = useCallback(() => {
+ if (!token) return;
+ hubFetch('/v1/groups/invitations', { token })
+ .then(data => setItems(data.invitations || []))
+ .catch(() => setItems([]));
+ }, [token]);
+ useEffect(() => { load(); }, [load]);
+
+ const answer = async (inv, verb) => {
+ setBusy(inv.group_id); setError('');
+ try {
+ await hubFetch(`/v1/groups/${inv.group_id}/invitation/${verb}`,
+ { method: 'POST', token });
+ setItems(prev => prev.filter(i => i.group_id !== inv.group_id));
+ if (verb === 'accept' && onAccepted) onAccepted(inv.group_id);
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setBusy('');
+ }
+ };
+
+ if (!items.length) return null;
+ return html`
+ <div class="settings-section">
+ <h3 class="settings-heading">${t('home.invitations')}</h3>
+ <p class="settings-hint">${t('home.invitation_hint')}</p>
+ ${error && html`<p class="error-msg">${error}</p>`}
+ <ul class="invite-links">
+ ${items.map(inv => html`
+ <li key=${inv.group_id} style="display:flex;gap:8px;align-items:center;
+ flex-wrap:wrap;word-break:break-word;margin:4px 0">
+ <strong><${GroupName} name=${inv.name} owner=${inv.owner_username} /></strong>
+ ${inv.invited_by && html`<span style="color:var(--text-dim)">
+ ${t('home.invited_by', { name: inv.invited_by })}</span>`}
+ <button class="admin-btn" type="button" disabled=${busy === inv.group_id}
+ onClick=${() => answer(inv, 'accept')}>${t('home.accept')}</button>
+ <button class="admin-btn" type="button" disabled=${busy === inv.group_id}
+ onClick=${() => answer(inv, 'decline')}>${t('home.decline')}</button>
+ </li>
+ `)}
+ </ul>
+ </div>
+ `;
+}
+
+function HomePage({ groups, notifications, onMarkRead, onPurge, allowPublicGroups = true,
+ token, onGroupsChanged }) {
const [setupDismissed, setSetupDismissed] = useState(false);
if (groups.length === 0) {
@@ -539,6 +598,7 @@ function HomePage({ groups, notifications, onMarkRead, onPurge, allowPublicGroup
return html`
<div>
<h2>${t('home.welcome')}</h2>
+ <${PendingInvitations} token=${token} onAccepted=${onGroupsChanged} />
<${NotificationFeed} notifications=${notifications}
onMarkRead=${onMarkRead} onPurge=${onPurge} />
<${JoinByLink} hubOrigin=${platform.hubOrigin()} />
@@ -555,6 +615,7 @@ function HomePage({ groups, notifications, onMarkRead, onPurge, allowPublicGroup
return html`
<div>
<h2>${t('home.my_groups')}</h2>
+ <${PendingInvitations} token=${token} onAccepted=${onGroupsChanged} />
<${NotificationFeed} notifications=${notifications}
onMarkRead=${onMarkRead} onPurge=${onPurge} />
<div class="group-grid">
@@ -958,6 +1019,15 @@ function App() {
fetchNotifications();
}, [user]);
+ // After an invitation is accepted: the group is ours now, and only the hub
+ // knows its row the way `/mine` answers it.
+ const reloadGroups = useCallback(() => {
+ if (!user) return;
+ hubFetch('/v1/groups/mine', { token: user.token })
+ .then(data => setGroups(data.groups || []))
+ .catch(() => {});
+ }, [user]);
+
// The group list lives here, so an edit made three components down has to come
// back up rather than be re-fetched: a reload would drop the WebRTC connection
// the page is holding.
@@ -1216,6 +1286,7 @@ function App() {
? html`<${LazyAdminPage} token=${user.token} role=${user.role} />`
: html`<${HomePage} groups=${groups} notifications=${notifications}
allowPublicGroups=${allowPublicGroups}
+ token=${user.token} onGroupsChanged=${reloadGroups}
onMarkRead=${markRead} onPurge=${purgeNotifications} />`;
} else if (route === '/settings') {
page = html`<${SettingsPage} user=${user} theme=${theme}
@@ -1233,6 +1304,7 @@ function App() {
} else {
page = html`<${HomePage} groups=${groups} notifications=${notifications}
allowPublicGroups=${allowPublicGroups}
+ token=${user.token} onGroupsChanged=${reloadGroups}
onMarkRead=${markRead} onPurge=${purgeNotifications} />`;
}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
index fba8a0e..048f831 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
@@ -410,7 +410,14 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
onAppDirectories, onRefreshIndex,
onPaired, onLeft }) {
const [members, setMembers] = useState([]);
+ // Asked and not answered — the owner is the only one told (the hub leaves
+ // the field out for anyone else).
+ const [invited, setInvited] = useState([]);
const [adminId, setAdminId] = useState('');
+ // Nodes other than the owner's that asked to serve this group, and what the
+ // owner answered. Only the owner reads it.
+ const [hosts, setHosts] = useState([]);
+ const [hostBusy, setHostBusy] = useState('');
const [loading, setLoading] = useState(true);
const [inviteUser, setInviteUser] = useState('');
const [inviting, setInviting] = useState(false);
@@ -802,6 +809,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
hubFetch(`/v1/groups/${groupId}/members`, { token })
.then(data => {
setMembers(data.members || []);
+ setInvited(data.invited || []);
setAdminId(data.admin_id || '');
})
.catch(() => {})
@@ -810,6 +818,30 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
useEffect(() => { loadMembers(); }, [loadMembers]);
+ const ownsGroup = Boolean(group && group.is_admin);
+ const loadHosts = useCallback(() => {
+ if (!ownsGroup) return;
+ hubFetch(`/v1/groups/${groupId}/hosts`, { token })
+ .then(data => setHosts(data.hosts || []))
+ .catch(() => setHosts([]));
+ }, [groupId, token, ownsGroup]);
+
+ useEffect(() => { loadHosts(); }, [loadHosts]);
+
+ const decideHost = useCallback(async (host, approve) => {
+ setHostBusy(host.node_id);
+ setError('');
+ try {
+ await hubFetch(`/v1/groups/${groupId}/hosts/${host.node_id}`,
+ { method: approve ? 'POST' : 'DELETE', token });
+ loadHosts();
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setHostBusy('');
+ }
+ }, [groupId, token, loadHosts]);
+
useEffect(() => {
hubFetch('/v1/users/me/preferences', { token })
.then(prefs => setInviteByEmail(prefs[INVITE_EMAIL_PREF] === 'true'))
@@ -1382,12 +1414,58 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
</td>
</tr>
`)}
+ ${invited.map(m => html`
+ <tr key=${m.user_id}>
+ <td>${m.username}</td>
+ <td><span class="badge">${t('members.invited')}</span></td>
+ <td class="admin-actions">
+ ${isAdmin && html`
+ <button class="admin-btn danger" disabled=${removing === m.user_id}
+ onClick=${async () => {
+ if (!await ask(t('members.remove_confirm', { user: m.username }))) return;
+ removeMember(m);
+ }}>
+ ${removing === m.user_id ? '...' : t('members.remove')}
+ </button>
+ `}
+ </td>
+ </tr>
+ `)}
</tbody>
</table>
${isAdmin && members.length > 1 && html`
<p class="settings-hint">${t('members.remove_hint')}</p>
`}
</${CollapsibleSection}>
+
+ ${ownsGroup && html`
+ <${CollapsibleSection} title=${t('hosts.title')}>
+ <p class="settings-hint">${t('hosts.hint')}</p>
+ ${hosts.length === 0 && html`<p class="settings-hint">${t('hosts.none')}</p>`}
+ ${hosts.length > 0 && html`
+ <table class="admin-table">
+ <tbody>
+ ${hosts.map(h => html`
+ <tr key=${h.node_id}>
+ <td>${t('hosts.from', { name: h.username })}
+ <br /><code class="node-key">${h.pk_node}</code></td>
+ <td><span class="badge">${t(`hosts.status_${h.status}`)}</span>
+ ${h.online && html` <span class="badge">${t('hosts.online')}</span>`}</td>
+ <td class="admin-actions">
+ ${h.status !== 'approved' && html`
+ <button class="admin-btn" disabled=${hostBusy === h.node_id}
+ onClick=${() => decideHost(h, true)}>${t('hosts.approve')}</button>`}
+ ${h.status !== 'refused' && html`
+ <button class="admin-btn danger" disabled=${hostBusy === h.node_id}
+ onClick=${() => decideHost(h, false)}>${t('hosts.refuse')}</button>`}
+ </td>
+ </tr>
+ `)}
+ </tbody>
+ </table>
+ `}
+ </${CollapsibleSection}>
+ `}
</div>
`;
}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
index a83f44b..3c0f1ec 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -1222,4 +1222,22 @@ export default {
// Names made writable everywhere when saved (portable-name.js)
'transfers.renamed': "Umbenannt: „{name}“ ist nicht auf jedem System ein gültiger Name",
'transfers.renamed_n': "Namen geändert, damit sie auf jedem System gültig sind: {n}",
+
+ // Invitations waiting for an answer, and the nodes a group owner approves
+ 'home.invitations': "Einladungen",
+ 'home.invitation_hint': "Jemand hat Sie zu diesen Gruppen hinzugefügt. Nehmen Sie nur die an, die Sie erwarten: Eine Gruppe, der Sie beitreten, sieht Ihre Adresse, wenn Sie sie öffnen.",
+ 'home.invited_by': "eingeladen von {name}",
+ 'home.accept': "Annehmen",
+ 'home.decline': "Ablehnen",
+ 'members.invited': "eingeladen",
+ 'hosts.title': "Hosts",
+ 'hosts.hint': "Ihre eigenen Nodes stellen diese Gruppe ohne Rückfrage bereit. Ein anderer Node tut es erst, wenn Sie ihn hier genehmigen; ein Node, der anfragt, steht unten.",
+ 'hosts.none': "Kein anderer Node hat angefragt, diese Gruppe bereitzustellen.",
+ 'hosts.from': "Ein Node von {name}",
+ 'hosts.status_pending': "wartet auf Ihre Antwort",
+ 'hosts.status_approved': "genehmigt",
+ 'hosts.status_refused': "abgelehnt",
+ 'hosts.approve': "Genehmigen",
+ 'hosts.refuse': "Ablehnen",
+ 'hosts.online': "online",
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index 46c5094..947c61d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -1203,4 +1203,22 @@ export default {
// Names made writable everywhere when saved (portable-name.js)
'transfers.renamed': "Renamed: “{name}” is not a valid name on every system",
'transfers.renamed_n': "Names changed to be valid on every system: {n}",
+
+ // Invitations waiting for an answer, and the nodes a group owner approves
+ 'home.invitations': "Invitations",
+ 'home.invitation_hint': "Somebody added you to these groups. Accept only the ones you expect: a group you join can see your address when you open it.",
+ 'home.invited_by': "invited by {name}",
+ 'home.accept': "Accept",
+ 'home.decline': "Decline",
+ 'members.invited': "invited",
+ 'hosts.title': "Hosts",
+ 'hosts.hint': "Your own nodes serve this group without asking. Another node serves it only once you approve it here; a node that asks is listed below.",
+ 'hosts.none': "No other node has asked to host this group.",
+ 'hosts.from': "A node of {name}",
+ 'hosts.status_pending': "waiting for your answer",
+ 'hosts.status_approved': "approved",
+ 'hosts.status_refused': "refused",
+ 'hosts.approve': "Approve",
+ 'hosts.refuse': "Refuse",
+ 'hosts.online': "online",
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
index 0d0e865..a0220d8 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -1216,4 +1216,22 @@ export default {
// Names made writable everywhere when saved (portable-name.js)
'transfers.renamed': "Renombrado: «{name}» no es un nombre válido en todos los sistemas",
'transfers.renamed_n': "Nombres cambiados para ser válidos en todos los sistemas: {n}",
+
+ // Invitations waiting for an answer, and the nodes a group owner approves
+ 'home.invitations': "Invitaciones",
+ 'home.invitation_hint': "Alguien le ha añadido a estos grupos. Acepte solo los que espera: un grupo al que se une ve su dirección cuando lo abre.",
+ 'home.invited_by': "invitado por {name}",
+ 'home.accept': "Aceptar",
+ 'home.decline': "Rechazar",
+ 'members.invited': "invitado",
+ 'hosts.title': "Anfitriones",
+ 'hosts.hint': "Sus propios nodes sirven este grupo sin preguntar. Otro node lo sirve solo cuando usted lo aprueba aquí; un node que lo pide aparece abajo.",
+ 'hosts.none': "Ningún otro node ha pedido alojar este grupo.",
+ 'hosts.from': "Un node de {name}",
+ 'hosts.status_pending': "esperando su respuesta",
+ 'hosts.status_approved': "aprobado",
+ 'hosts.status_refused': "rechazado",
+ 'hosts.approve': "Aprobar",
+ 'hosts.refuse': "Rechazar",
+ 'hosts.online': "en línea",
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
index 3c86cd7..c4bc37e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -1231,4 +1231,22 @@ export default {
// Names made writable everywhere when saved (portable-name.js)
'transfers.renamed': "Renommé : « {name} » n’est pas un nom valide sur tous les systèmes",
'transfers.renamed_n': "Noms modifiés pour être valides sur tous les systèmes : {n}",
+
+ // Invitations waiting for an answer, and the nodes a group owner approves
+ 'home.invitations': "Invitations",
+ 'home.invitation_hint': "Quelqu'un vous a ajouté à ces groupes. N'acceptez que ceux que vous attendez : un groupe que vous rejoignez voit votre adresse quand vous l'ouvrez.",
+ 'home.invited_by': "invité par {name}",
+ 'home.accept': "Accepter",
+ 'home.decline': "Refuser",
+ 'members.invited': "invité",
+ 'hosts.title': "Hôtes",
+ 'hosts.hint': "Vos propres nodes servent ce groupe sans rien demander. Un autre node ne le sert qu'une fois approuvé ici ; un node qui le demande apparaît ci-dessous.",
+ 'hosts.none': "Aucun autre node n'a demandé à héberger ce groupe.",
+ 'hosts.from': "Un node de {name}",
+ 'hosts.status_pending': "en attente de votre réponse",
+ 'hosts.status_approved': "approuvé",
+ 'hosts.status_refused': "refusé",
+ 'hosts.approve': "Approuver",
+ 'hosts.refuse': "Refuser",
+ 'hosts.online': "en ligne",
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
index 3a74b4d..59505b8 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -1230,4 +1230,22 @@ export default {
// Names made writable everywhere when saved (portable-name.js)
'transfers.renamed': "Rinominato: «{name}» non è un nome valido su tutti i sistemi",
'transfers.renamed_n': "Nomi modificati per essere validi su tutti i sistemi: {n}",
+
+ // Invitations waiting for an answer, and the nodes a group owner approves
+ 'home.invitations': "Inviti",
+ 'home.invitation_hint': "Qualcuno ti ha aggiunto a questi gruppi. Accetta solo quelli che ti aspetti: un gruppo a cui ti unisci vede il tuo indirizzo quando lo apri.",
+ 'home.invited_by': "invitato da {name}",
+ 'home.accept': "Accetta",
+ 'home.decline': "Rifiuta",
+ 'members.invited': "invitato",
+ 'hosts.title': "Host",
+ 'hosts.hint': "I tuoi node servono questo gruppo senza chiedere. Un altro node lo serve solo dopo che lo approvi qui; un node che lo chiede compare qui sotto.",
+ 'hosts.none': "Nessun altro node ha chiesto di ospitare questo gruppo.",
+ 'hosts.from': "Un node di {name}",
+ 'hosts.status_pending': "in attesa della tua risposta",
+ 'hosts.status_approved': "approvato",
+ 'hosts.status_refused': "rifiutato",
+ 'hosts.approve': "Approva",
+ 'hosts.refuse': "Rifiuta",
+ 'hosts.online': "online",
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
index e0c38de..bba9564 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -1214,4 +1214,22 @@ export default {
// Names made writable everywhere when saved (portable-name.js)
'transfers.renamed': "名前を変更しました:「{name}」はすべてのシステムで有効な名前ではありません",
'transfers.renamed_n': "すべてのシステムで有効になるよう変更した名前:{n}",
+
+ // Invitations waiting for an answer, and the nodes a group owner approves
+ 'home.invitations': "招待",
+ 'home.invitation_hint': "誰かがあなたをこれらのグループに追加しました。心当たりのあるものだけを承諾してください。参加したグループは、開いたときにあなたのアドレスを知ることができます。",
+ 'home.invited_by': "{name} からの招待",
+ 'home.accept': "承諾",
+ 'home.decline': "辞退",
+ 'members.invited': "招待中",
+ 'hosts.title': "ホスト",
+ 'hosts.hint': "あなた自身の node は確認なしでこのグループを提供します。他の node は、ここで承認した後にのみ提供します。申請した node は下に表示されます。",
+ 'hosts.none': "このグループのホストを申請した node は他にありません。",
+ 'hosts.from': "{name} の node",
+ 'hosts.status_pending': "あなたの返答待ち",
+ 'hosts.status_approved': "承認済み",
+ 'hosts.status_refused': "拒否済み",
+ 'hosts.approve': "承認",
+ 'hosts.refuse': "拒否",
+ 'hosts.online': "オンライン",
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
index 83b9eed..87e5b87 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -1232,4 +1232,22 @@ export default {
// Names made writable everywhere when saved (portable-name.js)
'transfers.renamed': "Hernoemd: ‘{name}’ is niet op elk systeem een geldige naam",
'transfers.renamed_n': "Namen aangepast zodat ze op elk systeem geldig zijn: {n}",
+
+ // Invitations waiting for an answer, and the nodes a group owner approves
+ 'home.invitations': "Uitnodigingen",
+ 'home.invitation_hint': "Iemand heeft u aan deze groepen toegevoegd. Accepteer alleen de groepen die u verwacht: een groep waar u lid van wordt, ziet uw adres wanneer u hem opent.",
+ 'home.invited_by': "uitgenodigd door {name}",
+ 'home.accept': "Accepteren",
+ 'home.decline': "Weigeren",
+ 'members.invited': "uitgenodigd",
+ 'hosts.title': "Hosts",
+ 'hosts.hint': "Uw eigen nodes bedienen deze groep zonder te vragen. Een andere node doet dat pas nadat u hem hier goedkeurt; een node die erom vraagt, staat hieronder.",
+ 'hosts.none': "Geen andere node heeft gevraagd deze groep te hosten.",
+ 'hosts.from': "Een node van {name}",
+ 'hosts.status_pending': "wacht op uw antwoord",
+ 'hosts.status_approved': "goedgekeurd",
+ 'hosts.status_refused': "geweigerd",
+ 'hosts.approve': "Goedkeuren",
+ 'hosts.refuse': "Weigeren",
+ 'hosts.online': "online",
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
index 3edba2d..6d81b25 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -1258,4 +1258,22 @@ export default {
// Names made writable everywhere when saved (portable-name.js)
'transfers.renamed': "Zmieniono nazwę: „{name}” nie jest prawidłową nazwą w każdym systemie",
'transfers.renamed_n': "Nazwy zmienione, by były prawidłowe w każdym systemie: {n}",
+
+ // Invitations waiting for an answer, and the nodes a group owner approves
+ 'home.invitations': "Zaproszenia",
+ 'home.invitation_hint': "Ktoś dodał Cię do tych grup. Akceptuj tylko te, których się spodziewasz: grupa, do której dołączysz, widzi Twój adres, gdy ją otwierasz.",
+ 'home.invited_by': "zaprasza {name}",
+ 'home.accept': "Akceptuj",
+ 'home.decline': "Odrzuć",
+ 'members.invited': "zaproszony",
+ 'hosts.title': "Hosty",
+ 'hosts.hint': "Twoje własne node'y obsługują tę grupę bez pytania. Inny node robi to dopiero po Twojej akceptacji tutaj; node, który o to prosi, jest widoczny poniżej.",
+ 'hosts.none': "Żaden inny node nie prosił o hostowanie tej grupy.",
+ 'hosts.from': "Node użytkownika {name}",
+ 'hosts.status_pending': "czeka na Twoją odpowiedź",
+ 'hosts.status_approved': "zaakceptowany",
+ 'hosts.status_refused': "odrzucony",
+ 'hosts.approve': "Akceptuj",
+ 'hosts.refuse': "Odrzuć",
+ 'hosts.online': "online",
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
index 3f44570..7b12ea5 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -1217,4 +1217,22 @@ export default {
// Names made writable everywhere when saved (portable-name.js)
'transfers.renamed': "Renomeado: “{name}” não é um nome válido em todos os sistemas",
'transfers.renamed_n': "Nomes alterados para serem válidos em todos os sistemas: {n}",
+
+ // Invitations waiting for an answer, and the nodes a group owner approves
+ 'home.invitations': "Convites",
+ 'home.invitation_hint': "Alguém adicionou você a estes grupos. Aceite apenas os que espera: um grupo em que você entra vê o seu endereço quando você o abre.",
+ 'home.invited_by': "convidado por {name}",
+ 'home.accept': "Aceitar",
+ 'home.decline': "Recusar",
+ 'members.invited': "convidado",
+ 'hosts.title': "Hosts",
+ 'hosts.hint': "Seus próprios nodes servem este grupo sem perguntar. Outro node só o serve depois que você o aprova aqui; um node que pede aparece abaixo.",
+ 'hosts.none': "Nenhum outro node pediu para hospedar este grupo.",
+ 'hosts.from': "Um node de {name}",
+ 'hosts.status_pending': "aguardando sua resposta",
+ 'hosts.status_approved': "aprovado",
+ 'hosts.status_refused': "recusado",
+ 'hosts.approve': "Aprovar",
+ 'hosts.refuse': "Recusar",
+ 'hosts.online': "online",
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
index 1168460..9f3c902 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
@@ -1203,4 +1203,22 @@ export default {
// Names made writable everywhere when saved (portable-name.js)
'transfers.renamed': "已重命名:“{name}”并非在所有系统上都是有效的名称",
'transfers.renamed_n': "为在所有系统上有效而修改的名称:{n}",
+
+ // Invitations waiting for an answer, and the nodes a group owner approves
+ 'home.invitations': "邀请",
+ 'home.invitation_hint': "有人把你加入了这些群组。只接受你预期的邀请:你加入的群组在你打开它时可以看到你的地址。",
+ 'home.invited_by': "由 {name} 邀请",
+ 'home.accept': "接受",
+ 'home.decline': "拒绝",
+ 'members.invited': "已邀请",
+ 'hosts.title': "主机",
+ 'hosts.hint': "你自己的 node 无需询问即可提供此群组。其他 node 只有在你于此处批准后才会提供;提出申请的 node 列在下方。",
+ 'hosts.none': "没有其他 node 申请托管此群组。",
+ 'hosts.from': "{name} 的 node",
+ 'hosts.status_pending': "等待你的答复",
+ 'hosts.status_approved': "已批准",
+ 'hosts.status_refused': "已拒绝",
+ 'hosts.approve': "批准",
+ 'hosts.refuse': "拒绝",
+ 'hosts.online': "在线",
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index 98d010e..04c2d23 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -653,7 +653,10 @@ class MeshBayTransport {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this._accessToken}`,
},
- body: JSON.stringify({ node_pk: this._nodePkTarget || '' }),
+ // The token names this connection's group and no other: it is handed to
+ // the node's operator, who has no business learning every group this
+ // account belongs to.
+ body: JSON.stringify({ node_pk: this._nodePkTarget || '', group_id: this._groupId }),
});
if (!r.ok) throw new Error(`Could not obtain a node token: ${r.status}`);
return (await r.json()).mnp_token;