summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js80
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/i18n.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/style.css9
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 }) {
</div>
</form>
`}
+ ${ulState && html`
+ <div class="dl-bar">
+ <span class="spinner"></span>
+ <span class="dl-name">${ulState.name}</span>
+ <div class="dl-progress">
+ <div class="dl-fill" style=${`width:${ulState.total
+ ? Math.round(ulState.sent / ulState.total * 100) : 0}%`}></div>
+ </div>
+ <span class="dl-pct">
+ ${ulState.indexing
+ ? t('group.upload_indexing')
+ : `${ulState.total ? Math.round(ulState.sent / ulState.total * 100) : 0}%`}
+ </span>
+ </div>
+ `}
${dlState && html`
<div class="dl-bar">
<span class="dl-name">${dlState.name}</span>
@@ -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 }) {
</div>
</div>
`}
+ ${phase === 'ready' && content?.type === 'pdf' && html`
+ <object data=${blobUrlRef.current} type="application/pdf"
+ class="preview-pdf" aria-label=${entry.name}>
+ <p class="page-message">${t('preview.pdf_fallback')}</p>
+ </object>
+ `}
${phase === 'ready' && content?.type === 'image' && html`
<div class="preview-image-wrap">
<img class="preview-image" src=${blobUrlRef.current} alt=${entry.name} />
@@ -1542,27 +1578,6 @@ function MembersPanel({ groupId, group, token, transportRef, gekRef,
return html`
<div class="members-panel">
- <table class="admin-table">
- <thead>
- <tr>
- <th>${t('admin.col_username')}</th>
- <th>${t('members.group_role')}</th>
- </tr>
- </thead>
- <tbody>
- ${members.map(m => html`
- <tr key=${m.user_id}>
- <td>${m.username}</td>
- <td>
- ${m.user_id === adminId
- ? html`<span class="badge" style="background:var(--accent);color:var(--accent-text)">${t('members.owner')}</span>`
- : html`<span class="badge">${t('members.member')}</span>`
- }
- </td>
- </tr>
- `)}
- </tbody>
- </table>
${isAdmin && html`
<form class="invite-form" onSubmit=${doInvite}>
<h4>${t('members.invite_title')}</h4>
@@ -1585,6 +1600,27 @@ function MembersPanel({ groupId, group, token, transportRef, gekRef,
</div>
</form>
`}
+ <table class="admin-table">
+ <thead>
+ <tr>
+ <th>${t('admin.col_username')}</th>
+ <th>${t('members.group_role')}</th>
+ </tr>
+ </thead>
+ <tbody>
+ ${members.map(m => html`
+ <tr key=${m.user_id}>
+ <td>${m.username}</td>
+ <td>
+ ${m.user_id === adminId
+ ? html`<span class="badge" style="background:var(--accent);color:var(--accent-text)">${t('members.owner')}</span>`
+ : html`<span class="badge">${t('members.member')}</span>`
+ }
+ </td>
+ </tr>
+ `)}
+ </tbody>
+ </table>
${isNodeAdmin && html`
<form class="invite-form" onSubmit=${doPair}>
<h4>${t('members.pair_title')}</h4>
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);
+}