diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-23 17:01:07 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-23 17:01:07 +0200 |
| commit | cd85808c13926c89a97987d320ac26391eae3267 (patch) | |
| tree | e3e1b12a8ceb8a5ed5b9c9790b6971a2fa2d9d00 /packages/meshbay-hub/src/meshbay_hub/static/group-settings.js | |
| parent | 3d8c1acf785ff7389a68cc515a5c9324dee8de41 (diff) | |
| download | meshbay-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.js | 69 |
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> `} |