mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-03-28 17:42:45 +01:00
feat(ui): add channel/contact sidebar for wide screens (desktop/tablet)
On screens >= 992px (lg breakpoint), show a persistent sidebar panel: - Group chat: channel list with unread badges, active highlight, muted state - DM: conversation/contact list with search, unread dots, type badges - Desktop contact header with info button replaces mobile selector - Mobile/narrow screens unchanged (dropdown/top selector still used) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,183 @@ main {
|
||||
min-height: 0; /* Important for flex children */
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
Channel Sidebar (Group Chat) - visible on lg+ screens
|
||||
============================================================================= */
|
||||
|
||||
.channel-sidebar {
|
||||
display: none;
|
||||
width: 250px;
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid #dee2e6;
|
||||
background: #f8f9fa;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.channel-sidebar-header {
|
||||
padding: 0.6rem 0.75rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
color: #495057;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.channel-sidebar-list {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.channel-sidebar-item {
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-size: 0.85rem;
|
||||
transition: background-color 0.15s;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.channel-sidebar-item:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.channel-sidebar-item.active {
|
||||
background-color: #e7f1ff;
|
||||
border-left: 3px solid #0d6efd;
|
||||
padding-left: calc(0.75rem - 3px);
|
||||
font-weight: 500;
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
.channel-sidebar-item.muted {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.channel-sidebar-item .channel-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.channel-sidebar-item .sidebar-unread-badge {
|
||||
background-color: #0d6efd;
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
padding: 1px 6px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: bold;
|
||||
min-width: 18px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Show sidebar and hide dropdown on wide screens */
|
||||
@media (min-width: 992px) {
|
||||
.channel-sidebar {
|
||||
display: flex;
|
||||
}
|
||||
#channelSelector {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
DM Sidebar (Direct Messages) - visible on lg+ screens
|
||||
============================================================================= */
|
||||
|
||||
.dm-sidebar {
|
||||
display: none;
|
||||
width: 280px;
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid #dee2e6;
|
||||
background: #f8f9fa;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dm-sidebar-header {
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.dm-sidebar-list {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dm-sidebar-separator {
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
color: #6c757d;
|
||||
background: #f0f0f0;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.dm-sidebar-item {
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-size: 0.85rem;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.dm-sidebar-item:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.dm-sidebar-item.active {
|
||||
background-color: #e7f5ee;
|
||||
border-left: 3px solid #198754;
|
||||
padding-left: calc(0.75rem - 3px);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dm-sidebar-item .contact-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dm-sidebar-item .sidebar-unread-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #0d6efd;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dm-sidebar-item .badge {
|
||||
font-size: 0.65rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Show DM sidebar and hide mobile selector on wide screens */
|
||||
@media (min-width: 992px) {
|
||||
.dm-sidebar {
|
||||
display: flex;
|
||||
}
|
||||
.dm-mobile-selector {
|
||||
display: none !important;
|
||||
}
|
||||
.dm-desktop-header {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dm-desktop-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Messages Container */
|
||||
.messages-container {
|
||||
background-color: #ffffff;
|
||||
|
||||
@@ -689,11 +689,12 @@ function setupEventListeners() {
|
||||
loadDeviceInfo();
|
||||
});
|
||||
|
||||
// Channel selector
|
||||
// Channel selector (dropdown, visible on mobile)
|
||||
document.getElementById('channelSelector').addEventListener('change', function(e) {
|
||||
currentChannelIdx = parseInt(e.target.value);
|
||||
localStorage.setItem('mc_active_channel', currentChannelIdx);
|
||||
loadMessages();
|
||||
updateChannelSidebarActive();
|
||||
|
||||
// Show notification only if we have a valid selection
|
||||
const selectedOption = e.target.options[e.target.selectedIndex];
|
||||
@@ -2888,6 +2889,9 @@ function updateUnreadBadges() {
|
||||
|
||||
// Update app icon badge
|
||||
updateAppBadge();
|
||||
|
||||
// Update channel sidebar badges (lg+ screens)
|
||||
updateChannelSidebarBadges();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3111,6 +3115,9 @@ function populateChannelSelector(channels) {
|
||||
}
|
||||
|
||||
console.log(`[populateChannelSelector] Loaded ${channels.length} channels, active: ${currentChannelIdx}`);
|
||||
|
||||
// Also populate sidebar (lg+ screens)
|
||||
populateChannelSidebar();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3179,6 +3186,104 @@ function displayChannelsList(channels) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate channel sidebar (visible on lg+ screens)
|
||||
*/
|
||||
function populateChannelSidebar() {
|
||||
const list = document.getElementById('channelSidebarList');
|
||||
if (!list) return;
|
||||
|
||||
list.innerHTML = '';
|
||||
|
||||
const channels = availableChannels.length > 0
|
||||
? availableChannels
|
||||
: [{index: 0, name: 'Public', key: ''}];
|
||||
|
||||
channels.forEach(channel => {
|
||||
if (!channel || typeof channel.index === 'undefined' || !channel.name) return;
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'channel-sidebar-item';
|
||||
item.dataset.channelIdx = channel.index;
|
||||
|
||||
if (channel.index === currentChannelIdx) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
if (mutedChannels.has(channel.index)) {
|
||||
item.classList.add('muted');
|
||||
}
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'channel-name';
|
||||
nameSpan.textContent = channel.name;
|
||||
item.appendChild(nameSpan);
|
||||
|
||||
// Unread badge
|
||||
const unread = unreadCounts[channel.index] || 0;
|
||||
if (unread > 0 && channel.index !== currentChannelIdx && !mutedChannels.has(channel.index)) {
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'sidebar-unread-badge';
|
||||
badge.textContent = unread;
|
||||
item.appendChild(badge);
|
||||
}
|
||||
|
||||
item.addEventListener('click', () => {
|
||||
currentChannelIdx = channel.index;
|
||||
localStorage.setItem('mc_active_channel', currentChannelIdx);
|
||||
loadMessages();
|
||||
updateChannelSidebarActive();
|
||||
// Also sync dropdown for consistency
|
||||
const selector = document.getElementById('channelSelector');
|
||||
if (selector) selector.value = currentChannelIdx;
|
||||
});
|
||||
|
||||
list.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update active state on channel sidebar items
|
||||
*/
|
||||
function updateChannelSidebarActive() {
|
||||
const list = document.getElementById('channelSidebarList');
|
||||
if (!list) return;
|
||||
|
||||
list.querySelectorAll('.channel-sidebar-item').forEach(item => {
|
||||
const idx = parseInt(item.dataset.channelIdx);
|
||||
item.classList.toggle('active', idx === currentChannelIdx);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update unread badges on channel sidebar
|
||||
*/
|
||||
function updateChannelSidebarBadges() {
|
||||
const list = document.getElementById('channelSidebarList');
|
||||
if (!list) return;
|
||||
|
||||
list.querySelectorAll('.channel-sidebar-item').forEach(item => {
|
||||
const idx = parseInt(item.dataset.channelIdx);
|
||||
const unread = unreadCounts[idx] || 0;
|
||||
const isMuted = mutedChannels.has(idx);
|
||||
|
||||
// Update muted state
|
||||
item.classList.toggle('muted', isMuted);
|
||||
|
||||
// Update or remove badge
|
||||
let badge = item.querySelector('.sidebar-unread-badge');
|
||||
if (unread > 0 && idx !== currentChannelIdx && !isMuted) {
|
||||
if (!badge) {
|
||||
badge = document.createElement('span');
|
||||
badge.className = 'sidebar-unread-badge';
|
||||
item.appendChild(badge);
|
||||
}
|
||||
badge.textContent = unread;
|
||||
} else if (badge) {
|
||||
badge.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle mute state for a channel
|
||||
*/
|
||||
|
||||
@@ -324,6 +324,25 @@ function setupEventListeners() {
|
||||
scrollToBottomBtn.classList.remove('visible');
|
||||
});
|
||||
}
|
||||
|
||||
// DM Sidebar search input (lg+ screens)
|
||||
const sidebarSearch = document.getElementById('dmSidebarSearch');
|
||||
if (sidebarSearch) {
|
||||
sidebarSearch.addEventListener('input', () => {
|
||||
populateDmSidebar(sidebarSearch.value);
|
||||
});
|
||||
}
|
||||
|
||||
// Desktop info button (lg+ screens)
|
||||
const desktopInfoBtn = document.getElementById('dmDesktopInfoBtn');
|
||||
if (desktopInfoBtn) {
|
||||
desktopInfoBtn.addEventListener('click', () => {
|
||||
const modal = new bootstrap.Modal(document.getElementById('dmContactInfoModal'));
|
||||
populateContactInfoModal();
|
||||
loadPathSection();
|
||||
modal.show();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -423,12 +442,16 @@ function populateConversationSelector() {
|
||||
window._dmDropdownItems = { conversations, contacts };
|
||||
renderDropdownItems('');
|
||||
|
||||
// Also populate DM sidebar (lg+ screens)
|
||||
populateDmSidebar('');
|
||||
|
||||
// Update search input if conversation is selected — re-resolve name in case contacts loaded
|
||||
if (currentConversationId) {
|
||||
const bestName = resolveConversationName(currentConversationId);
|
||||
if (!isPubkey(bestName)) currentRecipient = bestName;
|
||||
const input = document.getElementById('dmContactSearchInput');
|
||||
if (input) input.value = displayName(currentRecipient);
|
||||
updateDmDesktopHeader();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,6 +542,149 @@ function createDropdownItem(name, conversationId, isUnread, contact) {
|
||||
return el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the DM sidebar (visible on lg+ screens).
|
||||
* Mirrors the dropdown data structure but renders as a persistent list.
|
||||
*/
|
||||
function populateDmSidebar(query) {
|
||||
const list = document.getElementById('dmSidebarList');
|
||||
if (!list) return;
|
||||
|
||||
list.innerHTML = '';
|
||||
|
||||
const q = (query || '').toLowerCase().trim();
|
||||
const { conversations = [], contacts = [] } = window._dmDropdownItems || {};
|
||||
|
||||
const filteredConvs = q
|
||||
? conversations.filter(item => (item.name || '').toLowerCase().includes(q))
|
||||
: conversations;
|
||||
|
||||
const filteredContacts = q
|
||||
? contacts.filter(c => (c.name || '').toLowerCase().includes(q))
|
||||
: contacts;
|
||||
|
||||
if (filteredConvs.length > 0) {
|
||||
const sep = document.createElement('div');
|
||||
sep.className = 'dm-sidebar-separator';
|
||||
sep.textContent = 'Recent conversations';
|
||||
list.appendChild(sep);
|
||||
|
||||
filteredConvs.forEach(item => {
|
||||
list.appendChild(createSidebarItem(
|
||||
item.name, item.conversationId, item.isUnread, item.contact));
|
||||
});
|
||||
}
|
||||
|
||||
if (filteredContacts.length > 0) {
|
||||
const sep = document.createElement('div');
|
||||
sep.className = 'dm-sidebar-separator';
|
||||
sep.textContent = 'Contacts';
|
||||
list.appendChild(sep);
|
||||
|
||||
filteredContacts.forEach(contact => {
|
||||
const prefix = contact.public_key_prefix || contact.public_key?.substring(0, 12) || '';
|
||||
const convId = `pk_${prefix}`;
|
||||
list.appendChild(createSidebarItem(
|
||||
contact.name, convId, false, contact));
|
||||
});
|
||||
}
|
||||
|
||||
if (filteredConvs.length === 0 && filteredContacts.length === 0) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'dm-sidebar-separator text-center';
|
||||
empty.textContent = q ? 'No matches' : 'No contacts available';
|
||||
list.appendChild(empty);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single sidebar item element for the DM sidebar.
|
||||
*/
|
||||
function createSidebarItem(name, conversationId, isUnread, contact) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'dm-sidebar-item';
|
||||
el.dataset.conversationId = conversationId;
|
||||
|
||||
if (conversationId === currentConversationId) {
|
||||
el.classList.add('active');
|
||||
}
|
||||
|
||||
if (isUnread) {
|
||||
const dot = document.createElement('span');
|
||||
dot.className = 'sidebar-unread-dot';
|
||||
el.appendChild(dot);
|
||||
}
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'contact-name';
|
||||
nameSpan.textContent = displayName(name);
|
||||
el.appendChild(nameSpan);
|
||||
|
||||
if (contact && contact.type_label) {
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'badge';
|
||||
const colors = { COM: 'bg-primary', REP: 'bg-success', ROOM: 'bg-info', SENS: 'bg-warning' };
|
||||
badge.classList.add(colors[contact.type_label] || 'bg-secondary');
|
||||
badge.textContent = contact.type_label;
|
||||
el.appendChild(badge);
|
||||
}
|
||||
|
||||
el.addEventListener('click', () => selectConversationFromSidebar(conversationId, name));
|
||||
return el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle selection from the DM sidebar.
|
||||
*/
|
||||
async function selectConversationFromSidebar(conversationId, name) {
|
||||
await selectConversation(conversationId);
|
||||
if (name && !isPubkey(name)) currentRecipient = name;
|
||||
updateDmSidebarActive();
|
||||
// Move focus to message input
|
||||
const msgInput = document.getElementById('dmMessageInput');
|
||||
if (msgInput && !msgInput.disabled) msgInput.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update active state on DM sidebar items.
|
||||
*/
|
||||
function updateDmSidebarActive() {
|
||||
const list = document.getElementById('dmSidebarList');
|
||||
if (!list) return;
|
||||
|
||||
list.querySelectorAll('.dm-sidebar-item').forEach(item => {
|
||||
const convId = item.dataset.conversationId;
|
||||
// Flexible matching: handle prefix upgrades
|
||||
let isActive = convId === currentConversationId;
|
||||
if (!isActive && currentConversationId && convId) {
|
||||
// Match if one is a prefix of the other (pk_ based)
|
||||
if (convId.startsWith('pk_') && currentConversationId.startsWith('pk_')) {
|
||||
const a = convId.substring(3);
|
||||
const b = currentConversationId.substring(3);
|
||||
isActive = a.startsWith(b) || b.startsWith(a);
|
||||
}
|
||||
}
|
||||
item.classList.toggle('active', isActive);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the desktop contact header (visible on lg+ screens).
|
||||
*/
|
||||
function updateDmDesktopHeader() {
|
||||
const nameEl = document.getElementById('dmDesktopContactName');
|
||||
const infoBtn = document.getElementById('dmDesktopInfoBtn');
|
||||
if (!nameEl) return;
|
||||
|
||||
if (currentRecipient) {
|
||||
nameEl.textContent = displayName(currentRecipient);
|
||||
if (infoBtn) infoBtn.disabled = false;
|
||||
} else {
|
||||
nameEl.textContent = '';
|
||||
if (infoBtn) infoBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle selection from the searchable dropdown.
|
||||
*/
|
||||
@@ -582,6 +748,10 @@ async function selectConversation(conversationId) {
|
||||
sendBtn.disabled = false;
|
||||
}
|
||||
|
||||
// Update desktop header and sidebar (lg+ screens)
|
||||
updateDmDesktopHeader();
|
||||
updateDmSidebarActive();
|
||||
|
||||
// Load messages
|
||||
await loadMessages();
|
||||
}
|
||||
@@ -623,11 +793,15 @@ function clearConversation() {
|
||||
<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 dropdown above or start a new chat from channel messages</small>
|
||||
<small class="text-muted">Choose from the list or start a new chat from channel messages</small>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Update desktop header and sidebar
|
||||
updateDmDesktopHeader();
|
||||
updateDmSidebarActive();
|
||||
|
||||
updateCharCounter();
|
||||
}
|
||||
|
||||
|
||||
@@ -189,110 +189,134 @@
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
<div class="container-fluid d-flex flex-column" style="height: 100vh;">
|
||||
<!-- Conversation Selector Bar -->
|
||||
<div class="row border-bottom bg-light">
|
||||
<div class="col-12 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>
|
||||
<!-- 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>
|
||||
</div>
|
||||
<!-- Messages Container -->
|
||||
<div class="row flex-grow-1 overflow-hidden" style="min-height: 0;">
|
||||
<div class="col-12 position-relative" style="height: 100%;">
|
||||
<!-- 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>
|
||||
<!-- 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 d-none">
|
||||
<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>
|
||||
<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 dropdown above or start a new chat from channel messages</small>
|
||||
<!-- 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>
|
||||
<!-- 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>
|
||||
</div>
|
||||
|
||||
<!-- Send Message Form -->
|
||||
<div class="row border-top bg-light">
|
||||
<div class="col-12">
|
||||
<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>
|
||||
<!-- 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>
|
||||
<!-- Emoji picker popup (hidden by default) -->
|
||||
<div id="dmEmojiPickerPopup" class="emoji-picker-popup hidden"></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 class="d-flex justify-content-end">
|
||||
<small class="text-muted"><span id="dmCharCounter">0</span> / 150</small>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<div class="row border-top">
|
||||
<div class="col-12">
|
||||
<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>
|
||||
|
||||
@@ -73,92 +73,99 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid d-flex flex-column" style="height: 100%;">
|
||||
<!-- Messages Container -->
|
||||
<div class="row flex-grow-1 overflow-hidden" style="min-height: 0;">
|
||||
<div class="col-12 position-relative" style="height: 100%;">
|
||||
<!-- Filter bar overlay -->
|
||||
<div id="filterBar" class="filter-bar">
|
||||
<div class="filter-bar-inner">
|
||||
<div class="filter-input-wrapper">
|
||||
<input type="text" id="filterInput" class="filter-bar-input" placeholder="Filter messages..." autocomplete="off">
|
||||
<!-- Filter mentions autocomplete popup -->
|
||||
<div id="filterMentionsPopup" class="mentions-popup filter-mentions-popup hidden">
|
||||
<div class="mentions-list" id="filterMentionsList"></div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" id="filterMeBtn" class="filter-bar-btn filter-bar-btn-me" title="Filter my messages">
|
||||
<i class="bi bi-person-fill"></i>
|
||||
</button>
|
||||
<span id="filterMatchCount" class="filter-match-count"></span>
|
||||
<button type="button" id="filterClearBtn" class="filter-bar-btn filter-bar-btn-clear" title="Clear">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
<button type="button" id="filterCloseBtn" class="filter-bar-btn filter-bar-btn-close" title="Close">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Main content: sidebar + chat -->
|
||||
<div class="d-flex flex-grow-1 overflow-hidden" style="min-height: 0;">
|
||||
<!-- Channel Sidebar (visible on lg+ screens) -->
|
||||
<div id="channelSidebar" class="channel-sidebar">
|
||||
<div class="channel-sidebar-header">
|
||||
<i class="bi bi-broadcast-pin"></i> Channels
|
||||
</div>
|
||||
<div id="messagesContainer" class="messages-container h-100 overflow-auto p-3">
|
||||
<div id="messagesList">
|
||||
<!-- Messages will be loaded here via JavaScript -->
|
||||
<div class="text-center text-muted py-5">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-3">Loading messages...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="channel-sidebar-list" id="channelSidebarList">
|
||||
<!-- Populated by JavaScript -->
|
||||
</div>
|
||||
<!-- Scroll to bottom button -->
|
||||
<button id="scrollToBottomBtn" class="scroll-to-bottom-btn" title="Scroll to bottom">
|
||||
<i class="bi bi-chevron-double-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Send Message Form -->
|
||||
<div class="row border-top bg-light">
|
||||
<div class="col-12">
|
||||
<form id="sendMessageForm" class="p-3">
|
||||
<div class="emoji-picker-container">
|
||||
<div class="input-group">
|
||||
<textarea
|
||||
id="messageInput"
|
||||
class="form-control"
|
||||
placeholder="Type a message..."
|
||||
rows="2"
|
||||
maxlength="500"
|
||||
required
|
||||
></textarea>
|
||||
<button type="button" class="btn btn-outline-secondary" id="emojiBtn" title="Insert emoji">
|
||||
<i class="bi bi-emoji-smile"></i>
|
||||
<!-- Chat Area -->
|
||||
<div class="flex-grow-1 d-flex flex-column" style="min-width: 0;">
|
||||
<!-- Messages Container -->
|
||||
<div class="flex-grow-1 position-relative overflow-hidden" style="min-height: 0;">
|
||||
<!-- Filter bar overlay -->
|
||||
<div id="filterBar" class="filter-bar">
|
||||
<div class="filter-bar-inner">
|
||||
<div class="filter-input-wrapper">
|
||||
<input type="text" id="filterInput" class="filter-bar-input" placeholder="Filter messages..." autocomplete="off">
|
||||
<!-- Filter mentions autocomplete popup -->
|
||||
<div id="filterMentionsPopup" class="mentions-popup filter-mentions-popup hidden">
|
||||
<div class="mentions-list" id="filterMentionsList"></div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" id="filterMeBtn" class="filter-bar-btn filter-bar-btn-me" title="Filter my messages">
|
||||
<i class="bi bi-person-fill"></i>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary px-4" id="sendBtn">
|
||||
<i class="bi bi-send"></i>
|
||||
<span id="filterMatchCount" class="filter-match-count"></span>
|
||||
<button type="button" id="filterClearBtn" class="filter-bar-btn filter-bar-btn-clear" title="Clear">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
<button type="button" id="filterCloseBtn" class="filter-bar-btn filter-bar-btn-close" title="Close">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Emoji picker popup (hidden by default) -->
|
||||
<div id="emojiPickerPopup" class="emoji-picker-popup hidden"></div>
|
||||
<!-- Mentions autocomplete popup (hidden by default) -->
|
||||
<div id="mentionsPopup" class="mentions-popup hidden">
|
||||
<div class="mentions-list" id="mentionsList"></div>
|
||||
</div>
|
||||
<div id="messagesContainer" class="messages-container h-100 overflow-auto p-3">
|
||||
<div id="messagesList">
|
||||
<!-- Messages will be loaded here via JavaScript -->
|
||||
<div class="text-center text-muted py-5">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-3">Loading messages...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<small id="charCounter" class="text-muted">0 / 135</small>
|
||||
<!-- Scroll to bottom button -->
|
||||
<button id="scrollToBottomBtn" 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="sendMessageForm" class="p-3">
|
||||
<div class="emoji-picker-container">
|
||||
<div class="input-group">
|
||||
<textarea
|
||||
id="messageInput"
|
||||
class="form-control"
|
||||
placeholder="Type a message..."
|
||||
rows="2"
|
||||
maxlength="500"
|
||||
required
|
||||
></textarea>
|
||||
<button type="button" class="btn btn-outline-secondary" id="emojiBtn" title="Insert emoji">
|
||||
<i class="bi bi-emoji-smile"></i>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary px-4" id="sendBtn">
|
||||
<i class="bi bi-send"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Emoji picker popup (hidden by default) -->
|
||||
<div id="emojiPickerPopup" class="emoji-picker-popup hidden"></div>
|
||||
<!-- Mentions autocomplete popup (hidden by default) -->
|
||||
<div id="mentionsPopup" class="mentions-popup hidden">
|
||||
<div class="mentions-list" id="mentionsList"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<small id="charCounter" class="text-muted">0 / 135</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="statusText">
|
||||
<i class="bi bi-circle-fill text-secondary"></i> Connecting...
|
||||
</span>
|
||||
<span id="lastRefresh">Updated: Never</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<div class="row border-top">
|
||||
<div class="col-12">
|
||||
<div class="p-2 small text-muted d-flex justify-content-between align-items-center">
|
||||
<span id="statusText">
|
||||
<i class="bi bi-circle-fill text-secondary"></i> Connecting...
|
||||
</span>
|
||||
<span id="lastRefresh">Updated: Never</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user