From 952242f62b7de72b82386a265842518b89f7ee04 Mon Sep 17 00:00:00 2001 From: sravan Date: Thu, 2 Jul 2026 17:16:08 +0530 Subject: [PATCH 1/4] fix(brand/ux): real Biz Connect logo, branded loading screen, phone login, no blank gap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace the leftover 'Your Virtual CXO' logo.png with the app icon (C mark) in every header (home/index/connect/dashboard) + the app icon on share.html; delete logo.png. - Loading screen now shows the orbit mark + 'Biz Connect' wordmark + 'Loading…' (was bare). - Keep the branded splash up until the chat list finishes loading — no blank/stuck gap after login (previously hidden before loadSidebar()). - Login accepts email OR phone (field was type=email, which rejected phone numbers and so blocked the mobile-login that triggers the account merge). Label → 'Email or phone'. Co-Authored-By: Claude Opus 4.8 --- server/public/connect.html | 2 +- server/public/dashboard.html | 2 +- server/public/home.html | 20 +++++++++++++------- server/public/index.html | 2 +- server/public/logo.png | Bin 19390 -> 0 bytes server/public/share.html | 2 +- 6 files changed, 17 insertions(+), 11 deletions(-) delete mode 100644 server/public/logo.png diff --git a/server/public/connect.html b/server/public/connect.html index c95ab12..46c14c2 100644 --- a/server/public/connect.html +++ b/server/public/connect.html @@ -76,7 +76,7 @@ Home
-
Biz Connect
+
Biz Connect
diff --git a/server/public/dashboard.html b/server/public/dashboard.html index 4957ae5..ace138f 100644 --- a/server/public/dashboard.html +++ b/server/public/dashboard.html @@ -92,7 +92,7 @@
-
Biz Connect · Dashboard
+
Biz Connect · Dashboard
diff --git a/server/public/home.html b/server/public/home.html index 7ab0b56..2c349e6 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -682,8 +682,11 @@ /* ---- Loading / toast ---- */ .loading{position:fixed;inset:0;display:grid;place-items:center;background:#1F3B73;z-index:9000;color:rgba(255,255,255,.82);font-size:.9rem;} - .loading .ld-inner{display:flex;flex-direction:column;align-items:center;gap:1rem;} + .loading .ld-inner{display:flex;flex-direction:column;align-items:center;gap:.85rem;} .loading .ld-inner img{width:72px;height:72px;} + .loading .ld-wm{font-size:1.35rem;font-weight:800;color:#fff;letter-spacing:.3px;margin-top:.15rem;} + .loading .ld-wm b{color:var(--brand);font-weight:800;} + .loading .ld-sub{color:rgba(255,255,255,.66);font-size:.82rem;} /* Chat thread loading (light pane) and call-connecting (dark stage) — branded, centered. */ .thread-loading{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.7rem;color:var(--muted);font-size:.86rem;} .call-connecting{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1rem;background:#16294F;color:rgba(255,255,255,.82);font-size:.95rem;} @@ -757,16 +760,16 @@ - -
Loading…
+
Biz Connect
Loading…
- +
Biz Connect
@@ -2850,7 +2853,7 @@ async function renderLogin(){ `:''}
- Email + Email or phone Password${pwField('li_pw','password')} @@ -2905,15 +2908,18 @@ async function doRegister(){ setupPush(); // register the notification service worker + subscribe to Web Push (if granted) { const cl=document.getElementById('chatlist'); if(cl) enablePullRefresh(cl, loadSidebar); } // pull-to-refresh the chat list setTimeout(maybeNotifPrompt, 1500); // gentle "enable notifications" prompt if still undecided (key for iOS PWA) - document.getElementById('loading').style.display='none'; // Fast-path: when opened from a notification, show the chat immediately (only needs the // thread fetch) and load the sidebar in the background — don't make the reload wait on it. let oid=null, okind='dm'; try{ const q=new URLSearchParams(location.search); oid=q.get('openId'); okind=q.get('openKind')||'dm'; if(oid) history.replaceState(null,'','/home'); }catch(_){} if(oid){ selectChat(okind, oid); // open the chat right away (only waits on the thread fetch) + document.getElementById('loading').style.display='none'; // the chat pane shows its own thread loader loadSidebar().then(()=>{ try{ const it=rowFor(okind,oid); const nm=document.querySelector('#chatPanel .convo-head .nm'); if(it&&nm) nm.textContent=it.name; }catch(_){} }); - } else { await loadSidebar(); renderChatPanel(); } + } else { + await loadSidebar(); renderChatPanel(); + document.getElementById('loading').style.display='none'; // keep the branded splash up until the list is ready (no blank/stuck gap) + } })(); diff --git a/server/public/index.html b/server/public/index.html index e2abd0a..194ab8d 100644 --- a/server/public/index.html +++ b/server/public/index.html @@ -55,7 +55,7 @@
- +
Biz Connect
diff --git a/server/public/logo.png b/server/public/logo.png deleted file mode 100644 index aba2c5cf24ff65f87c91e9218a636fe905568f72..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19390 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 Home
- +
Biz Connect
Secure, instant remote support — no downloads, you stay in control.
From 7674c456f226fcb4f43d4064a500320b0401f1ae Mon Sep 17 00:00:00 2001 From: sravan Date: Thu, 2 Jul 2026 17:26:22 +0530 Subject: [PATCH 2/4] fix(brand): header shows a plain white C + gold dot on the blue bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The header is already blue, so the app-icon-in-a-white-pill looked like blue→white→blue→white nesting. Use a transparent mark (mark-light.svg: white C + gold dot) placed directly on the blue header (home/index/connect/dashboard), dropping the white background + padding. share.html keeps the full app icon (it's a light card, not the blue bar). Co-Authored-By: Claude Opus 4.8 --- server/public/connect.html | 2 +- server/public/dashboard.html | 2 +- server/public/home.html | 6 +++--- server/public/index.html | 2 +- server/public/mark-light.svg | 5 +++++ 5 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 server/public/mark-light.svg diff --git a/server/public/connect.html b/server/public/connect.html index 46c14c2..fd75491 100644 --- a/server/public/connect.html +++ b/server/public/connect.html @@ -76,7 +76,7 @@ Home
-
Biz Connect
+
Biz Connect
diff --git a/server/public/dashboard.html b/server/public/dashboard.html index ace138f..48b6950 100644 --- a/server/public/dashboard.html +++ b/server/public/dashboard.html @@ -92,7 +92,7 @@
-
Biz Connect · Dashboard
+
Biz Connect · Dashboard
diff --git a/server/public/home.html b/server/public/home.html index 2c349e6..23e60f2 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -760,7 +760,7 @@ - @@ -769,7 +769,7 @@ function twemojify(el){ try{ if(el && window.twemoji) window.twemoji.parse(el, {
- +
Biz Connect
@@ -2845,7 +2845,7 @@ async function renderLogin(){ const aw=document.getElementById('authwrap'); aw.style.display='flex'; let regOpen=false; try{ regOpen=(await (await fetch('/api/setup-state')).json()).registrationOpen; }catch(_){} aw.innerHTML=`
-
Biz Connect
Biz Connect
+
Biz Connect
Biz Connect

Welcome back

Sign in to access chats, screen share and connect.
${regOpen?`
diff --git a/server/public/index.html b/server/public/index.html index 194ab8d..6acc252 100644 --- a/server/public/index.html +++ b/server/public/index.html @@ -55,7 +55,7 @@
- +
Biz Connect
diff --git a/server/public/mark-light.svg b/server/public/mark-light.svg new file mode 100644 index 0000000..b92f4d7 --- /dev/null +++ b/server/public/mark-light.svg @@ -0,0 +1,5 @@ + + + + + From 9bc109528e2f528a58e93b430aa825073ab36337 Mon Sep 17 00:00:00 2001 From: sravan Date: Thu, 2 Jul 2026 17:39:38 +0530 Subject: [PATCH 3/4] fix(brand/ux): real brand C on header, yellow read-tick, sign-in spinner, notif DP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #5: use the ACTUAL brand C (extracted transparent white C + gold dot from the master app-icon) on the blue headers instead of a hand-drawn mark; login card keeps the app icon. - #3: read-receipt double tick on my own (blue) bubble is now brand yellow (was blue-on-blue, invisible). - #2: Sign-in button shows a spinner + 'Signing in…' on submit. - #1: web notification uses the sender/group DP URL directly as the icon (drawing an external DP to canvas tainted it → silently fell back to initials). Co-Authored-By: Claude Opus 4.8 --- server/public/connect.html | 2 +- server/public/dashboard.html | 2 +- server/public/home.html | 36 +++++++++++++++++++---------------- server/public/index.html | 2 +- server/public/mark-light.png | Bin 0 -> 15061 bytes server/public/mark-light.svg | 5 ----- 6 files changed, 23 insertions(+), 24 deletions(-) create mode 100644 server/public/mark-light.png delete mode 100644 server/public/mark-light.svg diff --git a/server/public/connect.html b/server/public/connect.html index fd75491..9e7612d 100644 --- a/server/public/connect.html +++ b/server/public/connect.html @@ -76,7 +76,7 @@ Home
-
Biz Connect
+
Biz Connect
diff --git a/server/public/dashboard.html b/server/public/dashboard.html index 48b6950..e5f8e67 100644 --- a/server/public/dashboard.html +++ b/server/public/dashboard.html @@ -92,7 +92,7 @@
-
Biz Connect · Dashboard
+
Biz Connect · Dashboard
diff --git a/server/public/home.html b/server/public/home.html index 23e60f2..5cece71 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -301,8 +301,9 @@ .sys-msg{align-self:center;font-size:.76rem;color:var(--muted);background:rgba(0,0,0,.04);padding:.25rem .7rem;border-radius:99px;margin:.2rem 0;max-width:90%;text-align:center;} .bubble .t{display:flex;align-items:center;justify-content:flex-end;gap:.25rem;font-size:.64rem;opacity:.65;margin-top:.15rem;} .bubble .rcpt{display:inline-flex;} - .bubble.mine .rcpt{opacity:.8;} - .bubble.mine .rcpt.seen{color:#8fd3ff;opacity:1;} + .bubble.mine .rcpt{opacity:.85;} + /* Read receipt on my own (blue) bubble: brand yellow so the double tick actually stands out. */ + .bubble.mine .rcpt.seen{color:var(--brand);opacity:1;} .att-img{cursor:zoom-in;} .fmt-bar{display:flex;align-items:center;gap:.05rem;padding:.3rem .4rem .1rem;flex-wrap:wrap;border-bottom:1px dashed var(--line);} .fmt-bar button{border:none;background:transparent;color:var(--muted);cursor:pointer;width:30px;height:30px;border-radius:7px;display:grid;place-items:center;} @@ -662,6 +663,9 @@ .authcard .sub{color:var(--muted);font-size:.9rem;text-align:center;margin-bottom:1.2rem;} /* Blue card + gold CTA so the sign-in screen carries BOTH brand colours (never mono-blue). */ .authcard .gobtn{background:var(--brand);color:var(--blue-d);} + .gobtn:disabled{opacity:.9;cursor:default;} + .btn-spin{display:inline-block;width:15px;height:15px;border:2px solid rgba(22,41,79,.3);border-top-color:var(--blue-d);border-radius:50%;animation:btnspin .6s linear infinite;vertical-align:-3px;margin-right:7px;} + @keyframes btnspin{to{transform:rotate(360deg);}} .authcard .gobtn:hover{filter:brightness(.95);} .authtabs{display:flex;gap:.5rem;margin-bottom:1.1rem;} .authtabs button{flex:1;background:#eef1f6;color:var(--muted);font-weight:600;border:none;border-radius:9px;padding:.5rem;cursor:pointer;font-size:.9rem;} @@ -760,7 +764,7 @@ - @@ -769,7 +773,7 @@ function twemojify(el){ try{ if(el && window.twemoji) window.twemoji.parse(el, {
- +
Biz Connect
@@ -2015,16 +2019,14 @@ function notify(title, body, kind, id){ }).catch(()=>{}); return; } - // Web / PWA: standard Notification. Icon = the sender's/group's DP (falling back to their - // initials, then the brand app icon) — never the old wordmark. badge = brand mark. + // Web / PWA: standard Notification. Use the sender's/group's DP URL DIRECTLY as the icon — + // the browser fetches notification icons cross-origin fine, whereas drawing an external DP to a + // canvas taints it and silently fell back to initials (the bug). Brand icon only if no DP. if(!('Notification' in window) || Notification.permission!=='granted') return; - notifAvatarDataUrl(kind, id, String(title||'').replace(/^[^\p{L}\p{N}]+/u,'')).then(avatar=>{ - const n=new Notification(title, { body, icon: avatar || '/icon-192.png', badge:'/icon-192.png' }); - n.onclick=()=>{ n.close(); openFromNotif(kind, id); }; - setTimeout(()=>{ try{ n.close(); }catch(_){} }, 8000); - }).catch(()=>{ - try{ const n=new Notification(title, { body, icon:'/icon-192.png' }); n.onclick=()=>{ n.close(); openFromNotif(kind, id); }; }catch(_){} - }); + const _row=rowFor(kind,id)||{}; + const n=new Notification(title, { body, icon: _row.avatar || '/icon-192.png', badge:'/icon-192.png' }); + n.onclick=()=>{ n.close(); openFromNotif(kind, id); }; + setTimeout(()=>{ try{ n.close(); }catch(_){} }, 8000); }catch(_){} } let _audioCtx=null; @@ -2845,7 +2847,7 @@ async function renderLogin(){ const aw=document.getElementById('authwrap'); aw.style.display='flex'; let regOpen=false; try{ regOpen=(await (await fetch('/api/setup-state')).json()).registrationOpen; }catch(_){} aw.innerHTML=`
-
Biz Connect
Biz Connect
+
Biz Connect
Biz Connect

Welcome back

Sign in to access chats, screen share and connect.
${regOpen?`
@@ -2880,10 +2882,12 @@ async function renderLogin(){ } async function doLogin(){ clearErr('li_err'); + const btn=document.getElementById('li_btn'); const orig=btn?btn.innerHTML:''; + if(btn){ btn.disabled=true; btn.innerHTML='Signing in…'; } // show progress on Enter/click try{ await postJSON('/api/login',{email:document.getElementById('li_email').value,password:document.getElementById('li_pw').value,remember:document.getElementById('li_remember').checked}); - location.reload(); - }catch(e){ showErr('li_err', /invalid credentials/i.test(e.message)?'Incorrect email or password. Please try again.':e.message); } + location.reload(); // the branded splash takes over from here + }catch(e){ if(btn){ btn.disabled=false; btn.innerHTML=orig; } showErr('li_err', /invalid credentials/i.test(e.message)?'Incorrect email or password. Please try again.':e.message); } } async function doRegister(){ clearErr('rg_err'); diff --git a/server/public/index.html b/server/public/index.html index 6acc252..d2fecaf 100644 --- a/server/public/index.html +++ b/server/public/index.html @@ -55,7 +55,7 @@
- +
Biz Connect
diff --git a/server/public/mark-light.png b/server/public/mark-light.png new file mode 100644 index 0000000000000000000000000000000000000000..ae0bb463ac4a46b699c19cc77937431ceadf90c8 GIT binary patch literal 15061 zcmaibbyS?svh5&)ySoJ$+&#DjNN{)8APMd+fdIiFxCMd@?lM?#3-0dj4sU+vtb5

Xt~2U8t}GN14VQhhiGLIFrP1*8#Ak`*LB$ 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 - - - - From 51e206279b5af9cdd1f417f50693ddeab40c40b5 Mon Sep 17 00:00:00 2001 From: sravan Date: Thu, 2 Jul 2026 17:45:20 +0530 Subject: [PATCH 4/4] feat(chat): edit message (#4) Sender can edit their own text messages: a pencil action on the bubble loads the text into the composer in an 'Editing' mode; saving updates the body, marks it 'edited', and pushes the change live to the other side/tabs (chat-edited, mirroring delete). Adds messages.edited_at + editBody(). Co-Authored-By: Claude Opus 4.8 --- server/db.js | 2 ++ server/public/home.html | 49 ++++++++++++++++++++++++++++++++++++----- server/repos.js | 1 + server/routes.js | 20 +++++++++++++++++ 4 files changed, 66 insertions(+), 6 deletions(-) diff --git a/server/db.js b/server/db.js index a267a97..9957a55 100644 --- a/server/db.js +++ b/server/db.js @@ -216,6 +216,8 @@ try { db.exec('ALTER TABLE messages ADD COLUMN msg_type TEXT'); } catch (e) { /* // Deleted ("delete for everyone"): the row stays so threads/ordering hold, but body+attachment // are cleared and clients render a "This message was deleted" placeholder. try { db.exec('ALTER TABLE messages ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0'); } catch (e) { /* exists */ } +// A message can be edited by its sender; edited_at marks it (shows an "edited" label). +try { db.exec('ALTER TABLE messages ADD COLUMN edited_at INTEGER'); } catch (e) { /* exists */ } // User-set presence status: 'active' | 'away' | 'onleave'. ('incall' is derived live, not stored.) try { db.exec("ALTER TABLE users ADD COLUMN status TEXT NOT NULL DEFAULT 'active'"); } catch (e) { /* exists */ } // BizGaze person-id (s.userId): the SAME value whether the person signs in with their email or diff --git a/server/public/home.html b/server/public/home.html index 5cece71..d065eee 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -471,6 +471,9 @@ .bubble:hover .react-btn,.bubble.show-actions .react-btn{opacity:1;pointer-events:auto;} .del-btn{position:absolute;top:-9px;right:58px;background:var(--card);color:var(--red);border:1px solid var(--line);border-radius:50%;width:22px;height:22px;line-height:1;cursor:pointer;opacity:0;pointer-events:none;transition:opacity .12s;box-shadow:0 1px 3px rgba(0,0,0,.12);display:grid;place-items:center;} .bubble:hover .del-btn,.bubble.show-actions .del-btn{opacity:1;pointer-events:auto;} + .edit-btn{position:absolute;top:-9px;right:84px;background:var(--card);color:var(--blue);border:1px solid var(--line);border-radius:50%;width:22px;height:22px;line-height:1;cursor:pointer;opacity:0;pointer-events:none;transition:opacity .12s;box-shadow:0 1px 3px rgba(0,0,0,.12);display:grid;place-items:center;} + .bubble:hover .edit-btn,.bubble.show-actions .edit-btn{opacity:1;pointer-events:auto;} + .bubble .t .edited{font-style:italic;opacity:.8;} .bubble.deleted{opacity:.85;} .bubble.deleted .del-msg{font-style:italic;color:var(--muted);font-size:.9rem;display:inline-flex;align-items:center;gap:.3rem;} .bubble.mine.deleted .del-msg{color:rgba(255,255,255,.85);} @@ -764,7 +767,7 @@ - @@ -1290,8 +1293,9 @@ function bubbleHTML(m){ + sender + quote + att + renderMsgBody(m) + pollHTML(m) + '' + '' + + ((mine && !m.deleted && m.body && !m.poll)?'':'') + (mine?'':'') - + ''+pEsc(fmtClock(m.created_at))+rcpt+'' + + ''+pEsc(fmtClock(m.created_at))+(m.edited_at?' · edited':'')+rcpt+'' + reacts + seen + '

'; } // reply + emoji state/helpers @@ -1320,6 +1324,36 @@ function setReply(m){ const inp=document.getElementById('msgInput'); if(inp) inp.focus(); } function clearReply(){ replyTarget=null; const bar=document.getElementById('replyBar'); if(bar){ bar.style.display='none'; bar.innerHTML=''; } } +// Edit an existing message (sender only): load its text into the composer, show an "Editing" bar; +// the next send saves the edit instead of posting a new message. +let editTarget=null; +function startEdit(m){ + if(!m) return; clearReply(); editTarget=m; + const bar=document.getElementById('replyBar'); if(bar){ bar.innerHTML=''+ic('edit',13)+' Editing message'+ic('x',15)+''; bar.style.display='flex'; const x=document.getElementById('editCancel'); if(x) x.onclick=cancelEdit; } + const inp=document.getElementById('msgInput'); if(inp){ inp.value=m.body||''; autoGrow(inp); inp.focus(); } +} +function cancelEdit(){ editTarget=null; const bar=document.getElementById('replyBar'); if(bar){ bar.style.display='none'; bar.innerHTML=''; } const inp=document.getElementById('msgInput'); if(inp){ inp.value=''; autoGrow(inp); } } +async function saveEdit(text){ + const m=editTarget; if(!m) return; + if(!text){ cancelEdit(); return; } // empty → treat as cancel (delete is a separate action) + if(text===(m.body||'')){ cancelEdit(); return; } // no change + editTarget=null; const bar=document.getElementById('replyBar'); if(bar){ bar.style.display='none'; bar.innerHTML=''; } + const inp=document.getElementById('msgInput'); if(inp){ inp.value=''; autoGrow(inp); } + try{ + await postJSON('/api/messages/edit',{ id:m.id, body:text }); + const t=THREAD.find(x=>x.id===m.id); if(t){ t.body=text; t.edited_at=Date.now(); updateBubble(t); } + THREAD_CACHE.forEach(arr=>arr.forEach(x=>{ if(x.id===m.id){ x.body=text; x.edited_at=Date.now(); } })); + // keep the chat-list preview in sync if this was the last message + const it=rowFor(selected.kind, selected.id); if(it && it.last_from_me){ const last=THREAD[THREAD.length-1]; if(last && last.id===m.id){ it.last_body=text; renderChats(searchVal()); } } + }catch(e){ toast(e.message||'Could not edit'); } +} +// A message I can see was edited by its sender → update it in place (thread + cache + list preview). +function onChatEdited(d){ + if(!d||!d.id) return; + const t=THREAD.find(x=>x.id===d.id); if(t){ t.body=d.body; t.edited_at=d.edited_at||Date.now(); updateBubble(t); } + THREAD_CACHE.forEach(arr=>arr.forEach(x=>{ if(x.id===d.id){ x.body=d.body; x.edited_at=d.edited_at||Date.now(); } })); + try{ loadSidebar(); }catch(_){} // refresh last-message previews +} // attachments let pendingAttach=null; function fmtSize(b){ b=+b||0; if(b<1024) return b+' B'; if(b<1048576) return Math.round(b/1024)+' KB'; return (b/1048576).toFixed(1)+' MB'; } @@ -1635,7 +1669,7 @@ async function openConvo(kind,id){ el.ondrop=(e)=>{ const f=e.dataTransfer&&e.dataTransfer.files&&e.dataTransfer.files[0]; if(f){ e.preventDefault(); el.classList.remove('drag-over'); uploadFile(f); } }; const back=document.getElementById('convoBack'); if(back) back.onclick=showWelcome; const form=document.getElementById('composer'); if(form) form.addEventListener('submit',(e)=>{ e.preventDefault(); sendMessage(); }); - clearReply(); pendingAttach=null; hideAttach(); + clearReply(); cancelEdit(); pendingAttach=null; hideAttach(); const ab2=document.getElementById('attachBtn'); if(ab2) ab2.onclick=()=>{ const fi=document.getElementById('fileInput'); if(fi) fi.click(); }; const fi=document.getElementById('fileInput'); if(fi) fi.onchange=()=>{ if(fi.files&&fi.files[0]) uploadFile(fi.files[0]); }; const eb=document.getElementById('emojiBtn'); if(eb) eb.onclick=(e)=>{ e.stopPropagation(); emojiOpen?closeEmoji():openEmoji('compose', eb); }; @@ -1664,6 +1698,7 @@ async function openConvo(kind,id){ const po=e.target.closest('.poll-opt'); if(po){ if(!po.disabled) votePoll(po.dataset.poll, +po.dataset.idx); return; } const pcl=e.target.closest('.poll-close'); if(pcl){ closePoll(pcl.dataset.poll); return; } const rb=e.target.closest('.reply-btn'); if(rb){ const mm=THREAD.find(x=>x.id===rb.dataset.id); if(mm) setReply(mm); return; } + const ed=e.target.closest('.edit-btn'); if(ed){ const mm=THREAD.find(x=>x.id===ed.dataset.edit); if(mm) startEdit(mm); return; } const dl=e.target.closest('.del-btn'); if(dl){ deleteMessage(dl.dataset.del); return; } const ab=e.target.closest('.react-btn'); if(ab){ openEmojiForReact(ab.dataset.id, ab); return; } const ch=e.target.closest('.react-chip'); if(ch){ reactToMessage(ch.dataset.id, ch.dataset.emoji); return; } @@ -1864,7 +1899,9 @@ async function selectChat(kind,id){ } async function sendMessage(){ const inp=document.getElementById('msgInput'); if(!inp) return; - const text=inp.value.trim(); if((!text&&!pendingAttach)||!selected) return; + const text=inp.value.trim(); + if(editTarget){ saveEdit(text); return; } // in edit mode → save the edit instead of sending new + if((!text&&!pendingAttach)||!selected) return; inp.value=''; inp.style.height='auto'; setDraft(selected.kind, selected.id, ''); // sent → clear the draft const replyTo=replyTarget?replyTarget.id:null; const attachmentId=pendingAttach?pendingAttach.id:null; @@ -1875,7 +1912,7 @@ async function sendMessage(){ // Dedup by id: the server echoes our message over WS (to sync other tabs) and that echo // can arrive BEFORE this POST resolves, so onChatMessage may have already appended it. if(!THREAD.some(x=>x.id===m.id)){ THREAD.push(m); appendBubble(m); } - clearReply(); pendingAttach=null; hideAttach(); + clearReply(); cancelEdit(); pendingAttach=null; hideAttach(); { const ck=selected.kind+':'+selected.id; if(THREAD_CACHE.has(ck)){ const a=THREAD_CACHE.get(ck); if(!a.some(x=>x.id===m.id)) a.push(m); } } const it=rowFor(selected.kind,selected.id); if(it){ it.last_body=m.body||(m.attachment?'📎 '+(m.attachment.name||'Attachment'):''); it.last_at=m.created_at; it.last_from_me=true; it.last_status='sent'; it.last_msg_id=m.id; it.unread=0; } @@ -2113,7 +2150,7 @@ function connectChatWs(){ try{ chatWs=new WebSocket((location.protocol==='https:'?'wss://':'ws://')+location.host+'/ws'); chatWs.onopen=()=>{ try{ chatWs.send(JSON.stringify({type:'chat-hello'})); }catch(_){} }; - chatWs.onmessage=(e)=>{ let d; try{ d=JSON.parse(e.data); }catch(_){ return; } if(d.type==='chat-message' && d.message) onChatMessage(d.message); else if(d.type==='chat-deleted') onChatDeleted(d); else if(d.type==='chat-reaction') onChatReaction(d); else if(d.type==='poll-update' && d.poll) onPollUpdate(d); else if(d.type==='chat-read') onChatRead(d); else if(d.type==='chat-delivered') onChatDelivered(d); else if(d.type==='group-read') onGroupRead(d); else if(d.type==='group-call') onGroupCall(d); else if(d.type==='dm-call') onDmCall(d); else if(d.type==='presence') onPresence(d); else if(d.type==='group-update') onGroupUpdate(d); else if(d.type==='call-invite') showCallInvite(d.room, d.byName); else if(d.type==='meeting-invite') showMeetingInvite(d.meeting); else if(d.type==='meeting-reminder') showMeetingReminder(d.meeting); else if(d.type==='meeting-cancelled') showMeetingCancelled(d.meeting); else if(d.type==='group-role') onGroupRole(d); }; + chatWs.onmessage=(e)=>{ let d; try{ d=JSON.parse(e.data); }catch(_){ return; } if(d.type==='chat-message' && d.message) onChatMessage(d.message); else if(d.type==='chat-deleted') onChatDeleted(d); else if(d.type==='chat-edited') onChatEdited(d); else if(d.type==='chat-reaction') onChatReaction(d); else if(d.type==='poll-update' && d.poll) onPollUpdate(d); else if(d.type==='chat-read') onChatRead(d); else if(d.type==='chat-delivered') onChatDelivered(d); else if(d.type==='group-read') onGroupRead(d); else if(d.type==='group-call') onGroupCall(d); else if(d.type==='dm-call') onDmCall(d); else if(d.type==='presence') onPresence(d); else if(d.type==='group-update') onGroupUpdate(d); else if(d.type==='call-invite') showCallInvite(d.room, d.byName); else if(d.type==='meeting-invite') showMeetingInvite(d.meeting); else if(d.type==='meeting-reminder') showMeetingReminder(d.meeting); else if(d.type==='meeting-cancelled') showMeetingCancelled(d.meeting); else if(d.type==='group-role') onGroupRole(d); }; chatWs.onclose=()=>{ setTimeout(connectChatWs, 3000); }; // auto-reconnect }catch(_){} } diff --git a/server/repos.js b/server/repos.js index 2fc92dc..e97f44d 100644 --- a/server/repos.js +++ b/server/repos.js @@ -188,6 +188,7 @@ const messages = { byAttachment: (attachmentId) => db.prepare('SELECT * FROM messages WHERE attachment_id=? LIMIT 1').get(attachmentId), setPoll: (messageId, pollId) => db.prepare('UPDATE messages SET poll_id=? WHERE id=?').run(pollId, messageId), markDelivered: (id) => db.prepare('UPDATE messages SET delivered_at=? WHERE id=? AND delivered_at IS NULL').run(now(), id), + editBody: (id, body) => db.prepare('UPDATE messages SET body=?, edited_at=? WHERE id=?').run(body, now(), id), // Delete-for-everyone: clear the content but keep the row (renders as a placeholder). markDeleted: (id) => db.prepare("UPDATE messages SET deleted=1, body='', attachment_id=NULL, poll_id=NULL WHERE id=?").run(id), // Shared media/files in a conversation (group) or DM — newest first. diff --git a/server/routes.js b/server/routes.js index 0466ea6..133536b 100644 --- a/server/routes.js +++ b/server/routes.js @@ -1282,6 +1282,26 @@ route('POST', '/api/messages/delete', async (req, res) => { else { try { CHAT.pushToUser(m.recipient_id, evt); } catch (_) {} try { CHAT.pushToUser(u.id, evt); } catch (_) {} } json(res, 200, { ok: true }); }); +// Edit a message (sender only, text only) — updates the body + marks it edited, and pushes the +// change live to the other side / other tabs (mirrors the delete broadcast). +route('POST', '/api/messages/edit', async (req, res) => { + const u = currentUser(req); + if (!u) return json(res, 401, { error: 'unauthorized' }); + const { id, body } = await readBody(req); + if (!id || typeof body !== 'string') return json(res, 400, { error: 'id and body required' }); + const text = body.trim(); + if (!text) return json(res, 400, { error: 'message cannot be empty' }); + const m = R.messages.byId(id); + if (!m || m.team_id !== u.team_id) return json(res, 404, { error: 'not found' }); + if (m.sender_id !== u.id) return json(res, 403, { error: 'you can only edit your own messages' }); + if (m.deleted) return json(res, 400, { error: 'cannot edit a deleted message' }); + R.messages.editBody(id, text); + const edited = R.messages.byId(id); + const evt = { type: 'chat-edited', id, body: text, edited_at: edited.edited_at, conversation_id: m.conversation_id || null }; + if (m.conversation_id) { for (const mid of R.conversations.members(m.conversation_id)) { try { CHAT.pushToUser(mid, evt); } catch (_) {} } } + else { try { CHAT.pushToUser(m.recipient_id, evt); } catch (_) {} try { CHAT.pushToUser(u.id, evt); } catch (_) {} } + json(res, 200, { ok: true, edited_at: edited.edited_at }); +}); // Favourite/unfavourite a conversation (per user). target = 'dm:' or 'group:'. route('POST', '/api/favorites', async (req, res) => { const u = currentUser(req);