From 40a9b4e3bfcc14b6c4935b3f71e377122f700009 Mon Sep 17 00:00:00 2001 From: MarekWo Date: Sun, 28 Dec 2025 14:31:14 +0100 Subject: [PATCH] feat: Conditional DM button visibility based on contacts list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The DM button is now only shown for users who are in the device's contacts list, ensuring that direct messages will actually be delivered. This prevents users from attempting to send DMs to recipients who cannot receive them. Changes: - Added parse_contacts() and get_contacts_list() functions to cli.py for parsing meshcli contacts output - Created /api/contacts endpoint to retrieve contact names from device - Modified frontend app.js to fetch and cache contacts list on page load - Updated createMessageElement() to conditionally render DM button only when sender is in contacts list - Updated README.md with note about DM button visibility requirement 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- README.md | 1 + app/meshcore/cli.py | 51 ++++++++++++++++++++++++++++++++++++++++++++ app/routes/api.py | 33 ++++++++++++++++++++++++++++ app/static/js/app.js | 39 ++++++++++++++++++++++++++++++--- 4 files changed, 121 insertions(+), 3 deletions(-) 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 = `
${escapeHtml(msg.sender)} @@ -347,9 +354,11 @@ function createMessageElement(msg) { - + ${senderInContacts ? ` + + ` : ''}
` : ''} `; @@ -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 */