diff --git a/app/static/css/style.css b/app/static/css/style.css index 13d3baa..7471c43 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -38,14 +38,103 @@ main { gap: 0.75rem; } +/* ============================================================================= + Message Wrapper - New Layout with Avatar + ============================================================================= */ + +/* Message wrapper - contains avatar + message container */ +.message-wrapper { + display: flex; + gap: 0.5rem; + max-width: 85%; + animation: fadeIn 0.3s ease-in; +} + +.message-wrapper.own { + align-self: flex-end; + flex-direction: row-reverse; +} + +.message-wrapper.other { + align-self: flex-start; +} + +/* Avatar circle */ +.message-avatar { + flex-shrink: 0; + width: 36px; + height: 36px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.85rem; + font-weight: 600; + color: white; + text-transform: uppercase; +} + +.message-avatar.emoji { + font-size: 1.25rem; + background-color: transparent !important; +} + +/* Message container - holds sender row + bubble + actions */ +.message-container { + display: flex; + flex-direction: column; + min-width: 0; /* Allow text truncation */ +} + +/* Sender row - name and time above bubble */ +.message-sender-row { + display: flex; + align-items: baseline; + gap: 0.5rem; + margin-bottom: 0.25rem; + padding-left: 0.25rem; +} + +.message-sender { + font-weight: 600; + font-size: 0.875rem; + color: #0d6efd; +} + +.message-sender-row .message-time { + font-size: 0.7rem; + color: #6c757d; +} + +/* Message footer for own messages (time below bubble) */ +.message-footer { + display: flex; + padding-right: 0.5rem; + margin-top: 0.25rem; +} + +.message-footer.own { + justify-content: flex-end; +} + +.message-footer .message-time { + font-size: 0.7rem; + color: #6c757d; +} + +/* Message action buttons container */ +.message-actions { + display: flex; + gap: 0.25rem; + margin-top: 0.25rem; +} + /* Message Bubbles */ .message { - max-width: 70%; padding: 0.75rem 1rem; border-radius: 1rem; border: 1px solid var(--msg-border); word-wrap: break-word; - animation: fadeIn 0.3s ease-in; } @keyframes fadeIn { @@ -55,35 +144,15 @@ main { /* Own Messages (right-aligned) */ .message.own { - align-self: flex-end; background-color: var(--msg-own-bg); border-color: #b8daff; } /* Other Messages (left-aligned) */ .message.other { - align-self: flex-start; background-color: var(--msg-other-bg); } -/* Message Header */ -.message-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0.25rem; - font-size: 0.875rem; -} - -.message-sender { - font-weight: 600; - color: #0d6efd; -} - -.message.own .message-sender { - color: #084298; -} - .message-time { font-size: 0.75rem; color: #6c757d; @@ -190,15 +259,28 @@ main { /* Responsive Design */ @media (max-width: 768px) { - .message, - .dm-message { - max-width: 85%; + .message-wrapper { + max-width: 90%; } - .message-header { + .message-avatar { + width: 32px; + height: 32px; + font-size: 0.75rem; + } + + .message-avatar.emoji { + font-size: 1.1rem; + } + + .message-sender { font-size: 0.8rem; } + .dm-message { + max-width: 85%; + } + #messageInput, #dmMessageInput { font-size: 0.9rem; diff --git a/app/static/js/app.js b/app/static/js/app.js index 539392a..326a7b5 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -658,8 +658,8 @@ function displayMessages(messages) { * Create message DOM element */ function createMessageElement(msg) { - const div = document.createElement('div'); - div.className = `message ${msg.is_own ? 'own' : 'other'}`; + const wrapper = document.createElement('div'); + wrapper.className = `message-wrapper ${msg.is_own ? 'own' : 'other'}`; const time = formatTime(msg.timestamp); @@ -671,31 +671,53 @@ function createMessageElement(msg) { metaInfo += ` | Hops: ${msg.path_len}`; } - div.innerHTML = ` -
- ${escapeHtml(msg.sender)} - ${time} -
-
${processMessageContent(msg.content)}
- ${metaInfo ? `
${metaInfo}
` : ''} - ${!msg.is_own ? ` -
- - - ${contactsGeoCache[msg.sender] ? ` - - ` : ''} + if (msg.is_own) { + // Own messages: right-aligned, no avatar + wrapper.innerHTML = ` +
+
+
${processMessageContent(msg.content)}
+
+
- ` : ''} - `; + `; + } else { + // Other messages: left-aligned with avatar + const avatar = generateAvatar(msg.sender); - return div; + wrapper.innerHTML = ` +
+ ${avatar.content} +
+
+
+ ${escapeHtml(msg.sender)} + ${time} +
+
+
${processMessageContent(msg.content)}
+ ${metaInfo ? `
${metaInfo}
` : ''} +
+
+ + + ${contactsGeoCache[msg.sender] ? ` + + ` : ''} +
+
+ `; + } + + return wrapper; } /** @@ -1407,6 +1429,78 @@ function escapeHtml(text) { return div.innerHTML; } +// ============================================================================= +// Avatar Generation Functions +// ============================================================================= + +/** + * Generate a consistent color based on string hash + * @param {string} str - Input string (username) + * @returns {string} HSL color string + */ +function getAvatarColor(str) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + // Generate hue from hash (0-360), keep saturation and lightness fixed for readability + const hue = Math.abs(hash) % 360; + return `hsl(${hue}, 65%, 45%)`; +} + +/** + * Extract first emoji from a string + * @param {string} str - Input string + * @returns {string|null} First emoji found or null + */ +function extractFirstEmoji(str) { + // Regex to match emojis (including compound emojis with ZWJ sequences) + const emojiRegex = /(\p{Emoji_Presentation}|\p{Emoji}\uFE0F)(\u200D(\p{Emoji_Presentation}|\p{Emoji}\uFE0F))*/u; + const match = str.match(emojiRegex); + return match ? match[0] : null; +} + +/** + * Get initials from a username + * @param {string} name - Username + * @returns {string} 1-2 character initials + */ +function getInitials(name) { + // Remove emojis first + const cleanName = name.replace(/(\p{Emoji_Presentation}|\p{Emoji}\uFE0F)(\u200D(\p{Emoji_Presentation}|\p{Emoji}\uFE0F))*/gu, '').trim(); + + if (!cleanName) return '?'; + + // Split by common separators (space, underscore, dash) + const parts = cleanName.split(/[\s_\-]+/).filter(p => p.length > 0); + + if (parts.length >= 2) { + // Two or more words: use first letter of first two words + return (parts[0][0] + parts[1][0]).toUpperCase(); + } else if (parts.length === 1) { + // Single word: use first letter only + return parts[0][0].toUpperCase(); + } + + return '?'; +} + +/** + * Generate avatar HTML for a username + * @param {string} name - Username + * @returns {object} { content: string, color: string } + */ +function generateAvatar(name) { + const emoji = extractFirstEmoji(name); + const color = getAvatarColor(name); + + if (emoji) { + return { content: emoji, color: color, isEmoji: true }; + } else { + return { content: getInitials(name), color: color, isEmoji: false }; + } +} + /** * Load last seen timestamps from server */