Moderation follow-ups: block on contact profile, z-index, account suspend, reporter notify
Addresses tester feedback on the Report/Block feature: 1. Block/Unblock is now on the CONTACT's profile card (openMiniProfile), not only in a message's ⋮/long-press menu — you can block someone straight from their info popup. 2. Admin Delete/confirm popups were appearing BEHIND the Reports window. The moderation modals used z-index 100000 (above bzConfirm's .modal-ov at 9800); lowered them to 9750 so the confirm dialog sits on top. 3. Clarified admin action. "Block" is a PERSONAL mute (per guideline 1.2) and does not touch login. The report view now offers a real account action instead: "Suspend account" (deactivate -> signed out + cannot log in) with a confirm, toggling to "Reactivate account" — both reversible in-place, driven by a new reportedActive flag on /api/reports. (Uses the existing /api/users/manage deactivate/activate.) 4. Resolving a report now notifies the reporter (live 'report-resolved' event + background push), and admins get a 'report-new' activity entry. repos: reports.byId. Verified: moderation suite 18/18 (adds reportedActive + suspend->login-blocked->reactivate->login-restored). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+15
-7
@@ -929,6 +929,9 @@
|
||||
.mini-profile .mp-id span{font-size:.74rem;color:var(--muted);}
|
||||
.mini-profile .mp-acts{display:flex;gap:.4rem;}
|
||||
.mini-profile .mp-msg{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:.35rem;border:none;border-radius:9px;background:var(--blue);color:#fff;font:inherit;font-size:.84rem;font-weight:600;padding:.5rem;cursor:pointer;}
|
||||
.mini-profile .mp-block{width:100%;margin-top:.4rem;display:inline-flex;align-items:center;justify-content:center;gap:.35rem;border:1px solid var(--line);border-radius:9px;background:#fff;color:#b91c1c;font:inherit;font-size:.82rem;font-weight:600;padding:.45rem;cursor:pointer;}
|
||||
.mini-profile .mp-block.unblock{color:var(--blue);}
|
||||
.mini-profile .mp-block:hover{background:#fef2f2;}
|
||||
.mini-profile .mp-msg:hover{filter:brightness(1.08);}
|
||||
.mini-profile .mp-dp{flex:0 0 auto;border:1px solid var(--line);background:var(--card);color:var(--blue);border-radius:9px;width:38px;display:grid;place-items:center;cursor:pointer;}
|
||||
.spk-menu .mm-opt{display:flex;align-items:center;gap:.55rem;}
|
||||
@@ -2296,7 +2299,8 @@ function openMiniProfile(uid, anchor){
|
||||
+'<div class="mp-acts">'
|
||||
+'<button class="mp-msg" id="mpMsg">'+ic('chat',15)+' Message</button>'
|
||||
+(row.avatar?'<button class="mp-dp" id="mpDp" title="View photo">'+ic('image',15)+'</button>':'')
|
||||
+'</div>';
|
||||
+'</div>'
|
||||
+'<button class="mp-block'+(BLOCKED.has(uid)?' unblock':'')+'" id="mpBlock">'+ic('ban',14)+' '+(BLOCKED.has(uid)?'Unblock':'Block')+'</button>';
|
||||
document.body.appendChild(card);
|
||||
const r=anchor.getBoundingClientRect();
|
||||
card.style.left=Math.max(8, Math.min(r.left, window.innerWidth-card.offsetWidth-8))+'px';
|
||||
@@ -2304,6 +2308,7 @@ function openMiniProfile(uid, anchor){
|
||||
card.style.top=((below+card.offsetHeight<window.innerHeight-8)?below:Math.max(8,above))+'px';
|
||||
card.querySelector('#mpMsg').onclick=()=>{ card.remove(); switchTab('chat'); selectChat('dm', uid); };
|
||||
const dp=card.querySelector('#mpDp'); if(dp) dp.onclick=()=>{ card.remove(); openLightbox(row.avatar); };
|
||||
const mb=card.querySelector('#mpBlock'); if(mb) mb.onclick=()=>{ card.remove(); toggleBlock(uid, name); };
|
||||
const close=(e)=>{ if(!card.contains(e.target)){ card.remove(); document.removeEventListener('mousedown',close); } };
|
||||
setTimeout(()=>document.addEventListener('mousedown',close),0);
|
||||
}
|
||||
@@ -2339,7 +2344,7 @@ function reportMessage(m){
|
||||
document.querySelectorAll('.bz-modal-ov').forEach(x=>x.remove());
|
||||
const reasons=['Spam','Harassment or bullying','Inappropriate or offensive','Other'];
|
||||
const ov=document.createElement('div'); ov.className='bz-modal-ov';
|
||||
ov.style.cssText='position:fixed;inset:0;z-index:100000;background:rgba(16,26,53,.45);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;padding:20px';
|
||||
ov.style.cssText='position:fixed;inset:0;z-index:9750;background:rgba(16,26,53,.45);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;padding:20px';
|
||||
ov.innerHTML='<div style="background:#fff;border-radius:16px;max-width:360px;width:100%;padding:18px;box-shadow:0 20px 60px rgba(0,0,0,.3)">'
|
||||
+'<h3 style="margin:0 0 4px;font-size:1.05rem;display:flex;align-items:center;gap:.4rem;color:#1f2430">'+ic('flag',18)+'Report message</h3>'
|
||||
+'<p style="margin:0 0 12px;color:#6b7280;font-size:.85rem">This message is sent to your workspace admins for review.</p>'
|
||||
@@ -2374,7 +2379,7 @@ async function openBlockedManager(){
|
||||
let list=[]; try{ const b=await fetch('/api/users/blocked').then(r=>r.json()); if(b&&Array.isArray(b.users)) list=b.users; if(b&&Array.isArray(b.ids)) BLOCKED=new Set(b.ids); }catch(_){}
|
||||
document.querySelectorAll('.bz-modal-ov').forEach(x=>x.remove());
|
||||
const ov=document.createElement('div'); ov.className='bz-modal-ov';
|
||||
ov.style.cssText='position:fixed;inset:0;z-index:100000;background:rgba(16,26,53,.45);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;padding:20px';
|
||||
ov.style.cssText='position:fixed;inset:0;z-index:9750;background:rgba(16,26,53,.45);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;padding:20px';
|
||||
const rows=list.length?list.map(u=>'<div style="display:flex;align-items:center;gap:.6rem;padding:.5rem .1rem;border-bottom:1px solid #f0f2f6"><span style="flex:1;font-size:.9rem;color:#1f2430">'+pEsc(u.name||'Unknown')+'</span><button data-u="'+pEsc(u.id)+'" style="padding:.35rem .7rem;border:1px solid #e6e9ef;background:#fff;border-radius:8px;font-size:.82rem;cursor:pointer;color:#1F3B73">Unblock</button></div>').join(''):'<p style="color:#6b7280;font-size:.88rem;text-align:center;padding:1.2rem 0">You haven’t blocked anyone.</p>';
|
||||
ov.innerHTML='<div style="background:#fff;border-radius:16px;max-width:380px;width:100%;padding:18px;box-shadow:0 20px 60px rgba(0,0,0,.3)">'
|
||||
+'<h3 style="margin:0 0 10px;font-size:1.05rem;display:flex;align-items:center;gap:.4rem;color:#1f2430">'+ic('ban',18)+'Blocked users</h3>'
|
||||
@@ -2393,14 +2398,16 @@ async function openReportsAdmin(){
|
||||
let list=[]; try{ list=await fetch('/api/reports').then(r=>r.json()); }catch(_){} if(!Array.isArray(list)) list=[];
|
||||
document.querySelectorAll('.bz-modal-ov').forEach(x=>x.remove());
|
||||
const ov=document.createElement('div'); ov.className='bz-modal-ov';
|
||||
ov.style.cssText='position:fixed;inset:0;z-index:100000;background:rgba(16,26,53,.45);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;padding:20px';
|
||||
ov.style.cssText='position:fixed;inset:0;z-index:9750;background:rgba(16,26,53,.45);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;padding:20px';
|
||||
const row=(r)=>'<div data-id="'+pEsc(r.id)+'" style="padding:.6rem 0;border-bottom:1px solid #f0f2f6">'
|
||||
+'<div style="font-size:.9rem;color:#1f2430"><b>'+pEsc(r.reported)+'</b> <span style="color:#6b7280">reported by</span> '+pEsc(r.reporter)+(r.status==='resolved'?' <span style="color:#16a34a;font-size:.78rem">· resolved</span>':'')+'</div>'
|
||||
+(r.reason?'<div style="font-size:.82rem;color:#b45309;margin-top:.15rem">'+pEsc(r.reason)+'</div>':'')
|
||||
+(r.snippet?'<div style="font-size:.82rem;color:#6b7280;font-style:italic;margin-top:.2rem">“'+pEsc(r.snippet)+'”</div>':'')
|
||||
+'<div style="display:flex;gap:.4rem;flex-wrap:wrap;margin-top:.45rem">'
|
||||
+'<button data-del="'+pEsc(r.messageId)+'" style="padding:.3rem .6rem;border:1px solid #fca5a5;background:#fff;color:#b91c1c;border-radius:8px;font-size:.78rem;cursor:pointer">Delete message</button>'
|
||||
+'<button data-block="'+pEsc(r.reportedId)+'" data-name="'+pEsc(r.reported)+'" style="padding:.3rem .6rem;border:1px solid #e6e9ef;background:#fff;color:#1f2430;border-radius:8px;font-size:.78rem;cursor:pointer">Block user</button>'
|
||||
+(r.reportedActive
|
||||
? '<button data-suspend="'+pEsc(r.reportedId)+'" data-name="'+pEsc(r.reported)+'" style="padding:.3rem .6rem;border:1px solid #fca5a5;background:#fff;color:#b91c1c;border-radius:8px;font-size:.78rem;cursor:pointer">Suspend account</button>'
|
||||
: '<button data-reactivate="'+pEsc(r.reportedId)+'" data-name="'+pEsc(r.reported)+'" style="padding:.3rem .6rem;border:1px solid #bfdbfe;background:#fff;color:#1F3B73;border-radius:8px;font-size:.78rem;cursor:pointer">Reactivate account</button>')
|
||||
+(r.status!=='resolved'?'<button data-resolve="'+pEsc(r.id)+'" style="padding:.3rem .6rem;border:1px solid #e6e9ef;background:#fff;color:#1F3B73;border-radius:8px;font-size:.78rem;cursor:pointer">Mark resolved</button>':'')
|
||||
+'</div></div>';
|
||||
const bodyHTML=list.length?list.map(row).join(''):'<p style="color:#6b7280;font-size:.88rem;text-align:center;padding:1.2rem 0">No reports.</p>';
|
||||
@@ -2413,7 +2420,8 @@ async function openReportsAdmin(){
|
||||
ov.addEventListener('click', async (e)=>{
|
||||
if(e.target===ov||e.target.closest('[data-x]')){ ov.remove(); return; }
|
||||
const del=e.target.closest('[data-del]'); if(del){ if(await bzConfirm('Delete this message for everyone in the chat?', {title:'Delete message?', okText:'Delete', danger:true})){ try{ await postJSON('/api/messages/delete',{ id:del.dataset.del }); try{ markMsgDeleted(del.dataset.del); }catch(_){} toast('Message deleted'); del.textContent='Deleted'; del.disabled=true; }catch(err){ toast((err&&err.message)||'Could not delete'); } } return; }
|
||||
const bl=e.target.closest('[data-block]'); if(bl){ await toggleBlock(bl.dataset.block, bl.dataset.name); return; }
|
||||
const sp=e.target.closest('[data-suspend]'); if(sp){ if(await bzConfirm('Suspend '+sp.dataset.name+'? They will be signed out and can’t log in until an admin reactivates them.', {title:'Suspend account?', okText:'Suspend', danger:true})){ try{ await postJSON('/api/users/manage',{ id:sp.dataset.suspend, action:'deactivate' }); toast('Account suspended'); ov.remove(); openReportsAdmin(); }catch(err){ toast((err&&err.message)||'Could not suspend'); } } return; }
|
||||
const ra=e.target.closest('[data-reactivate]'); if(ra){ try{ await postJSON('/api/users/manage',{ id:ra.dataset.reactivate, action:'activate' }); toast('Account reactivated'); ov.remove(); openReportsAdmin(); }catch(err){ toast((err&&err.message)||'Could not reactivate'); } return; }
|
||||
const rs=e.target.closest('[data-resolve]'); if(rs){ try{ await postJSON('/api/reports/resolve',{ id:rs.dataset.resolve, status:'resolved' }); const row=rs.closest('[data-id]'); if(row) row.style.opacity=.55; rs.remove(); toast('Marked resolved'); }catch(err){ toast('Could not update'); } return; }
|
||||
});
|
||||
}
|
||||
@@ -4746,7 +4754,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); };
|
||||
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.'}); else if(d.type==='report-new') addNotif({icon:'flag', text:'A message was reported — open “Reported messages” to review.'}); };
|
||||
chatWs.onclose=()=>{ if(_chatReconnectT) clearTimeout(_chatReconnectT); _chatReconnectT=setTimeout(connectChatWs, 3000); }; // auto-reconnect (single pending timer)
|
||||
}catch(_){}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user