#14: after editing a message the edited text reappeared in the composer as a draft and re-sent as a new message. The input listener saved a draft while editing, but saveEdit/cancelEdit cleared only the input value, not the stored draft — so openConvo restored it. Now editing never writes a draft, and save/cancel clear it. #8: a contact's subtitle flapped between "last seen …" and "Offline". A loadSidebar rebuild replaced the row with the server's lastSeen, which is sometimes null (the disconnect touchSeen is fire-and-forget and can lag the presence broadcast). Now the client keeps last-seen sticky (never overwrites a known value with null) and the server never broadcasts a null last-seen for an offline user. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1604,6 +1604,7 @@ function openStorage(){
|
||||
let ME={};
|
||||
let CONTACTS=[]; // team users (for new DMs / picking group members)
|
||||
let ROWS=[]; // sidebar items: {kind:'dm'|'group', id, name, online?, members?, last_body, last_at, last_from_me, unread}
|
||||
const _lastSeen={}; // #8: userId -> last-seen ts, kept STICKY so a rebuild/presence event that lacks it can't flap the label back to "Offline"
|
||||
let selected=null; // {kind,id} or null = welcome
|
||||
let convoIsGroup=false; // the open thread is a group (drives per-message sender labels)
|
||||
let THREAD=[];
|
||||
@@ -2032,6 +2033,10 @@ async function loadSidebar(){
|
||||
const selfRow=items.find(i=>i.kind==='dm' && i.id===(ME&&ME.id));
|
||||
if(selfRow){ selfRow.self=true; selfRow.name='You'; selfRow.avatar=ME.avatarUrl||selfRow.avatar||null; }
|
||||
else if(ME&&ME.id){ items.push({ kind:'dm', id:ME.id, name:'You', self:true, avatar:ME.avatarUrl||null, online:false, last_body:'', last_at:0, last_from_me:false, unread:0 }); }
|
||||
// #8: keep last-seen sticky. The server occasionally returns null (a presence broadcast can race ahead of
|
||||
// the last_seen DB write), which used to flip the subtitle from "last seen …" to "Offline". Remember any
|
||||
// known value and backfill nulls from it so the label stays stable.
|
||||
items.forEach(it=>{ if(it.kind==='dm'&&!it.self){ if(it.lastSeen) _lastSeen[it.id]=it.lastSeen; else if(_lastSeen[it.id]) it.lastSeen=_lastSeen[it.id]; } });
|
||||
ROWS=items;
|
||||
renderChats(searchVal());
|
||||
updateRailUnread();
|
||||
@@ -2291,13 +2296,14 @@ function startEdit(m){
|
||||
const bar=document.getElementById('replyBar'); if(bar){ bar.innerHTML='<span>'+ic('edit',13)+' Editing message</span><span class="rx" id="editCancel">'+ic('x',15)+'</span>'; 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); } }
|
||||
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); } try{ if(selected) setDraft(selected.kind, selected.id, ''); }catch(_){} } // #14: clear the draft too
|
||||
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{ if(selected) setDraft(selected.kind, selected.id, ''); }catch(_){} // #14: clear the draft, else the edited text reappears and re-sends as a NEW message
|
||||
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); }
|
||||
@@ -2636,7 +2642,7 @@ async function startOrJoinDmCall(otherId){
|
||||
function onPresence(d){
|
||||
if(!d||!d.userId) return;
|
||||
const it=rowFor('dm', d.userId);
|
||||
if(it){ it.online=!!d.online; it.status=d.status||'active'; if(d.lastSeen) it.lastSeen=d.lastSeen; renderChats(searchVal()); } // #2: keep last-seen fresh
|
||||
if(it){ it.online=!!d.online; it.status=d.status||'active'; if(d.lastSeen){ it.lastSeen=d.lastSeen; _lastSeen[d.userId]=d.lastSeen; } renderChats(searchVal()); } // #2/#8: keep last-seen fresh + sticky
|
||||
if(selected && selected.kind==='dm' && selected.id===d.userId && it){
|
||||
const sub=document.querySelector('#convoTitle .st'); if(sub) sub.textContent=dmSubLabel(it);
|
||||
const dot=document.querySelector('.convo-head .avatar .dot'); if(dot) dot.className='dot '+statusCls(it);
|
||||
@@ -3296,7 +3302,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', ()=>{ maybeAutocorrect(inpEl); autoGrow(inpEl); setDraft(kind,id,inpEl.value); noteTyping(); }); // auto-correct common typos, keep the draft in sync + emit "typing…"
|
||||
inpEl.addEventListener('input', ()=>{ maybeAutocorrect(inpEl); autoGrow(inpEl); if(!editTarget) setDraft(kind,id,inpEl.value); noteTyping(); }); // auto-correct common typos, keep the draft in sync (#14: not while editing) + emit "typing…"
|
||||
inpEl.addEventListener('blur', ()=>{ stopTyping(); setTimeout(()=>{ const a=document.activeElement; const stillTyping=a&&(a.tagName==='TEXTAREA'||a.tagName==='INPUT'); if(!stillTyping && window.__bzResetKb) window.__bzResetKb(); }, 120); }); // #1: composer lost focus → keyboard is closing → drop the --kb lift so no empty gap remains
|
||||
inpEl.addEventListener('keydown', (e)=>{ if(e.key==='Enter' && !e.shiftKey && !isMobileUA()){ if(mentionItems && mentionItems.length) return; e.preventDefault(); sendMessage(); } }); // Desktop: Enter sends, Shift+Enter = newline. Mobile: the Return key inserts a newline (default) — you send with the send button, like every native chat app.
|
||||
// Desktop: LEFT-click a red-squiggled word → native spelling suggestions right there (the shell
|
||||
|
||||
Reference in New Issue
Block a user