- Configurable tier-based Auto Play with a saved user toggle.

Admin tier assignment without payment via Users > Edit > Tier Override.
Fixed live group membership refresh, polling, unread indicators, and 
incoming voice clip population.
Added asset cache busting so clients receive the fixes immediately.
This commit is contained in:
Ty Clifford
2026-06-08 15:59:04 -04:00
parent 063b8dc3e8
commit d0e4a870be
9 changed files with 209 additions and 42 deletions
+30 -7
View File
@@ -350,12 +350,14 @@ if ($isLoggedIn && $_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username']);
$color = trim($_POST['color']);
$newpass = trim($_POST['new_password']);
$manualPlanId = max(0, (int)($_POST['manual_plan_id'] ?? 0));
$errors = [];
if ($username === '') $errors[] = 'Username required.';
if (!preg_match('/^[a-zA-Z0-9_\-]+$/', $username)) $errors[] = 'Username: letters, numbers, _ and - only.';
if (strlen($username) > (int)cfgVal('chat.max_username_length', 12)) $errors[] = 'Username too long.';
if (!preg_match('/^#[0-9a-fA-F]{6}$/', $color)) $errors[] = 'Invalid color hex.';
if ($manualPlanId > 0 && !planById($manualPlanId)) $errors[] = 'Invalid tier override.';
if ($errors) { flash(implode(' ', $errors), 'err'); redirect('tab=users'); }
@@ -366,7 +368,8 @@ if ($isLoggedIn && $_SERVER['REQUEST_METHOD'] === 'POST') {
flash('Username already taken.', 'err'); redirect('tab=users');
}
db()->prepare("UPDATE users SET username = ?, color = ? WHERE id = ?")->execute([$username, $color, $id]);
db()->prepare("UPDATE users SET username = ?, color = ?, manual_plan_id = ? WHERE id = ?")
->execute([$username, $color, $manualPlanId ?: null, $id]);
// Also update username in messages (denormalised)
db()->prepare("UPDATE messages SET username = ?, color = ? WHERE user_id = ?")->execute([$username, $color, $id]);
@@ -374,7 +377,7 @@ if ($isLoggedIn && $_SERVER['REQUEST_METHOD'] === 'POST') {
$hash = password_hash($newpass, PASSWORD_BCRYPT, ['cost' => (int)cfgVal('security.bcrypt_cost', 10)]);
db()->prepare("UPDATE users SET password_hash = ? WHERE id = ?")->execute([$hash, $id]);
}
flash("User #$id updated.");
flash("User #$id updated, including tier assignment.");
redirect('tab=users');
}
@@ -1502,10 +1505,13 @@ td.actions{white-space:nowrap;width:1px}
<?php
$allUsers = db()->query("
SELECT u.*,
SELECT u.*,p.name AS billing_plan_name,mp.name AS manual_plan_name,
(SELECT COUNT(*) FROM messages m WHERE m.user_id = u.id) as msg_count,
(SELECT COUNT(*) FROM sessions s WHERE s.user_id = u.id) as sess_count
FROM users u ORDER BY u.created_at DESC
FROM users u
LEFT JOIN plans p ON p.id=u.plan_id
LEFT JOIN plans mp ON mp.id=u.manual_plan_id
ORDER BY u.created_at DESC
")->fetchAll();
?>
@@ -1520,7 +1526,7 @@ td.actions{white-space:nowrap;width:1px}
<div class="tbl-wrap">
<table>
<thead><tr>
<th>ID</th><th>Username</th><th>Color</th><th>Messages</th>
<th>ID</th><th>Username</th><th>Tier</th><th>Color</th><th>Messages</th>
<th>Sessions</th><th>Last Seen</th><th>Joined</th><th>Actions</th>
</tr></thead>
<tbody>
@@ -1530,6 +1536,12 @@ td.actions{white-space:nowrap;width:1px}
<td style="color:<?= esc($u['color']) ?>;font-family:var(--mono);font-weight:600">
<?= esc($u['username']) ?>
</td>
<td>
<span class="badge <?= $u['manual_plan_id'] ? 'badge-o' : 'badge-b' ?>">
<?= esc($u['manual_plan_name'] ?: $u['billing_plan_name'] ?: 'Free') ?>
</span>
<div class="dim"><?= $u['manual_plan_id'] ? 'ADMIN' : 'BILLING' ?></div>
</td>
<td>
<div style="display:flex;align-items:center;gap:6px">
<span style="background:<?= esc($u['color']) ?>;width:14px;height:14px;border-radius:50%;display:inline-block;flex-shrink:0"></span>
@@ -1543,7 +1555,7 @@ td.actions{white-space:nowrap;width:1px}
<td class="actions">
<div class="btn-group">
<button class="btn btn-sm btn-g"
onclick="openEditUser(<?= $u['id'] ?>,'<?= esc(addslashes($u['username'])) ?>','<?= esc($u['color']) ?>')">
onclick='openEditUser(<?= json_encode((int)$u['id']) ?>,<?= json_encode($u['username']) ?>,<?= json_encode($u['color']) ?>,<?= json_encode((int)($u['manual_plan_id'] ?? 0)) ?>)'>
EDIT
</button>
<form method="post" style="display:inline">
@@ -1780,6 +1792,16 @@ td.actions{white-space:nowrap;width:1px}
<label>New Password <small style="color:var(--t2);text-transform:none">(leave blank to keep current)</small></label>
<input type="password" name="new_password" autocomplete="new-password" placeholder="leave blank to keep…">
</div>
<div class="field">
<label>Tier Override</label>
<select name="manual_plan_id" id="edit-user-plan">
<option value="0">Follow Stripe / billing status</option>
<?php foreach (plans(false) as $plan): ?>
<option value="<?= (int)$plan['id'] ?>"><?= esc($plan['name']) ?> (admin assigned)</option>
<?php endforeach; ?>
</select>
<div class="dim" style="margin-top:5px">An admin tier grants its features without payment and takes precedence over Stripe.</div>
</div>
<div class="alert warn" style="font-size:10px;margin-top:4px;margin-bottom:0">
⚠ Changing username also updates it in all their messages.
</div>
@@ -1818,11 +1840,12 @@ function openEditMsg(id, src, text, username, rqs) {
}
// ── Edit user ──────────────────────────────────────────────────────────────
function openEditUser(id, username, color) {
function openEditUser(id, username, color, manualPlanId) {
document.getElementById('edit-user-id').value = id;
document.getElementById('edit-user-name').value = username;
document.getElementById('edit-color-txt').value = color;
document.getElementById('edit-color-pick').value = color;
document.getElementById('edit-user-plan').value = String(manualPlanId || 0);
updateSwatch('edit-color-swatch', color);
openModal('modal-edit-user');
}