/** * GoingsOn - Sharing (Groups) Module * Shared-workspace admin: your identity public key, the groups you belong to, * and (for groups you administer) member management. Sharing a project into a * group lives on the project itself; this section is the group side of it. * * Backend: SyncKit group commands (Groups p4 / GO -> SyncStore M4). Admin * commands round-trip to the server via the SyncKit client; listing members is * admin-only (the server gates it), so member panels render only for groups you * administer; a listMembers failure is treated as "not the admin". */ (function() { 'use strict'; const esc = GoingsOn.utils.escapeHtml; const escAttr = GoingsOn.utils.escapeAttrValue; /** * Render the sharing section into a settings container. Loads the groups you * belong to, your public key, and each group's members (admin-only). * @param {HTMLElement} container */ async function renderSharingSection(container) { let groups; try { groups = await GoingsOn.api.groups.list(); } catch (err) { // The commands require a configured sync client; guide the user there. container.innerHTML = `

Sharing

Connect cloud sync to share projects with a group.

`; return; } // Your public key is what an admin needs to add you. Non-fatal if // encryption is not set up yet. let pubkey = null; try { pubkey = await GoingsOn.api.groups.myPubkey(); } catch (_) {} // Members are admin-only; fetch per group and swallow the not-admin error. const cards = await Promise.all(groups.map(async (g) => { let members = null; try { members = await GoingsOn.api.groups.listMembers(g.id); } catch (_) {} return renderGroupCard(g, members); })); const pubkeyBlock = pubkey ? `

Share this public key with a group admin so they can add you. It is not a secret.

` : `

Set up sync encryption first. Your sharing key is created with it.

`; const groupsBlock = groups.length ? cards.join('') : `

You are not in any groups yet. Create one to share a project.

`; container.innerHTML = `

Sharing

Your public key

${pubkeyBlock}

Create a group

You become the group admin and can add members and share projects into it.

Your groups

${groupsBlock}
`; } /** * One group card. When `members` is null the caller is not the group admin, * so only the name and a member badge render; otherwise the admin member * panel (list + add + remove) renders. */ function renderGroupCard(group, members) { const gid = escAttr(group.id); if (members === null) { return `
${esc(group.name)} Member
`; } const memberRows = members.map((m) => { const isAdmin = m.role === 'admin'; const removeBtn = isAdmin ? '' : ``; return `
${esc(m.email)} ${esc(m.role)} ${removeBtn}
`; }).join(''); return `
${esc(group.name)} Admin
${memberRows || '

No members yet.

'}
`; } /** * Re-render the sharing section if the settings overlay is open. Called after * every mutation so the panel reflects server state. */ function refreshSharingSection() { const overlay = document.getElementById('settings-overlay'); const container = document.getElementById('settings-content'); if (overlay && !overlay.classList.contains('hidden') && container) { return renderSharingSection(container); } } async function createGroup() { const input = document.getElementById('new-group-name'); const name = input?.value.trim(); if (!name) { GoingsOn.ui.showToast('Enter a group name', 'error'); return; } try { await GoingsOn.api.groups.create(name); GoingsOn.groups.invalidate(); // New group name must resolve in badges. GoingsOn.ui.showToast(`Group "${name}" created`); refreshSharingSection(); } catch (err) { GoingsOn.ui.showToast('Failed to create group: ' + GoingsOn.utils.getErrorMessage(err), 'error'); } } async function copyMyGroupPubkey() { const input = document.getElementById('my-group-pubkey'); if (!input) return; const value = input.value; try { await navigator.clipboard.writeText(value); GoingsOn.ui.showToast('Public key copied'); } catch (_) { // Clipboard API unavailable; select the field so the user can copy it. input.focus(); input.select(); GoingsOn.ui.showToast('Press Ctrl/Cmd+C to copy', 'info'); } } async function addGroupMember(groupId) { const email = document.getElementById(`add-member-email-${groupId}`)?.value.trim(); const pubkey = document.getElementById(`add-member-pubkey-${groupId}`)?.value.trim(); if (!email || !pubkey) { GoingsOn.ui.showToast('Enter the member email and their public key', 'error'); return; } try { await GoingsOn.api.groups.addMember(groupId, email, pubkey); GoingsOn.ui.showToast(`Added ${email}`); refreshSharingSection(); } catch (err) { GoingsOn.ui.showToast('Failed to add member: ' + GoingsOn.utils.getErrorMessage(err), 'error'); } } async function removeGroupMember(groupId, userId, email) { const confirmed = await GoingsOn.ui.showConfirmDialog( 'Remove member', `Remove ${email} from this group? They keep whatever they already synced but see nothing new.`, { confirmText: 'Remove', danger: true } ); if (!confirmed) return; try { await GoingsOn.api.groups.removeMember(groupId, userId); GoingsOn.ui.showToast(`Removed ${email}`); refreshSharingSection(); } catch (err) { GoingsOn.ui.showToast('Failed to remove member: ' + GoingsOn.utils.getErrorMessage(err), 'error'); } } // Populate GoingsOn Namespace Object.assign(GoingsOn.settings, { renderSharingSection, createGroup, copyMyGroupPubkey, addGroupMember, removeGroupMember, }); })();