aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-25 17:10:15 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-25 17:10:15 +0200
commit90c69477d5f701158112b3c294eff26312f89da6 (patch)
tree2691ca096da5cf93482bc750c7a03599fbc44dc8 /packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
parent8bb94a39609f57be2c486f579849eebb04606808 (diff)
downloadmeshbay-90c69477d5f701158112b3c294eff26312f89da6.tar.gz
feat: invitation links no longer bound to an e-mail addressHEADmain
A link is redeemable by whoever opens it first, so it can be sent by any messaging app. The address is optional (mail + label only); a link lives 7 days, fixed. Adds a Share button; see MESHBAY_DESIGN.md §3.4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/group-settings.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-settings.js37
1 files changed, 26 insertions, 11 deletions
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 529a33d..a510e63 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
@@ -897,10 +897,11 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
// ── Invitation links (docs/MESHBAY_DESIGN.md §3.4) ──────────────────
//
// Two halves, in the order that leaves nothing half-made: the node's code
- // first, then the hub's ticket bound to the address; a ticket the hub then
- // refuses takes the code back with it, since a code nobody can reach the node
- // with only occupies one of the group's twenty places. The code reaches the
- // hub only when the box asks the hub to write the mail.
+ // first, then the hub's ticket; a ticket the hub then refuses takes the code
+ // back with it, since a code nobody can reach the node with only occupies one
+ // of the group's twenty places. The address is optional and binds nothing:
+ // the link is for whoever opens it first, so it can go by any messaging app.
+ // The code reaches the hub only when the box asks the hub to write the mail.
const [linkEmail, setLinkEmail] = useState('');
const [linking, setLinking] = useState(false);
const [linkError, setLinkError] = useState('');
@@ -932,7 +933,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
const doCreateLink = useCallback(async (e) => {
e.preventDefault();
const email = linkEmail.trim();
- if (!email) return;
+ const mailIt = inviteByEmail && Boolean(email);
setLinking(true);
setLinkError('');
setNewLink(null);
@@ -951,8 +952,8 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
method: 'POST', token,
body: {
email, expires_at: node.expires_at, node_invite_id: node.invite_id,
- send_email: inviteByEmail,
- ...(inviteByEmail ? { node_pk: n, code: node.code } : {}),
+ send_email: mailIt,
+ ...(mailIt ? { node_pk: n, code: node.code } : {}),
},
});
} catch (err) {
@@ -960,7 +961,6 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
throw err;
}
setNewLink({
- email,
link: inviteLinkHere({ g: groupId, t: ticket.ticket, n, c: node.code }),
emailStatus: ticket.email_status,
});
@@ -1004,6 +1004,16 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
} catch { /* the field is selectable */ }
}, [newLink]);
+ // The system share sheet, where there is one (phones mostly): the way a link
+ // reaches a messaging app without a round trip through the clipboard.
+ const canShare = typeof navigator !== 'undefined' && typeof navigator.share === 'function';
+ const shareLink = useCallback(async () => {
+ if (!newLink) return;
+ try {
+ await navigator.share({ title: t('members.link_share_title'), url: newLink.link });
+ } catch { /* dismissed; the field and Copy are still there */ }
+ }, [newLink]);
+
if (loading) return html`<p class="page-message">${t('explore.loading')}</p>`;
const isOwner = Boolean(isAdmin);
@@ -1067,13 +1077,17 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
<form onSubmit=${doCreateLink}>
${newLink && html`
<div class="success-msg" style="margin-bottom:8px">
- <p>${t('members.link_ready', { email: newLink.email })}</p>
+ <p>${t('members.link_ready')}</p>
<div class="form-row">
<input type="text" readonly value=${newLink.link}
onFocus=${e => e.target.select()} />
<button class="admin-btn" type="button" onClick=${copyLink}>
${linkCopied ? t('members.link_copied') : t('members.link_copy')}
</button>
+ ${canShare && html`
+ <button class="admin-btn" type="button" onClick=${shareLink}>
+ ${t('members.link_share')}
+ </button>`}
</div>
${newLink.emailStatus === 'sent'
? html`<p style="color:var(--success)">${t('members.link_email_sent')}</p>`
@@ -1085,7 +1099,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
<div class="form-row">
<input type="email" placeholder=${t('members.link_email_placeholder')}
value=${linkEmail} onInput=${e => setLinkEmail(e.target.value)}
- disabled=${!connected || !operatorPaired} required />
+ disabled=${!connected || !operatorPaired} />
<button class="admin-btn" type="submit"
disabled=${linking || !connected || !operatorPaired}>
${linking ? '...' : t('members.link_btn')}
@@ -1104,7 +1118,8 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
${links.map(l => html`
<li key=${l.link_id} style="display:flex;gap:8px;align-items:center;
flex-wrap:wrap;word-break:break-word;margin:4px 0">
- <span>${l.email}</span>
+ <span>${l.email || t('members.link_unlabelled',
+ { date: new Date(l.created_at).toLocaleDateString() })}</span>
<span style="color:var(--text-dim)">
${l.status === 'expired'
? t('members.link_status_expired')