diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-11 15:39:09 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-11 15:39:09 +0200 |
| commit | 3ee7f817d532208df1d95ecd86def12e5bd53a18 (patch) | |
| tree | 2778dc1a64220e75662b9aceeafe4e8e384cf06b /packages | |
| parent | cc90dc943fcd0e7bbf52674fb3f95ff097026f4a (diff) | |
| download | meshbay-3ee7f817d532208df1d95ecd86def12e5bd53a18.tar.gz | |
fix(ui): group creation error, chat UUIDs, file preview, action menu
Bug fixes:
- Group creation "[object Object]" error: removed dead pkcs8 import code
that threw before GEK wrapping, added array detail handling in hubFetch
- Chat shows usernames instead of UUIDs (sender_name passed through node)
- Join button: navigate to group on "Already a member" instead of error
UI improvements:
- Loading spinner animation for async states (connecting, fetching)
- File action menu (3-dot dropdown: View, Download, Play)
- Click filename to preview inline (images, text/code files)
- FilePreview overlay for images and text files
- Chat file attachment button (upload to node + structured message)
- Chat attachment display (icon, filename, size)
- Member panel: "Group admin" badge instead of plain "Admin" text
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'packages')
5 files changed, 428 insertions, 53 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index dda4a09..92bf8f4 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -110,7 +110,10 @@ async function hubFetch(path, { method = 'GET', body, token } = {}) { const r = await fetch(HUB + path, opts); if (!r.ok) { const err = await r.json().catch(() => ({ detail: r.statusText })); - throw new Error(err.detail || r.statusText); + const detail = Array.isArray(err.detail) + ? err.detail.map(e => e.msg || JSON.stringify(e)).join(', ') + : (err.detail || r.statusText); + throw new Error(String(detail)); } return r.json(); } @@ -417,9 +420,14 @@ function ExplorePage({ token, myGroupIds }) { setJoining(gid); try { await hubFetch(`/v1/groups/${gid}/join`, { method: 'POST', token }); - window.location.reload(); + navigate(`/group/${gid}`); + setTimeout(() => window.location.reload(), 100); } catch (err) { - alert(err.message); + if (err.message.includes('Already a member')) { + navigate(`/group/${gid}`); + } else { + alert(err.message); + } } finally { setJoining(null); } @@ -492,24 +500,19 @@ function CreateGroupPage({ token, onCreated }) { body: { name: name.trim(), visibility, join_policy: joinPolicy }, }); - if (window.MeshBayCrypto && window.MeshBayKeys && _sessionKeys) { - const gek = window.MeshBayCrypto.generateGEK(); - const skXB64 = _sessionKeys.skXB64; - const skXRaw = Uint8Array.from(atob(skXB64), c => c.charCodeAt(0)); - const skX = await crypto.subtle.importKey( - 'pkcs8', skXRaw, { name: 'X25519' }, true, ['deriveBits']); - const pkXRaw = new Uint8Array( - await crypto.subtle.exportKey('raw', - (await crypto.subtle.generateKey({ name: 'X25519' }, true, ['deriveBits'])).publicKey)); - - const meResp = await hubFetch('/v1/users/me', { token }); - const pubkeys = await hubFetch(`/v1/users/${meResp.username}/pubkeys`); - const pkXBytes = Uint8Array.from(atob(pubkeys.pk_x25519), c => c.charCodeAt(0)); - - const bundle = await window.MeshBayCrypto.wrapGEK(gek, pkXBytes); - await hubFetch(`/v1/groups/${data.group_id}/members/${meResp.username}/gek`, { - method: 'POST', token, body: bundle, - }); + if (window.MeshBayCrypto) { + try { + const gek = window.MeshBayCrypto.generateGEK(); + const meResp = await hubFetch('/v1/users/me', { token }); + const pubkeys = await hubFetch(`/v1/users/${meResp.username}/pubkeys`); + const pkXBytes = Uint8Array.from(atob(pubkeys.pk_x25519), c => c.charCodeAt(0)); + const bundle = await window.MeshBayCrypto.wrapGEK(gek, pkXBytes); + await hubFetch(`/v1/groups/${data.group_id}/members/${meResp.username}/gek`, { + method: 'POST', token, body: bundle, + }); + } catch (e) { + console.warn('GEK wrap skipped:', e); + } } if (onCreated) onCreated(); @@ -621,8 +624,10 @@ function GroupPage({ groupId, group, token, username }) { const [currentPath, setCurrentPath] = useState(''); const [dlState, setDlState] = useState(null); const [videoEntry, setVideoEntry] = useState(null); + const [previewEntry, setPreviewEntry] = useState(null); const [tab, setTab] = useState('files'); const [uploading, setUploading] = useState(false); + const [menuOpen, setMenuOpen] = useState(null); const transportRef = useRef(null); const gekRef = useRef(null); @@ -817,7 +822,11 @@ function GroupPage({ groupId, group, token, username }) { <div> <div class="group-header"> <h2>${group ? group.name : t('group.default_name')}</h2> - <span class="status-badge ${statusClass}">${statusLabel}</span> + <span class="status-badge ${statusClass}"> + ${(status === 'discovering' || status === 'connecting' || status === 'fetching') + && html`<span class="spinner"></span>${' '}`} + ${statusLabel} + </span> </div> ${error && html`<div class="error-msg" style="margin-bottom:12px">${error}</div>`} ${dlState && html` @@ -890,27 +899,52 @@ function GroupPage({ groupId, group, token, username }) { <td></td> </tr> `)} - ${sorted.map(e => html` - <tr class="file-row" key=${e.id}> - <td>${FILE_ICONS[e.type] || FILE_ICONS.other}</td> - <td class="file-name">${e.name}</td> - <td class="file-size">${formatSize(e.size)}</td> - <td class="file-type td-type">${e.type}</td> - <td class="file-date td-date">${formatDate(e.added_at)}</td> - <td> - ${e.type === 'video' && html` - <button class="play-btn" onClick=${() => setVideoEntry(e)} - disabled=${!!dlState || !!videoEntry} title="${t('group.play')}" - \u{25B6} - </button> - `} - <button class="dl-btn" onClick=${() => downloadFile(e)} - disabled=${!!dlState} title="${t('group.download')}" - \u{2B07} - </button> - </td> - </tr> - `)} + ${sorted.map(e => { + const canPreview = ['image', 'video', 'document'].includes(e.type) + || e.name.match(/\.(txt|md|json|csv|log|xml|yaml|yml|ini|conf|py|js|html|css|sh|c|h|java|rs|go|rb|toml)$/i); + return html` + <tr class="file-row" key=${e.id}> + <td>${FILE_ICONS[e.type] || FILE_ICONS.other}</td> + <td class="file-name"> + ${canPreview + ? html`<a class="file-link" onClick=${() => { + if (e.type === 'video') setVideoEntry(e); + else setPreviewEntry(e); + }}>${e.name}</a>` + : e.name + } + </td> + <td class="file-size">${formatSize(e.size)}</td> + <td class="file-type td-type">${e.type}</td> + <td class="file-date td-date">${formatDate(e.added_at)}</td> + <td class="file-actions-cell"> + <button class="file-menu-btn" onClick=${(ev) => { + ev.stopPropagation(); + setMenuOpen(menuOpen === e.id ? null : e.id); + }}>${'\u{22EE}'}</button> + ${menuOpen === e.id && html` + <div class="file-menu"> + ${canPreview && html` + <button onClick=${() => { + setMenuOpen(null); + if (e.type === 'video') setVideoEntry(e); + else setPreviewEntry(e); + }}>${t('group.view')}</button> + `} + <button onClick=${() => { setMenuOpen(null); downloadFile(e); }}> + ${t('group.download')} + </button> + ${e.type === 'video' && html` + <button onClick=${() => { setMenuOpen(null); setVideoEntry(e); }}> + ${t('group.play')} + </button> + `} + </div> + `} + </td> + </tr> + `; + })} ${sorted.length === 0 && subdirs.length === 0 && html` <tr><td colspan="6" class="file-empty"> ${filter ? t('group.empty_filter') : t('group.empty_dir')} @@ -921,7 +955,8 @@ function GroupPage({ groupId, group, token, username }) { `} ${tab === 'chat' && html` - <${ChatPanel} transportRef=${transportRef} username=${username} /> + <${ChatPanel} transportRef=${transportRef} username=${username} + entries=${entries} /> `} ${tab === 'members' && html` @@ -937,6 +972,13 @@ function GroupPage({ groupId, group, token, username }) { ${(status === 'discovering' || status === 'connecting' || status === 'fetching') && html` <p class="page-message">${statusLabel}</p> `} + ${previewEntry && html` + <${FilePreview} + entry=${previewEntry} + transportRef=${transportRef} + gekRef=${gekRef} + onClose=${() => setPreviewEntry(null)} /> + `} ${videoEntry && html` <${VideoPlayer} entry=${videoEntry} @@ -948,6 +990,112 @@ function GroupPage({ groupId, group, token, username }) { `; } +// ── File Preview (text, images) ───────────────────────────────────────── + +const TEXT_EXTS = /\.(txt|md|json|csv|log|xml|yaml|yml|ini|conf|py|js|html|css|sh|c|h|java|rs|go|rb|toml)$/i; +const IMAGE_EXTS = /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)$/i; + +function FilePreview({ entry, transportRef, gekRef, onClose }) { + const [phase, setPhase] = useState('loading'); + const [progress, setProgress] = useState(0); + const [content, setContent] = useState(null); + const [error, setError] = useState(''); + const blobUrlRef = useRef(null); + + useEffect(() => { + let cancelled = false; + const load = async () => { + const transport = transportRef.current; + if (!transport || !transport.connected) { + setError(t('video.err_transport')); + setPhase('error'); + 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); + let downloaded = 0; + const chunks = await pipelinedDownload( + transport, gekRef.current, entry.id, totalChunks, + (bytes) => { downloaded += bytes; setProgress(downloaded / entry.size); }, + ); + if (cancelled) return; + + if (entry.name.match(IMAGE_EXTS)) { + const ext = entry.name.split('.').pop().toLowerCase(); + const mime = ext === 'svg' ? 'image/svg+xml' + : ext === 'png' ? 'image/png' + : ext === 'gif' ? 'image/gif' + : ext === 'webp' ? 'image/webp' + : 'image/jpeg'; + const blob = new Blob(chunks, { type: mime }); + blobUrlRef.current = URL.createObjectURL(blob); + setContent({ type: 'image' }); + } else { + const decoder = new TextDecoder('utf-8', { fatal: false }); + const text = chunks.map(c => decoder.decode(c, { stream: true })).join(''); + setContent({ type: 'text', text: text.slice(0, 500000) }); + } + setPhase('ready'); + } catch (err) { + if (!cancelled) { setError(err.message); setPhase('error'); } + } + }; + load(); + return () => { cancelled = true; }; + }, [entry]); + + useEffect(() => { + const onKey = (e) => { if (e.key === 'Escape') onClose(); }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [onClose]); + + useEffect(() => { + return () => { + if (blobUrlRef.current) { + URL.revokeObjectURL(blobUrlRef.current); + blobUrlRef.current = null; + } + }; + }, []); + + return html` + <div class="video-overlay" onClick=${(e) => { + if (e.target.classList.contains('video-overlay')) onClose(); + }}> + <div class="video-top-bar"> + <span class="video-title">${entry.name} (${formatSize(entry.size)})</span> + <button class="video-close" onClick=${onClose} title="${t('video.close')}">✕</button> + </div> + ${phase === 'loading' && html` + <div class="video-loading"> + <div class="video-loading-label">${t('video.loading', { name: entry.name })}</div> + <div class="video-progress-bar"> + <div class="video-progress-fill" style="width:${Math.round(progress * 100)}%"></div> + </div> + </div> + `} + ${phase === 'ready' && content?.type === 'image' && html` + <div class="preview-image-wrap"> + <img class="preview-image" src=${blobUrlRef.current} alt=${entry.name} /> + </div> + `} + ${phase === 'ready' && content?.type === 'text' && html` + <div class="preview-text-wrap"> + <pre class="preview-text">${content.text}</pre> + </div> + `} + ${phase === 'error' && html` + <div class="video-error">${error}</div> + `} + </div> + `; +} + function _b64ToU8(b64) { const bin = atob(b64); const arr = new Uint8Array(bin.length); @@ -1027,14 +1175,19 @@ function MembersPanel({ groupId, group, token }) { <thead> <tr> <th>${t('admin.col_username')}</th> - <th>${t('members.col_role')}</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 ? t('members.admin') : t('members.member')}</td> + <td> + ${m.user_id === adminId + ? html`<span class="badge" style="background:var(--accent);color:var(--accent-text)">${t('members.group_admin')}</span>` + : html`<span class="badge">${t('members.member')}</span>` + } + </td> </tr> `)} </tbody> @@ -1066,10 +1219,18 @@ function formatTime(ts) { return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + ' ' + time; } -function ChatPanel({ transportRef, username }) { +function _parsePayload(raw) { + if (typeof raw === 'string' && raw.startsWith('{')) { + try { return JSON.parse(raw); } catch { /* not JSON */ } + } + return null; +} + +function ChatPanel({ transportRef, username, entries }) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [sending, setSending] = useState(false); + const [attaching, setAttaching] = useState(false); const listRef = useRef(null); const bottomRef = useRef(null); const loadedRef = useRef(false); @@ -1088,6 +1249,7 @@ function ChatPanel({ transportRef, username }) { transport.onChat = (msg) => { setMessages(prev => [...prev, { sender_id: msg.sender_id, + sender_name: msg.sender_name || '', payload: msg.payload, timestamp: msg.timestamp || Date.now() / 1000, thread_id: msg.thread_id, @@ -1112,9 +1274,10 @@ function ChatPanel({ transportRef, username }) { setSending(true); setInput(''); try { - await transport.sendChat(text, 0, null); + await transport.sendChat(text, 0, null, username); setMessages(prev => [...prev, { sender_id: username, + sender_name: username, payload: text, timestamp: Date.now() / 1000, thread_id: null, @@ -1126,6 +1289,38 @@ function ChatPanel({ transportRef, username }) { } }, [input, username]); + const attachFile = useCallback(async (e) => { + const file = e.target.files?.[0]; + if (!file) return; + e.target.value = ''; + const transport = transportRef.current; + if (!transport || !transport.connected) return; + setAttaching(true); + try { + const totalChunks = Math.ceil(file.size / CHUNK_SIZE); + for (let i = 0; i < totalChunks; i++) { + const slice = file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE); + const buf = new Uint8Array(await slice.arrayBuffer()); + await transport.uploadChunk(file.name, i, totalChunks, buf); + } + 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'; + const structured = JSON.stringify({ + text: '', attachment: { filename: file.name, size: file.size, type: ftype }, + }); + await transport.sendChat(structured, 0, null, username); + setMessages(prev => [...prev, { + sender_id: username, sender_name: username, + payload: structured, timestamp: Date.now() / 1000, thread_id: null, + }]); + } catch (err) { + alert(err.message); + } finally { + setAttaching(false); + } + }, [username]); + const onKeyDown = useCallback((e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); @@ -1140,15 +1335,31 @@ function ChatPanel({ transportRef, username }) { <div class="chat-empty">${t('chat.empty')}</div> `} ${messages.map((m, i) => { - const isOwn = m.sender_id === username; - const showSender = !isOwn && (i === 0 || messages[i - 1].sender_id !== m.sender_id); + const isOwn = m.sender_name === username || m.sender_id === username; + const displayName = m.sender_name || m.sender_id?.slice(0, 8) || '?'; + 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); + const att = parsed && parsed.attachment; return html` <div key=${i} class="chat-msg ${isOwn ? 'chat-msg-own' : ''}"> ${showSender && html` - <div class="chat-sender">${m.sender_id}</div> + <div class="chat-sender">${displayName}</div> `} <div class="chat-bubble ${isOwn ? 'chat-bubble-own' : ''}"> - <span class="chat-text">${m.payload}</span> + ${att ? html` + <div class="chat-attachment"> + ${att.type === 'image' + ? html`<div class="chat-att-img">${'\u{1F5BC}'} ${att.filename}</div>` + : att.type === 'video' + ? html`<div class="chat-att-file">${'\u{1F3AC}'} ${att.filename}</div>` + : html`<div class="chat-att-file">${'\u{1F4CE}'} ${att.filename}</div>` + } + <div class="chat-att-size">${formatSize(att.size)}</div> + </div> + ` : html` + <span class="chat-text">${m.payload}</span> + `} <span class="chat-time">${formatTime(m.timestamp)}</span> </div> </div> @@ -1157,6 +1368,10 @@ function ChatPanel({ transportRef, username }) { <div ref=${bottomRef} /> </div> <div class="chat-input-row"> + <label class="chat-attach" title="${t('chat.attach')}"> + ${attaching ? html`<span class="spinner"></span>` : '\u{1F4CE}'} + <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/i18n.js b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js index 0ec7d4a..8ac35e7 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/i18n.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js @@ -86,6 +86,7 @@ const en = { 'group.offline_hint': 'Files will appear when a node hosting this group connects.', 'group.upload': 'Upload', 'group.uploading': 'Uploading...', + 'group.view': 'View', 'group.err_transport': 'Transport module not loaded', // Status @@ -101,6 +102,7 @@ const en = { 'chat.empty': 'No messages yet. Start the conversation!', 'chat.placeholder': 'Type a message...', 'chat.send': 'Send', + 'chat.attach': 'Attach file', // Video player 'video.loading': 'Loading {name}...', @@ -204,7 +206,9 @@ const en = { // Members 'members.col_role': 'Role', + 'members.group_role': 'Group role', 'members.admin': 'Admin', + 'members.group_admin': 'Group admin', 'members.member': 'Member', 'members.invite_title': 'Invite member', 'members.username_placeholder': 'Username', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index 3aea648..6e1eedd 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -531,6 +531,155 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } font-style: italic; } +/* ── Spinner ─────────────────────────────────────────────────────────────── */ + +.spinner { + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid var(--border); + border-top: 2px solid var(--accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; + vertical-align: middle; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* ── File action menu ───────────────────────────────────────────────────── */ + +.file-actions-cell { + position: relative; + width: 40px; +} + +.file-menu-btn { + background: none; + border: 1px solid var(--border); + color: var(--text-secondary); + width: 30px; + height: 30px; + border-radius: 6px; + font-size: 1.1em; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + line-height: 1; +} +.file-menu-btn:hover { background: var(--bg-raised); border-color: var(--accent); color: var(--accent); } + +.file-menu { + position: absolute; + right: 0; + top: 100%; + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: 8px; + box-shadow: var(--shadow-lg); + z-index: 30; + min-width: 130px; + padding: 4px 0; + overflow: hidden; +} +.file-menu button { + display: block; + width: 100%; + padding: 8px 14px; + background: none; + border: none; + color: var(--text); + font-size: 0.85em; + text-align: left; + cursor: pointer; + border-radius: 0; +} +.file-menu button:hover { background: var(--bg-raised); } + +/* ── File link (clickable name) ─────────────────────────────────────────── */ + +.file-link { + color: var(--accent); + cursor: pointer; + text-decoration: none; +} +.file-link:hover { text-decoration: underline; } + +/* ── File preview overlay ───────────────────────────────────────────────── */ + +.preview-image-wrap { + display: flex; + align-items: center; + justify-content: center; + max-width: 90vw; + max-height: calc(100vh - 100px); + padding: 40px 20px 20px; +} + +.preview-image { + max-width: 100%; + max-height: calc(100vh - 120px); + object-fit: contain; + border-radius: 4px; +} + +.preview-text-wrap { + width: min(90vw, 900px); + max-height: calc(100vh - 100px); + overflow: auto; + padding: 60px 20px 20px; +} + +.preview-text { + background: var(--bg-surface); + color: var(--text); + padding: 20px; + border-radius: 8px; + font-size: 0.85em; + line-height: 1.6; + white-space: pre-wrap; + word-break: break-word; + overflow-x: auto; + margin: 0; + font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; + tab-size: 4; +} + +/* ── Chat attachment ────────────────────────────────────────────────────── */ + +.chat-attach { + display: flex; + align-items: center; + justify-content: center; + width: 38px; + height: 38px; + font-size: 1.2em; + cursor: pointer; + border-radius: 8px; + flex-shrink: 0; + color: var(--text-secondary); +} +.chat-attach:hover { background: var(--bg-raised); color: var(--accent); } + +.chat-attachment { + display: flex; + flex-direction: column; + gap: 2px; +} +.chat-att-img, .chat-att-file { + font-weight: 500; + font-size: 0.88em; +} +.chat-att-size { + font-size: 0.75em; + color: var(--text-dim); +} +.chat-bubble-own .chat-att-size { color: rgba(255, 255, 255, 0.6); } + /* ── Download button + progress ──────────────────────────────────────────── */ .dl-btn { diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js index 2f6680e..6cbcc3c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js @@ -164,13 +164,14 @@ class MeshBayTransport { return msg.messages || []; } - async sendChat(payload, iteration, threadId) { + async sendChat(payload, iteration, threadId, senderName) { const msg = await this._sendAndWait({ type: 'chat_msg', v: '0.1', payload: payload, iteration: iteration || 0, thread_id: threadId || null, + sender_name: senderName || null, }); return msg; } diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py index 94d9b04..07ceed3 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py @@ -266,6 +266,9 @@ class WebRTCPeerSession: def _do_chat_message(self, msg: dict) -> None: chat_store = self._ctx.get("chat_store") payload = msg.get("payload", "") + sender_name = msg.get("sender_name", "") + if sender_name: + self._ctx.setdefault("_user_names", {})[self._user_id] = sender_name if chat_store: raw = payload.encode() if isinstance(payload, str) else payload asyncio.ensure_future(chat_store.save_message( @@ -280,6 +283,7 @@ class WebRTCPeerSession: "type": MNP.CHAT_MESSAGE, "v": MNP_VERSION, "sender_id": msg.get("sender_id", self._user_id), + "sender_name": sender_name, "payload": payload, "thread_id": msg.get("thread_id"), "timestamp": __import__("time").time(), @@ -309,6 +313,7 @@ class WebRTCPeerSession: async def _send_chat_history(self, chat_store, since: float, limit: int) -> None: msgs = await chat_store.get_messages(since=since, limit=limit) + names = self._ctx.get("_user_names", {}) self._send({ "type": MNP.CHAT_HISTORY_RESPONSE, "v": MNP_VERSION, @@ -316,6 +321,7 @@ class WebRTCPeerSession: { "id": m.id, "sender_id": m.sender_id, + "sender_name": names.get(m.sender_id, ""), "payload": m.payload.decode("utf-8", errors="replace") if isinstance(m.payload, bytes) else m.payload, "timestamp": m.timestamp, |