mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-03-28 17:42:45 +01:00
feat: Conditional DM button visibility based on contacts list
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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():
|
||||
"""
|
||||
|
||||
@@ -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 = `
|
||||
<div class="message-header">
|
||||
<span class="message-sender">${escapeHtml(msg.sender)}</span>
|
||||
@@ -347,9 +354,11 @@ 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>
|
||||
<button class="btn btn-outline-secondary btn-sm btn-reply" onclick="startDmTo('${escapeHtml(msg.sender)}')">
|
||||
<i class="bi bi-envelope"></i> DM
|
||||
</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>
|
||||
` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user