From 08d57309ae9d22436c2f6a3c78cfdda5c712191a Mon Sep 17 00:00:00 2001 From: Ricky Stretch Date: Mon, 9 Mar 2026 22:34:04 -0400 Subject: [PATCH] v.0.3.6 message input box update --- .env.example | 2 +- backend/package.json | 2 +- backend/src/index.js | 13 +++++---- build.sh | 2 +- frontend/package.json | 2 +- frontend/src/components/Message.jsx | 2 +- frontend/src/components/MessageInput.css | 35 ----------------------- frontend/src/components/MessageInput.jsx | 36 ++++-------------------- frontend/src/components/Sidebar.jsx | 2 +- frontend/src/index.css | 7 +---- 10 files changed, 20 insertions(+), 83 deletions(-) diff --git a/.env.example b/.env.example index 4112bf0..2762e6b 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.3.5 +JAMA_VERSION=0.3.6 # Default admin credentials (used on FIRST RUN only) ADMIN_NAME=Admin User diff --git a/backend/package.json b/backend/package.json index 671124c..d3c0bfa 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "jama-backend", - "version": "0.3.5", + "version": "0.3.6", "description": "TeamChat backend server", "main": "src/index.js", "scripts": { diff --git a/backend/src/index.js b/backend/src/index.js index c242f8a..3c7beda 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -211,11 +211,14 @@ io.on('connection', (socket) => { } } - // Process @mentions + // Process @mentions — format is @[display name], look up user by display_name or name if (content) { - const mentions = content.match(/@\[([^\]]+)\]\((\d+)\)/g) || []; - for (const mention of mentions) { - const matchId = mention.match(/\((\d+)\)/)?.[1]; + const mentionNames = [...new Set((content.match(/@\[([^\]]+)\]/g) || []).map(m => m.slice(2, -1)))]; + for (const mentionName of mentionNames) { + const mentionedUser = db.prepare( + "SELECT id FROM users WHERE status = 'active' AND (LOWER(display_name) = LOWER(?) OR LOWER(name) = LOWER(?))" + ).get(mentionName, mentionName); + const matchId = mentionedUser?.id?.toString(); if (matchId && parseInt(matchId) !== userId) { const notifResult = db.prepare(` INSERT INTO notifications (user_id, type, message_id, group_id, from_user_id) @@ -240,7 +243,7 @@ io.on('connection', (socket) => { const senderName = socket.user?.display_name || socket.user?.name || 'Someone'; sendPushToUser(mentionedUserId, { title: `${senderName} mentioned you`, - body: (content || '').replace(/@\[[^\]]+\]\(\d+\)/g, (m) => '@' + m.match(/\[([^\]]+)\]/)?.[1]).slice(0, 100), + body: (content || '').replace(/@\[([^\]]+)\]/g, '@$1').slice(0, 100), url: '/', badge: 1, }).catch(() => {}); diff --git a/build.sh b/build.sh index 671727a..6dc1167 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.3.5}" +VERSION="${1:-0.3.6}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="jama" diff --git a/frontend/package.json b/frontend/package.json index 96ef78a..0ab0506 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "jama-frontend", - "version": "0.3.5", + "version": "0.3.6", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/components/Message.jsx b/frontend/src/components/Message.jsx index 944ef2d..7bb54d1 100644 --- a/frontend/src/components/Message.jsx +++ b/frontend/src/components/Message.jsx @@ -12,7 +12,7 @@ const QUICK_EMOJIS = ['👍', '❤️', '😂', '😮', '😢', '🙏']; function formatMsgContent(content) { if (!content) return ''; // First handle @mentions - let html = content.replace(/@\[([^\]]+)\]\(\d+\)/g, (_, name) => `@${name}`); + let html = content.replace(/@\[([^\]]+)\]/g, (_, name) => `@${name}`); // Then linkify bare URLs (not already inside a tag) html = html.replace(/(https?:\/\/[^\s<>"]+)/g, (url) => { // Trim trailing punctuation that's unlikely to be part of the URL diff --git a/frontend/src/components/MessageInput.css b/frontend/src/components/MessageInput.css index 96735b0..4bac913 100644 --- a/frontend/src/components/MessageInput.css +++ b/frontend/src/components/MessageInput.css @@ -237,38 +237,3 @@ } } -/* Mention display overlay — sits over the textarea to show formatted @names */ -.input-wrap { - position: relative; -} - -.msg-input-mirror { - position: absolute; - top: 0; left: 0; right: 0; - pointer-events: none; /* clicks pass through to textarea */ - overflow: hidden; - white-space: pre-wrap; - word-break: break-word; - color: var(--text-primary); - border-color: transparent; /* hide the border — textarea draws it */ - background: transparent; - z-index: 1; -} - -.msg-input-raw { - position: relative; - color: transparent; /* hide raw text — mirror shows it */ - caret-color: var(--text-primary); /* but keep the cursor visible */ - background: transparent; - z-index: 2; -} - -/* Bold mention chips inside the mirror */ -.mention-chip { - font-weight: 700; - color: var(--primary); - font-style: normal; -} - -/* Placeholder is on the raw textarea; mirror has none */ -.msg-input-mirror::placeholder { display: none; } diff --git a/frontend/src/components/MessageInput.jsx b/frontend/src/components/MessageInput.jsx index 25f4efe..ac1b097 100644 --- a/frontend/src/components/MessageInput.jsx +++ b/frontend/src/components/MessageInput.jsx @@ -12,20 +12,6 @@ function isEmojiOnly(str) { return emojiRegex.test(str.trim()); } - -// Convert raw mention syntax to display HTML for the input mirror overlay -function renderInputDisplay(raw) { - // Escape HTML special chars first - const escaped = raw - .replace(/&/g, '&') - .replace(//g, '>'); - // Replace @[name](id) with bold @name span - return escaped.replace(/@\[([^\]]+)\]\(\d+\)/g, - (_, name) => `@${name}` - ); -} - export default function MessageInput({ group, replyTo, onCancelReply, onSend, onTyping }) { const [text, setText] = useState(''); const [imageFile, setImageFile] = useState(null); @@ -39,7 +25,6 @@ export default function MessageInput({ group, replyTo, onCancelReply, onSend, on const [showAttachMenu, setShowAttachMenu] = useState(false); const [showEmojiPicker, setShowEmojiPicker] = useState(false); const inputRef = useRef(null); - const mirrorRef = useRef(null); const typingTimer = useRef(null); const wasTyping = useRef(false); const mentionStart = useRef(-1); @@ -109,10 +94,8 @@ export default function MessageInput({ group, replyTo, onCancelReply, onSend, on el.style.height = 'auto'; const lineHeight = parseFloat(getComputedStyle(el).lineHeight); const maxHeight = lineHeight * 5 + 20; - const newH = Math.min(el.scrollHeight, maxHeight) + 'px'; - el.style.height = newH; + el.style.height = Math.min(el.scrollHeight, maxHeight) + 'px'; el.style.overflowY = el.scrollHeight > maxHeight ? 'auto' : 'hidden'; - if (mirrorRef.current) mirrorRef.current.style.height = newH; const cur = e.target.selectionStart; const lastAt = val.lastIndexOf('@', cur - 1); @@ -142,8 +125,8 @@ export default function MessageInput({ group, replyTo, onCancelReply, onSend, on const insertMention = (user) => { const before = text.slice(0, mentionStart.current); const after = text.slice(inputRef.current.selectionStart); - const mention = `@[${user.display_name || user.name}](${user.id}) `; - setText(before + mention + after); + const name = user.display_name || user.name; + setText(before + `@[${name}] ` + after); setShowMention(false); setMentionResults([]); inputRef.current.focus(); @@ -177,10 +160,8 @@ export default function MessageInput({ group, replyTo, onCancelReply, onSend, on if (inputRef.current) { inputRef.current.style.height = 'auto'; inputRef.current.style.overflowY = 'hidden'; - if (mirrorRef.current) mirrorRef.current.style.height = 'auto'; } - // Tag emoji-only messages so they can be rendered large const emojiOnly = !!trimmed && isEmojiOnly(trimmed); await onSend({ content: trimmed || null, imageFile, linkPreview: lp, emojiOnly }); }; @@ -352,17 +333,10 @@ export default function MessageInput({ group, replyTo, onCancelReply, onSend, on )}
- {/* Mirror overlay renders formatted mention text over the transparent textarea */} -