diff --git a/server/public/home.html b/server/public/home.html index f85572f..6131bbe 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -139,11 +139,13 @@ /* Modern thin scrollbars for every scroll area (chat, modals, lists, …). Without this, Electron on Windows falls back to the classic scrollbar with up/down stepper arrows. */ - ::-webkit-scrollbar{width:6px;height:6px;} + ::-webkit-scrollbar{width:11px;height:11px;} ::-webkit-scrollbar-button,::-webkit-scrollbar-button:start:decrement,::-webkit-scrollbar-button:end:increment,::-webkit-scrollbar-button:vertical:start,::-webkit-scrollbar-button:vertical:end,::-webkit-scrollbar-button:horizontal:start,::-webkit-scrollbar-button:horizontal:end{display:none!important;width:0!important;height:0!important;} ::-webkit-scrollbar-track{background:transparent;} - ::-webkit-scrollbar-thumb{background:#c7d0dd;border-radius:9px;border:1px solid transparent;background-clip:content-box;} - ::-webkit-scrollbar-thumb:hover{background:#aab6c8;} + /* Was 6px with a 1px inset border (~4px grabbable) and no min height → the thumb was tiny and felt + "stuck" when dragged. Wider, solid, with a min length so it's easy to grab and drags smoothly. */ + ::-webkit-scrollbar-thumb{background:#b9c4d4;border-radius:8px;min-height:44px;} + ::-webkit-scrollbar-thumb:hover{background:#9fadc2;} ::-webkit-scrollbar-corner{background:transparent;} *{scrollbar-width:thin;scrollbar-color:#c7d0dd transparent;} @@ -151,7 +153,8 @@ .chatlist:hover{scrollbar-color:#c7d0dd transparent;} .chatlist::-webkit-scrollbar{width:6px;} .chatlist::-webkit-scrollbar-button,.chatlist::-webkit-scrollbar-button:start:decrement,.chatlist::-webkit-scrollbar-button:end:increment{display:none!important;height:0!important;width:0!important;} - .convo-msgs::-webkit-scrollbar{width:6px;} .convo-msgs::-webkit-scrollbar-button{display:none!important;height:0!important;width:0!important;} + .convo-msgs::-webkit-scrollbar{width:10px;} .convo-msgs::-webkit-scrollbar-button{display:none!important;height:0!important;width:0!important;} + .convo-msgs::-webkit-scrollbar-thumb{min-height:44px;} .chatlist::-webkit-scrollbar-thumb{background:transparent;border-radius:8px;} .chatlist:hover::-webkit-scrollbar-thumb{background:#c7d0dd;} /* Pull-to-refresh indicator (mobile) */ @@ -553,10 +556,13 @@ .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{max-width:360px;display:flex;flex-direction:column;max-height:80vh;} + /* overflow:hidden on the CARD so it never becomes a 2nd scroll container — .modal defaults to + overflow-y:auto, which nested against .seen-scroll and made the wheel/drag feel "stuck". */ + .seen-modal{max-width:360px;display:flex;flex-direction:column;max-height:80vh;overflow:hidden;} .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;} + /* THE single scroll region for the whole list; the two section headers stick as you scroll */ + .seen-modal .seen-scroll{flex:1 1 auto;overflow-y:auto;overscroll-behavior:contain;margin:.2rem -.4rem 0;padding:0 .4rem;min-height:0;} + .seen-modal .seen-div{height:1px;background:var(--line,#e3e8f2);margin:.5rem .2rem;} .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: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;} @@ -850,7 +856,7 @@ - @@ -941,6 +947,51 @@ function firstName(name){return String(name||'').trim().split(/\s+/)[0]||'there' const AV_COLORS=['#dbeafe','#e0e7ff','#dcfce7','#cffafe','#fae8ff','#fce7f3','#fee2e2','#fef3c7','#ecfccb','#fde4cf']; function avColor(name){let h=0;for(const c of String(name))h=(h*31+c.charCodeAt(0))>>>0;return AV_COLORS[h%AV_COLORS.length];} +// Auto-correct: curated high-confidence common-typo → fix map. Applied as you type (on a word +// boundary), so corrections happen automatically — no right-click menu. Works on web/desktop/mobile. +// Kept conservative (only unambiguous typos) to avoid "fighting" the user. Ctrl+Z reverts a fix. +const AUTOCORRECT={ + teh:'the',hte:'the',thier:'their',recieve:'receive',recieved:'received',beleive:'believe',beleived:'believed', + seperate:'separate',definately:'definitely',occured:'occurred',occuring:'occurring',untill:'until',wich:'which', + becuase:'because',becasue:'because',accross:'across',adress:'address',adressed:'addressed',alot:'a lot', + arguement:'argument',calender:'calendar',cemetary:'cemetery',changable:'changeable',collegue:'colleague', + comming:'coming',commited:'committed',concious:'conscious',definetly:'definitely',dissapoint:'disappoint', + embarass:'embarrass',enviroment:'environment',existance:'existence',familier:'familiar',finaly:'finally', + foriegn:'foreign',fourty:'forty',freind:'friend',gaurd:'guard',goverment:'government',grammer:'grammar', + happend:'happened',harrass:'harass',immediatly:'immediately',independant:'independent',infomation:'information', + knowlege:'knowledge',liason:'liaison',libary:'library',lisence:'license',maintainance:'maintenance', + mispell:'misspell',neccessary:'necessary',necesary:'necessary',noticable:'noticeable',occassion:'occasion', + occurance:'occurrence',ocurrence:'occurrence',paralell:'parallel',parliment:'parliament',particurly:'particularly', + perhasp:'perhaps',persistant:'persistent',posession:'possession',prefered:'preferred',privelege:'privilege', + probaly:'probably',probelm:'problem',proffesional:'professional',promiss:'promise',pronounciation:'pronunciation', + publically:'publicly',realy:'really',reccomend:'recommend',recomend:'recommend',refered:'referred', + relevent:'relevant',religous:'religious',rember:'remember',remeber:'remember',resturant:'restaurant', + rythm:'rhythm',secratary:'secretary',sieze:'seize',similiar:'similar',succesful:'successful',succesfully:'successfully', + suprise:'surprise',suprised:'surprised',tommorow:'tomorrow',tommorrow:'tomorrow',tounge:'tongue',truely:'truly', + unfortunatly:'unfortunately',usualy:'usually',vaccum:'vacuum',wierd:'weird',writen:'written',yeild:'yield', + yuo:'you',youre:"you're",cant:"can't",dont:"don't",doesnt:"doesn't",didnt:"didn't",wasnt:"wasn't",werent:"weren't", + isnt:"isn't",wouldnt:"wouldn't",couldnt:"couldn't",shouldnt:"shouldn't",hasnt:"hasn't",havent:"haven't", + ive:"I've",thats:"that's",whats:"what's",lets:"let's", + aer:'are',adn:'and',nad:'and',wnat:'want',waht:'what',knwo:'know', + woudl:'would',shoudl:'should',coudl:'could',funtion:'function',retrun:'return', +}; +// Apply an auto-correction if the caret just crossed a word boundary onto a known typo. +function maybeAutocorrect(el){ + try{ + if(!el||el.selectionStart==null||el.selectionStart!==el.selectionEnd) return; + const pos=el.selectionStart, val=el.value, ch=val[pos-1]; + if(!ch || !/[\s.,!?;:]/.test(ch)) return; // only right after a boundary char + const before=val.slice(0,pos-1); const m=before.match(/([A-Za-z']+)$/); if(!m) return; + const word=m[1], lower=word.toLowerCase(); const rep=AUTOCORRECT[lower]; + if(!rep) return; // not a known typo (native squiggles still flag others) + const fixed=/^[A-Z]/.test(word)?(rep.charAt(0).toUpperCase()+rep.slice(1)):rep; + if(fixed===word) return; + const start=pos-1-word.length; + el.value=val.slice(0,start)+fixed+val.slice(pos-1); + const np=start+fixed.length+1; el.setSelectionRange(np,np); + }catch(_){} +} + // All in-app toasts route through the branded BZToast (bizconnect-toast.js). We keep the single // toast(msg) entry point so every existing call site is unchanged, and classify by wording so // errors/successes get the right colour+icon; everything else shows as a neutral message. @@ -1732,7 +1783,7 @@ function showSeenByModal(msg){ 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); + 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+'
'; @@ -2019,7 +2070,7 @@ async function openConvo(kind,id){ if(inpEl){ const _d=getDraft(kind,id); if(_d){ inpEl.value=_d; autoGrow(inpEl); } // restore unsent draft inpEl.addEventListener('paste', onPaste); - inpEl.addEventListener('input', ()=>{ autoGrow(inpEl); setDraft(kind,id,inpEl.value); noteTyping(); }); // keep the draft in sync + emit "typing…" + inpEl.addEventListener('input', ()=>{ maybeAutocorrect(inpEl); autoGrow(inpEl); setDraft(kind,id,inpEl.value); noteTyping(); }); // auto-correct common typos, keep the draft in sync + emit "typing…" inpEl.addEventListener('blur', stopTyping); inpEl.addEventListener('keydown', (e)=>{ if(e.key==='Enter' && !e.shiftKey){ if(mentionItems && mentionItems.length) return; e.preventDefault(); sendMessage(); } }); // Enter sends, Shift+Enter = newline } @@ -2469,13 +2520,29 @@ function onNotifClear(d){ // Drop matching activity-center entries so the bell badge stays in sync. try{ if(Array.isArray(NOTIFS)){ const before=NOTIFS.length; NOTIFS=NOTIFS.filter(x=>!(x.link&&x.link.kind===d.kind&&x.link.id===d.id)); if(NOTIFS.length!==before){ saveNotifs&&saveNotifs(); updateBellBadge&&updateBellBadge(); } } }catch(_){} } +// Draw a round initials avatar to a PNG data URL (synchronous — no external image, so it never taints +// the canvas). Used as the desktop toast icon when a chat/group has no photo, so every toast shows one. +function initialsAvatarDataUrl(name, isGroup){ + try{ + const s=96, c=document.createElement('canvas'); c.width=s; c.height=s; const g=c.getContext('2d'); + g.beginPath(); g.arc(s/2,s/2,s/2,0,2*Math.PI); g.fillStyle=isGroup?'#1F3B73':avColor(name||'?'); g.fill(); + g.fillStyle=isGroup?'#fff':'#334155'; g.font='bold 40px system-ui,Segoe UI,sans-serif'; g.textAlign='center'; g.textBaseline='middle'; + g.fillText(initials(name||'?'), s/2, s/2+2); + return c.toDataURL('image/png'); + }catch(_){ return null; } +} function notify(title, body, kind, id, opts){ const persistent=!!(opts&&opts.persistent); try{ // Desktop app: native toast with the sender/group DP. Pass the DP URL directly — the shell // downloads it for the icon (drawing an external DP to a canvas here tainted it → initials). if(window.bizConnectNative && window.bizConnectNative.replyNotify){ - const _av=(rowFor(kind,id)||{}).avatar||null; + const _row=rowFor(kind,id)||{}; + // Groups (and contacts) with no photo URL had NO toast icon at all. Draw a generated icon + // (initials on a brand-tinted disc — no external image, so no canvas taint) so every toast, + // especially group messages, carries a recognizable avatar. + let _av=_row.avatar||null; + if(!_av){ try{ _av=initialsAvatarDataUrl(_row.name||title, kind==='group'); }catch(_){ _av=null; } } Promise.resolve(window.bizConnectNative.replyNotify({title, body, kind, id, avatar:_av, persistent})) .then(r=>{ if(!r) return; if(r.text) sendReplyTo(kind, id, r.text); // replied from the toast