v0.3.3 bug fixes
This commit is contained in:
@@ -7,7 +7,7 @@ TZ=UTC
|
|||||||
# Copy this file to .env and customize
|
# Copy this file to .env and customize
|
||||||
|
|
||||||
# Image version to run (set by build.sh, or use 'latest')
|
# 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)
|
# Default admin credentials (used on FIRST RUN only)
|
||||||
ADMIN_NAME=Admin User
|
ADMIN_NAME=Admin User
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jama-backend",
|
"name": "jama-backend",
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"description": "TeamChat backend server",
|
"description": "TeamChat backend server",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
2
build.sh
2
build.sh
@@ -13,7 +13,7 @@
|
|||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="${1:-0.3.2}"
|
VERSION="${1:-0.3.3}"
|
||||||
ACTION="${2:-}"
|
ACTION="${2:-}"
|
||||||
REGISTRY="${REGISTRY:-}"
|
REGISTRY="${REGISTRY:-}"
|
||||||
IMAGE_NAME="jama"
|
IMAGE_NAME="jama"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jama-frontend",
|
"name": "jama-frontend",
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -236,3 +236,39 @@
|
|||||||
min-width: 480px;
|
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; }
|
||||||
|
|||||||
@@ -12,6 +12,20 @@ function isEmojiOnly(str) {
|
|||||||
return emojiRegex.test(str.trim());
|
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(/>/g, '>');
|
||||||
|
// Replace @[name](id) with bold @name span
|
||||||
|
return escaped.replace(/@\[([^\]]+)\]\(\d+\)/g,
|
||||||
|
(_, name) => `<strong class="mention-chip">@${name}</strong>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function MessageInput({ group, replyTo, onCancelReply, onSend, onTyping }) {
|
export default function MessageInput({ group, replyTo, onCancelReply, onSend, onTyping }) {
|
||||||
const [text, setText] = useState('');
|
const [text, setText] = useState('');
|
||||||
const [imageFile, setImageFile] = useState(null);
|
const [imageFile, setImageFile] = useState(null);
|
||||||
@@ -25,6 +39,7 @@ export default function MessageInput({ group, replyTo, onCancelReply, onSend, on
|
|||||||
const [showAttachMenu, setShowAttachMenu] = useState(false);
|
const [showAttachMenu, setShowAttachMenu] = useState(false);
|
||||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
const mirrorRef = useRef(null);
|
||||||
const typingTimer = useRef(null);
|
const typingTimer = useRef(null);
|
||||||
const wasTyping = useRef(false);
|
const wasTyping = useRef(false);
|
||||||
const mentionStart = useRef(-1);
|
const mentionStart = useRef(-1);
|
||||||
@@ -94,8 +109,10 @@ export default function MessageInput({ group, replyTo, onCancelReply, onSend, on
|
|||||||
el.style.height = 'auto';
|
el.style.height = 'auto';
|
||||||
const lineHeight = parseFloat(getComputedStyle(el).lineHeight);
|
const lineHeight = parseFloat(getComputedStyle(el).lineHeight);
|
||||||
const maxHeight = lineHeight * 5 + 20;
|
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';
|
el.style.overflowY = el.scrollHeight > maxHeight ? 'auto' : 'hidden';
|
||||||
|
if (mirrorRef.current) mirrorRef.current.style.height = newH;
|
||||||
|
|
||||||
const cur = e.target.selectionStart;
|
const cur = e.target.selectionStart;
|
||||||
const lastAt = val.lastIndexOf('@', cur - 1);
|
const lastAt = val.lastIndexOf('@', cur - 1);
|
||||||
@@ -160,6 +177,7 @@ export default function MessageInput({ group, replyTo, onCancelReply, onSend, on
|
|||||||
if (inputRef.current) {
|
if (inputRef.current) {
|
||||||
inputRef.current.style.height = 'auto';
|
inputRef.current.style.height = 'auto';
|
||||||
inputRef.current.style.overflowY = 'hidden';
|
inputRef.current.style.overflowY = 'hidden';
|
||||||
|
if (mirrorRef.current) mirrorRef.current.style.height = 'auto';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tag emoji-only messages so they can be rendered large
|
// Tag emoji-only messages so they can be rendered large
|
||||||
@@ -334,10 +352,17 @@ export default function MessageInput({ group, replyTo, onCancelReply, onSend, on
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="input-wrap">
|
<div className="input-wrap">
|
||||||
|
{/* Mirror overlay renders formatted mention text over the transparent textarea */}
|
||||||
|
<div
|
||||||
|
ref={mirrorRef}
|
||||||
|
className="msg-input msg-input-mirror"
|
||||||
|
aria-hidden="true"
|
||||||
|
dangerouslySetInnerHTML={{ __html: renderInputDisplay(text) || '' }}
|
||||||
|
/>
|
||||||
<textarea
|
<textarea
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className="msg-input"
|
className="msg-input msg-input-raw"
|
||||||
placeholder={`Message ${group?.name || ''}...`}
|
placeholder={!text ? `Message ${group?.name || ''}...` : ''}
|
||||||
value={text}
|
value={text}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifica
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="group-last-msg truncate">
|
<span className="group-last-msg truncate">
|
||||||
{group.last_message || (group.is_readonly ? '📢 Read-only' : 'No messages yet')}
|
{(group.last_message || '').replace(/@\[([^\]]+)\]\(\d+\)/g, '@$1') || (group.is_readonly ? '📢 Read-only' : 'No messages yet')}
|
||||||
</span>
|
</span>
|
||||||
{notifs > 0 && <span className="badge shrink-0">{notifs}</span>}
|
{notifs > 0 && <span className="badge shrink-0">{notifs}</span>}
|
||||||
{hasUnread && notifs === 0 && <span className="badge badge-unread shrink-0">{unreadCount}</span>}
|
{hasUnread && notifs === 0 && <span className="badge badge-unread shrink-0">{unreadCount}</span>}
|
||||||
|
|||||||
@@ -372,3 +372,10 @@ a { color: inherit; text-decoration: none; }
|
|||||||
[data-theme="dark"] .mention-avatar {
|
[data-theme="dark"] .mention-avatar {
|
||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .msg-input-mirror {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .msg-input-raw {
|
||||||
|
caret-color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user