diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
12 files changed, 162 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} diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index ddeaeeb..df72382 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -79,6 +79,12 @@ export default { 'group.tab_members': 'Mitglieder', 'group.tab_settings': "Einstellungen", 'members.danger_leave_hint': "Sie verlieren den Zugriff auf die Dateien und den Chat dieser Gruppe.", + 'members.uploads_title': "Uploads", + 'members.uploads_on': "Mitglieder können Dateien hinzufügen", + 'members.uploads_off': "Nur Sie können Dateien hinzufügen", + 'members.uploads_disable': "Ausschalten", + 'members.uploads_enable': "Einschalten", + 'members.uploads_hint': "Gilt für alle außer Ihnen. Der Knoten lehnt den Upload selbst ab — es geht nicht darum, eine Schaltfläche zu verbergen.", 'members.danger_delete_hint': "Die Gruppe verschwindet für alle Mitglieder. Das lässt sich nicht rückgängig machen.", 'group.filter': 'Dateien filtern …', 'group.col_name': 'Name', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index 9a0f754..ce14f0b 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -80,6 +80,12 @@ export default { 'group.tab_members': 'Members', 'group.tab_settings': "Settings", 'members.danger_leave_hint': "You will lose access to this group's files and chat.", + 'members.uploads_title': "Uploads", + 'members.uploads_on': "Members can add files", + 'members.uploads_off': "Only you can add files", + 'members.uploads_disable': "Turn off", + 'members.uploads_enable': "Turn on", + 'members.uploads_hint': "Applies to everyone but you. The node refuses the upload itself, so this is not a matter of hiding a button.", 'members.danger_delete_hint': "This removes the group for every member. It cannot be undone.", 'group.filter': 'Filter files...', 'group.col_name': 'Name', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index 0c72445..2d9084b 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -77,6 +77,12 @@ export default { 'group.tab_members': 'Miembros', 'group.tab_settings': "Ajustes", 'members.danger_leave_hint': "Perderá el acceso a los archivos y al chat de este grupo.", + 'members.uploads_title': "Subidas", + 'members.uploads_on': "Los miembros pueden añadir archivos", + 'members.uploads_off': "Solo usted puede añadir archivos", + 'members.uploads_disable': "Desactivar", + 'members.uploads_enable': "Activar", + 'members.uploads_hint': "Se aplica a todos menos a usted. El nodo rechaza la subida por sí mismo: no se trata de ocultar un botón.", 'members.danger_delete_hint': "El grupo desaparece para todos sus miembros. No se puede deshacer.", 'group.filter': 'Filtrar archivos...', 'group.col_name': 'Nombre', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index 0dec5cf..9aa2857 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -78,6 +78,12 @@ export default { 'group.tab_members': 'Membres', 'group.tab_settings': "Paramètres", 'members.danger_leave_hint': "Vous perdrez l’accès aux fichiers et à la discussion de ce groupe.", + 'members.uploads_title': "Envois de fichiers", + 'members.uploads_on': "Les membres peuvent ajouter des fichiers", + 'members.uploads_off': "Vous seul pouvez ajouter des fichiers", + 'members.uploads_disable': "Désactiver", + 'members.uploads_enable': "Activer", + 'members.uploads_hint': "S’applique à tout le monde sauf vous. C’est le nœud qui refuse l’envoi : il ne s’agit pas de masquer un bouton.", 'members.danger_delete_hint': "Le groupe disparaît pour tous ses membres. C’est irréversible.", 'group.filter': 'Filtrer les fichiers...', 'group.col_name': 'Nom', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index 81b6a3a..379e7cb 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -78,6 +78,12 @@ export default { 'group.tab_members': 'Membri', 'group.tab_settings': "Impostazioni", 'members.danger_leave_hint': "Perderai l’accesso ai file e alla chat di questo gruppo.", + 'members.uploads_title': "Caricamenti", + 'members.uploads_on': "I membri possono aggiungere file", + 'members.uploads_off': "Solo tu puoi aggiungere file", + 'members.uploads_disable': "Disattiva", + 'members.uploads_enable': "Attiva", + 'members.uploads_hint': "Vale per tutti tranne te. È il nodo a rifiutare il caricamento: non si tratta di nascondere un pulsante.", 'members.danger_delete_hint': "Il gruppo scompare per tutti i membri. Non è reversibile.", 'group.filter': 'Filtra i file...', 'group.col_name': 'Nome', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index 11c6e31..45e7aa5 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -76,6 +76,12 @@ export default { 'group.tab_members': 'メンバー', 'group.tab_settings': "設定", 'members.danger_leave_hint': "このグループのファイルとチャットにアクセスできなくなります。", + 'members.uploads_title': "アップロード", + 'members.uploads_on': "メンバーはファイルを追加できます", + 'members.uploads_off': "あなただけがファイルを追加できます", + 'members.uploads_disable': "無効にする", + 'members.uploads_enable': "有効にする", + 'members.uploads_hint': "あなた以外の全員に適用されます。ノード自身がアップロードを拒否するため、ボタンを隠すだけの話ではありません。", 'members.danger_delete_hint': "グループはすべてのメンバーから消えます。元に戻せません。", 'group.filter': 'ファイルを絞り込み…', 'group.col_name': '名前', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index 1af0eb1..37d3e24 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -79,6 +79,12 @@ export default { 'group.tab_members': 'Leden', 'group.tab_settings': "Instellingen", 'members.danger_leave_hint': "U verliest de toegang tot de bestanden en de chat van deze groep.", + 'members.uploads_title': "Uploads", + 'members.uploads_on': "Leden kunnen bestanden toevoegen", + 'members.uploads_off': "Alleen u kunt bestanden toevoegen", + 'members.uploads_disable': "Uitschakelen", + 'members.uploads_enable': "Inschakelen", + 'members.uploads_hint': "Geldt voor iedereen behalve u. De node weigert de upload zelf — het gaat niet om het verbergen van een knop.", 'members.danger_delete_hint': "De groep verdwijnt voor alle leden. Dit kan niet ongedaan worden gemaakt.", 'group.filter': 'Bestanden filteren...', 'group.col_name': 'Naam', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index f57dc90..6766515 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -83,6 +83,12 @@ export default { 'group.tab_members': 'Członkowie', 'group.tab_settings': "Ustawienia", 'members.danger_leave_hint': "Utracisz dostęp do plików i czatu tej grupy.", + 'members.uploads_title': "Przesyłanie plików", + 'members.uploads_on': "Członkowie mogą dodawać pliki", + 'members.uploads_off': "Tylko Ty możesz dodawać pliki", + 'members.uploads_disable': "Wyłącz", + 'members.uploads_enable': "Włącz", + 'members.uploads_hint': "Dotyczy wszystkich poza Tobą. To węzeł odrzuca przesłanie — nie chodzi o ukrycie przycisku.", 'members.danger_delete_hint': "Grupa zniknie dla wszystkich członków. Tego nie można cofnąć.", 'group.filter': 'Filtruj pliki...', 'group.col_name': 'Nazwa', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index 0f1586f..fa46887 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -79,6 +79,12 @@ export default { 'group.tab_members': 'Membros', 'group.tab_settings': "Configurações", 'members.danger_leave_hint': "Você perderá o acesso aos arquivos e ao chat deste grupo.", + 'members.uploads_title': "Envios", + 'members.uploads_on': "Os membros podem adicionar arquivos", + 'members.uploads_off': "Somente você pode adicionar arquivos", + 'members.uploads_disable': "Desativar", + 'members.uploads_enable': "Ativar", + 'members.uploads_hint': "Vale para todos menos você. O nó recusa o envio por conta própria: não se trata de esconder um botão.", 'members.danger_delete_hint': "O grupo desaparece para todos os membros. Não há como desfazer.", 'group.filter': 'Filtrar arquivos...', 'group.col_name': 'Nome', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index 89927bb..3276db8 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -76,6 +76,12 @@ export default { 'group.tab_members': '成员', 'group.tab_settings': "设置", 'members.danger_leave_hint': "您将无法再访问该群组的文件和聊天。", + 'members.uploads_title': "上传", + 'members.uploads_on': "成员可以添加文件", + 'members.uploads_off': "只有您可以添加文件", + 'members.uploads_disable': "关闭", + 'members.uploads_enable': "开启", + 'members.uploads_hint': "适用于除您之外的所有人。节点自身会拒绝上传,并非只是隐藏按钮。", 'members.danger_delete_hint': "该群组将对所有成员消失,且无法恢复。", 'group.filter': '筛选文件…', 'group.col_name': '名称', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js index 853ae75..a0509a0 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js @@ -103,6 +103,7 @@ class MeshBayTransport { set onStreamEnd(fn) { this._onStreamEnd = fn; } set onStreamError(fn) { this._onStreamError = fn; } set onIndexSync(fn) { this._onIndexSync = fn; } + set onUploadPolicy(fn) { this._onUploadPolicy = fn; } get sessionKeys() { return this._sessionKeys; } @@ -592,6 +593,24 @@ class MeshBayTransport { * Only the node can do this: its roster decides who it serves. Removing them * on the hub is the other half, and neither implies the other. */ + /** + * Turn uploading by ordinary members on or off. + * + * Signed by the operator like any other privileged operation — the node + * refuses an unsigned one, which is what stops a member turning it back on. + */ + async setMemberUpload(allowed, signFn) { + const msg = await this._sendAndWait({ + type: 'member_upload', v: '0.1', allowed: Boolean(allowed), + }); + if (msg.type === 'error') throw new Error(msg.detail); + if (msg.type === 'admin_challenge') { + return this._authorizeAdminOp( + msg, 'member_upload', allowed ? 'on' : 'off', signFn); + } + return msg; + } + async revokeMember(userId, signFn) { const msg = await this._sendAndWait({ type: 'member_revoke', v: '0.1', user_id: userId, @@ -1079,6 +1098,14 @@ class MeshBayTransport { return; } + // The operator changed who may upload. Unsolicited: it arrives at everyone + // connected, not only at whoever asked. It still has to reach a pending + // caller — the operator's own request resolves on this reply — so it falls + // through to the matching below rather than returning here. + if (msg.type === 'member_upload_ack' && this._onUploadPolicy) { + this._onUploadPolicy(Boolean(msg.allowed)); + } + if (msg.type === 'index_sync' && msg.entries) { if (this._onIndexSync) this._onIndexSync(msg); const oldest = this._pending.entries().next(); |