fix: group toast icon, left-click spell suggestions, scroll no-yank (0.1.12/batch66)

1. Group message toast had no icon: the generated group icon is a data: URL, but
   the 0.1.11 handler only used ALREADY-cached icons and skipped it. data: URLs
   are synchronous — use them immediately, so every group toast shows an icon.
2. Spell suggestions on a LEFT click: clicking a red-squiggled word in the message
   box now pops the native suggestions menu (renderer asks the shell to synthesize
   a right-click at that point → real dictionary suggestions). No right-click.
3. Scroll felt "stuck" because an incoming message yanked you to the bottom even
   when you'd scrolled up to read history (worse now that bg messages arrive live).
   appendBubble now keeps your position unless you were at the bottom / it's your
   own message, and surfaces the "jump to latest" control instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 14:57:02 +05:30
parent 1dfb6b16cb
commit aa4bb2902d
4 changed files with 55 additions and 9 deletions
+38 -6
View File
@@ -103,9 +103,14 @@ ipcMain.handle('reply-notification', async (_e, payload = {}) => {
// Contacts are also pre-warmed on load (precache-avatars), so the photo is usually already cached.
let img = null;
try {
const c = payload.avatar && avatarCache.get(payload.avatar);
if (c && fs.existsSync(c)) img = c;
else if (payload.avatar) avatarToTempPng(payload.avatar).catch(() => {}); // warm for next time
const src = payload.avatar;
if (src && /^data:image\//i.test(src)) {
img = await avatarToTempPng(src); // generated icon (initials/group) — synchronous, always use immediately
} else if (src) {
const c = avatarCache.get(src);
if (c && fs.existsSync(c)) img = c; // http DP already warmed (contacts pre-cached on load) → instant
else avatarToTempPng(src).catch(() => {}); // not cached yet → fire now, warm for next time
}
} catch (_) {}
return await new Promise((resolve) => {
let done = false;
@@ -222,10 +227,23 @@ function createWindow() {
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.
// Spell-check menu. Two ways in:
// - RIGHT-click: full editing menu (suggestions + cut/copy/paste), the standard desktop behavior.
// - LEFT-click on a misspelled word: the renderer asks us (spell-suggest) to synthesize a
// right-click at that point, so Chromium hands us the real dictionary suggestions — then we show
// a SUGGESTIONS-ONLY menu. This gives the user corrections on a plain left click.
win.webContents.on('context-menu', (_e, params) => {
const fromLeftClick = spellClickPending; spellClickPending = false;
if (fromLeftClick) {
if (!params.misspelledWord) return; // clicked a correctly-spelled word → no menu, don't disturb typing
const menu = new Menu();
for (const s of (params.dictionarySuggestions || [])) menu.append(new MenuItem({ label: s, click: () => win.webContents.replaceMisspelling(s) }));
if (!menu.items.length) menu.append(new MenuItem({ label: 'No suggestions', enabled: false }));
menu.append(new MenuItem({ type: 'separator' }));
menu.append(new MenuItem({ label: 'Add to dictionary', click: () => win.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord) }));
menu.popup();
return;
}
const menu = new Menu();
for (const s of (params.dictionarySuggestions || [])) {
menu.append(new MenuItem({ label: s, click: () => win.webContents.replaceMisspelling(s) }));
@@ -244,6 +262,20 @@ function createWindow() {
if (menu.items.length) menu.popup();
});
}
// Set just before we synthesize a right-click from a renderer LEFT-click, so the context-menu handler
// knows to show a suggestions-only menu (and to stay silent on correctly-spelled words).
let spellClickPending = false;
ipcMain.on('spell-suggest', (_e, pos) => {
try {
if (!win || win.isDestroyed() || !pos) return;
const x = Math.round(pos.x), y = Math.round(pos.y);
if (!(x >= 0 && y >= 0)) return;
spellClickPending = true;
win.webContents.sendInputEvent({ type: 'mouseDown', x, y, button: 'right', clickCount: 1 });
win.webContents.sendInputEvent({ type: 'mouseUp', x, y, button: 'right', clickCount: 1 });
setTimeout(() => { spellClickPending = false; }, 500); // guard: clear if no context-menu fired
} catch (_) { spellClickPending = false; }
});
// The full Connect experience needs several web capabilities that Electron denies by
// default. We grant them for our own trusted origin:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "biz-connect-desktop",
"version": "0.1.11",
"version": "0.1.12",
"description": "Biz Connect technician desktop client — loads the Connect web UI with native screen capture",
"author": {
"name": "BizGaze",
+3
View File
@@ -25,6 +25,9 @@ contextBridge.exposeInMainWorld('bizConnectNative', Object.freeze({
// Pre-warm the notification DP cache with contact photo URLs (called after chats load) so the first
// toast from anyone already has their photo — no per-notification download lag.
precacheAvatars: (urls) => { try { return ipcRenderer.invoke('precache-avatars', urls); } catch (_) { return Promise.resolve(false); } },
// Ask the shell to show native spelling suggestions for the word at page coords (x,y) — used to bring
// up corrections on a LEFT click in the message box (not just right-click).
spellSuggestAt: (x, y) => { try { ipcRenderer.send('spell-suggest', { x, y }); } catch (_) {} },
// Manual "Check for updates" from Settings. Resolves {status:'available'|'current'|'dev'|'error', version?}.
// On 'available' the shell downloads in the background and prompts to restart when ready.
checkForUpdates: () => ipcRenderer.invoke('check-updates'),