From b50750466622dd6cda0fc084d39dfce000ad0081 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Tue, 11 Aug 2026 16:12:16 +0200 Subject: fix(ui): upload chunk size, cached file display, chat names, file delete, inline thumbnails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Upload chunks capped at 64KB to avoid WebRTC DataChannel max-message-size - Show cached files immediately while WebRTC connects (tabs visible during connection) - Persist sender_name in chat store (SQLite) — no more UUID display in history - File delete action in menu (node admin only, enforced server-side) - FILE_DELETE / FILE_DELETE_ACK MNP message types - Inline image thumbnails in chat attachments (download+decrypt, Signal-style) - Member panel: "Owner" label instead of "Group admin" to avoid hub/group admin confusion - Create group page: hint about needing a node - Refresh index after chat file attachment upload Co-Authored-By: Claude Opus 4.6 --- packages/meshbay-hub/src/meshbay_hub/static/app.js | 105 ++++++++++++++++++--- 1 file changed, 93 insertions(+), 12 deletions(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/app.js') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 92bf8f4..34c2517 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -527,6 +527,9 @@ function CreateGroupPage({ token, onCreated }) { return html`

${t('create_group.title')}

+

+ ${t('create_group.hint')} +

${error && html`

${error}

`}
`} - ${status === 'connected' && html` + ${(status === 'connected' || (cached && entries.length > 0)) && html`
@@ -939,6 +964,12 @@ function GroupPage({ groupId, group, token, username }) { ${t('group.play')} `} + ${group && group.is_admin && status === 'connected' && html` + + `}
`} @@ -954,15 +985,23 @@ function GroupPage({ groupId, group, token, username }) { `} - ${tab === 'chat' && html` + ${tab === 'chat' && status === 'connected' && html` <${ChatPanel} transportRef=${transportRef} username=${username} - entries=${entries} /> + entries=${entries} gekRef=${gekRef} onRefreshIndex=${refreshIndex} /> + `} + ${tab === 'chat' && status !== 'connected' && html` +

${' '}${t('status.connecting')}

`} ${tab === 'members' && html` <${MembersPanel} groupId=${groupId} group=${group} token=${token} /> `} `} + ${cached && entries.length > 0 && status !== 'connected' && tab === 'files' && html` +

+ ${' '}${t('status.connecting')} +

+ `} ${status === 'offline' && html`

${t('group.offline_title')} @@ -1184,7 +1223,7 @@ function MembersPanel({ groupId, group, token }) { ${m.username} ${m.user_id === adminId - ? html`${t('members.group_admin')}` + ? html`${t('members.owner')}` : html`${t('members.member')}` } @@ -1226,7 +1265,47 @@ function _parsePayload(raw) { return null; } -function ChatPanel({ transportRef, username, entries }) { +function ChatImage({ filename, entries, transportRef, gekRef }) { + const [blobUrl, setBlobUrl] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let cancelled = false; + const load = async () => { + const transport = transportRef.current; + if (!transport || !transport.connected) { setLoading(false); return; } + const entry = entries.find(e => e.name === filename); + if (!entry) { setLoading(false); return; } + try { + if (!gekRef.current && window.MeshBayCrypto) { + const gekB64 = await transport.fetchGEK(); + gekRef.current = await window.MeshBayCrypto.importGEK(gekB64); + } + const totalChunks = Math.ceil(entry.size / CHUNK_SIZE); + const chunks = await pipelinedDownload(transport, gekRef.current, entry.id, totalChunks); + if (cancelled) return; + const ext = filename.split('.').pop().toLowerCase(); + const mime = ext === 'png' ? 'image/png' : ext === 'gif' ? 'image/gif' + : ext === 'webp' ? 'image/webp' : ext === 'svg' ? 'image/svg+xml' : 'image/jpeg'; + const blob = new Blob(chunks, { type: mime }); + setBlobUrl(URL.createObjectURL(blob)); + } catch { /* ignore */ } + if (!cancelled) setLoading(false); + }; + load(); + return () => { cancelled = true; }; + }, [filename]); + + useEffect(() => { + return () => { if (blobUrl) URL.revokeObjectURL(blobUrl); }; + }, [blobUrl]); + + if (loading) return html`

`; + if (!blobUrl) return html`
${'\u{1F5BC}'} ${filename}
`; + return html`${filename}`; +} + +function ChatPanel({ transportRef, username, entries, gekRef, onRefreshIndex }) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [sending, setSending] = useState(false); @@ -1297,12 +1376,13 @@ function ChatPanel({ transportRef, username, entries }) { if (!transport || !transport.connected) return; setAttaching(true); try { - const totalChunks = Math.ceil(file.size / CHUNK_SIZE); + const totalChunks = Math.ceil(file.size / UPLOAD_CHUNK_SIZE); for (let i = 0; i < totalChunks; i++) { - const slice = file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE); + 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); } + if (onRefreshIndex) await onRefreshIndex(); const ext = file.name.split('.').pop().toLowerCase(); const ftype = ['jpg','jpeg','png','gif','webp','svg'].includes(ext) ? 'image' : ['mp4','webm','mkv','mov','avi'].includes(ext) ? 'video' : 'file'; @@ -1319,7 +1399,7 @@ function ChatPanel({ transportRef, username, entries }) { } finally { setAttaching(false); } - }, [username]); + }, [username, onRefreshIndex]); const onKeyDown = useCallback((e) => { if (e.key === 'Enter' && !e.shiftKey) { @@ -1336,7 +1416,7 @@ function ChatPanel({ transportRef, username, entries }) { `} ${messages.map((m, i) => { const isOwn = m.sender_name === username || m.sender_id === username; - const displayName = m.sender_name || m.sender_id?.slice(0, 8) || '?'; + const displayName = m.sender_name || '?'; const showSender = !isOwn && (i === 0 || (messages[i - 1].sender_name || messages[i - 1].sender_id) !== (m.sender_name || m.sender_id)); const parsed = _parsePayload(m.payload); @@ -1350,7 +1430,8 @@ function ChatPanel({ transportRef, username, entries }) { ${att ? html`
${att.type === 'image' - ? html`
${'\u{1F5BC}'} ${att.filename}
` + ? html`<${ChatImage} filename=${att.filename} entries=${entries} + transportRef=${transportRef} gekRef=${gekRef} />` : att.type === 'video' ? html`
${'\u{1F3AC}'} ${att.filename}
` : html`
${'\u{1F4CE}'} ${att.filename}
` -- cgit v1.2.3