refactor(dm): Show all contacts in DM dropdown selector

Changed DM approach from conditional button visibility to showing all
available contacts directly in the DM page dropdown. This provides better
UX and performance.

Changes:
- Reverted conditional DM button visibility in app.js (button always shows)
- Removed contacts loading from main page (app.js)
- Added loadContacts() function to dm.js to fetch contacts from API
- Modified populateConversationSelector() to show:
  1. Existing conversations (with history) first
  2. Separator: "--- Available contacts ---"
  3. All contacts from device who aren't in conversations yet
- Users can now start new DM conversations with any contact
- Updated README.md with new DM workflow description

Benefits:
- Simpler and more intuitive UX
- Better performance (no checks on every message)
- Users can proactively start conversations
- Clear visibility of who's available for DM

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2025-12-28 15:15:50 +01:00
parent 94528c2a96
commit 2e009b2d3e
4 changed files with 893 additions and 65 deletions
+3 -36
View File
@@ -11,7 +11,6 @@ let currentChannelIdx = 0; // Current active channel (0 = Public)
let availableChannels = []; // List of channels from API
let lastSeenTimestamps = {}; // Track last seen message timestamp per channel
let unreadCounts = {}; // Track unread message counts per channel
let contactsList = []; // List of contacts from API (for DM button visibility)
// DM state (for badge updates on main page)
let dmLastSeenTimestamps = {}; // Track last seen DM timestamp per conversation
@@ -41,9 +40,6 @@ document.addEventListener('DOMContentLoaded', async function() {
// This ensures channels are available for checkForUpdates()
await loadChannels();
// Load contacts list (for DM button visibility)
loadContacts();
// Now load other data (can run in parallel)
loadArchiveList();
loadMessages();
@@ -339,9 +335,6 @@ function createMessageElement(msg) {
metaInfo += ` | Hops: ${msg.path_len}`;
}
// Check if sender is in contacts (for DM button visibility)
const senderInContacts = contactsList.includes(msg.sender);
div.innerHTML = `
<div class="message-header">
<span class="message-sender">${escapeHtml(msg.sender)}</span>
@@ -354,11 +347,9 @@ function createMessageElement(msg) {
<button class="btn btn-outline-secondary btn-sm btn-reply" onclick="replyTo('${escapeHtml(msg.sender)}')">
<i class="bi bi-reply"></i> Reply
</button>
${senderInContacts ? `
<button class="btn btn-outline-secondary btn-sm btn-reply" onclick="startDmTo('${escapeHtml(msg.sender)}')">
<i class="bi bi-envelope"></i> DM
</button>
` : ''}
<button class="btn btn-outline-secondary btn-sm btn-reply" onclick="startDmTo('${escapeHtml(msg.sender)}')">
<i class="bi bi-envelope"></i> DM
</button>
</div>
` : ''}
`;
@@ -966,30 +957,6 @@ async function loadChannels() {
}
}
/**
* Load contacts list from device
* This is used to determine if DM button should be shown for a sender
*/
async function loadContacts() {
try {
console.log('[loadContacts] Fetching contacts from API...');
const response = await fetch('/api/contacts');
const data = await response.json();
if (data.success) {
contactsList = data.contacts || [];
console.log(`[loadContacts] Loaded ${contactsList.length} contacts:`, contactsList);
} else {
console.error('[loadContacts] Error loading contacts:', data.error);
contactsList = [];
}
} catch (error) {
console.error('[loadContacts] Exception:', error.message || error);
contactsList = [];
}
}
/**
* Fallback: ensure Public channel exists in dropdown even if API fails
*/
+90 -27
View File
@@ -7,6 +7,7 @@
let currentConversationId = null;
let currentRecipient = null;
let dmConversations = [];
let contactsList = []; // List of all contacts from device
let dmLastSeenTimestamps = {};
let autoRefreshInterval = null;
let lastMessageTimestamp = 0; // Track latest message timestamp for smart refresh
@@ -113,19 +114,47 @@ function setupEventListeners() {
}
}
/**
* Load contacts from device
*/
async function loadContacts() {
try {
const response = await fetch('/api/contacts');
const data = await response.json();
if (data.success) {
contactsList = data.contacts || [];
console.log(`[DM] Loaded ${contactsList.length} contacts:`, contactsList);
} else {
console.error('[DM] Failed to load contacts:', data.error);
contactsList = [];
}
} catch (error) {
console.error('[DM] Error loading contacts:', error);
contactsList = [];
}
}
/**
* Load conversations from API
*/
async function loadConversations() {
try {
const response = await fetch('/api/dm/conversations?days=7');
const data = await response.json();
// Load both conversations and contacts in parallel
const [convResponse, _] = await Promise.all([
fetch('/api/dm/conversations?days=7'),
loadContacts()
]);
if (data.success) {
dmConversations = data.conversations || [];
const convData = await convResponse.json();
if (convData.success) {
dmConversations = convData.conversations || [];
populateConversationSelector();
} else {
console.error('Failed to load conversations:', data.error);
console.error('Failed to load conversations:', convData.error);
// Still populate selector with just contacts
populateConversationSelector();
}
} catch (error) {
console.error('Error loading conversations:', error);
@@ -134,39 +163,73 @@ async function loadConversations() {
/**
* Populate the conversation selector dropdown
* Shows both existing conversations and all contacts
*/
function populateConversationSelector() {
const selector = document.getElementById('dmConversationSelector');
if (!selector) return;
// Clear existing options (keep first placeholder)
// Clear existing options
selector.innerHTML = '<option value="">Select chat...</option>';
if (dmConversations.length === 0) {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = 'No conversations yet';
opt.disabled = true;
selector.appendChild(opt);
return;
// Track which names are already in conversations
const conversationNames = new Set();
// 1. Add existing conversations (with history)
if (dmConversations.length > 0) {
dmConversations.forEach(conv => {
const opt = document.createElement('option');
opt.value = conv.conversation_id;
// Show unread indicator
const lastSeen = dmLastSeenTimestamps[conv.conversation_id] || 0;
const isUnread = conv.last_message_timestamp > lastSeen;
let label = conv.display_name;
if (isUnread) {
label = `* ${label}`;
}
opt.textContent = label;
selector.appendChild(opt);
// Track this name
conversationNames.add(conv.display_name);
});
}
dmConversations.forEach(conv => {
// 2. Add separator if we have both conversations and contacts
if (dmConversations.length > 0 && contactsList.length > 0) {
const separator = document.createElement('option');
separator.disabled = true;
separator.textContent = '--- Available contacts ---';
selector.appendChild(separator);
}
// 3. Add all contacts from device (skip those already in conversations)
if (contactsList.length > 0) {
contactsList.forEach(contactName => {
// Skip if already in conversations
if (conversationNames.has(contactName)) {
return;
}
const opt = document.createElement('option');
// Create conversation_id as name_<contactName>
opt.value = `name_${contactName}`;
opt.textContent = contactName;
selector.appendChild(opt);
});
}
// Show message if no conversations and no contacts
if (dmConversations.length === 0 && contactsList.length === 0) {
const opt = document.createElement('option');
opt.value = conv.conversation_id;
// Show unread indicator
const lastSeen = dmLastSeenTimestamps[conv.conversation_id] || 0;
const isUnread = conv.last_message_timestamp > lastSeen;
let label = conv.display_name;
if (isUnread) {
label = `* ${label}`;
}
opt.textContent = label;
opt.value = '';
opt.textContent = 'No contacts available';
opt.disabled = true;
selector.appendChild(opt);
});
}
// If we have a current conversation, select it
if (currentConversationId) {