diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/app.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 77 |
1 files changed, 75 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 62bbfa5..0843d1d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -1304,6 +1304,10 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth, const [nodeRoots, setNodeRoots] = useState([]); const [isNodeAdmin, setIsNodeAdmin] = useState(false); + // Whether ordinary members may upload here. The node decides and enforces it; + // this only says whether to offer the controls. Defaults to true so a node + // that predates the setting behaves as it always did. + const [memberUpload, setMemberUpload] = useState(true); // Paired ≠ operator account. `is_node_admin` says the hub account owning this // node is the one connecting; this says the node pinned *this browser's* key // as an operator key. Only the second one lets you sign an invite, and only @@ -1392,6 +1396,11 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth, _pendingJoinCode = null; if (cancelled) return; setIsNodeAdmin(!!ack.is_node_admin); + setMemberUpload(ack.member_upload !== false); + // Changed while we are connected, by an operator who may be someone + // else entirely. Without this the button stays until a reconnection, + // and a button that is still there is a button people press. + transport.onUploadPolicy = (allowed) => setMemberUpload(allowed); setOperatorPaired(transport.memberRole === 'operator'); // A first join to this node generated an identity for it; leave it with @@ -1792,6 +1801,10 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth, ? selectedFiles[0] : null; const deletableFiles = selectedFiles.filter( e => isNodeAdmin || (userId && e.uploader_id === userId)); + + // Asked in two places — the Files toolbar and the chat composer — so it is + // answered once. The operator is never locked out of their own node. + const mayUpload = memberUpload || isNodeAdmin; const run = (fn) => { setSelecting(false); setSelected(new Set()); @@ -1963,11 +1976,13 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth, ${tab === 'files' && status === 'connected' && html` <div class="file-toolbar"> <div class="toolbar-group"> + ${mayUpload && html` <label class="tb-btn primary"> <${Icon} name="upload" /> ${t('group.upload')} <input type="file" multiple style="display:none" onChange=${uploadFile} /> </label> + `} ${canCreateDir && html` <button class="tb-btn" onClick=${makeDirectory}> <${Icon} name="folder-plus" /> ${t('group.mkdir')} @@ -2085,6 +2100,7 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth, ${tab === 'chat' && status === 'connected' && html` <${ChatPanel} transportRef=${transportRef} username=${username} entries=${entries} gekRef=${gekRef} onRefreshIndex=${refreshIndex} + mayUpload=${mayUpload} onPreview=${(entry) => { if (entry.type === 'video') setVideoEntry(entry); else setPreviewEntry(entry); @@ -2099,6 +2115,8 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth, transportRef=${transportRef} gekRef=${gekRef} isNodeAdmin=${isNodeAdmin} userId=${userId} operatorPaired=${operatorPaired} connected=${status === 'connected'} + memberUpload=${memberUpload} + onMemberUpload=${(allowed) => setMemberUpload(allowed)} onLeft=${onLeft} onPaired=${() => setOperatorPaired(true)} /> `} @@ -2272,7 +2290,7 @@ function _b64ToU8(b64) { */ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, isNodeAdmin, userId, operatorPaired, connected, - onPaired, onLeft }) { + memberUpload, onMemberUpload, onPaired, onLeft }) { const [members, setMembers] = useState([]); const [adminId, setAdminId] = useState(''); const [loading, setLoading] = useState(true); @@ -2350,6 +2368,37 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, } }, [pairCode, transportRef, userId]); + const [uploadBusy, setUploadBusy] = useState(false); + const [uploadMsg, setUploadMsg] = useState(''); + + /** + * Close or open uploading for everyone who is not the operator. + * + * Signed, like removing a member: the node refuses an unsigned instruction, + * so this is a request to the node rather than a decision taken here. The + * button does not move until the node has said it did it. + */ + const setUploads = useCallback(async (allowed) => { + const transport = transportRef && transportRef.current; + setUploadMsg(''); + setUploadBusy(true); + try { + if (!transport || !transport.connected) { + throw new Error('Not connected to the node'); + } + const sk = transport.sessionKeys && transport.sessionKeys.skEdB64; + const signFn = (sk && window.MeshBayKeys) + ? (transcript) => window.MeshBayKeys.signBytes(sk, transcript) + : null; + await transport.setMemberUpload(allowed, signFn); + if (onMemberUpload) onMemberUpload(allowed); + } catch (err) { + setUploadMsg(err.message); + } finally { + setUploadBusy(false); + } + }, [transportRef, onMemberUpload]); + const [removing, setRemoving] = useState(''); /** @@ -2504,6 +2553,27 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, </div> `} + ${/* Operator only, and only with a live connection: the node is what + holds and enforces this, so there is nothing to show or change + without one. */ isNodeAdmin && connected && html` + <div class="settings-section"> + <h3 class="settings-heading">${t('members.uploads_title')}</h3> + <div class="settings-row"> + <span class="settings-label"> + ${memberUpload ? t('members.uploads_on') : t('members.uploads_off')} + </span> + <button class="admin-btn" disabled=${uploadBusy} + onClick=${() => setUploads(!memberUpload)}> + ${uploadBusy ? '...' + : (memberUpload ? t('members.uploads_disable') + : t('members.uploads_enable'))} + </button> + </div> + <p class="settings-hint">${t('members.uploads_hint')}</p> + ${uploadMsg && html`<p class="error-msg">${uploadMsg}</p>`} + </div> + `} + ${connected && html` <div class="settings-section"> <h3 class="settings-heading">${t('device.mine_title')}</h3> @@ -2744,7 +2814,8 @@ function ChatImage({ filename, entries, transportRef, gekRef }) { return html`<img class="chat-att-thumb" src=${blobUrl} alt=${filename} />`; } -function ChatPanel({ transportRef, username, entries, gekRef, onRefreshIndex, onPreview }) { +function ChatPanel({ transportRef, username, entries, gekRef, onRefreshIndex, + onPreview, mayUpload = true }) { const [messages, setMessages] = useState([]); const [hasMore, setHasMore] = useState(false); const [loadingOlder, setLoadingOlder] = useState(false); @@ -3041,11 +3112,13 @@ function ChatPanel({ transportRef, username, entries, gekRef, onRefreshIndex, on </button> `} <div class="chat-input-row"> + ${mayUpload && html` <label class="chat-attach" title="${t('chat.attach')}"> ${attaching ? html`<span class="spinner"></span>` : html`<${Icon} name="clip" />`} <input type="file" style="display:none" onChange=${attachFile} disabled=${attaching} /> </label> + `} <textarea class="chat-input" rows="1" placeholder="${t('chat.placeholder')}" value=${input} |