- opt.value = `name_${contactName}`;
- opt.textContent = contactName;
- selector.appendChild(opt);
+ filteredContacts.forEach(contact => {
+ const prefix = contact.public_key_prefix || contact.public_key?.substring(0, 12) || '';
+ const convId = `pk_${prefix}`;
+ dropdown.appendChild(createDropdownItem(
+ contact.name, convId, false, contact));
});
}
- // Show message if no conversations and no contacts
- if (dmConversations.length === 0 && contactsList.length === 0) {
- const opt = document.createElement('option');
- opt.value = '';
- opt.textContent = 'No contacts available';
- opt.disabled = true;
- selector.appendChild(opt);
+ if (filteredConvs.length === 0 && filteredContacts.length === 0) {
+ const empty = document.createElement('div');
+ empty.className = 'dm-dropdown-separator text-center';
+ empty.textContent = q ? 'No matches' : 'No contacts available';
+ dropdown.appendChild(empty);
+ }
+}
+
+/**
+ * Create a single dropdown item element.
+ */
+function createDropdownItem(name, conversationId, isUnread, contact) {
+ const el = document.createElement('div');
+ el.className = 'dm-contact-item';
+
+ if (isUnread) {
+ const dot = document.createElement('span');
+ dot.style.cssText = 'color: #0d6efd; font-weight: bold;';
+ dot.textContent = '*';
+ el.appendChild(dot);
}
- // If we have a current conversation, select it
- if (currentConversationId) {
- selector.value = currentConversationId;
+ const nameSpan = document.createElement('span');
+ nameSpan.className = 'contact-name';
+ nameSpan.textContent = displayName(name);
+ el.appendChild(nameSpan);
+
+ if (contact && contact.type_label) {
+ const badge = document.createElement('span');
+ badge.className = 'badge';
+ const colors = { CLI: 'bg-primary', REP: 'bg-success', ROOM: 'bg-info', SENS: 'bg-warning' };
+ badge.classList.add(colors[contact.type_label] || 'bg-secondary');
+ badge.textContent = contact.type_label;
+ el.appendChild(badge);
}
+
+ el.addEventListener('click', () => selectConversationFromDropdown(conversationId, name));
+ return el;
+}
+
+/**
+ * Handle selection from the searchable dropdown.
+ */
+function selectConversationFromDropdown(conversationId, name) {
+ const input = document.getElementById('dmContactSearchInput');
+ const dropdown = document.getElementById('dmContactDropdown');
+ if (input) input.value = displayName(name);
+ if (dropdown) dropdown.style.display = 'none';
+ selectConversation(conversationId);
}
/**
@@ -368,8 +474,11 @@ async function selectConversation(conversationId) {
if (conv && conv.display_name) {
currentRecipient = conv.display_name;
} else {
- // Extract name from conversation_id
- if (conversationId.startsWith('name_')) {
+ // 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) + '...';
@@ -378,11 +487,13 @@ async function selectConversation(conversationId) {
}
}
- // Update selector if not already selected
- const selector = document.getElementById('dmConversationSelector');
- if (selector && selector.value !== conversationId) {
- selector.value = conversationId;
- }
+ // Update search input
+ const searchInput = document.getElementById('dmContactSearchInput');
+ if (searchInput) searchInput.value = displayName(currentRecipient);
+
+ // Enable info button
+ const infoBtn = document.getElementById('dmContactInfoBtn');
+ if (infoBtn) infoBtn.disabled = false;
// Enable input
const input = document.getElementById('dmMessageInput');
@@ -409,6 +520,12 @@ function clearConversation() {
// Clear from localStorage
localStorage.removeItem('mc_active_dm_conversation');
+ // Reset search input and info button
+ const searchInput = document.getElementById('dmContactSearchInput');
+ if (searchInput) searchInput.value = '';
+ const infoBtn = document.getElementById('dmContactInfoBtn');
+ if (infoBtn) infoBtn.disabled = true;
+
// Disable input
const input = document.getElementById('dmMessageInput');
const sendBtn = document.getElementById('dmSendBtn');
@@ -436,6 +553,130 @@ function clearConversation() {
updateCharCounter();
}
+/**
+ * Find contact object matching a conversation ID.
+ */
+function findCurrentContactByConvId(convId) {
+ if (!convId) return null;
+ let pkPrefix = '';
+ if (convId.startsWith('pk_')) {
+ pkPrefix = convId.substring(3);
+ }
+ if (pkPrefix) {
+ return contactsList.find(c => c.public_key && c.public_key.startsWith(pkPrefix)) || null;
+ }
+ // Fallback: match by name
+ if (convId.startsWith('name_')) {
+ const name = convId.substring(5);
+ return contactsList.find(c => c.name === name) || null;
+ }
+ return null;
+}
+
+/**
+ * Find current contact from contactsList.
+ */
+function findCurrentContact() {
+ return findCurrentContactByConvId(currentConversationId);
+}
+
+/**
+ * Minimal relative time formatter.
+ */
+function formatRelativeTimeDm(timestamp) {
+ if (!timestamp) return 'Never';
+ const diff = Math.floor(Date.now() / 1000) - timestamp;
+ if (diff < 60) return 'Just now';
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
+ return `${Math.floor(diff / 86400)}d ago`;
+}
+
+/**
+ * Populate the Contact Info modal body.
+ */
+function populateContactInfoModal() {
+ const body = document.getElementById('dmContactInfoBody');
+ if (!body) return;
+
+ const contact = findCurrentContact();
+ if (!contact) {
+ body.innerHTML = 'No contact information available.
';
+ return;
+ }
+
+ body.innerHTML = '';
+
+ // Name + type badge
+ const nameRow = document.createElement('div');
+ nameRow.className = 'd-flex align-items-center gap-2 mb-3';
+ const nameEl = document.createElement('h6');
+ nameEl.className = 'mb-0';
+ nameEl.textContent = contact.name;
+ nameRow.appendChild(nameEl);
+
+ if (contact.type_label) {
+ const badge = document.createElement('span');
+ badge.className = 'badge';
+ const colors = { CLI: 'bg-primary', REP: 'bg-success', ROOM: 'bg-info', SENS: 'bg-warning' };
+ badge.classList.add(colors[contact.type_label] || 'bg-secondary');
+ badge.textContent = contact.type_label;
+ nameRow.appendChild(badge);
+ }
+ body.appendChild(nameRow);
+
+ // Public key
+ const keyDiv = document.createElement('div');
+ keyDiv.className = 'text-muted small font-monospace mb-2';
+ keyDiv.style.cursor = 'pointer';
+ keyDiv.textContent = contact.public_key_prefix || contact.public_key?.substring(0, 12) || '';
+ keyDiv.title = 'Click to copy full public key';
+ keyDiv.onclick = () => {
+ const pk = contact.public_key || contact.public_key_prefix || '';
+ navigator.clipboard.writeText(pk).then(() => {
+ showNotification('Public key copied', 'info');
+ }).catch(() => {});
+ };
+ body.appendChild(keyDiv);
+
+ // Last advert
+ if (contact.last_seen || contact.last_advert) {
+ const ts = contact.last_seen || contact.last_advert;
+ const diff = Math.floor(Date.now() / 1000) - ts;
+ let icon = '🔴';
+ if (diff < 300) icon = '🟢';
+ else if (diff < 3600) icon = '🟡';
+ const div = document.createElement('div');
+ div.className = 'small mb-2';
+ div.textContent = `${icon} Last advert: ${formatRelativeTimeDm(ts)}`;
+ body.appendChild(div);
+ }
+
+ // Path/route
+ if (contact.path_or_mode) {
+ const div = document.createElement('div');
+ div.className = 'small mb-2';
+ const mode = contact.path_or_mode;
+ if (mode === 'Flood') {
+ div.innerHTML = ' Flood';
+ } else if (mode === 'Direct') {
+ div.innerHTML = ' Direct';
+ } else {
+ const hops = mode.split('→').length;
+ div.innerHTML = ` ${mode} (${hops} hops)`;
+ }
+ body.appendChild(div);
+ }
+
+ // GPS
+ if (contact.adv_lat && contact.adv_lon && (contact.adv_lat !== 0 || contact.adv_lon !== 0)) {
+ const div = document.createElement('div');
+ div.className = 'small mb-2';
+ div.innerHTML = ` ${contact.adv_lat.toFixed(4)}, ${contact.adv_lon.toFixed(4)}`;
+ body.appendChild(div);
+ }
+}
+
/**
* Load messages for current conversation
*/
diff --git a/app/templates/dm.html b/app/templates/dm.html
index 970f1f1..ac93b01 100644
--- a/app/templates/dm.html
+++ b/app/templates/dm.html
@@ -57,6 +57,50 @@
max-width: 100%;
}
}
+
+ /* Searchable contact dropdown */
+ .dm-contact-dropdown {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ z-index: 1050;
+ max-height: 300px;
+ overflow-y: auto;
+ background: #fff;
+ border: 1px solid #dee2e6;
+ border-top: none;
+ border-radius: 0 0 0.375rem 0.375rem;
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
+ }
+ .dm-contact-item {
+ padding: 0.5rem 0.75rem;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ border-bottom: 1px solid #f0f0f0;
+ }
+ .dm-contact-item:hover,
+ .dm-contact-item.active {
+ background-color: #e9ecef;
+ }
+ .dm-contact-item .contact-name {
+ flex-grow: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .dm-contact-item .badge {
+ font-size: 0.7rem;
+ }
+ .dm-dropdown-separator {
+ padding: 0.25rem 0.75rem;
+ font-size: 0.75rem;
+ color: #6c757d;
+ background: #f8f9fa;
+ font-weight: 600;
+ }
@@ -67,14 +111,23 @@
+
+
+