diff --git a/.env.example b/.env.example index d9c559c..f9155f9 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.2 +JAMA_VERSION=0.3.3 # Default admin credentials (used on FIRST RUN only) ADMIN_NAME=Admin User diff --git a/backend/package.json b/backend/package.json index 23686b7..084892a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "jama-backend", - "version": "0.3.2", + "version": "0.3.3", "description": "TeamChat backend server", "main": "src/index.js", "scripts": { diff --git a/build.sh b/build.sh index cc41eb7..e36d70e 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.3.2}" +VERSION="${1:-0.3.3}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="jama" diff --git a/frontend/package.json b/frontend/package.json index 7b4baf1..c838230 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "jama-frontend", - "version": "0.3.2", + "version": "0.3.3", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/components/MessageInput.css b/frontend/src/components/MessageInput.css index d812234..96735b0 100644 --- a/frontend/src/components/MessageInput.css +++ b/frontend/src/components/MessageInput.css @@ -236,3 +236,39 @@ min-width: 480px; } } + +/* 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 627078a..25f4efe 100644 --- a/frontend/src/components/MessageInput.jsx +++ b/frontend/src/components/MessageInput.jsx @@ -12,6 +12,20 @@ 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); @@ -25,6 +39,7 @@ 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); @@ -94,8 +109,10 @@ export default function MessageInput({ group, replyTo, onCancelReply, onSend, on el.style.height = 'auto'; const lineHeight = parseFloat(getComputedStyle(el).lineHeight); const maxHeight = lineHeight * 5 + 20; - el.style.height = Math.min(el.scrollHeight, maxHeight) + 'px'; + const newH = Math.min(el.scrollHeight, maxHeight) + 'px'; + el.style.height = newH; 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); @@ -160,6 +177,7 @@ 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 @@ -334,10 +352,17 @@ export default function MessageInput({ group, replyTo, onCancelReply, onSend, on )}