#10: deleting the last message showed "No messages yet" in the chat list. The sidebar sent an empty last_body for a deleted (content-cleared) row; now it sends a last_deleted flag and the row renders "This message was deleted" (or "You deleted this message"), matching the in-thread placeholder. #18: added "Delete for me" alongside "Delete for everyone". A new message_hidden table records a per-user hide; the thread + sidebar (last message, unread) filter out the requesting user's hidden messages, and the hide is echoed to their other devices (chat-hidden). "Delete for me" is offered on any message; "Delete for everyone" stays sender-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+15
-2
@@ -210,6 +210,7 @@
|
||||
.chat-row.unread .chat-prev{color:var(--ink);font-weight:500;}
|
||||
.prev-tick{display:inline-flex;vertical-align:middle;color:var(--muted);} .prev-tick.read{color:#2563eb;} .prev-tick svg{width:14px;height:14px;}
|
||||
.draft-lbl{color:#dc2626;font-weight:600;}
|
||||
.prev-del{display:inline-flex;align-items:center;gap:.3rem;color:var(--muted);font-style:italic;opacity:.85;}
|
||||
.chat-row.unread .chat-name{font-weight:700;}
|
||||
.badge{flex:0 0 auto;background:var(--brand);color:var(--blue);font-size:.7rem;font-weight:800;min-width:19px;height:19px;border-radius:99px;padding:0 .35rem;display:grid;place-items:center;}
|
||||
.no-results{padding:2rem 1rem;text-align:center;color:var(--muted);font-size:.85rem;}
|
||||
@@ -1914,6 +1915,7 @@ function rowPreviewHTML(it){
|
||||
const draft=getDraft(it.kind,it.id);
|
||||
if(draft && !active){ return '<span class="draft-lbl">Draft:</span> '+pEsc(draft); } // unsent draft (#1)
|
||||
if(it.callActive){ return '<span class="call-on">'+ic('phone',12)+' Ongoing call</span>'; }
|
||||
if(it.last_deleted){ return '<span class="prev-del">'+ic('trash',12)+' '+(it.last_from_me?'You deleted this message':'This message was deleted')+'</span>'; } // #10
|
||||
const prevBody=isGifUrl(it.last_body)?'🎞️ GIF':it.last_body; // #5: don't show the raw GIF URL
|
||||
if(it.last_from_me && it.last_status && it.last_body){ // my last message → tick (#3)
|
||||
return '<span class="prev-tick '+it.last_status+'">'+ic(it.last_status==='sent'?'check':'checkCheck',13)+'</span> '+pEsc(prevBody); }
|
||||
@@ -2234,7 +2236,8 @@ function openMsgMore(msgId, anchor){
|
||||
items.push({ic:'reply', label:'Reply', fn:()=>setReply(m)});
|
||||
if((m.body||m.attachment) && !m.poll) items.push({ic:'arrowRight', label:'Forward', fn:()=>enterSelect(m.id)});
|
||||
if(m.body) items.push({ic:'copy', label:'Copy', fn:()=>copyMessageText(m)});
|
||||
if(mine) items.push({ic:'trash', label:'Delete', danger:true, fn:()=>deleteMessage(m.id)});
|
||||
items.push({ic:'trash', label:'Delete for me', danger:true, fn:()=>deleteForMe(m.id)}); // #18: hide from my view only
|
||||
if(mine && !m.deleted) items.push({ic:'trash', label:'Delete for everyone', danger:true, fn:()=>deleteMessage(m.id)}); // #18: unsend for all
|
||||
const menu=document.createElement('div'); menu.className='spk-menu msg-more-menu';
|
||||
menu.innerHTML=items.map((it,i)=>'<button class="spk-opt mm-opt'+(it.danger?' danger':'')+'" data-i="'+i+'">'+ic(it.ic,15)+'<span>'+pEsc(it.label)+'</span></button>').join('');
|
||||
document.body.appendChild(menu);
|
||||
@@ -2573,6 +2576,16 @@ function markMsgDeleted(id){
|
||||
if(changed) renderThread();
|
||||
}
|
||||
function onChatDeleted(d){ if(!d||!d.id) return; markMsgDeleted(d.id); try{ loadSidebar(); }catch(_){} } // refresh last-message previews
|
||||
// #18 Delete-for-me: hide only from MY view (echoed to all my devices) — the message stays for everyone else.
|
||||
async function deleteForMe(id){ if(!(await bzConfirm('This will remove the message from your chat on all your devices. Others will still see it.', {title:'Delete for me?', okText:'Delete', danger:true}))) return; try{ await postJSON('/api/messages/hide',{id}); removeMsgLocally(id); }catch(e){ toast(e.message||'Could not delete'); } }
|
||||
function removeMsgLocally(id){
|
||||
let changed=false;
|
||||
const i=THREAD.findIndex(x=>x.id===id); if(i>=0){ THREAD.splice(i,1); changed=true; }
|
||||
THREAD_CACHE.forEach(arr=>{ const j=arr.findIndex(x=>x.id===id); if(j>=0) arr.splice(j,1); });
|
||||
if(changed) renderThread();
|
||||
try{ loadSidebar(); }catch(_){} // last-message preview may roll back to the previous message
|
||||
}
|
||||
function onChatHidden(d){ if(!d||!d.id) return; removeMsgLocally(d.id); } // another of my devices hid it
|
||||
// DM delivered: recipient's client acked → second (grey) tick.
|
||||
function onChatDelivered(d){ if(!d||!d.id) return;
|
||||
const m=THREAD.find(x=>x.id===d.id); if(m && !m.delivered_at){ m.delivered_at=Date.now(); updateBubble(m); }
|
||||
@@ -4291,7 +4304,7 @@ function connectChatWs(){
|
||||
if(_chatReconnectT){ clearTimeout(_chatReconnectT); _chatReconnectT=null; }
|
||||
chatWs=new WebSocket((location.protocol==='https:'?'wss://':'ws://')+location.host+'/ws');
|
||||
chatWs.onopen=()=>{ try{ chatWs.send(JSON.stringify({type:'chat-hello'})); }catch(_){} if(_chatConnectedOnce) resyncChat(); _chatConnectedOnce=true; };
|
||||
chatWs.onmessage=(e)=>{ let d; try{ d=JSON.parse(e.data); }catch(_){ return; } if(d.type==='chat-message' && d.message) onChatMessage(d.message); else if(d.type==='chat-deleted') onChatDeleted(d); else if(d.type==='chat-edited') onChatEdited(d); else if(d.type==='chat-reaction') onChatReaction(d); else if(d.type==='poll-update' && d.poll) onPollUpdate(d); else if(d.type==='chat-read') onChatRead(d); else if(d.type==='chat-delivered') onChatDelivered(d); else if(d.type==='group-read') onGroupRead(d); else if(d.type==='group-call') onGroupCall(d); else if(d.type==='dm-call') onDmCall(d); else if(d.type==='call-taken') onCallTaken(d); else if(d.type==='call-answered') onCallAnswered(d); else if(d.type==='presence') onPresence(d); else if(d.type==='chat-typing') onTyping(d); else if(d.type==='notif-clear') onNotifClear(d); else if(d.type==='group-update') onGroupUpdate(d); else if(d.type==='call-invite') showCallInvite(d.room, d.byName); else if(d.type==='meeting-invite') showMeetingInvite(d.meeting); else if(d.type==='meeting-reminder') showMeetingReminder(d.meeting); else if(d.type==='meeting-cancelled') showMeetingCancelled(d.meeting); else if(d.type==='group-role') onGroupRole(d); };
|
||||
chatWs.onmessage=(e)=>{ let d; try{ d=JSON.parse(e.data); }catch(_){ return; } if(d.type==='chat-message' && d.message) onChatMessage(d.message); else if(d.type==='chat-deleted') onChatDeleted(d); else if(d.type==='chat-hidden') onChatHidden(d); else if(d.type==='chat-edited') onChatEdited(d); else if(d.type==='chat-reaction') onChatReaction(d); else if(d.type==='poll-update' && d.poll) onPollUpdate(d); else if(d.type==='chat-read') onChatRead(d); else if(d.type==='chat-delivered') onChatDelivered(d); else if(d.type==='group-read') onGroupRead(d); else if(d.type==='group-call') onGroupCall(d); else if(d.type==='dm-call') onDmCall(d); else if(d.type==='call-taken') onCallTaken(d); else if(d.type==='call-answered') onCallAnswered(d); else if(d.type==='presence') onPresence(d); else if(d.type==='chat-typing') onTyping(d); else if(d.type==='notif-clear') onNotifClear(d); else if(d.type==='group-update') onGroupUpdate(d); else if(d.type==='call-invite') showCallInvite(d.room, d.byName); else if(d.type==='meeting-invite') showMeetingInvite(d.meeting); else if(d.type==='meeting-reminder') showMeetingReminder(d.meeting); else if(d.type==='meeting-cancelled') showMeetingCancelled(d.meeting); else if(d.type==='group-role') onGroupRole(d); };
|
||||
chatWs.onclose=()=>{ if(_chatReconnectT) clearTimeout(_chatReconnectT); _chatReconnectT=setTimeout(connectChatWs, 3000); }; // auto-reconnect (single pending timer)
|
||||
}catch(_){}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user