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-23 17:01:07 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-23 17:01:07 +0200
commitcd85808c13926c89a97987d320ac26391eae3267 (patch)
treee3e1b12a8ceb8a5ed5b9c9790b6971a2fa2d9d00 /packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
parent3d8c1acf785ff7389a68cc515a5c9324dee8de41 (diff)
downloadmeshbay-cd85808c13926c89a97987d320ac26391eae3267.tar.gz
feat(hub): make mailing an invitation a remembered choice
A "Send the invitation by e-mail" box under the Invite member field, checked by default and stored as the invite_email preference. Unchecked, invite-notify is never called and the hub never sees the code. 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.js69
1 files changed, 52 insertions, 17 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 5f811e8..64ea3fa 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
@@ -9,6 +9,9 @@ import { hubFetch, navigate } from './hub-client.js';
import { availableApps, configurableApps } from './apps.js';
import * as platform from './platform.js';
+// The account preference behind the "send by e-mail" box. The hub's
+// ALLOWED_PREF_KEYS must list it, or every toggle snaps back.
+export const INVITE_EMAIL_PREF = 'invite_email';
// ── Shared Directories Table ────────────────────────────────────────────
@@ -410,6 +413,10 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
const [loading, setLoading] = useState(true);
const [inviteUser, setInviteUser] = useState('');
const [inviting, setInviting] = useState(false);
+ // Whether the hub mails the invitation. Checked, the hub is handed the code
+ // to write it into the mail — so it is the inviter's choice, remembered per
+ // account (docs/MESHBAY_DESIGN.md §3.4). Unchecked, the hub never sees it.
+ const [inviteByEmail, setInviteByEmail] = useState(true);
const [error, setError] = useState('');
// Node loopback state (Electron-only)
@@ -801,6 +808,23 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
useEffect(() => { loadMembers(); }, [loadMembers]);
+ useEffect(() => {
+ hubFetch('/v1/users/me/preferences', { token })
+ .then(prefs => setInviteByEmail(prefs[INVITE_EMAIL_PREF] !== 'false'))
+ .catch(() => {});
+ }, [token]);
+
+ const toggleInviteByEmail = useCallback(async (next) => {
+ setInviteByEmail(next);
+ try {
+ await hubFetch(`/v1/users/me/preferences/${INVITE_EMAIL_PREF}`, {
+ method: 'PUT', token, body: { value: next ? 'true' : 'false' },
+ });
+ } catch {
+ setInviteByEmail(!next);
+ }
+ }, [token]);
+
const isAdmin = group && group.is_admin;
const doInvite = useCallback(async (e) => {
@@ -838,23 +862,27 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
method: 'POST', token, body: {},
});
- // Send an email notification to the invitee with the code.
- // The hub decrypts their email server-side — the inviter never sees it.
- let emailStatus = 'no_email';
- try {
- const notif = await hubFetch(`/v1/groups/${groupId}/invite-notify`, {
- method: 'POST', token,
- // No group_name: the hub reads it from the group row it has already
- // loaded. Sending one offered a second answer to a settled question,
- // and that answer was the subject line of an email the hub signs.
- body: { username, code: result.code },
- });
- emailStatus = notif.status;
- } catch { /* best effort */ }
+ // Send an email notification to the invitee with the code, if the inviter
+ // asked for it. The hub decrypts their email server-side — the inviter
+ // never sees it — and so the hub reads the code; without the box ticked
+ // nothing below runs and the code is shown on this page and nowhere else.
+ let emailStatus = inviteByEmail ? 'no_email' : 'not_requested';
+ if (inviteByEmail) {
+ try {
+ const notif = await hubFetch(`/v1/groups/${groupId}/invite-notify`, {
+ method: 'POST', token,
+ // No group_name: the hub reads it from the group row it has already
+ // loaded. Sending one offered a second answer to a settled question,
+ // and that answer was the subject line of an email the hub signs.
+ body: { username, code: result.code },
+ });
+ emailStatus = notif.status;
+ } catch { /* best effort */ }
+ }
setInviteCode({
username, code: result.code, expires: result.expires_at,
- emailSent: emailStatus === 'sent',
+ emailStatus,
});
setInviteUser('');
loadMembers();
@@ -863,7 +891,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
} finally {
setInviting(false);
}
- }, [groupId, token, inviteUser, loadMembers, transportRef]);
+ }, [groupId, token, inviteUser, inviteByEmail, loadMembers, transportRef]);
if (loading) return html`<p class="page-message">${t('explore.loading')}</p>`;
@@ -893,9 +921,10 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
<div class="success-msg" style="margin-bottom:8px">
<p>${t('members.invite_code_ready', { user: inviteCode.username })}</p>
<p class="code-display">${inviteCode.code}</p>
- ${inviteCode.emailSent
+ ${inviteCode.emailStatus === 'sent'
? html`<p style="color:var(--success)">${t('members.invite_email_sent')}</p>`
- : html`<p style="color:var(--text-dim)">${t('members.invite_email_failed')}</p>`
+ : inviteCode.emailStatus !== 'not_requested'
+ && html`<p style="color:var(--text-dim)">${t('members.invite_email_failed')}</p>`
}
<p>${t('members.invite_code_hint')}</p>
</div>
@@ -909,6 +938,12 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
${inviting ? '...' : t('members.invite_btn')}
</button>
</div>
+ <label style="display:flex; gap:8px; align-items:flex-start; margin:6px 0 0;
+ font-size:0.88em; color:var(--text-secondary)">
+ <input type="checkbox" checked=${inviteByEmail}
+ onChange=${e => toggleInviteByEmail(e.target.checked)} />
+ <span>${t('members.invite_email_opt')}</span>
+ </label>
</form>
</div>
`}