v.0.3.6 message input box update

This commit is contained in:
2026-03-09 22:34:04 -04:00
parent 0f3983dc93
commit 08d57309ae
10 changed files with 20 additions and 83 deletions

View File

@@ -12,7 +12,7 @@ const QUICK_EMOJIS = ['👍', '❤️', '😂', '😮', '😢', '🙏'];
function formatMsgContent(content) {
if (!content) return '';
// First handle @mentions
let html = content.replace(/@\[([^\]]+)\]\(\d+\)/g, (_, name) => `<span class="mention">@${name}</span>`);
let html = content.replace(/@\[([^\]]+)\]/g, (_, name) => `<span class="mention">@${name}</span>`);
// 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

View File

@@ -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; }

View File

@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// 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 }) {
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
)}
<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
ref={inputRef}
className="msg-input msg-input-raw"
placeholder={!text ? `Message ${group?.name || ''}...` : ''}
className="msg-input"
placeholder={`Message ${group?.name || ''}...`}
value={text}
onChange={handleChange}
onKeyDown={handleKeyDown}

View File

@@ -106,7 +106,7 @@ export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifica
</div>
<div className="flex items-center justify-between gap-2">
<span className="group-last-msg truncate">
{(group.last_message || '').replace(/@\[([^\]]+)\]\(\d+\)/g, '@$1') || (group.is_readonly ? '📢 Read-only' : 'No messages yet')}
{(group.last_message || '').replace(/@\[([^\]]+)\]/g, '@$1') || (group.is_readonly ? '📢 Read-only' : 'No messages yet')}
</span>
{notifs > 0 && <span className="badge shrink-0">{notifs}</span>}
{hasUnread && notifs === 0 && <span className="badge badge-unread shrink-0">{unreadCount}</span>}

View File

@@ -373,12 +373,6 @@ a { color: inherit; text-decoration: none; }
background: var(--primary);
}
[data-theme="dark"] .msg-input-mirror {
color: var(--text-primary);
}
[data-theme="dark"] .msg-input-raw {
caret-color: var(--text-primary);
}
/* Night mode: .in bubble (dark surface) — base mention colour is too dark, use light primary */
[data-theme="dark"] .in .mention {
@@ -399,3 +393,4 @@ a { color: inherit; text-decoration: none; }
[data-theme="dark"] .reaction-btn:hover {
background: var(--primary-light);
}