Compare commits

...

2 Commits

Author SHA1 Message Date
MarekWo
a822d94317 fix: Align sender name baseline with timestamp in own messages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 21:13:36 +01:00
MarekWo
db6915f53f feat: Improve chat message display
- Add sender name above outgoing messages in group chat
- Display emoji-only messages in larger font size
- Fix leading space before text in quoted replies

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 20:42:27 +01:00
3 changed files with 71 additions and 4 deletions

View File

@@ -113,11 +113,12 @@ main {
color: #6c757d;
}
/* Message footer for own messages (time below bubble) */
/* Message footer for own messages (name + time above bubble) */
.message-footer {
display: flex;
align-items: baseline;
padding-right: 0.5rem;
margin-top: 0.25rem;
margin-bottom: 0.25rem;
}
.message-footer.own {
@@ -129,6 +130,13 @@ main {
color: #6c757d;
}
.message-footer .message-sender.own {
font-weight: 600;
font-size: 0.875rem;
color: #084298;
margin-right: 0.5rem;
}
/* Message action buttons container */
.message-actions {
display: flex;
@@ -680,6 +688,12 @@ main {
border-left-color: #084298;
}
/* Large Emoji for emoji-only messages */
.emoji-large {
font-size: 2.5rem;
line-height: 1.2;
}
/* Clickable Links in Messages */
.message-link {
color: #0d6efd;

View File

@@ -728,6 +728,7 @@ function createMessageElement(msg) {
wrapper.innerHTML = `
<div class="message-container">
<div class="message-footer own">
<span class="message-sender own">${escapeHtml(msg.sender)}</span>
<span class="message-time">${time}</span>
</div>
<div class="message own">

View File

@@ -11,6 +11,9 @@
function processMessageContent(content) {
if (!content) return '';
// Check if content (minus mentions) is emoji-only BEFORE any processing
const emojiOnlyInfo = checkEmojiOnlyContent(content);
// First escape HTML to prevent XSS
let processed = escapeHtml(content);
@@ -27,9 +30,58 @@ function processMessageContent(content) {
// 4. Convert URLs to links (and images to thumbnails)
processed = processUrls(processed);
// 5. If emoji-only, enlarge the emoji
if (emojiOnlyInfo.isEmojiOnly) {
processed = enlargeEmoji(processed, emojiOnlyInfo.hasMention);
}
return processed;
}
/**
* Check if content is emoji-only (excluding @[mentions])
* @param {string} text - Raw message content
* @returns {object} - { isEmojiOnly: boolean, hasMention: boolean }
*/
function checkEmojiOnlyContent(text) {
const hasMention = /@\[[^\]]+\]/.test(text);
// Remove @[...] patterns
const withoutMentions = text.replace(/@\[[^\]]+\]/g, '').trim();
if (!withoutMentions) {
return { isEmojiOnly: false, hasMention };
}
// Check if remaining is only emoji (using Unicode Extended_Pictographic)
// Matches emoji, modifiers, skin tones, ZWJ sequences, variation selectors, and whitespace
const emojiRegex = /^[\p{Extended_Pictographic}\p{Emoji_Modifier}\p{Emoji_Modifier_Base}\p{Emoji_Component}\uFE0F\u200D\s]+$/u;
const isEmojiOnly = emojiRegex.test(withoutMentions);
return { isEmojiOnly, hasMention };
}
/**
* Enlarge emoji in processed HTML
* @param {string} html - Processed HTML with mention badges
* @param {boolean} hasMention - Whether content has mentions
* @returns {string} - HTML with enlarged emoji
*/
function enlargeEmoji(html, hasMention) {
if (hasMention) {
// Add line break after mention badge, then wrap emoji in large class
// Pattern: closing </span> of mention badge, optional whitespace, then emoji
html = html.replace(
/(<\/span>)\s*([\p{Extended_Pictographic}\p{Emoji_Modifier}\p{Emoji_Modifier_Base}\p{Emoji_Component}\uFE0F\u200D\s]+)$/u,
'$1<br><span class="emoji-large">$2</span>'
);
} else {
// Just wrap everything in large emoji class
html = `<span class="emoji-large">${html}</span>`;
}
return html;
}
/**
* Convert @[Username] mentions to styled badges
* @param {string} text - HTML-escaped text
@@ -77,8 +129,8 @@ function processChannelLinks(text) {
* @returns {string} - Text with styled quotes
*/
function processQuotes(text) {
// Match »...« pattern (guillemets)
const quotePattern = /»([^«]+)«/g;
// Match »...« pattern (guillemets) including optional trailing whitespace
const quotePattern = /»([^«]+)«\s*/g;
return text.replace(quotePattern, (_match, quoted) => {
// Display without guillemets (styling is enough) + line break after