From adafc8c96050a48f02d171eeb1c4086a93b70ddb Mon Sep 17 00:00:00 2001 From: sravan Date: Fri, 10 Jul 2026 13:25:37 +0530 Subject: [PATCH] fix: seen-by layout, group-name notif, teams-style badge, excel paste, spellcheck (0.1.11/batch64) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Message-info popup: ONE scroll region for the whole list with sticky Seen / Not-seen headers (was a separate scrollbar per section), realigned. 2. Group message notifications now title = GROUP name, body = "Sender: text". 3. Unread taskbar badge: solid red rounded-square (Teams-style) + white outline instead of the gradient disc. 4. Pasting Excel/Sheets cells no longer uploads a screenshot — when the clipboard carries real text, the text is pasted; image-only clipboards still upload. 5. Spell check in the message box (red squiggles) with a right-click menu of corrections + add-to-dictionary; spellcheck enabled on the textarea. Co-Authored-By: Claude Opus 4.8 --- desktop/main.js | 34 +++++++++++++++++++++- desktop/package.json | 2 +- server/public/home.html | 64 +++++++++++++++++++++++++++-------------- 3 files changed, 76 insertions(+), 24 deletions(-) diff --git a/desktop/main.js b/desktop/main.js index 9ed7696..0b32c7a 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -8,7 +8,7 @@ // - external links open in the user's browser, not inside the app // // Server origin is configurable so the same build works against prod or a dev server. -const { app, BrowserWindow, session, desktopCapturer, shell, Menu, ipcMain, nativeImage, Notification } = require('electron'); +const { app, BrowserWindow, session, desktopCapturer, shell, Menu, MenuItem, ipcMain, nativeImage, Notification } = require('electron'); const path = require('path'); const fs = require('fs'); const os = require('os'); @@ -221,6 +221,28 @@ function createWindow() { if (!url.startsWith(SERVER_URL)) { shell.openExternal(url); return { action: 'deny' }; } return { action: 'allow' }; }); + + // Right-click menu: spelling corrections for the misspelled word under the cursor (+ add to + // dictionary), plus standard cut/copy/paste in editable fields. This is what makes the spell + // checker actionable — click a suggestion to fix the word in place. + win.webContents.on('context-menu', (_e, params) => { + const menu = new Menu(); + for (const s of (params.dictionarySuggestions || [])) { + menu.append(new MenuItem({ label: s, click: () => win.webContents.replaceMisspelling(s) })); + } + if (params.misspelledWord) { + if (params.dictionarySuggestions && params.dictionarySuggestions.length) menu.append(new MenuItem({ type: 'separator' })); + menu.append(new MenuItem({ label: 'Add to dictionary', click: () => win.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord) })); + } + if (params.isEditable || params.editFlags.canCopy) { + if (menu.items.length) menu.append(new MenuItem({ type: 'separator' })); + if (params.editFlags.canCut) menu.append(new MenuItem({ role: 'cut' })); + if (params.editFlags.canCopy) menu.append(new MenuItem({ role: 'copy' })); + if (params.isEditable) menu.append(new MenuItem({ role: 'paste' })); + if (params.isEditable && params.editFlags.canSelectAll) menu.append(new MenuItem({ role: 'selectAll' })); + } + if (menu.items.length) menu.popup(); + }); } // The full Connect experience needs several web capabilities that Electron denies by @@ -294,6 +316,16 @@ function configureSession() { ses.setPermissionRequestHandler((_wc, permission, callback) => callback(GRANTED.has(permission))); // Sync check (some getUserMedia paths query this before requesting) ses.setPermissionCheckHandler((_wc, permission) => GRANTED.has(permission)); + // Spell check for the message box (red squiggles) with right-click corrections. Uses the OS + // dictionaries; en-US by default plus whatever the OS UI language is, so mixed typing still checks. + try { + ses.setSpellCheckerEnabled(true); + const langs = ['en-US']; + const sys = (app.getLocale && app.getLocale()) || ''; + const avail = (ses.availableSpellCheckerLanguages || []); + if (sys && sys !== 'en-US' && (!avail.length || avail.includes(sys))) langs.push(sys); + ses.setSpellCheckerLanguages(langs); + } catch (_) {} } app.whenReady().then(() => { diff --git a/desktop/package.json b/desktop/package.json index 398d190..a51fd4e 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "biz-connect-desktop", - "version": "0.1.10", + "version": "0.1.11", "description": "Biz Connect technician desktop client — loads the Connect web UI with native screen capture", "author": { "name": "BizGaze", diff --git a/server/public/home.html b/server/public/home.html index 15a2ccb..f85572f 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -553,16 +553,20 @@ .modal input#grpName:focus,.modal input#giName:focus{outline:none;border-color:var(--brand);} /* Branded confirm dialog (replaces the OS/Electron window.confirm). */ .bz-confirm .bzc-msg{margin:.2rem 0 1.1rem;color:var(--ink);font-size:.92rem;line-height:1.5;} - .seen-modal .seen-list{max-height:38vh;overflow-y:auto;display:flex;flex-direction:column;gap:.15rem;} - .seen-modal .seen-row{display:flex;align-items:center;gap:.6rem;padding:.32rem .3rem;} + .seen-modal{max-width:360px;display:flex;flex-direction:column;max-height:80vh;} + .seen-modal>h3{flex:0 0 auto;} + /* ONE scroll region for the whole list; the two section headers stick to the top as you scroll */ + .seen-modal .seen-scroll{flex:1 1 auto;overflow-y:auto;margin:.2rem -.4rem 0;padding:0 .4rem;min-height:0;} + .seen-modal .seen-row{display:flex;align-items:center;gap:.6rem;padding:.34rem .2rem;} /* dark lettering on the pastel avatar (white was invisible on the light background), unless a real DP covers it */ - .seen-modal .seen-row .mini-av{width:32px;height:32px;flex:0 0 auto;border-radius:50%;display:grid;place-items:center;color:#334155;font-weight:700;font-size:.74rem;overflow:hidden;position:relative;} + .seen-modal .seen-row .mini-av{width:34px;height:34px;flex:0 0 auto;border-radius:50%;display:grid;place-items:center;color:#334155;font-weight:700;font-size:.76rem;overflow:hidden;position:relative;} .seen-modal .seen-row .mini-av .av-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;} .seen-modal .seen-row .sn{font-size:.92rem;color:var(--ink);} - .seen-modal .seen-sec{display:flex;align-items:center;gap:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.02em;text-transform:uppercase;color:var(--blue);margin:.7rem .2rem .25rem;} + .seen-modal .seen-sec{position:sticky;top:0;z-index:1;background:var(--card,#fff);display:flex;align-items:center;gap:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.02em;text-transform:uppercase;color:var(--blue);padding:.5rem .2rem .3rem;} .seen-modal .seen-sec.dim{color:var(--muted);} .seen-modal .seen-sec .dot{width:7px;height:7px;border-radius:50%;background:#cbd5e1;} .seen-modal .seen-sec .dot.ok{background:#22c55e;} + .seen-modal .bzc-actions{flex:0 0 auto;margin-top:.7rem;} .bzc-actions{display:flex;justify-content:flex-end;gap:.5rem;} .bzc-actions button{border:none;border-radius:10px;padding:.55rem 1rem;font-size:.9rem;font-weight:600;cursor:pointer;font-family:inherit;} .bzc-cancel{background:var(--blue-soft);color:var(--blue);} @@ -846,7 +850,7 @@ - @@ -1378,18 +1382,18 @@ function setDesktopBadge(count){ if(!(window.bizConnectNative && window.bizConnectNative.setUnread)) return; let dataUrl=null; if(count>0){ - // Draw at 2× for a crisp taskbar overlay. A vertical red→pink gradient pill with a white ring - // and a soft drop shadow reads as a modern notification badge (was a flat red disc). + // Teams-style overlay: a solid red rounded-square (squircle) with a bold white count and a thin + // white outline so it separates cleanly from the app icon on the taskbar. Drawn at 2× for crispness. const s=64, c=document.createElement('canvas'); c.width=s; c.height=s; const g=c.getContext('2d'); const label=count>99?'99+':String(count); - const cx=s/2, cy=s/2, r=s/2-4; - g.save(); g.shadowColor='rgba(0,0,0,.35)'; g.shadowBlur=5; g.shadowOffsetY=2; - const grad=g.createLinearGradient(0,cy-r,0,cy+r); grad.addColorStop(0,'#ff5b6e'); grad.addColorStop(1,'#e11d48'); - g.fillStyle=grad; g.beginPath(); g.arc(cx,cy,r,0,2*Math.PI); g.fill(); g.restore(); - g.lineWidth=3.5; g.strokeStyle='#fff'; g.beginPath(); g.arc(cx,cy,r,0,2*Math.PI); g.stroke(); // white ring pops on any taskbar + const pad=3, rad=17, x=pad, y=pad, w=s-pad*2, h=s-pad*2; + const rrect=(rx,ry,rw,rh,r)=>{ g.beginPath(); g.moveTo(rx+r,ry); g.arcTo(rx+rw,ry,rx+rw,ry+rh,r); g.arcTo(rx+rw,ry+rh,rx,ry+rh,r); g.arcTo(rx,ry+rh,rx,ry,r); g.arcTo(rx,ry,rx+rw,ry,r); g.closePath(); }; + g.save(); g.shadowColor='rgba(0,0,0,.28)'; g.shadowBlur=4; g.shadowOffsetY=1.5; + g.fillStyle='#e6394f'; rrect(x,y,w,h,rad); g.fill(); g.restore(); // solid notification red + g.lineWidth=3; g.strokeStyle='#fff'; rrect(x,y,w,h,rad); g.stroke(); // crisp white outline g.fillStyle='#fff'; g.textAlign='center'; g.textBaseline='middle'; - g.font='800 '+(label.length>2?26:(label.length>1?32:38))+'px system-ui,Segoe UI,sans-serif'; - g.fillText(label, cx, cy+2); + g.font='800 '+(label.length>2?26:(label.length>1?33:39))+'px system-ui,Segoe UI,sans-serif'; + g.fillText(label, s/2, s/2+2); dataUrl=c.toDataURL('image/png'); } window.bizConnectNative.setUnread(count, dataUrl); @@ -1481,7 +1485,7 @@ function convoShellHTML(it){ + '
' + '' + '' - + '' + + '' + (isG?'':'') + '' + '' @@ -1624,7 +1628,13 @@ function renderAttachBar(){ function hideAttach(){ pendingAttachs=[]; const bar=document.getElementById('attachBar'); if(bar){ bar.style.display='none'; bar.innerHTML=''; } const fi=document.getElementById('fileInput'); if(fi) fi.value=''; } // Paste an image from the clipboard (e.g. a screenshot) straight into the composer. function onPaste(e){ - const items=(e.clipboardData&&e.clipboardData.items)||[]; + const cd=e.clipboardData; if(!cd) return; + // Excel / Google Sheets put BOTH a bitmap AND the cell text on the clipboard. If there's real text, + // let the textarea paste it normally (don't upload the screenshot). Only upload the image when the + // clipboard is image-ONLY (a genuine screenshot / copied picture with no accompanying text). + const plain=(cd.getData&&cd.getData('text/plain'))||''; + if(plain&&plain.trim()) return; // → default paste of the text + const items=cd.items||[]; for(const it of items){ if(it.type&&it.type.indexOf('image')===0){ const blob=it.getAsFile(); @@ -1719,12 +1729,15 @@ function showSeenByModal(msg){ const chip=(name,avatar)=>''+(avatar?(''):'')+pEsc(initials(name))+''; const rowsFromNames=(arr)=>arr.map(n=>'
'+chip(n, avByName[String(n).trim().toLowerCase()]||null)+''+pEsc(n)+'
').join(''); const rowsFromMembers=(arr)=>arr.map(m=>'
'+chip(m.name, m.avatar||null)+''+pEsc(m.name)+'
').join(''); - let body=''; - if(seenNames.length) body+='
Seen · '+seenNames.length+'
'+rowsFromNames(seenNames)+'
'; - else body+='
No one has seen this yet.
'; - if(convoIsGroup && notSeen.length) body+='
Not seen yet · '+notSeen.length+'
'+rowsFromMembers(notSeen)+'
'; + let inner=''; + if(seenNames.length) inner+='
Seen · '+seenNames.length+'
'+rowsFromNames(seenNames); + else inner+='
No one has seen this yet.
'; + if(convoIsGroup && notSeen.length) inner+='
Not seen yet · '+notSeen.length+'
'+rowsFromMembers(notSeen); + // ONE scroll container for the whole list (section headers stick as you scroll) — not a scrollbar + // per section, which split the popup confusingly. + const body='
'+inner+'
'; const ov=document.createElement('div'); ov.className='modal-ov'; ov.id='seenModal'; - ov.innerHTML=''; + ov.innerHTML=''; document.body.appendChild(ov); ov.addEventListener('mousedown',e=>{ if(e.target===ov) ov.remove(); }); ov.querySelector('#seenClose').onclick=()=>ov.remove(); @@ -2546,7 +2559,14 @@ function onChatMessage(m){ // Popup rule: ping always. In-page popup only when the tab is VISIBLE but you're on another // chat. When the tab is HIDDEN, let Web Push show it (the SW). If push isn't active, fall // back to an in-page popup so hidden-tab users still get alerted. - if(notifOn(kind)){ playPing(); const wantPopup=!(isOpen && !document.hidden); if(wantPopup && !(document.hidden && pushActive)) notify((m.fromName||'New message'), m.body?(m.body.length>80?m.body.slice(0,80)+'…':m.body):'Sent an attachment', kind, rid); } + if(notifOn(kind)){ playPing(); const wantPopup=!(isOpen && !document.hidden); + if(wantPopup && !(document.hidden && pushActive)){ + const prev=m.body?(m.body.length>80?m.body.slice(0,80)+'…':m.body):'Sent an attachment'; + // Group: title = GROUP name, body = "Sender: message" (so you know which group it's from). + if(kind==='group') notify((it&&it.name)||m.groupName||'Group', (m.fromName?m.fromName+': ':'')+prev, kind, rid); + else notify((m.fromName||'New message'), prev, kind, rid); + } + } // Activity-center entries for things easy to miss. if(m.poll) addNotif({icon:'barChart', text:pEsc(m.fromName||'Someone')+' created a poll'+(m.poll.question?': '+pEsc(m.poll.question):''), link:{kind, id:rid}}); else if(kind==='dm' && wasNew) addNotif({icon:'chat', text:'New chat from '+pEsc(m.fromName||'someone'), link:{kind:'dm', id:rid}});