diff --git a/server/public/home.html b/server/public/home.html index 5a8d440..416ecdf 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -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){ +'
This message is sent to your workspace admins for review.
' @@ -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=>'You haven’t blocked anyone.
'; ov.innerHTML='No reports.
'; @@ -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(_){} } diff --git a/server/repos.js b/server/repos.js index 8dee6f2..de52c8b 100644 --- a/server/repos.js +++ b/server/repos.js @@ -442,6 +442,7 @@ const reports = { add: ({ id, teamId, messageId, reporterId, reportedId, reason, snippet }) => db.prepare('INSERT INTO message_reports (id,team_id,message_id,reporter_id,reported_id,reason,snippet,created_at,status) VALUES (?,?,?,?,?,?,?,?,?)') .run(id, teamId, messageId, reporterId, reportedId, reason || null, snippet || null, now(), 'open'), + byId: (id) => db.prepare('SELECT * FROM message_reports WHERE id=?').get(id), listForTeam: (teamId, limit = 200) => db.prepare('SELECT * FROM message_reports WHERE team_id=? ORDER BY created_at DESC LIMIT ?').all(teamId, limit), setStatus: (id, status) => db.prepare('UPDATE message_reports SET status=? WHERE id=?').run(status, id), openCountForTeam: async (teamId) => (await db.prepare("SELECT COUNT(*) AS c FROM message_reports WHERE team_id=? AND status='open'").get(teamId)).c, diff --git a/server/routes.js b/server/routes.js index baf3cd6..00ed15c 100644 --- a/server/routes.js +++ b/server/routes.js @@ -1725,8 +1725,10 @@ route('GET', '/api/reports', async (req, res) => { if (!u) return json(res, 401, { error: 'unauthorized' }); if (u.role !== 'admin') return json(res, 403, { error: 'admins only' }); const names = await namesFor(u.team_id); + const roster = await R.users.listByTenant(u.team_id); + const activeById = {}; roster.forEach((x) => { activeById[x.id] = x.active !== 0; }); // is the reported user still able to log in? const rows = await R.reports.listForTeam(u.team_id); - json(res, 200, rows.map((r) => ({ id: r.id, messageId: r.message_id, reporter: names[r.reporter_id] || 'Unknown', reported: names[r.reported_id] || 'Unknown', reportedId: r.reported_id, reason: r.reason || '', snippet: r.snippet || '', at: r.created_at, status: r.status }))); + json(res, 200, rows.map((r) => ({ id: r.id, messageId: r.message_id, reporter: names[r.reporter_id] || 'Unknown', reported: names[r.reported_id] || 'Unknown', reportedId: r.reported_id, reportedActive: activeById[r.reported_id] !== false, reason: r.reason || '', snippet: r.snippet || '', at: r.created_at, status: r.status }))); }); route('POST', '/api/reports/resolve', async (req, res) => { @@ -1735,7 +1737,13 @@ route('POST', '/api/reports/resolve', async (req, res) => { if (u.role !== 'admin') return json(res, 403, { error: 'admins only' }); const { id, status } = await readBody(req); if (!id) return json(res, 400, { error: 'id required' }); + const rep = await R.reports.byId(id); await R.reports.setStatus(id, status === 'open' ? 'open' : 'resolved'); + // Close the loop: tell the reporter their report was reviewed (live + background push). + if (rep && status !== 'open' && rep.reporter_id && rep.reporter_id !== u.id) { + try { CHAT.pushToUser(rep.reporter_id, { type: 'report-resolved' }); } catch (_) {} + try { PUSH.sendToUser(rep.reporter_id, { title: 'Report reviewed', body: 'An admin reviewed the message you reported.' }); } catch (_) {} + } json(res, 200, { ok: true }); });