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:
2026-08-20 11:50:55 +05:30
parent 10b2251efe
commit 928119725e
3 changed files with 25 additions and 8 deletions
+9 -1
View File
@@ -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 });
});