From 54b535d7102e9a68d9b37fb215623fbd4faff95e Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Fri, 14 Aug 2026 20:54:57 +0200 Subject: feat(ui): upload progress, PDF preview, invite form above the member list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three of the ten UI items, the ones that needed no decision. Upload showed a disabled button and nothing else — on a large file that is indistinguishable from a hang. It now has the same bar downloads have, filled from chunks the node has acknowledged rather than bytes read locally, and says "indexing" for the pause at the end: the node re-indexes on a filesystem event, so there is a real wait there with nothing to poll. PDFs open in the overlay through the browser's own viewer. The file is decrypted in the page as any other preview is, and shown from a blob: URL — nothing leaves the tab, and no external viewer is involved. The invite form sits above the member list, where the action is rather than after the thing it acts on. Co-Authored-By: Claude Opus 5 --- packages/meshbay-hub/src/meshbay_hub/static/app.js | 80 ++++++++++++++++------ .../meshbay-hub/src/meshbay_hub/static/i18n.js | 2 + .../meshbay-hub/src/meshbay_hub/static/style.css | 9 +++ 3 files changed, 69 insertions(+), 22 deletions(-) diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 3087e0e..cff001b 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -823,6 +823,7 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { const [previewEntry, setPreviewEntry] = useState(null); const [tab, setTab] = useState('files'); const [uploading, setUploading] = useState(false); + const [ulState, setUlState] = useState(null); const [menuOpen, setMenuOpen] = useState(null); const [isNodeAdmin, setIsNodeAdmin] = useState(false); const [needsCode, setNeedsCode] = useState(false); @@ -1020,13 +1021,20 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { if (!transport || !transport.connected) return; setUploading(true); setError(''); + setUlState({ name: file.name, sent: 0, total: file.size, indexing: false }); try { const totalChunks = Math.ceil(file.size / UPLOAD_CHUNK_SIZE); for (let i = 0; i < totalChunks; i++) { const slice = file.slice(i * UPLOAD_CHUNK_SIZE, (i + 1) * UPLOAD_CHUNK_SIZE); const buf = new Uint8Array(await slice.arrayBuffer()); await transport.uploadChunk(file.name, i, totalChunks, buf); + // Bytes actually acknowledged by the node, not bytes read locally. + setUlState(prev => prev && { ...prev, sent: Math.min(file.size, + (i + 1) * UPLOAD_CHUNK_SIZE) }); } + // The node re-indexes on a filesystem event; there is nothing to poll, so + // say what is happening instead of showing a finished bar and no file. + setUlState(prev => prev && { ...prev, indexing: true }); await new Promise(r => setTimeout(r, 2500)); const indexMsg = await transport.fetchIndex(); if (indexMsg.entries) setEntries(indexMsg.entries); @@ -1034,6 +1042,7 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { setError(err.message); } finally { setUploading(false); + setUlState(null); } }, []); @@ -1155,6 +1164,21 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { `} + ${ulState && html` +
+ + ${ulState.name} +
+
+
+ + ${ulState.indexing + ? t('group.upload_indexing') + : `${ulState.total ? Math.round(ulState.sent / ulState.total * 100) : 0}%`} + +
+ `} ${dlState && html`
${dlState.name} @@ -1361,7 +1385,13 @@ function FilePreview({ entry, transportRef, gekRef, onClose }) { ); if (cancelled) return; - if (entry.name.match(IMAGE_EXTS)) { + if (/\.pdf$/i.test(entry.name)) { + // Decrypted here and shown from a blob: URL — the bytes never leave + // the page, and the browser's own viewer renders them. + const blob = new Blob(chunks, { type: 'application/pdf' }); + blobUrlRef.current = URL.createObjectURL(blob); + setContent({ type: 'pdf' }); + } else if (entry.name.match(IMAGE_EXTS)) { const ext = entry.name.split('.').pop().toLowerCase(); const mime = ext === 'svg' ? 'image/svg+xml' : ext === 'png' ? 'image/png' @@ -1416,6 +1446,12 @@ function FilePreview({ entry, transportRef, gekRef, onClose }) {
`} + ${phase === 'ready' && content?.type === 'pdf' && html` + +

${t('preview.pdf_fallback')}

+
+ `} ${phase === 'ready' && content?.type === 'image' && html`
${entry.name} @@ -1542,27 +1578,6 @@ function MembersPanel({ groupId, group, token, transportRef, gekRef, return html`
- - - - - - - - - ${members.map(m => html` - - - - - `)} - -
${t('admin.col_username')}${t('members.group_role')}
${m.username} - ${m.user_id === adminId - ? html`${t('members.owner')}` - : html`${t('members.member')}` - } -
${isAdmin && html`

${t('members.invite_title')}

@@ -1585,6 +1600,27 @@ function MembersPanel({ groupId, group, token, transportRef, gekRef,
`} + + + + + + + + + ${members.map(m => html` + + + + + `)} + +
${t('admin.col_username')}${t('members.group_role')}
${m.username} + ${m.user_id === adminId + ? html`${t('members.owner')}` + : html`${t('members.member')}` + } +
${isNodeAdmin && html`

${t('members.pair_title')}

diff --git a/packages/meshbay-hub/src/meshbay_hub/static/i18n.js b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js index f0c5cef..e3e0def 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/i18n.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js @@ -116,6 +116,8 @@ const en = { 'video.loading': 'Loading {name}...', 'video.buffering': 'Buffering...', 'video.close': 'Close (Esc)', + 'preview.pdf_fallback': 'This browser will not display the PDF inline. Download it instead — it was decrypted here either way.', + 'group.upload_indexing': 'indexing…', 'video.err_transport': 'Transport not connected', 'video.err_mse': 'Codec not supported for streaming: {codec}', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index 81c4130..361ff7e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -1411,3 +1411,12 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } -webkit-box-orient: vertical; overflow: hidden; } + +/* PDF preview: fills the overlay, scrolls in its own viewer. */ +.preview-pdf { + width: min(1000px, 92vw); + height: 82vh; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-surface); +} -- cgit v1.2.3