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 --- .../meshbay-node/src/meshbay_node/chat/store.py | 39 ++++++++++++++-------- .../src/meshbay_node/transport/webrtc_server.py | 34 ++++++++++++++++++- 2 files changed, 59 insertions(+), 14 deletions(-) (limited to 'packages/meshbay-node/src/meshbay_node') diff --git a/packages/meshbay-node/src/meshbay_node/chat/store.py b/packages/meshbay-node/src/meshbay_node/chat/store.py index 1dbcc2b..a5b2d7d 100644 --- a/packages/meshbay-node/src/meshbay_node/chat/store.py +++ b/packages/meshbay-node/src/meshbay_node/chat/store.py @@ -17,17 +17,22 @@ log = logging.getLogger(__name__) _SCHEMA = """ CREATE TABLE IF NOT EXISTS messages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - sender_id TEXT NOT NULL, - iteration INTEGER NOT NULL, - payload BLOB NOT NULL, - timestamp REAL NOT NULL, - thread_id TEXT DEFAULT NULL + id INTEGER PRIMARY KEY AUTOINCREMENT, + sender_id TEXT NOT NULL, + iteration INTEGER NOT NULL, + payload BLOB NOT NULL, + timestamp REAL NOT NULL, + thread_id TEXT DEFAULT NULL, + sender_name TEXT DEFAULT '' ); CREATE INDEX IF NOT EXISTS idx_messages_ts ON messages(timestamp); CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages(thread_id); """ +_MIGRATE_SENDER_NAME = ( + "ALTER TABLE messages ADD COLUMN sender_name TEXT DEFAULT ''" +) + @dataclass class StoredMessage: @@ -37,6 +42,7 @@ class StoredMessage: payload: bytes timestamp: float thread_id: str | None + sender_name: str = "" class ChatStore: @@ -50,6 +56,10 @@ class ChatStore: self._db_path.parent.mkdir(parents=True, exist_ok=True) self._db = await aiosqlite.connect(str(self._db_path)) await self._db.executescript(_SCHEMA) + try: + await self._db.execute(_MIGRATE_SENDER_NAME) + except Exception: + pass await self._db.commit() async def close(self) -> None: @@ -70,13 +80,14 @@ class ChatStore: iteration: int, payload: bytes, thread_id: str | None = None, + sender_name: str = "", ) -> int: """Store a message. Returns the row id.""" ts = time.time() cursor = await self._db.execute( - "INSERT INTO messages (sender_id, iteration, payload, timestamp, thread_id) " - "VALUES (?, ?, ?, ?, ?)", - (sender_id, iteration, payload, ts, thread_id), + "INSERT INTO messages (sender_id, iteration, payload, timestamp, thread_id, sender_name) " + "VALUES (?, ?, ?, ?, ?, ?)", + (sender_id, iteration, payload, ts, thread_id, sender_name), ) await self._db.commit() return cursor.lastrowid @@ -88,28 +99,30 @@ class ChatStore: ) -> list[StoredMessage]: """Get messages after a timestamp, most recent last.""" cursor = await self._db.execute( - "SELECT id, sender_id, iteration, payload, timestamp, thread_id " + "SELECT id, sender_id, iteration, payload, timestamp, thread_id, sender_name " "FROM messages WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?", (since, limit), ) rows = await cursor.fetchall() return [ StoredMessage(id=r[0], sender_id=r[1], iteration=r[2], - payload=r[3], timestamp=r[4], thread_id=r[5]) + payload=r[3], timestamp=r[4], thread_id=r[5], + sender_name=r[6] or "") for r in rows ] async def get_thread(self, thread_id: str, limit: int = 100) -> list[StoredMessage]: """Get messages in a thread.""" cursor = await self._db.execute( - "SELECT id, sender_id, iteration, payload, timestamp, thread_id " + "SELECT id, sender_id, iteration, payload, timestamp, thread_id, sender_name " "FROM messages WHERE thread_id = ? ORDER BY timestamp ASC LIMIT ?", (thread_id, limit), ) rows = await cursor.fetchall() return [ StoredMessage(id=r[0], sender_id=r[1], iteration=r[2], - payload=r[3], timestamp=r[4], thread_id=r[5]) + payload=r[3], timestamp=r[4], thread_id=r[5], + sender_name=r[6] or "") for r in rows ] 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 6231310..15c8f66 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py @@ -116,6 +116,8 @@ class WebRTCPeerSession: self._do_chat_history(msg) elif mtype == MNP.FILE_UPLOAD: self._do_file_upload(msg) + elif mtype == MNP.FILE_DELETE: + self._do_file_delete(msg) else: log.warning("Unknown MNP message type on DataChannel: %s", mtype) except Exception as e: @@ -276,6 +278,7 @@ class WebRTCPeerSession: iteration=msg.get("iteration", 0), payload=raw, thread_id=msg.get("thread_id"), + sender_name=sender_name, )) peers = self._ctx.get("_peers", {}) @@ -321,7 +324,7 @@ class WebRTCPeerSession: { "id": m.id, "sender_id": m.sender_id, - "sender_name": names.get(m.sender_id, ""), + "sender_name": m.sender_name or names.get(m.sender_id, ""), "payload": m.payload.decode("utf-8", errors="replace") if isinstance(m.payload, bytes) else m.payload, "timestamp": m.timestamp, @@ -373,6 +376,35 @@ class WebRTCPeerSession: tmp_path.rename(final_path) log.info("Upload complete: %s (%d chunks)", safe_name, total_chunks) + def _do_file_delete(self, msg: dict) -> None: + ctx = self._group_ctx() + file_id = msg.get("file_id", "") + if not file_id: + self._send({"type": "error", "detail": "Missing file_id"}) + return + + node_user_id = self._ctx.get("node_user_id") + if node_user_id and self._user_id != node_user_id: + self._send({"type": "error", "detail": "Only node admin can delete files"}) + return + + entry = ctx["index"].get_entry(file_id) + if not entry: + self._send({"type": "error", "detail": "File not found"}) + return + + file_path = ctx["shared_root"] / entry.path / entry.name + if file_path.exists(): + file_path.unlink() + log.info("File deleted: %s", entry.name) + + ctx["index"].remove_entry(file_id) + self._send({ + "type": MNP.FILE_DELETE_ACK, + "v": MNP_VERSION, + "file_id": file_id, + }) + def _send(self, obj: dict) -> None: if self._channel and self._channel.readyState == "open": self._channel.send(_pack(obj)) -- cgit v1.2.3