From 62b89b654895601a1f44387ea3dd3cbed613a410 Mon Sep 17 00:00:00 2001 From: Ricky Stretch Date: Fri, 13 Mar 2026 16:25:33 -0400 Subject: [PATCH] v0.9.3 added user feature to disable participation in private messages --- .env.example | 2 +- backend/package.json | 2 +- backend/src/models/db.js | 7 ++ backend/src/routes/users.js | 16 ++--- build.sh | 2 +- frontend/package.json | 2 +- frontend/src/components/NewChatModal.jsx | 2 +- frontend/src/components/ProfileModal.jsx | 12 +++- frontend/src/components/UserProfilePopup.jsx | 68 ++++++++++++-------- frontend/src/utils/api.js | 2 +- 10 files changed, 72 insertions(+), 43 deletions(-) diff --git a/.env.example b/.env.example index 2f4ad63..eacec63 100644 --- a/.env.example +++ b/.env.example @@ -7,7 +7,7 @@ TZ=UTC # Copy this file to .env and customize # Image version to run (set by build.sh, or use 'latest') -JAMA_VERSION=0.9.2 +JAMA_VERSION=0.9.3 # Default admin credentials (used on FIRST RUN only) ADMIN_NAME=Admin User diff --git a/backend/package.json b/backend/package.json index f85cdf4..a24ffb3 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "jama-backend", - "version": "0.9.2", + "version": "0.9.3", "description": "TeamChat backend server", "main": "src/index.js", "scripts": { diff --git a/backend/src/models/db.js b/backend/src/models/db.js index 430dfe9..639b7b0 100644 --- a/backend/src/models/db.js +++ b/backend/src/models/db.js @@ -52,6 +52,7 @@ function initDb() { about_me TEXT, display_name TEXT, hide_admin_tag INTEGER NOT NULL DEFAULT 0, + allow_dm INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); @@ -170,6 +171,12 @@ function initDb() { console.log('[DB] Migration: added hide_admin_tag column'); } catch (e) { /* column already exists */ } + // Migration: add allow_dm if upgrading from older version + try { + db.exec("ALTER TABLE users ADD COLUMN allow_dm INTEGER NOT NULL DEFAULT 1"); + console.log('[DB] Migration: added allow_dm column'); + } catch (e) { /* column already exists */ } + // Migration: replace single-session active_sessions with per-device version try { const cols = db.prepare("PRAGMA table_info(active_sessions)").all().map(c => c.name); diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js index 791a481..f02ed75 100644 --- a/backend/src/routes/users.js +++ b/backend/src/routes/users.js @@ -49,7 +49,7 @@ function getDefaultPassword(db) { router.get('/', authMiddleware, adminMiddleware, (req, res) => { const db = getDb(); const users = db.prepare(` - SELECT id, name, email, role, status, is_default_admin, must_change_password, avatar, about_me, display_name, created_at, last_online + SELECT id, name, email, role, status, is_default_admin, must_change_password, avatar, about_me, display_name, allow_dm, created_at, last_online FROM users WHERE status != 'deleted' ORDER BY created_at ASC `).all(); @@ -66,7 +66,7 @@ router.get('/search', authMiddleware, (req, res) => { if (group && (group.type === 'private' || group.is_direct)) { // Private group or direct message — only show members of this group users = db.prepare(` - SELECT u.id, u.name, u.display_name, u.avatar, u.role, u.status, u.hide_admin_tag + SELECT u.id, u.name, u.display_name, u.avatar, u.role, u.status, u.hide_admin_tag, u.allow_dm FROM users u JOIN group_members gm ON gm.user_id = u.id AND gm.group_id = ? WHERE u.status = 'active' AND u.id != ? @@ -76,14 +76,14 @@ router.get('/search', authMiddleware, (req, res) => { } else { // Public group — all active users users = db.prepare(` - SELECT id, name, display_name, avatar, role, status, hide_admin_tag FROM users + SELECT id, name, display_name, avatar, role, status, hide_admin_tag, allow_dm FROM users WHERE status = 'active' AND id != ? AND (name LIKE ? OR display_name LIKE ?) LIMIT 10 `).all(req.user.id, `%${q}%`, `%${q}%`); } } else { users = db.prepare(` - SELECT id, name, display_name, avatar, role, status, hide_admin_tag FROM users + SELECT id, name, display_name, avatar, role, status, hide_admin_tag, allow_dm FROM users WHERE status = 'active' AND (name LIKE ? OR display_name LIKE ?) LIMIT 10 `).all(`%${q}%`, `%${q}%`); @@ -247,7 +247,7 @@ router.delete('/:id', authMiddleware, adminMiddleware, (req, res) => { // Update own profile — display name must be unique (req 6) router.patch('/me/profile', authMiddleware, (req, res) => { - const { displayName, aboutMe, hideAdminTag } = req.body; + const { displayName, aboutMe, hideAdminTag, allowDm } = req.body; const db = getDb(); if (displayName) { const conflict = db.prepare( @@ -255,9 +255,9 @@ router.patch('/me/profile', authMiddleware, (req, res) => { ).get(displayName, req.user.id); if (conflict) return res.status(400).json({ error: 'Display name already in use' }); } - db.prepare("UPDATE users SET display_name = ?, about_me = ?, hide_admin_tag = ?, updated_at = datetime('now') WHERE id = ?") - .run(displayName || null, aboutMe || null, hideAdminTag ? 1 : 0, req.user.id); - const user = db.prepare('SELECT id, name, email, role, status, avatar, about_me, display_name, hide_admin_tag FROM users WHERE id = ?').get(req.user.id); + db.prepare("UPDATE users SET display_name = ?, about_me = ?, hide_admin_tag = ?, allow_dm = ?, updated_at = datetime('now') WHERE id = ?") + .run(displayName || null, aboutMe || null, hideAdminTag ? 1 : 0, allowDm === false ? 0 : 1, req.user.id); + const user = db.prepare('SELECT id, name, email, role, status, avatar, about_me, display_name, hide_admin_tag, allow_dm FROM users WHERE id = ?').get(req.user.id); res.json({ user }); }); diff --git a/build.sh b/build.sh index fcb5020..b3450ca 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.9.2}" +VERSION="${1:-0.9.3}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="jama" diff --git a/frontend/package.json b/frontend/package.json index e8b3ff4..7202050 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "jama-frontend", - "version": "0.9.2", + "version": "0.9.3", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/components/NewChatModal.jsx b/frontend/src/components/NewChatModal.jsx index 7e53f0b..0409b15 100644 --- a/frontend/src/components/NewChatModal.jsx +++ b/frontend/src/components/NewChatModal.jsx @@ -142,7 +142,7 @@ export default function NewChatModal({ onClose, onCreated }) { )}
- {users.filter(u => u.id !== user.id).map(u => ( + {users.filter(u => u.id !== user.id && u.allow_dm !== 0).map(u => ( )} + diff --git a/frontend/src/components/UserProfilePopup.jsx b/frontend/src/components/UserProfilePopup.jsx index 03f8a6d..ac69803 100644 --- a/frontend/src/components/UserProfilePopup.jsx +++ b/frontend/src/components/UserProfilePopup.jsx @@ -97,34 +97,46 @@ export default function UserProfilePopup({ user: profileUser, anchorEl, onClose,

)} {!isSelf && onDirectMessage && ( - + profileUser.allow_dm === 0 ? ( +

+ DMs disabled by user +

+ ) : ( + + ) )}
); diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index ef44db8..f7520e3 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -63,7 +63,7 @@ export const api = { activateUser: (id) => req('PATCH', `/users/${id}/activate`), deleteUser: (id) => req('DELETE', `/users/${id}`), checkDisplayName: (name) => req('GET', `/users/check-display-name?name=${encodeURIComponent(name)}`), - updateProfile: (body) => req('PATCH', '/users/me/profile', body), // body: { displayName, aboutMe, hideAdminTag } + updateProfile: (body) => req('PATCH', '/users/me/profile', body), // body: { displayName, aboutMe, hideAdminTag, allowDm } uploadAvatar: (file) => { const form = new FormData(); form.append('avatar', file); return req('POST', '/users/me/avatar', form);