From 2788b92687edeb4bbf2d8136aa63d2ffb3766ee6 Mon Sep 17 00:00:00 2001 From: MarekWo Date: Thu, 25 Dec 2025 22:29:57 +0100 Subject: [PATCH] fix(dm): Improve DM UI consistency and fix conversation merging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix duplicate conversations in dropdown by merging pk_ and name_ IDs - Add intelligent refresh (only reload when new messages arrive) - Fix message alignment (own messages right, others left) - Change sent message status from 'timeout' to 'pending' - Add emoji picker button to DM page - Change message limit from 200 to 140 bytes (consistent with channels) - Update README.md with corrected DM documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- README.md | 11 ++-- app/meshcore/parser.py | 32 +++++++---- app/static/css/style.css | 7 +++ app/static/js/dm.js | 111 +++++++++++++++++++++++++++++++++++---- app/templates/dm.html | 68 ++++++++++++++++++++---- 5 files changed, 193 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index e53d8d9..4060abc 100644 --- a/README.md +++ b/README.md @@ -269,13 +269,14 @@ Access the Direct Messages feature: **Using the DM page:** 1. Select a conversation from the dropdown at the top (or one opens automatically if started from a message) -2. Type your message in the input field (max 200 bytes) -3. Press Enter or click Send -4. Click "Back" button to return to the main chat view +2. Type your message in the input field (max 140 bytes, same as channels) +3. Use the emoji picker button to insert emojis +4. Press Enter or click Send +5. Click "Back" button to return to the main chat view **Message status indicators:** -- ⏳ **Pending** (yellow) - Message sent, waiting for delivery confirmation -- ⏱️ **Timeout** (red) - Delivery confirmation not received within expected time +- ⏳ **Pending** (clock icon, yellow) - Message sent, awaiting delivery confirmation +- Note: Due to meshcore-cli limitations, we cannot track actual delivery status **Notifications:** - The bell icon shows a secondary green badge for unread DMs diff --git a/app/meshcore/parser.py b/app/meshcore/parser.py index 55f82a8..30a7f1d 100644 --- a/app/meshcore/parser.py +++ b/app/meshcore/parser.py @@ -452,8 +452,8 @@ def _parse_sent_dm_entry(entry: Dict) -> Optional[Dict]: text_hash = hash(text[:50]) & 0xFFFFFFFF dedup_key = f"sent_{timestamp}_{text_hash}" - # Status is always timeout for old messages (we don't have ACK tracking) - status = 'timeout' + # Keep the status from log file (pending by default, no ACK tracking available) + status = entry.get('status', 'pending') return { 'type': 'dm', @@ -610,21 +610,31 @@ def get_dm_conversations(days: Optional[int] = 7) -> List[Dict]: """ messages, pubkey_to_name = read_dm_messages(days=days) + # Build reverse mapping: name -> pubkey_prefix + name_to_pubkey = {name: pk for pk, name in pubkey_to_name.items()} + # Group messages by conversation + # Use canonical conversation_id: prefer pk_ if we know the pubkey for this name conversations = {} - for msg in messages: + def get_canonical_conv_id(msg): + """Get canonical conversation ID, preferring pubkey-based IDs.""" conv_id = msg['conversation_id'] - # For incoming messages with pubkey, also try to merge with name-based if conv_id.startswith('pk_'): - pk = conv_id[3:] - name = pubkey_to_name.get(pk) - # Check if there's a name-based conversation we should merge - name_conv_id = f"name_{name}" if name else None - if name_conv_id and name_conv_id in conversations: - # Merge into pubkey-based conversation - conversations[conv_id] = conversations.pop(name_conv_id) + return conv_id + + # For name-based IDs, check if we have a pubkey mapping + if conv_id.startswith('name_'): + name = conv_id[5:] + pk = name_to_pubkey.get(name) + if pk: + return f"pk_{pk}" + + return conv_id + + for msg in messages: + conv_id = get_canonical_conv_id(msg) if conv_id not in conversations: conversations[conv_id] = { diff --git a/app/static/css/style.css b/app/static/css/style.css index 5694512..2df6e5c 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -362,6 +362,13 @@ main { background-color: #fafafa; } +/* DM Messages List (flex container for message alignment) */ +#dmMessagesList { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + @media (max-width: 576px) { .dm-messages-container { height: calc(100vh - 200px); diff --git a/app/static/js/dm.js b/app/static/js/dm.js index 785ba87..277a267 100644 --- a/app/static/js/dm.js +++ b/app/static/js/dm.js @@ -9,6 +9,7 @@ let currentRecipient = null; let dmConversations = []; let dmLastSeenTimestamps = {}; let autoRefreshInterval = null; +let lastMessageTimestamp = 0; // Track latest message timestamp for smart refresh // Initialize on page load document.addEventListener('DOMContentLoaded', async function() { @@ -20,6 +21,9 @@ document.addEventListener('DOMContentLoaded', async function() { // Setup event listeners setupEventListeners(); + // Setup emoji picker + setupEmojiPicker(); + // Load conversations into dropdown await loadConversations(); @@ -272,9 +276,13 @@ function displayMessages(messages) { Send a message to start the conversation `; + lastMessageTimestamp = 0; return; } + // Update last message timestamp for smart refresh + lastMessageTimestamp = Math.max(...messages.map(m => m.timestamp)); + container.innerHTML = ''; messages.forEach(msg => { @@ -371,7 +379,8 @@ async function sendMessage() { } /** - * Setup auto-refresh + * Setup intelligent auto-refresh + * Only refreshes UI when new messages arrive */ function setupAutoRefresh() { const checkInterval = 10000; // 10 seconds @@ -380,17 +389,43 @@ function setupAutoRefresh() { // Reload conversations to update unread indicators await loadConversations(); - // If viewing a conversation, reload messages + // If viewing a conversation, check for new messages if (currentConversationId) { - await loadMessages(); + await checkForNewMessages(); } }, checkInterval); - console.log('Auto-refresh enabled'); + console.log('Intelligent auto-refresh enabled'); } /** - * Update character counter + * Check for new messages without full reload + * Only reloads UI when new messages are detected + */ +async function checkForNewMessages() { + if (!currentConversationId) return; + + try { + // Fetch only to check for updates + const response = await fetch(`/api/dm/messages?conversation_id=${encodeURIComponent(currentConversationId)}&limit=1`); + const data = await response.json(); + + if (data.success && data.messages && data.messages.length > 0) { + const latestTs = data.messages[data.messages.length - 1].timestamp; + + // Only reload if there are newer messages + if (latestTs > lastMessageTimestamp) { + console.log('New DM messages detected, refreshing...'); + await loadMessages(); + } + } + } catch (error) { + console.error('Error checking for new messages:', error); + } +} + +/** + * Update character counter (counts UTF-8 bytes, limit is 140) */ function updateCharCounter() { const input = document.getElementById('dmMessageInput'); @@ -399,19 +434,77 @@ function updateCharCounter() { const encoder = new TextEncoder(); const byteLength = encoder.encode(input.value).length; + const maxBytes = 140; counter.textContent = byteLength; - if (byteLength > 180) { + // Visual warning when approaching limit + if (byteLength >= maxBytes * 0.9) { counter.classList.add('text-danger'); - counter.classList.remove('text-warning'); - } else if (byteLength > 150) { - counter.classList.remove('text-danger'); + counter.classList.remove('text-warning', 'text-muted'); + } else if (byteLength >= maxBytes * 0.75) { + counter.classList.remove('text-danger', 'text-muted'); counter.classList.add('text-warning'); } else { counter.classList.remove('text-danger', 'text-warning'); + counter.classList.add('text-muted'); } } +/** + * Setup emoji picker + */ +function setupEmojiPicker() { + const emojiBtn = document.getElementById('dmEmojiBtn'); + const emojiPickerPopup = document.getElementById('dmEmojiPickerPopup'); + const messageInput = document.getElementById('dmMessageInput'); + + if (!emojiBtn || !emojiPickerPopup || !messageInput) { + console.log('Emoji picker elements not found'); + return; + } + + // Create emoji-picker element + const picker = document.createElement('emoji-picker'); + emojiPickerPopup.appendChild(picker); + + // Toggle emoji picker on button click + emojiBtn.addEventListener('click', function(e) { + e.stopPropagation(); + emojiPickerPopup.classList.toggle('hidden'); + }); + + // Insert emoji into input when selected + picker.addEventListener('emoji-click', function(event) { + const emoji = event.detail.unicode; + const cursorPos = messageInput.selectionStart; + const textBefore = messageInput.value.substring(0, cursorPos); + const textAfter = messageInput.value.substring(messageInput.selectionEnd); + + // Insert emoji at cursor position + messageInput.value = textBefore + emoji + textAfter; + + // Update cursor position (after emoji) + const newCursorPos = cursorPos + emoji.length; + messageInput.setSelectionRange(newCursorPos, newCursorPos); + + // Update character counter + updateCharCounter(); + + // Focus back on input + messageInput.focus(); + + // Hide picker after selection + emojiPickerPopup.classList.add('hidden'); + }); + + // Close emoji picker when clicking outside + document.addEventListener('click', function(e) { + if (!emojiPickerPopup.contains(e.target) && e.target !== emojiBtn && !emojiBtn.contains(e.target)) { + emojiPickerPopup.classList.add('hidden'); + } + }); +} + /** * Load DM last seen timestamps from localStorage */ diff --git a/app/templates/dm.html b/app/templates/dm.html index 892adea..ff30090 100644 --- a/app/templates/dm.html +++ b/app/templates/dm.html @@ -19,6 +19,45 @@ + + + + @@ -64,19 +103,26 @@
-
- - +
+
+ + + +
+ +
- 0 / 200 + 0 / 140