diff --git a/README.md b/README.md index 54ada42..d0e97cf 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,7 @@ Access the Direct Messages feature: **From channel messages:** - Click the "DM" button next to any message to start a private chat with that user - You'll be redirected to the DM page with that conversation selected +- **Note:** The DM button is only visible for users who are in your contacts list (meshcli: `contacts`). This ensures that direct messages will actually be delivered to the recipient. **Using the DM page:** 1. Select a conversation from the dropdown at the top (or one opens automatically if started from a message) diff --git a/app/meshcore/cli.py b/app/meshcore/cli.py index 4f7cc88..43fbd5c 100644 --- a/app/meshcore/cli.py +++ b/app/meshcore/cli.py @@ -121,6 +121,57 @@ def get_contacts() -> Tuple[bool, str]: return success, stdout or stderr +def parse_contacts(output: str) -> List[str]: + """ + Parse meshcli contacts output to extract contact names. + + Expected formats: + - "ContactName" (simple list) + - "ContactName (type)" (with type info) + - Any line containing contact information + + Args: + output: Raw output from meshcli contacts command + + Returns: + List of contact names (unique) + """ + contacts = [] + + for line in output.split('\n'): + line = line.strip() + + # Skip empty lines and potential headers + if not line or line.startswith('---') or line.lower().startswith('contact'): + continue + + # Extract contact name (before parentheses or special chars) + # Handle formats like "ContactName" or "ContactName (type)" + name_match = re.match(r'^([^\s()\[\]]+)', line) + if name_match: + contact_name = name_match.group(1).strip() + if contact_name and contact_name not in contacts: + contacts.append(contact_name) + + return contacts + + +def get_contacts_list() -> Tuple[bool, List[str], str]: + """ + Get parsed list of contact names from the device. + + Returns: + Tuple of (success, contact_names_list, error_message) + """ + success, output = get_contacts() + + if not success: + return False, [], output + + contacts = parse_contacts(output) + return True, contacts, "" + + def clean_inactive_contacts(hours: int = 48) -> Tuple[bool, str]: """ Remove contacts inactive for specified hours. diff --git a/app/routes/api.py b/app/routes/api.py index 87f6645..7991775 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -224,6 +224,39 @@ def get_status(): }), 500 +@api_bp.route('/contacts', methods=['GET']) +def get_contacts(): + """ + Get list of contacts from the device. + + Returns: + JSON with list of contact names + """ + try: + success, contacts, error = cli.get_contacts_list() + + if success: + return jsonify({ + 'success': True, + 'contacts': contacts, + 'count': len(contacts) + }), 200 + else: + return jsonify({ + 'success': False, + 'error': error or 'Failed to get contacts', + 'contacts': [] + }), 500 + + except Exception as e: + logger.error(f"Error getting contacts: {e}") + return jsonify({ + 'success': False, + 'error': str(e), + 'contacts': [] + }), 500 + + @api_bp.route('/contacts/cleanup', methods=['POST']) def cleanup_contacts(): """ diff --git a/app/static/js/app.js b/app/static/js/app.js index e19280b..3352ee1 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -11,6 +11,7 @@ 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 @@ -40,6 +41,9 @@ 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(); @@ -335,6 +339,9 @@ 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 = `
` : ''} `; @@ -957,6 +966,30 @@ 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 */