diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..d26fa39 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Bash(/mnt/c/Program Files/nodejs/npm.cmd run build)", + "Bash(cmd.exe /c \"npm run build\")", + "Bash(cmd.exe /c \"cd /d d:\\\\_projects\\\\gitea\\\\jama\\\\frontend && npm run build 2>&1\")", + "Bash(cmd.exe /c \"cd /d d:\\\\_projects\\\\gitea\\\\jama\\\\frontend && npm run build\")", + "Bash(powershell.exe -Command \"cd ''d:\\\\_projects\\\\gitea\\\\jama\\\\frontend''; & ''C:\\\\Program Files\\\\nodejs\\\\npm.cmd'' run build 2>&1\")", + "Bash(powershell.exe -Command \"Get-Command npm -ErrorAction SilentlyContinue; \\(Get-Command node -ErrorAction SilentlyContinue\\).Source\")" + ] + } +} diff --git a/backend/package.json b/backend/package.json index ffb4d7b..6c42091 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-backend", - "version": "0.12.28", + "version": "0.12.29", "description": "RosterChirp backend server", "main": "src/index.js", "scripts": { diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js index f375305..3a980ec 100644 --- a/backend/src/routes/users.js +++ b/backend/src/routes/users.js @@ -40,26 +40,30 @@ router.get('/', authMiddleware, teamManagerMiddleware, async (req, res) => { }); // Search users +// When q is empty (full-list load by GroupManagerPage / NewChatModal) — return ALL active users, +// no LIMIT, so the complete roster is available for member-picker UIs. +// When q is non-empty (typed search / mention autocomplete) — keep LIMIT 10 for performance. router.get('/search', authMiddleware, async (req, res) => { const { q, groupId } = req.query; + const isTyped = q && q.length > 0; try { let users; if (groupId) { const group = await queryOne(req.schema, 'SELECT type, is_direct FROM groups WHERE id = $1', [parseInt(groupId)]); if (group && (group.type === 'private' || group.is_direct)) { users = await query(req.schema, - "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=$1 WHERE u.status='active' AND u.id!=$2 AND (u.name ILIKE $3 OR u.display_name ILIKE $3) LIMIT 10", + `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=$1 WHERE u.status='active' AND u.id!=$2 AND (u.name ILIKE $3 OR u.display_name ILIKE $3) ORDER BY u.name ASC${isTyped ? ' LIMIT 10' : ''}`, [parseInt(groupId), req.user.id, `%${q}%`] ); } else { users = await query(req.schema, - "SELECT id,name,display_name,avatar,role,status,hide_admin_tag,allow_dm FROM users WHERE status='active' AND id!=$1 AND (name ILIKE $2 OR display_name ILIKE $2) LIMIT 10", + `SELECT id,name,display_name,avatar,role,status,hide_admin_tag,allow_dm FROM users WHERE status='active' AND id!=$1 AND (name ILIKE $2 OR display_name ILIKE $2) ORDER BY name ASC${isTyped ? ' LIMIT 10' : ''}`, [req.user.id, `%${q}%`] ); } } else { users = await query(req.schema, - "SELECT id,name,display_name,avatar,role,status,hide_admin_tag,allow_dm FROM users WHERE status='active' AND (name ILIKE $1 OR display_name ILIKE $1) LIMIT 10", + `SELECT id,name,display_name,avatar,role,status,hide_admin_tag,allow_dm FROM users WHERE status='active' AND (name ILIKE $1 OR display_name ILIKE $1) ORDER BY name ASC${isTyped ? ' LIMIT 10' : ''}`, [`%${q}%`] ); } diff --git a/build.sh b/build.sh index 9e6f4dc..3971e3f 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.12.28}" +VERSION="${1:-0.12.29}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="rosterchirp" diff --git a/frontend/package.json b/frontend/package.json index 19b1eed..d5391f1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-frontend", - "version": "0.12.28", + "version": "0.12.29", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/components/Message.css b/frontend/src/components/Message.css index aa59c8a..a441d09 100644 --- a/frontend/src/components/Message.css +++ b/frontend/src/components/Message.css @@ -83,6 +83,9 @@ color: var(--text-secondary); margin-bottom: 3px; padding: 0 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } /* Reply preview */ diff --git a/frontend/src/components/ProfileModal.jsx b/frontend/src/components/ProfileModal.jsx index 34cb13f..815d32c 100644 --- a/frontend/src/components/ProfileModal.jsx +++ b/frontend/src/components/ProfileModal.jsx @@ -1,13 +1,18 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useAuth } from '../contexts/AuthContext.jsx'; import { useToast } from '../contexts/ToastContext.jsx'; import { api } from '../utils/api.js'; import Avatar from './Avatar.jsx'; +const LS_FONT_KEY = 'rosterchirp_font_scale'; +const MIN_SCALE = 0.8; +const MAX_SCALE = 2.0; + export default function ProfileModal({ onClose }) { const { user, updateUser } = useAuth(); const toast = useToast(); + const [isMobile, setIsMobile] = useState(() => window.innerWidth < 768); const [displayName, setDisplayName] = useState(user?.display_name || ''); const [savedDisplayName, setSavedDisplayName] = useState(user?.display_name || ''); const [displayNameWarning, setDisplayNameWarning] = useState(''); @@ -16,7 +21,7 @@ export default function ProfileModal({ onClose }) { const [newPw, setNewPw] = useState(''); const [confirmPw, setConfirmPw] = useState(''); const [loading, setLoading] = useState(false); - const [tab, setTab] = useState('profile'); // 'profile' | 'password' | 'notifications' + const [tab, setTab] = useState('profile'); // 'profile' | 'password' | 'notifications' | 'appearance' const [pushTesting, setPushTesting] = useState(false); const [pushResult, setPushResult] = useState(null); const [notifPermission, setNotifPermission] = useState( @@ -25,6 +30,23 @@ export default function ProfileModal({ onClose }) { const [hideAdminTag, setHideAdminTag] = useState(!!user?.hide_admin_tag); const [allowDm, setAllowDm] = useState(user?.allow_dm !== 0); + const savedScale = parseFloat(localStorage.getItem(LS_FONT_KEY)); + const [fontScale, setFontScale] = useState( + (savedScale >= MIN_SCALE && savedScale <= MAX_SCALE) ? savedScale : 1.0 + ); + + useEffect(() => { + const onResize = () => setIsMobile(window.innerWidth < 768); + window.addEventListener('resize', onResize); + return () => window.removeEventListener('resize', onResize); + }, []); + + const applyFontScale = (val) => { + setFontScale(val); + document.documentElement.style.setProperty('--font-scale', val); + localStorage.setItem(LS_FONT_KEY, val); + }; + const handleSaveProfile = async () => { if (displayNameWarning) return toast('Display name is already in use', 'error'); setLoading(true); @@ -102,12 +124,27 @@ export default function ProfileModal({ onClose }) { - {/* Tabs */} -