From e1d3534624558662f7fa706c2934c4ff87727c71 Mon Sep 17 00:00:00 2001 From: MarekWo Date: Tue, 17 Mar 2026 19:13:03 +0100 Subject: [PATCH] fix(dm): resolve contact names from device, not backend pubkeys - Add resolveConversationName() that prioritizes device contacts over backend display_name (which falls back to pubkey when DB JOIN fails) - Add isPubkey() guard to prevent overwriting good names with hex strings - Add arrow key navigation (Up/Down) in searchable contact dropdown - Auto-focus message input after selecting contact from dropdown - Skip filtering when search input contains a pubkey (show all contacts) - Keep search input and placeholder in sync with best known name Co-Authored-By: Claude Opus 4.6 --- app/static/js/dm.js | 115 ++++++++++++++++++++++++++++++-------------- 1 file changed, 80 insertions(+), 35 deletions(-) diff --git a/app/static/js/dm.js b/app/static/js/dm.js index b63caf7..499c3fb 100644 --- a/app/static/js/dm.js +++ b/app/static/js/dm.js @@ -19,10 +19,43 @@ let chatSocket = null; // SocketIO connection to /chat namespace */ function displayName(name) { if (!name) return 'Unknown'; - if (/^[0-9a-f]{64}$/i.test(name)) return name.substring(0, 8) + '...'; + if (/^[0-9a-f]{12,64}$/i.test(name)) return name.substring(0, 8) + '...'; return name; } +/** + * Check if a string looks like a hex pubkey (not a real name) + */ +function isPubkey(name) { + return !name || /^[0-9a-f]{8,64}$/i.test(name) || /^[0-9a-f]{6,}\.\.\./i.test(name); +} + +/** + * Resolve the best display name for a conversation ID. + * Priority: contactsList (device) > conversations API > pubkey fallback. + */ +function resolveConversationName(conversationId) { + // Try device contacts first (most reliable source of names) + const contact = findCurrentContactByConvId(conversationId); + if (contact && contact.name && !isPubkey(contact.name)) return contact.name; + + // Try conversations list + let conv = dmConversations.find(c => c.conversation_id === conversationId); + if (!conv && conversationId && conversationId.startsWith('pk_')) { + const prefix = conversationId.substring(3); + conv = dmConversations.find(c => + c.conversation_id.startsWith('pk_') && + (c.conversation_id.substring(3).startsWith(prefix) || prefix.startsWith(c.conversation_id.substring(3))) + ); + } + if (conv && conv.display_name && !isPubkey(conv.display_name)) return conv.display_name; + + // Fallback + if (conversationId && conversationId.startsWith('name_')) return conversationId.substring(5); + if (conversationId && conversationId.startsWith('pk_')) return conversationId.substring(3, 11) + '...'; + return 'Unknown'; +} + /** * Connect to SocketIO /chat namespace for real-time DM and ACK updates */ @@ -207,8 +240,23 @@ function setupEventListeners() { searchInput.blur(); } else if (e.key === 'Enter') { e.preventDefault(); - const first = contactDropdown.querySelector('.dm-contact-item'); - if (first) first.click(); + const active = contactDropdown.querySelector('.dm-contact-item.active'); + const target = active || contactDropdown.querySelector('.dm-contact-item'); + if (target) target.click(); + } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { + e.preventDefault(); + const items = Array.from(contactDropdown.querySelectorAll('.dm-contact-item')); + if (items.length === 0) return; + const activeIdx = items.findIndex(el => el.classList.contains('active')); + items.forEach(el => el.classList.remove('active')); + let nextIdx; + if (e.key === 'ArrowDown') { + nextIdx = activeIdx < 0 ? 0 : Math.min(activeIdx + 1, items.length - 1); + } else { + nextIdx = activeIdx <= 0 ? 0 : activeIdx - 1; + } + items[nextIdx].classList.add('active'); + items[nextIdx].scrollIntoView({ block: 'nearest' }); } }); } @@ -363,8 +411,10 @@ function populateConversationSelector() { window._dmDropdownItems = { conversations, contacts }; renderDropdownItems(''); - // Update search input if conversation is selected - if (currentConversationId && currentRecipient) { + // Update search input if conversation is selected — re-resolve name in case contacts loaded + if (currentConversationId) { + const bestName = resolveConversationName(currentConversationId); + if (!isPubkey(bestName)) currentRecipient = bestName; const input = document.getElementById('dmContactSearchInput'); if (input) input.value = displayName(currentRecipient); } @@ -379,13 +429,15 @@ function renderDropdownItems(query) { dropdown.innerHTML = ''; const q = query.toLowerCase().trim(); + // If query looks like a pubkey hex, don't filter — show all items instead + const qIsPubkey = /^[0-9a-f]{6,}\.{0,3}$/i.test(q); const { conversations = [], contacts = [] } = window._dmDropdownItems || {}; - const filteredConvs = q + const filteredConvs = (q && !qIsPubkey) ? conversations.filter(item => (item.name || '').toLowerCase().includes(q)) : conversations; - const filteredContacts = q + const filteredContacts = (q && !qIsPubkey) ? contacts.filter(c => (c.name || '').toLowerCase().includes(q)) : contacts; @@ -465,7 +517,10 @@ async function selectConversationFromDropdown(conversationId, name) { // Override search input with the known name (selectConversation may not resolve it) const input = document.getElementById('dmContactSearchInput'); if (input && name) input.value = displayName(name); - if (name) currentRecipient = name; + if (name && !isPubkey(name)) currentRecipient = name; + // Move focus to message input for immediate typing + const msgInput = document.getElementById('dmMessageInput'); + if (msgInput && !msgInput.disabled) msgInput.focus(); } /** @@ -477,16 +532,13 @@ async function selectConversation(conversationId) { // Save to localStorage for next visit localStorage.setItem('mc_active_dm_conversation', conversationId); - // Find the conversation to get recipient name (exact or prefix match) - let conv = dmConversations.find(c => c.conversation_id === conversationId); - if (!conv && conversationId.startsWith('pk_')) { - // Partial match: saved ID may have different prefix length than API + // Upgrade to full conversation_id if prefix match found + if (conversationId.startsWith('pk_')) { const prefix = conversationId.substring(3); - conv = dmConversations.find(c => + const conv = dmConversations.find(c => c.conversation_id.startsWith('pk_') && (c.conversation_id.substring(3).startsWith(prefix) || prefix.startsWith(c.conversation_id.substring(3))) ); - // Upgrade to full conversation_id if found if (conv) { conversationId = conv.conversation_id; currentConversationId = conversationId; @@ -494,21 +546,8 @@ async function selectConversation(conversationId) { } } - if (conv && conv.display_name) { - currentRecipient = conv.display_name; - } else { - // Try to find name from contactsList - const contact = findCurrentContactByConvId(conversationId); - if (contact && contact.name) { - currentRecipient = contact.name; - } else if (conversationId.startsWith('name_')) { - currentRecipient = conversationId.substring(5); - } else if (conversationId.startsWith('pk_')) { - currentRecipient = conversationId.substring(3, 11) + '...'; - } else { - currentRecipient = 'Unknown'; - } - } + // Resolve name: prefer device contacts over backend data + currentRecipient = resolveConversationName(conversationId); // Update search input const searchInput = document.getElementById('dmContactSearchInput'); @@ -718,13 +757,19 @@ async function loadMessages() { if (data.success) { displayMessages(data.messages); - // Update recipient if we got a better name - if (data.display_name && data.display_name !== 'Unknown') { + // Update recipient if backend has a better (non-pubkey) name + if (data.display_name && !isPubkey(data.display_name)) { currentRecipient = data.display_name; - const input = document.getElementById('dmMessageInput'); - if (input) { - input.placeholder = `Message ${displayName(currentRecipient)}...`; - } + } + // Always update placeholder with best known name + const msgInput = document.getElementById('dmMessageInput'); + if (msgInput) { + msgInput.placeholder = `Message ${displayName(currentRecipient)}...`; + } + // Keep search input in sync + const searchInput = document.getElementById('dmContactSearchInput'); + if (searchInput && !isPubkey(currentRecipient)) { + searchInput.value = displayName(currentRecipient); } // Mark as read