diff --git a/server/db.js b/server/db.js index 861fb93..12ebf90 100644 --- a/server/db.js +++ b/server/db.js @@ -218,6 +218,7 @@ try { db.exec('ALTER TABLE messages ADD COLUMN msg_type TEXT'); } catch (e) { /* 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 */ } +try { db.exec('ALTER TABLE messages ADD COLUMN fwd_from TEXT'); } catch (e) { /* exists — original sender name when a message was forwarded (#5) */ } // 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 3cd752f..29c47c8 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -149,8 +149,9 @@ .chatlist{overflow-y:auto;flex:1 1 auto;padding:.4rem;scrollbar-width:thin;scrollbar-color:transparent transparent;} .chatlist:hover{scrollbar-color:#c7d0dd transparent;} - .chatlist::-webkit-scrollbar{width:7px;} - .chatlist::-webkit-scrollbar-button{display:none;height:0;width:0;} + .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;} .chatlist::-webkit-scrollbar-thumb{background:transparent;border-radius:8px;} .chatlist:hover::-webkit-scrollbar-thumb{background:#c7d0dd;} /* Pull-to-refresh indicator (mobile) */ @@ -261,11 +262,11 @@ .gi-media-row .gmr-r{color:var(--muted);font-size:.85rem;font-weight:600;} .gi-media-strip{display:flex;gap:.3rem;margin:.4rem 0 .2rem;overflow:hidden;} .gms-thumb{width:60px;height:60px;object-fit:cover;border-radius:8px;background:#f1f5f9;cursor:pointer;flex:0 0 auto;} - /* Media / Docs / Links — clean underline tabs (active = green underline, no chip borders) */ + /* Media / Docs / Links — clean underline tabs (active = brand-blue underline, no chip borders) */ .mv-tabs{display:flex;gap:0;border-bottom:1px solid var(--line);margin:.5rem 0 0;} .mv-tabs .mp-tab{flex:1;border:none;background:transparent;color:var(--muted);font-size:.95rem;font-weight:600;padding:.6rem .3rem .65rem;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;text-align:center;} .mv-tabs .mp-tab:hover{color:var(--ink);} - .mv-tabs .mp-tab.on{color:var(--ink);font-weight:700;border-bottom-color:#16a34a;} + .mv-tabs .mp-tab.on{color:var(--blue);font-weight:700;border-bottom-color:var(--blue);} .mv-body{max-height:58vh;overflow:auto;margin-top:.5rem;} /* Shared-media tiles: image thumbnail, or audio/video tile with download + duration */ .sh-cell{margin:0;display:flex;flex-direction:column;gap:.28rem;min-width:0;} @@ -290,7 +291,7 @@ .convo-body{flex:1;display:grid;place-items:center;text-align:center;color:var(--muted);padding:2rem;} .convo-body .big{font-size:2.4rem;margin-bottom:.4rem;} /* message thread */ - .convo-msgs{flex:1;overflow-y:auto;padding:1rem 1.2rem;display:flex;flex-direction:column;gap:.35rem;background:var(--bg);} + .convo-msgs{flex:1;overflow-y:auto;overflow-anchor:none;padding:1rem 1.2rem;display:flex;flex-direction:column;gap:.35rem;background:var(--bg);} /* overflow-anchor:none so prepending older history doesn't fight our manual scroll anchor */ .bubble{max-width:72%;padding:.5rem .75rem;border-radius:14px;font-size:.9rem;line-height:1.4;white-space:pre-wrap;word-break:break-word;box-shadow:0 1px 2px rgba(20,30,60,.06);} .bubble.them{align-self:flex-start;background:#fff;border:1px solid var(--line);color:var(--ink);border-bottom-left-radius:4px;} .bubble.mine{align-self:flex-end;background:var(--blue);color:#fff;border-bottom-right-radius:4px;} @@ -319,16 +320,16 @@ .bubble .msg-list li{margin:.05rem 0;} .bubble code{background:rgba(0,0,0,.08);padding:.05rem .3rem;border-radius:5px;font-size:.86em;font-family:ui-monospace,Consolas,monospace;} .bubble.mine code{background:rgba(255,255,255,.2);} - .lightbox{position:fixed;inset:0;z-index:6000;background:rgba(8,12,22,.88);display:flex;align-items:center;justify-content:center;} + .lightbox{position:fixed;inset:0;z-index:9900;background:rgba(8,12,22,.88);display:flex;align-items:center;justify-content:center;} /* above modals (9800) so the media→image preview opens on top */ .lightbox img{max-width:92vw;max-height:88vh;border-radius:10px;box-shadow:0 16px 50px rgba(0,0,0,.5);} .lightbox .lb-close,.lightbox .lb-dl{position:absolute;top:18px;border:none;background:rgba(255,255,255,.14);color:#fff;width:44px;height:44px;border-radius:50%;display:grid;place-items:center;cursor:pointer;text-decoration:none;} .lightbox .lb-close{right:18px;} .lightbox .lb-dl{right:74px;} .lightbox .lb-close:hover,.lightbox .lb-dl:hover{background:rgba(255,255,255,.28);} - .lightbox .lb-nav{position:absolute;top:50%;transform:translateY(-50%);border:none;background:rgba(255,255,255,.14);color:#fff;width:50px;height:50px;border-radius:50%;display:grid;place-items:center;cursor:pointer;} - .lightbox .lb-nav:hover{background:rgba(255,255,255,.3);} - .lightbox .lb-prev{left:18px;} .lightbox .lb-next{right:18px;} - @media(max-width:560px){ .lightbox .lb-nav{width:42px;height:42px;} .lightbox .lb-prev{left:6px;} .lightbox .lb-next{right:6px;} } + .lightbox img{max-width:82vw;} + .lightbox .lb-nav{position:absolute;top:50%;transform:translateY(-50%);border:none;background:rgba(255,255,255,.16);color:#fff;width:32px;height:32px;border-radius:50%;display:grid;place-items:center;cursor:pointer;} + .lightbox .lb-nav:hover{background:rgba(255,255,255,.34);} + .lightbox .lb-prev{left:14px;} .lightbox .lb-next{right:14px;} .composer{display:flex;align-items:flex-end;gap:.5rem;padding:.6rem .8rem;border-top:1px solid var(--line);background:var(--card);} .composer-box{flex:1;min-width:0;border:1.5px solid var(--line);border-radius:16px;background:#fbfcfe;display:flex;flex-direction:column;overflow:hidden;} .composer-box:focus-within{border-color:var(--blue);} @@ -440,6 +441,9 @@ .convo{position:relative;} .bubble{position:relative;} .bubble .quote{border-left:3px solid var(--line);padding:.22rem .5rem;margin-bottom:.3rem;font-size:.78rem;border-radius:6px;color:#33384a;cursor:pointer;} + .bubble .fwd-label{display:flex;align-items:center;gap:.2rem;font-size:.72rem;font-style:italic;opacity:.7;margin-bottom:.2rem;} + .bubble .fwd-label svg{transform:scaleX(-1) rotate(0deg);} + .bubble.mine .fwd-label{color:#e6edfb;} .bubble.them .fwd-label{color:var(--muted);} /* All message actions live in ONE hover pill anchored to the bubble's top-right corner. It overlaps the bubble slightly so there's no dead gap — on a short message the icons can't float off into empty space where the hover (and the buttons) would vanish before you can click. */ @@ -449,18 +453,27 @@ .msg-actions button:hover{background:var(--blue-soft);} .msg-actions .del-btn{color:var(--red);} .msg-actions .del-btn:hover{background:#fee2e2;} /* #1 Forward: message selection mode + target picker */ - .bubble.selected{outline:2px solid var(--blue);outline-offset:2px;} + /* Selection mode: a checkbox appears on every message; selected ones highlight in green (distinct + from the blue message bubbles) with a filled tick. */ body.sel-mode .bubble{cursor:pointer;} body.sel-mode .msg-actions{display:none!important;} + body.sel-mode .bubble::after{content:"";position:absolute;top:-7px;width:20px;height:20px;border-radius:50%;border:2px solid var(--muted);background:var(--card);z-index:7;} + body.sel-mode .bubble.them::after{left:-8px;} body.sel-mode .bubble.mine::after{right:-8px;} + body.sel-mode .bubble.selected::after{border-color:#16a34a;background:#16a34a url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E") center/12px no-repeat;} + .bubble.selected{outline:2px solid #16a34a;outline-offset:2px;} + body.sel-mode .bubble.selected{background:#eafaf0;} + body.sel-mode .bubble.mine.selected{background:#16a34a;} .sel-bar{position:fixed;left:50%;bottom:18px;transform:translateX(-50%);z-index:9000;display:flex;align-items:center;gap:.7rem;background:var(--card);border:1px solid var(--line);border-radius:999px;padding:.4rem .5rem .4rem .4rem;box-shadow:0 10px 30px rgba(20,30,60,.25);} .sel-bar .sb-x{border:none;background:var(--blue-soft);color:var(--blue);width:34px;height:34px;border-radius:50%;display:grid;place-items:center;cursor:pointer;} .sel-bar .sb-n{font-size:.9rem;font-weight:600;color:var(--ink);} .sel-bar .sb-fwd{border:none;background:var(--blue);color:#fff;border-radius:999px;padding:.5rem .9rem;font-weight:700;cursor:pointer;display:inline-flex;align-items:center;gap:.3rem;} .sel-bar .sb-fwd:hover{background:var(--blue-d);} .fwd-list{max-height:44vh;overflow-y:auto;display:flex;flex-direction:column;gap:2px;} - .fwd-item{display:flex;align-items:center;gap:.6rem;padding:.4rem .5rem;border-radius:10px;cursor:pointer;} - .fwd-item:hover,.fwd-item.sel{background:var(--blue-soft);} + .fwd-item{display:flex;align-items:center;gap:.6rem;padding:.45rem .5rem;border-radius:10px;cursor:pointer;border:1px solid transparent;} + .fwd-item:hover{background:#f2f6fc;} + .fwd-item.sel{background:var(--blue-soft);border-color:var(--blue);} .fwd-item .fi-name{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:.92rem;} - .fwd-item .fi-check{opacity:0;color:var(--blue);} .fwd-item.sel .fi-check{opacity:1;} + .fwd-item .fi-check{width:20px;height:20px;flex:0 0 auto;border:2px solid var(--line);border-radius:6px;display:grid;place-items:center;color:transparent;} + .fwd-item.sel .fi-check{background:var(--blue);border-color:var(--blue);color:#fff;} .fwd-item .mini-av{width:34px;height:34px;flex:0 0 auto;border-radius:50%;display:grid;place-items:center;color:#fff;font-weight:700;font-size:.8rem;overflow:hidden;} .fwd-item .mini-av img{width:100%;height:100%;object-fit:cover;} .reply-bar{display:flex;align-items:center;gap:.5rem;padding:.45rem .8rem;border-top:1px solid var(--line);background:#eef3fb;font-size:.82rem;color:var(--muted);} @@ -527,14 +540,15 @@ .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;} - .bz-confirm .bzc-actions{display:flex;justify-content:flex-end;gap:.5rem;} - .bz-confirm .bzc-actions button{border:none;border-radius:10px;padding:.55rem 1rem;font-size:.9rem;font-weight:600;cursor:pointer;font-family:inherit;} - .bz-confirm .bzc-cancel{background:var(--blue-soft);color:var(--blue);} - .bz-confirm .bzc-cancel:hover{background:#dbe6fa;} - .bz-confirm .bzc-ok{background:var(--blue);color:#fff;} - .bz-confirm .bzc-ok:hover{background:var(--blue-d);} - .bz-confirm .bzc-ok.danger{background:var(--red);} - .bz-confirm .bzc-ok.danger:hover{background:#991b1b;} + .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);} + .bzc-cancel:hover{background:#dbe6fa;} + .bzc-ok{background:var(--blue);color:#fff;} + .bzc-ok:hover{background:var(--blue-d);} + .bzc-ok:disabled{opacity:.5;cursor:not-allowed;} + .bzc-ok.danger{background:var(--red);} + .bzc-ok.danger:hover{background:#991b1b;} /* "typing…" indicator inside the conversation, just above the composer. */ .typing-row{padding:.1rem .95rem .3rem;min-height:0;color:var(--blue);font-size:.78rem;font-style:italic;font-weight:600;display:none;align-items:center;gap:.4rem;} .typing-row.show{display:flex;} @@ -807,9 +821,9 @@
- + - @@ -1034,7 +1048,7 @@ async function reloadThread(){ if(!selected) return; const kind=selected.kind, i // ---- Older-history pagination: the thread loads the newest ~500; scrolling to the top fetches the // previous page (messages older than the oldest one loaded) and prepends them, preserving scroll. ---- const PAGE=500; -let _hasMoreOlder=false, _loadingOlder=false; +let _hasMoreOlder=false, _loadingOlder=false, _olderCd=0; function threadUrl(kind,id,before){ return (kind==='group'?'/api/messages/thread?group='+encodeURIComponent(id):'/api/messages/thread?with='+encodeURIComponent(id))+(before?('&before='+before):''); } async function loadOlder(){ if(_loadingOlder || !_hasMoreOlder || !selected || !THREAD.length) return; @@ -1050,7 +1064,7 @@ async function loadOlder(){ renderThread(true); // keep scroll (don't jump to bottom) if(box) box.scrollTop = box.scrollHeight - prevH + prevTop; // anchor on the same message } else { _hasMoreOlder=false; } - _loadingOlder=false; + _loadingOlder=false; _olderCd=Date.now()+500; // brief cooldown so it can't re-fire and fight the scroll } // Keep loading older pages until a target timestamp is in the loaded window (used by search jump-to). async function ensureLoadedBack(at){ let guard=0; while(_hasMoreOlder && THREAD.length && THREAD[0].created_at > at && guard<60){ await loadOlder(); guard++; } } @@ -1465,7 +1479,7 @@ function bubbleHTML(m){ } let seen=''; return '