Android CI build + delete-chat (self-only) + SFU copy refresh
- codemagic.yaml: add an Android workflow that builds an installable debug APK (Linux instance, no signing needed to test) — the Android equivalent of the iOS TestFlight loop. mobile/scripts/android-patch.js injects the camera/mic/notification permissions into the CI-generated manifest (android/ is gitignored, same as ios/). - Delete chat (self-only): new POST /api/messages/clear bulk-hides a whole DM (or group) for the requester via the existing per-user message_hidden mechanism — the other party keeps their copy entirely. "Delete chat" button added to the DM contact-info panel; chat-cleared synced to my other devices. Verified end-to-end on Postgres (A clears -> empty for A, B untouched). - Meetings copy: SFU is live, so drop the "coming soon" / "small group (mesh)" wording on the welcome card and the Meetings header. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+24
-3
@@ -1934,11 +1934,13 @@ async function openSharedItems(kind,id,name){
|
||||
ov.innerHTML='<div class="modal gi"><div class="gi-head" style="margin-bottom:.6rem"><button class="gi-photo" id="shPhoto" title="View photo"><span class="avatar" style="width:46px;height:46px;flex:0 0 46px;background:'+avColor(name)+'">'+pEsc(initials(name||'?'))+((r0&&r0.avatar)?'<img class="av-img" src="'+pEsc(r0.avatar)+'" alt="" onerror="this.remove()">':'')+'</span></button><div class="gi-name"><div class="gi-title-row"><span class="gi-title">'+pEsc(name||'')+'</span><button class="fav-star'+(fav?' on':'')+'" id="shFav" title="'+(fav?'Remove from favourites':'Add to favourites')+'">'+ic('star',16)+'</button></div><div class="gi-sub"><span class="st-dot '+statusCls(r0)+'"></span>'+statusLabel(r0)+'</div></div><button class="iconbtn" id="shClose">'+ic('x',18)+'</button></div>'
|
||||
+mediaRowHTML()
|
||||
+((kind==='dm' && id!==(ME&&ME.id))?'<button id="shBlock" style="width:100%;margin-top:.7rem;display:inline-flex;align-items:center;justify-content:center;gap:.4rem;border:1px solid '+(BLOCKED.has(id)?'#c7d6f0':'#f1c4c4')+';border-radius:10px;background:#fff;color:'+(BLOCKED.has(id)?'#1F3B73':'#b91c1c')+';font-size:.9rem;font-weight:600;padding:.6rem;cursor:pointer">'+ic('ban',16)+' '+(BLOCKED.has(id)?'Unblock contact':'Block contact')+'</button>':'')
|
||||
+((kind==='dm' && id!==(ME&&ME.id))?'<button id="shDelChat" style="width:100%;margin-top:.5rem;display:inline-flex;align-items:center;justify-content:center;gap:.4rem;border:1px solid #f1c4c4;border-radius:10px;background:#fff;color:#b91c1c;font-size:.9rem;font-weight:600;padding:.6rem;cursor:pointer">'+ic('trash',16)+' Delete chat</button>':'')
|
||||
+'</div>';
|
||||
document.body.appendChild(ov);
|
||||
ov.onclick=e=>{ if(e.target===ov) ov.remove(); };
|
||||
ov.querySelector('#shClose').onclick=()=>ov.remove();
|
||||
const bb=ov.querySelector('#shBlock'); if(bb) bb.onclick=()=>{ ov.remove(); toggleBlock(id, name); }; // block/unblock this contact from their info panel
|
||||
const dcb=ov.querySelector('#shDelChat'); if(dcb) dcb.onclick=()=>{ ov.remove(); deleteChat('dm', id); }; // delete this chat for me only
|
||||
const sp=ov.querySelector('#shPhoto'); if(sp) sp.onclick=()=>{ const r=rowFor(kind,id)||r0; if(r&&r.avatar) openLightbox(r.avatar); else toast('No profile photo'); }; // #5: click the DP in the info view to preview it
|
||||
const fb=ov.querySelector('#shFav'); if(fb) fb.onclick=async()=>{ const r=rowFor(kind,id); const on=!(r&&r.favorite); if(r) r.favorite=on; fb.classList.toggle('on',on); fb.title=on?'Remove from favourites':'Add to favourites'; try{ await postJSON('/api/favorites',{kind,id,on}); }catch(_){} renderChats(searchVal()); };
|
||||
wireMediaEntry(ov, kind, id, name);
|
||||
@@ -2141,7 +2143,7 @@ function welcomeHTML(){
|
||||
+ '<div class="wcards">'
|
||||
+ '<div class="wcard" data-go="share"><div class="wi"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg></div><h3>Share Screen</h3><p>Show your screen with a 6-digit code</p></div>'
|
||||
+ '<div class="wcard" data-go="connect"><div class="wi"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg></div><h3>Connect Screen</h3><p>Enter a customer\'s code to help</p></div>'
|
||||
+ '<div class="wcard" data-go="meeting"><div class="wi"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg></div><h3>Meeting</h3><p>Multi-party video — coming soon</p></div>'
|
||||
+ '<div class="wcard" data-go="meeting"><div class="wi"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg></div><h3>Meeting</h3><p>Start or join a video meeting</p></div>'
|
||||
+ '</div></div>';
|
||||
}
|
||||
function wireWelcome(){ document.querySelectorAll('#chatPanel .wcard').forEach(card=>{ card.onclick=()=>switchTab(card.dataset.go); }); }
|
||||
@@ -2833,6 +2835,25 @@ async function deleteForMe(id){ if(!(await bzConfirm('This will remove the messa
|
||||
// #18: the branded delete dialog IS the confirmation, so it calls these confirm-free cores directly.
|
||||
async function _doDeleteForMe(id){ try{ await postJSON('/api/messages/hide',{id}); removeMsgLocally(id); }catch(e){ toast(e.message||'Could not delete'); } }
|
||||
async function _doDeleteForEveryone(id){ try{ await postJSON('/api/messages/delete',{id}); markMsgDeleted(id); }catch(e){ toast(e.message||'Could not delete'); } }
|
||||
// "Delete chat" — SELF-ONLY. Hides the whole conversation from MY view (and my other devices); the other
|
||||
// person keeps their copy entirely (server bulk-hides via the same per-user mechanism as "Delete for me").
|
||||
async function deleteChat(kind, id){
|
||||
if(!(await bzConfirm('This removes the chat for you only. The other person keeps their copy.', {title:'Delete this chat?', okText:'Delete', danger:true}))) return;
|
||||
try{
|
||||
await postJSON('/api/messages/clear', kind==='group' ? { group:id } : { with:id });
|
||||
_dropConversation(kind, id);
|
||||
toast('Chat deleted');
|
||||
}catch(e){ toast(e.message||'Could not delete chat'); }
|
||||
}
|
||||
// A conversation cleared here or on another of my devices → drop it from the list + close it if it's open.
|
||||
function onChatCleared(d){ if(d&&d.id) _dropConversation(d.kind||'dm', d.id); }
|
||||
function _dropConversation(kind, id){
|
||||
ROWS = ROWS.filter(r=>!(r.kind===kind && r.id===id));
|
||||
try{ THREAD_CACHE.delete(kind+':'+id); }catch(_){}
|
||||
if(selected && selected.kind===kind && selected.id===id){ try{ showWelcome(); }catch(_){} }
|
||||
try{ renderChats(searchVal()); }catch(_){}
|
||||
try{ updateRailUnread(); }catch(_){}
|
||||
}
|
||||
// #18: one "Delete" entry opens a branded dialog with clear choices + a cancel (✕). "Delete for everyone"
|
||||
// only appears on your OWN, not-yet-deleted messages; "Delete for me" is always available (even to hide a
|
||||
// tombstone). Clicking the backdrop or ✕ cancels.
|
||||
@@ -4766,7 +4787,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-hidden') onChatHidden(d); else if(d.type==='chat-pinned') onChatPinned(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); else if(d.type==='report-resolved'){ addNotif({icon:'flag', text:'An admin reviewed a message you reported.'}); try{ playPing(); }catch(_){} try{ if(window.BZToast) BZToast.info('An admin reviewed a message you reported.'); else toast('An admin reviewed a message you reported.'); }catch(_){} } else if(d.type==='report-new'){ addNotif({icon:'flag', text:'A message was reported — open “Reported messages” to review.'}); try{ if(window.BZToast) BZToast.info('A message was reported — review it in Reported messages.'); }catch(_){} } };
|
||||
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-cleared') onChatCleared(d); else if(d.type==='chat-pinned') onChatPinned(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); else if(d.type==='report-resolved'){ addNotif({icon:'flag', text:'An admin reviewed a message you reported.'}); try{ playPing(); }catch(_){} try{ if(window.BZToast) BZToast.info('An admin reviewed a message you reported.'); else toast('An admin reviewed a message you reported.'); }catch(_){} } else if(d.type==='report-new'){ addNotif({icon:'flag', text:'A message was reported — open “Reported messages” to review.'}); try{ if(window.BZToast) BZToast.info('A message was reported — review it in Reported messages.'); }catch(_){} } };
|
||||
chatWs.onclose=()=>{ if(_chatReconnectT) clearTimeout(_chatReconnectT); _chatReconnectT=setTimeout(connectChatWs, 3000); }; // auto-reconnect (single pending timer)
|
||||
}catch(_){}
|
||||
}
|
||||
@@ -5110,7 +5131,7 @@ function renderMeetingLobby(){
|
||||
const el=document.getElementById('meetingPanel'); if(!el) return;
|
||||
el.innerHTML='<div class="meet-dash">'
|
||||
+ '<div class="md-top">'
|
||||
+ '<div class="md-title"><h1>Meetings</h1><p>Start or join a video meeting, or schedule one for later. Small group (mesh) for now — larger rooms coming with the SFU.</p></div>'
|
||||
+ '<div class="md-title"><h1>Meetings</h1><p>Start or join a video meeting, or schedule one for later.</p></div>'
|
||||
+ '<div class="md-actions">'
|
||||
+ '<div class="md-join"><input id="meetCode" placeholder="Enter code" inputmode="numeric" maxlength="6"><button class="btn primary" id="meetJoinBtn">Join</button></div>'
|
||||
+ '<button class="btn" id="meetStart">'+ic('video',16)+' Start a meeting</button>'
|
||||
|
||||
Reference in New Issue
Block a user