mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-03-28 17:42:45 +01:00
516 lines
25 KiB
HTML
516 lines
25 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
<title>Direct Messages - mc-webui</title>
|
|
|
|
<!-- Favicon -->
|
|
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='images/apple-touch-icon.png') }}">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/favicon-32x32.png') }}">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='images/favicon-16x16.png') }}">
|
|
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
|
|
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
|
|
|
<!-- Bootstrap 5 CSS (local) -->
|
|
<link href="{{ url_for('static', filename='vendor/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet">
|
|
<!-- Bootstrap Icons (local) -->
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/bootstrap-icons/bootstrap-icons.css') }}">
|
|
|
|
<!-- Leaflet CSS (for repeater map picker) -->
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
|
crossorigin="" />
|
|
|
|
<!-- Custom CSS -->
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
|
|
|
<!-- Emoji Picker (local) -->
|
|
<script type="module" src="{{ url_for('static', filename='vendor/emoji-picker-element/index.js') }}"></script>
|
|
<style>
|
|
emoji-picker {
|
|
--emoji-size: 1.5rem;
|
|
--num-columns: 8;
|
|
}
|
|
.emoji-picker-container {
|
|
position: relative;
|
|
}
|
|
.emoji-picker-popup {
|
|
position: absolute;
|
|
bottom: 100%;
|
|
right: 0;
|
|
z-index: 1000;
|
|
margin-bottom: 0.5rem;
|
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
|
border-radius: 0.5rem;
|
|
overflow: hidden;
|
|
}
|
|
.emoji-picker-popup.hidden {
|
|
display: none;
|
|
}
|
|
|
|
/* Mobile responsive adjustments */
|
|
@media (max-width: 576px) {
|
|
emoji-picker {
|
|
--emoji-size: 1.25rem;
|
|
--num-columns: 6;
|
|
}
|
|
.emoji-picker-popup {
|
|
right: auto;
|
|
left: 0;
|
|
width: 100%;
|
|
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;
|
|
}
|
|
|
|
/* Path management styles */
|
|
.path-list-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
padding: 0.35rem 0.5rem;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.25rem;
|
|
margin-bottom: 0.25rem;
|
|
font-size: 0.8rem;
|
|
background: #fff;
|
|
}
|
|
.path-list-item.primary {
|
|
border-color: #0d6efd;
|
|
background: #f0f7ff;
|
|
}
|
|
.path-list-item .path-hex {
|
|
font-family: monospace;
|
|
font-size: 0.75rem;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.path-list-item .path-label {
|
|
color: #6c757d;
|
|
font-size: 0.7rem;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.path-list-item .path-actions {
|
|
margin-left: auto;
|
|
display: flex;
|
|
gap: 0.15rem;
|
|
flex-shrink: 0;
|
|
}
|
|
.path-list-item .path-actions .btn {
|
|
padding: 0 0.25rem;
|
|
font-size: 0.7rem;
|
|
line-height: 1.2;
|
|
}
|
|
.path-section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.path-section-header h6 {
|
|
font-size: 0.85rem;
|
|
margin: 0;
|
|
}
|
|
.repeater-picker-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.4rem 0.5rem;
|
|
cursor: pointer;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
font-size: 0.8rem;
|
|
}
|
|
.repeater-picker-item:hover {
|
|
background-color: #e9ecef;
|
|
}
|
|
.repeater-picker-item .badge {
|
|
font-family: monospace;
|
|
font-size: 0.7rem;
|
|
}
|
|
.path-uniqueness-warning {
|
|
color: #dc3545;
|
|
font-size: 0.75rem;
|
|
}
|
|
/* Leaflet z-index fix for Bootstrap modal */
|
|
#rptLeafletMap { z-index: 1; }
|
|
#rptLeafletMap .leaflet-top,
|
|
#rptLeafletMap .leaflet-bottom { z-index: 1000; }
|
|
/* Map modal backdrop stacks above Contact Info modal */
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Main Content -->
|
|
<main>
|
|
<div class="container-fluid d-flex flex-column" style="height: 100vh;">
|
|
<!-- Main content: sidebar + chat -->
|
|
<div class="d-flex flex-grow-1 overflow-hidden" style="min-height: 0;">
|
|
<!-- DM Sidebar (visible on lg+ screens) -->
|
|
<div id="dmSidebar" class="dm-sidebar">
|
|
<div class="dm-sidebar-header">
|
|
<input type="text"
|
|
id="dmSidebarSearch"
|
|
class="form-control form-control-sm"
|
|
placeholder="Search contacts..."
|
|
autocomplete="off">
|
|
</div>
|
|
<div class="dm-sidebar-list" id="dmSidebarList">
|
|
<!-- Populated by JavaScript -->
|
|
</div>
|
|
</div>
|
|
<!-- Chat Area -->
|
|
<div class="flex-grow-1 d-flex flex-column" style="min-width: 0;">
|
|
<!-- Conversation Selector Bar (mobile only, hidden on lg+) -->
|
|
<div class="dm-mobile-selector border-bottom bg-light">
|
|
<div class="p-2">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<!-- Searchable contact selector -->
|
|
<div class="position-relative flex-grow-1" id="dmContactSearchWrapper">
|
|
<input type="text"
|
|
id="dmContactSearchInput"
|
|
class="form-control"
|
|
placeholder="Select chat..."
|
|
autocomplete="off">
|
|
<div id="dmContactDropdown" class="dm-contact-dropdown" style="display: none;"></div>
|
|
</div>
|
|
<!-- Clear search button -->
|
|
<button type="button"
|
|
class="btn btn-outline-secondary flex-shrink-0"
|
|
id="dmClearSearchBtn"
|
|
title="Clear selection"
|
|
style="display: none;">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
<!-- Contact info button -->
|
|
<button type="button"
|
|
class="btn btn-outline-secondary flex-shrink-0"
|
|
id="dmContactInfoBtn"
|
|
title="Contact info"
|
|
disabled>
|
|
<i class="bi bi-info-circle"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Desktop contact header (visible on lg+ when sidebar is shown) -->
|
|
<div class="dm-desktop-header border-bottom bg-light">
|
|
<div class="p-2 d-flex align-items-center gap-2">
|
|
<span id="dmDesktopContactName" class="fw-medium flex-grow-1 text-truncate"></span>
|
|
<button type="button"
|
|
class="btn btn-outline-secondary btn-sm flex-shrink-0"
|
|
id="dmDesktopInfoBtn"
|
|
title="Contact info"
|
|
disabled>
|
|
<i class="bi bi-info-circle"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<!-- Messages Container -->
|
|
<div class="flex-grow-1 position-relative overflow-hidden" style="min-height: 0;">
|
|
<!-- Filter bar overlay -->
|
|
<div id="dmFilterBar" class="filter-bar">
|
|
<div class="filter-bar-inner">
|
|
<input type="text" id="dmFilterInput" class="filter-bar-input" placeholder="Filter messages..." autocomplete="off">
|
|
<span id="dmFilterMatchCount" class="filter-match-count"></span>
|
|
<button type="button" id="dmFilterClearBtn" class="filter-bar-btn filter-bar-btn-clear" title="Clear">
|
|
<i class="bi bi-x"></i>
|
|
</button>
|
|
<button type="button" id="dmFilterCloseBtn" class="filter-bar-btn filter-bar-btn-close" title="Close">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="dmMessagesContainer" class="messages-container h-100 overflow-auto p-3">
|
|
<div id="dmMessagesList">
|
|
<!-- Placeholder shown when no conversation selected -->
|
|
<div class="dm-empty-state">
|
|
<i class="bi bi-envelope"></i>
|
|
<p class="mb-1">Select a conversation</p>
|
|
<small class="text-muted">Choose from the list or start a new chat from channel messages</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Scroll to bottom button -->
|
|
<button id="dmScrollToBottomBtn" class="scroll-to-bottom-btn" title="Scroll to bottom">
|
|
<i class="bi bi-chevron-double-down"></i>
|
|
</button>
|
|
</div>
|
|
<!-- Send Message Form -->
|
|
<div class="border-top bg-light">
|
|
<form id="dmSendForm" class="p-3">
|
|
<div class="emoji-picker-container">
|
|
<div class="input-group">
|
|
<textarea
|
|
id="dmMessageInput"
|
|
class="form-control"
|
|
placeholder="Type a message..."
|
|
rows="2"
|
|
maxlength="500"
|
|
disabled
|
|
></textarea>
|
|
<button type="button" class="btn btn-outline-secondary" id="dmEmojiBtn" title="Insert emoji">
|
|
<i class="bi bi-emoji-smile"></i>
|
|
</button>
|
|
<button type="submit" class="btn btn-success px-4" id="dmSendBtn" disabled>
|
|
<i class="bi bi-send"></i>
|
|
</button>
|
|
</div>
|
|
<!-- Emoji picker popup (hidden by default) -->
|
|
<div id="dmEmojiPickerPopup" class="emoji-picker-popup hidden"></div>
|
|
</div>
|
|
<div class="d-flex justify-content-end">
|
|
<small class="text-muted"><span id="dmCharCounter">0</span> / 150</small>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<!-- Status Bar -->
|
|
<div class="border-top">
|
|
<div class="p-2 small text-muted d-flex justify-content-between align-items-center">
|
|
<span id="dmStatusText">
|
|
<i class="bi bi-circle-fill text-secondary"></i> Connecting...
|
|
</span>
|
|
<span id="dmLastRefresh">Updated: Never</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Floating Action Buttons -->
|
|
<div class="fab-container" id="dmFabContainer">
|
|
<button class="fab fab-toggle" id="dmFabToggle" title="Hide buttons">
|
|
<i class="bi bi-chevron-right"></i>
|
|
</button>
|
|
<button class="fab fab-filter" id="dmFilterFab" title="Filter Messages">
|
|
<i class="bi bi-funnel-fill"></i>
|
|
</button>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Contact Info Modal -->
|
|
<div class="modal fade" id="dmContactInfoModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h6 class="modal-title"><i class="bi bi-person-circle"></i> Contact Info</h6>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="dmContactInfoBody"></div>
|
|
<!-- Path management section (populated dynamically) -->
|
|
<div class="modal-body border-top pt-2 pb-1" id="dmPathSection" style="display: none;">
|
|
<div class="path-section-header">
|
|
<h6><i class="bi bi-signpost-split"></i> Paths</h6>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" id="dmAddPathBtn" title="Add path">
|
|
<i class="bi bi-plus-lg"></i>
|
|
</button>
|
|
</div>
|
|
<div id="dmPathList"></div>
|
|
<!-- Path action buttons -->
|
|
<div class="d-flex justify-content-end gap-2 mt-1">
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" id="dmClearPathsBtn"
|
|
title="Delete all configured paths from database">
|
|
<i class="bi bi-trash"></i> Clear Paths
|
|
</button>
|
|
<button type="button" class="btn btn-outline-danger btn-sm" id="dmResetFloodBtn"
|
|
title="Reset device path to FLOOD mode">
|
|
<i class="bi bi-broadcast"></i> Reset to FLOOD
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<div class="d-flex align-items-center justify-content-between w-100">
|
|
<div class="d-flex gap-3">
|
|
<div class="form-check form-switch" title="Auto Retry: resend DM if no ACK received">
|
|
<input class="form-check-input" type="checkbox" id="dmAutoRetryToggle" checked>
|
|
<label class="form-check-label small" for="dmAutoRetryToggle">Auto Retry</label>
|
|
</div>
|
|
<div class="form-check form-switch" title="Keep path: don't auto-reset to FLOOD after failed retries">
|
|
<input class="form-check-input" type="checkbox" id="dmNoAutoFloodToggle">
|
|
<label class="form-check-label small" for="dmNoAutoFloodToggle">Keep path</label>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Repeater Map Picker Modal -->
|
|
<div class="modal fade" id="repeaterMapModal" tabindex="-1" style="z-index: 1080;">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header py-2">
|
|
<h6 class="modal-title"><i class="bi bi-geo-alt"></i> Select Repeater from Map</h6>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<div class="d-flex align-items-center gap-2 px-3 py-2 border-bottom bg-light">
|
|
<div class="form-check form-switch mb-0">
|
|
<input class="form-check-input" type="checkbox" id="rptMapCachedSwitch">
|
|
<label class="form-check-label small" for="rptMapCachedSwitch">Cached</label>
|
|
</div>
|
|
<span class="text-muted small ms-auto" id="rptMapCount"></span>
|
|
</div>
|
|
<div id="rptLeafletMap" style="height: 400px; width: 100%;"></div>
|
|
</div>
|
|
<div class="modal-footer py-2">
|
|
<span class="me-auto small text-muted" id="rptMapSelected">Click a repeater on the map</span>
|
|
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
<button type="button" class="btn btn-sm btn-primary" id="rptMapAddBtn" disabled>Add</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Path Modal -->
|
|
<div class="modal fade" id="addPathModal" tabindex="-1" style="z-index: 1070;">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header py-2">
|
|
<h6 class="modal-title"><i class="bi bi-signpost-split"></i> Add Path</h6>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="dmAddPathForm">
|
|
<div class="mb-2">
|
|
<label class="form-label small mb-1">Hash Size</label>
|
|
<div class="btn-group btn-group-sm w-100" role="group">
|
|
<input type="radio" class="btn-check" name="pathHashSize" id="pathHash1" value="1" checked>
|
|
<label class="btn btn-outline-secondary" for="pathHash1">1B (max 64)</label>
|
|
<input type="radio" class="btn-check" name="pathHashSize" id="pathHash2" value="2">
|
|
<label class="btn btn-outline-secondary" for="pathHash2">2B (max 32)</label>
|
|
<input type="radio" class="btn-check" name="pathHashSize" id="pathHash3" value="3">
|
|
<label class="btn btn-outline-secondary" for="pathHash3">3B (max 21)</label>
|
|
</div>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label small mb-1">Path (hex)</label>
|
|
<div class="input-group input-group-sm">
|
|
<input type="text" class="form-control font-monospace" id="dmPathHexInput"
|
|
placeholder="e.g. 5e,e7 or 5e34,e761" autocomplete="off">
|
|
<button type="button" class="btn btn-outline-secondary" id="dmPickRepeaterBtn"
|
|
title="Pick repeater from list">
|
|
<i class="bi bi-plus-circle"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary" id="dmPickRepeaterMapBtn"
|
|
title="Pick repeater from map">
|
|
<i class="bi bi-geo-alt"></i>
|
|
</button>
|
|
</div>
|
|
<div id="dmPathUniquenessWarning" class="path-uniqueness-warning mt-1" style="display: none;"></div>
|
|
</div>
|
|
<!-- Repeater picker (hidden by default) -->
|
|
<div id="dmRepeaterPicker" style="display: none;" class="border rounded mb-2">
|
|
<div class="d-flex border-bottom">
|
|
<div class="btn-group btn-group-sm flex-shrink-0" role="group">
|
|
<input type="radio" class="btn-check" name="repeaterSearchMode" id="rptSearchName" value="name" checked>
|
|
<label class="btn btn-outline-secondary border-0 rounded-0" for="rptSearchName">Name</label>
|
|
<input type="radio" class="btn-check" name="repeaterSearchMode" id="rptSearchId" value="id">
|
|
<label class="btn btn-outline-secondary border-0 rounded-0" for="rptSearchId">ID</label>
|
|
</div>
|
|
<input type="text" class="form-control form-control-sm border-0"
|
|
id="dmRepeaterSearch" placeholder="Search by name..." autocomplete="off">
|
|
</div>
|
|
<div id="dmRepeaterList" style="max-height: 180px; overflow-y: auto;"></div>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label small mb-1">Label (optional)</label>
|
|
<input type="text" class="form-control form-control-sm" id="dmPathLabelInput"
|
|
placeholder="e.g. via Mountain RPT" maxlength="50">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer py-2">
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="dmCancelPathBtn" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-sm btn-primary" id="dmSavePathBtn">Add Path</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toast container for notifications -->
|
|
<div class="toast-container position-fixed top-0 start-0 p-3">
|
|
<div id="notificationToast" class="toast" role="alert">
|
|
<div class="toast-header">
|
|
<strong class="me-auto">mc-webui</strong>
|
|
<button type="button" class="btn-close" data-bs-dismiss="toast"></button>
|
|
</div>
|
|
<div class="toast-body"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bootstrap 5 JS Bundle (local) -->
|
|
<script src="{{ url_for('static', filename='vendor/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
|
|
|
|
<!-- Message Content Processing Utilities (must load before dm.js) -->
|
|
<script src="{{ url_for('static', filename='js/message-utils.js') }}"></script>
|
|
|
|
<!-- Filter Utilities (must load before dm.js) -->
|
|
<script src="{{ url_for('static', filename='js/filter-utils.js') }}"></script>
|
|
|
|
<!-- Leaflet JS (for repeater map picker) -->
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
|
crossorigin=""></script>
|
|
|
|
<!-- SocketIO for real-time updates -->
|
|
<script src="{{ url_for('static', filename='vendor/socket.io/socket.io.min.js') }}"></script>
|
|
|
|
<!-- Custom JS -->
|
|
<script src="{{ url_for('static', filename='js/dm.js') }}"></script>
|
|
|
|
<script>
|
|
// Pass configuration from Flask to JavaScript
|
|
window.MC_CONFIG = {
|
|
deviceName: "{{ device_name }}",
|
|
initialConversation: "{{ initial_conversation or '' }}"
|
|
};
|
|
</script>
|
|
</body>
|
|
</html>
|