Обновление chat_list.html: улучшена фильтрация, скрытие пустых категорий и кнопок, оптимизация JS и CSS.

This commit is contained in:
admin i 2025-11-30 09:44:18 +03:00
parent 19abf17740
commit 46a7bbdb07
7 changed files with 448 additions and 51 deletions

121
CHAT_LIST_SIDEBAR_FILTER.md Normal file
View File

@ -0,0 +1,121 @@
# Фильтрация чатов с боковой панелью типов
## Описание изменений
Страница списка чатов (`/communications/chats/`) была реорганизована с добавлением боковой панели для фильтрации по типам чатов.
## Структура
### Двухколоночный макет
1. **Левая колонка (боковая панель)** - фиксированная ширина (120px на desktop, адаптивно до 60px на mobile)
- Кнопки-иконки для каждого типа чата
- Бейджи с количеством чатов в каждой категории
- Sticky-позиционирование (прилипает при скролле)
2. **Правая колонка (основной список)** - flex-grow для занятия оставшегося пространства
- Все секции чатов (глобальный, отделы, личные, группы, каналы, объявления)
- Поиск и существующие фильтры
## Типы чатов
Доступные типы для фильтрации:
- **Все** (`all`) - показывает все чаты
- **Глобальный** (`global`) - глобальный чат организации
- **Отделы** (`department`) - чаты отделов
- **Личные** (`private`) - личные чаты между пользователями
- **Группы** (`group`) - групповые чаты
- **Каналы** (`channel`) - каналы для широковещательных сообщений
- **Объявления** (`announcement`) - каналы объявлений
## Функциональность
### Фильтрация
- Клик по кнопке типа чата показывает только чаты этого типа
- Кнопка "Все" показывает все чаты
- Активная кнопка подсвечивается синим цветом
- При наведении иконка увеличивается (scale 1.1)
### Счетчики
- Каждая кнопка показывает количество чатов в категории
- Счетчики обновляются динамически при поиске
- Пустые категории скрывают бейдж (display: none)
### Адаптивность
**Desktop (>992px):**
- Ширина панели: 120px
- Отображаются иконки + текст + бейджи
**Tablet (768-992px):**
- Ширина панели: 90px
- Уменьшенный шрифт текста
**Mobile (576-768px):**
- Ширина панели: 70px
- Еще более компактный вид
**Small Mobile (<576px):**
- Ширина панели: 60px
- Скрывается текст, остаются только иконки
- Только title атрибут для понимания назначения
## Файлы изменений
### HTML
`backend/templates/communications/chat_list.html`
- Добавлена структура `.d-flex` с `.chat-type-sidebar` и `.chat-list-main`
- Кнопки фильтрации с иконками и бейджами
- JavaScript функции для подсчета и фильтрации
### CSS
`backend/static/css/components/chat-list-enhanced.css`
- Стили для `.chat-type-sidebar`
- Стили для `.chat-type-btn` (обычное, hover, active состояния)
- Адаптивные медиа-запросы для всех размеров экрана
- Sticky-позиционирование боковой панели
## Интеграция с существующими функциями
### Поиск
- Поиск работает в рамках отфильтрованных секций
- Счетчики обновляются после применения поиска
- При смене фильтра поиск переприменяется
### WebSocket
- Обновления чатов в реальном времени сохраняются
- Счетчики могут быть обновлены при получении новых сообщений
### Создание чата
- Модальное окно создания чата не изменено
- После создания нового чата рекомендуется добавить обновление счетчиков
## Будущие улучшения
1. **Динамическое обновление счетчиков при WebSocket событиях**
- Обновлять бейджи при получении новых сообщений
- Показывать непрочитанные сообщения в каждой категории
2. **Сохранение выбранного фильтра в localStorage**
- Запоминать последний выбранный тип
- Восстанавливать при следующем посещении
3. **Анимация при переключении**
- Плавное появление/исчезновение секций
- Transition для смены активной кнопки
4. **Drag & Drop для переупорядочивания кнопок**
- Позволить пользователю настроить порядок типов
- Сохранять настройку в профиле
## Тестирование
- ✅ Переключение между типами чатов
- ✅ Подсчет количества чатов
- ✅ Адаптивность на разных экранах
- ✅ Совместимость с поиском
- ✅ Sticky-позиционирование боковой панели
- ⏳ WebSocket обновления счетчиков (требует доработки)

View File

@ -1,10 +1,12 @@
/* ===== CHAT LIST ENHANCED STYLES ===== */
/* Context Menu Styles */
.context-menu {
position: absolute;
background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 4px 0;
min-width: 200px;
z-index: 9999;
@ -32,12 +34,31 @@
border-color: #dee2e6;
}
/* Chat List Update Animation */
/* Chat List Animations */
.chat-row {
border-top: 1px solid var(--bs-border-color);
}
.chat-row.updated {
background: #e3f2fd;
transition: background 0.3s ease-out;
}
/* Chat Section Headers */
.chat-section .card-header {
text-align: center;
border-bottom: 1px solid var(--bs-border-color);
}
.chat-section .card-header .card-icon {
display: none;
}
.chat-section .card-header .card-meta {
width: 100%;
text-align: center;
}
/* Pinned Chat Indicator */
.chat-row.pinned {
background: #fffbeb;
@ -61,12 +82,13 @@
}
/* Chat Avatars */
.chat-avatar {
.chat-avatar,
.card-icon img {
width: 44px;
height: 44px;
border-radius: 50%;
border-radius: 8px; /* Овальная форма вместо круглой */
object-fit: cover;
border: 2px solid #dee2e6;
border: 2px solid var(--bs-border-color);
}
.chat-avatar-group {
@ -77,29 +99,17 @@
.chat-avatar-group img {
margin-left: -8px;
border: 2px solid white;
border-radius: 8px;
}
.chat-avatar-group img:first-child {
margin-left: 0;
}
/* Chat Actions Menu Button */
/* Chat Actions Menu */
.chat-actions-menu {
display: flex;
align-items: center;
position: static !important;
}
.chat-row {
overflow: visible !important;
}
.list-chats {
overflow: visible !important;
}
.card {
overflow: visible !important;
}
.chat-actions-menu .btn {
@ -119,8 +129,7 @@
.chat-actions-menu .dropdown-menu {
font-size: 0.9rem;
min-width: 200px;
position: absolute !important;
z-index: 1050 !important;
z-index: 1050;
}
.chat-actions-menu .dropdown-item {
@ -131,3 +140,125 @@
width: 20px;
text-align: center;
}
/* ===== CHAT TYPE SIDEBAR ===== */
:root {
--chat-sidebar-offset: 120px;
}
.chat-type-sidebar {
flex-shrink: 0;
align-self: flex-start;
position: sticky;
top: calc(var(--navbar-h, 60px) + var(--chat-sidebar-offset, 120px));
}
.chat-type-sidebar .card {
max-height: calc(100vh - var(--navbar-h, 60px) - var(--chat-sidebar-offset, 120px) - 24px);
overflow-y: auto;
}
.chat-type-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.35rem;
position: relative;
cursor: pointer;
transition: background-color 0.2s ease, opacity 0.2s ease;
}
.chat-type-btn[style*="display: none"] {
display: none !important;
}
.chat-type-btn:hover {
background-color: var(--bs-light) !important;
}
.chat-type-btn.active {
background-color: var(--bs-primary) !important;
color: white !important;
}
.chat-type-btn.active i {
color: white;
}
.chat-type-btn .badge {
position: absolute;
top: 0.25rem;
right: 0.25rem;
min-width: 1.25rem;
height: 1.25rem;
padding: 0.15rem 0.3rem;
border-radius: 50%;
font-size: 0.65rem;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
.chat-type-btn.active .badge {
background-color: rgba(255, 255, 255, 0.3) !important;
color: white !important;
border-color: white !important;
}
/* ===== MOBILE VERSION ===== */
@media (max-width: 768px) {
.chat-list-wrapper {
flex-direction: column !important;
}
.chat-type-sidebar {
width: 100% !important;
order: -1;
position: static;
}
.chat-type-sidebar .card {
max-height: none;
overflow: hidden;
padding: 0.25rem 0.5rem;
width: 100%;
}
.chat-type-sidebar .list-group {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
gap: 0.5rem;
width: 100%;
overflow-x: auto;
overflow-y: hidden;
padding: 0 0.25rem 0.25rem;
scrollbar-width: thin;
scroll-snap-type: x proximity;
-webkit-overflow-scrolling: touch;
}
.chat-type-btn {
flex: 0 0 auto;
min-width: 60px;
max-width: 80px;
padding: 0.5rem 0.25rem !important;
border-radius: 0.75rem !important;
border: 1px solid var(--bs-border-color) !important;
scroll-snap-align: start;
}
.chat-type-btn i {
font-size: 1.25rem !important;
}
.chat-type-btn .badge {
font-size: 0.6rem;
min-width: 1.1rem;
height: 1.1rem;
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["../../scss/components/_chat-list-modern.scss"],"names":[],"mappings":"CAIA,qBACE,aACA,MACA,+BACA,6BAEA,4BANF,qBAOI,sBACA,UAKJ,kBACE,WACA,gBACA,8CACA,4BACA,eACA,aACA,sBACA,UACA,gBACA,SACA,sBACA,+BACA,gBAEA,4BAfF,kBAgBI,WACA,mBACA,mBACA,kBACA,+CACA,gBACA,gBACA,gBACA,kBACA,eACA,WAGA,qCACE,YAKN,eACE,aACA,sBACA,mBACA,uBACA,WACA,qBACA,YACA,yBACA,0BACA,eACA,kBACA,wBACA,kBACA,eAEA,qBACE,4CACA,wBAGF,sBACE,6BACA,WACA,sDAEA,uCACE,gBACA,wBAIJ,iBACE,iBACA,cAGF,gCACE,gBACA,gBACA,kBACA,gBACA,eACA,gBACA,uBACA,mBAGF,gCACE,kBACA,QACA,UACA,4BACA,WACA,mBACA,cACA,iBACA,gBACA,eACA,YACA,aACA,mBACA,uBACA,cAEA,qCACE,aAIJ,4BAtEF,eAuEI,mBACA,UACA,mBACA,eAEA,iBACE,kBAGF,gCACE,iBACA,eACA,mBAGF,gCACE,kBACA,SACA,WACA,oBAMN,iBACE,OACA,gBACA,4BACA,UACA,YAEA,4BAPF,iBAQI,oBAIJ,kBACE,uBACA,+CACA,aACA,mBACA,8BACA,SACA,eAEA,qBACE,SACA,kBACA,gBACA,qBACA,aACA,mBACA,UAEA,uBACE,iBACA,wBAIJ,oCACE,4CACA,wBACA,sBACA,mBACA,iBACA,gBAIJ,gBACE,UACA,+BACA,gBAEA,4BALF,gBAMI,iBAIF,mCACE,UAGF,yCACE,yBAGF,yCACE,mBACA,kBAEA,+CACE,mBAMN,iBACE,aACA,kBACA,WACA,oBACA,2DACA,wBACA,kBACA,eAEA,4BACE,mBAGF,uBACE,6CAGF,wBACE,gFACA,wCAEA,gCACE,YACA,8BACA,kBACA,WACA,WACA,wBACA,kBACA,UAIJ,wBACE,0CACA,qCAGF,yBACE,oCAGF,4BA3CF,iBA4CI,qBACA,aAIJ,wBACE,GACE,4CAEF,KACE,0BAIJ,iBACE,WACA,YACA,mBACA,cACA,aACA,mBACA,uBACA,kBACA,wGACA,wBACA,gBACA,kBAEA,qBACE,WACA,YACA,iBAGF,sCACE,WACA,kBACA,WACA,UACA,WACA,YACA,6BACA,sBACA,kBAGF,4BAhCF,iBAiCI,WACA,YACA,kBAIJ,kBACE,OACA,YACA,aACA,sBACA,WAGF,iBACE,aACA,mBACA,8BACA,UAGF,gBACE,iBACA,gBACA,qBACA,SACA,gBACA,uBACA,mBACA,OACA,qBAEA,sBACE,wBAIJ,eACE,iBACA,0BACA,mBACA,cAGF,kBACE,aACA,mBACA,UACA,iBACA,0BAEA,kCACE,gBACA,qBACA,cAGF,gCACE,gBACA,uBACA,mBACA,OACA,YAIJ,eACE,aACA,mBACA,UACA,eAEA,sBACE,gBACA,qBACA,gBAGF,6BACE,4BACA,WACA,mBACA,sBACA,gBACA,gBACA,eACA,kBAIJ,kBACE,kBACA,SACA,WACA,UACA,uBACA,WAEA,yCACE,UAGF,uBACE,qBACA,kBAKJ,iBACE,aACA,sBACA,mBACA,uBACA,oBACA,kBACA,0BAEA,mBACE,eACA,yBACA,mBACA,WAGF,oBACE,iBACA,gBACA,qBACA,oBAGF,mBACE,gBACA,mBACA,gBAGF,sBACE,iBAKJ,mBACE,aACA,mBACA,uBACA,aACA,0BAEA,mCACE,WACA,YACA,oBAKJ,4BACE,kBACE,aAEA,qBACE,iBAEA,uBACE,kBAKN,iBACE,eAGF,iBACE,WACA,YACA,eAGF,gBACE,gBAGF,kBACE","file":"chat-list-modern.css"}

View File

@ -17,7 +17,56 @@
<div class="col-12">
{# ===== НОВАЯ ШАПКА С КОМПОНЕНТОМ ===== #}
{% include "includes/components/page_header.html" with icon="bi-chat-dots" title="Мои чаты" search_id="chatSearch" search_placeholder="Поиск по названию или участникам" has_filters=True filters_target="chatFilters" filters_template="communications/_filters.html" create_as_card=True create_modal="chatCreateModal" create_text="Создать чат" %}
{% include "includes/components/page_header.html" with icon="bi-chat-dots" title="Мои чаты" search_id="chatSearch" search_placeholder="Поиск по названию или участникам" create_as_card=True create_modal="chatCreateModal" create_text="Создать чат" %}
{# ===== ДВУХКОЛОНОЧНАЯ СТРУКТУРА ===== #}
<div class="d-flex gap-3 chat-list-wrapper">
{# Левая колонка - иконки типов чатов #}
<div class="chat-type-sidebar">
<div class="card">
<div class="list-group list-group-flush">
<button type="button" class="list-group-item list-group-item-action text-center py-3 chat-type-btn active"
data-chat-type="all" title="Все чаты">
<i class="bi-chat-dots fs-4 d-block"></i>
<span class="badge rounded-pill bg-secondary" data-count="all">0</span>
</button>
<button type="button" class="list-group-item list-group-item-action text-center py-3 chat-type-btn"
data-chat-type="global" title="Глобальный">
<i class="bi-globe fs-4 d-block"></i>
<span class="badge rounded-pill bg-secondary" data-count="global">0</span>
</button>
<button type="button" class="list-group-item list-group-item-action text-center py-3 chat-type-btn"
data-chat-type="department" title="Отделы">
<i class="bi-diagram-3 fs-4 d-block"></i>
<span class="badge rounded-pill bg-secondary" data-count="department">0</span>
</button>
<button type="button" class="list-group-item list-group-item-action text-center py-3 chat-type-btn"
data-chat-type="private" title="Личные">
<i class="bi-person-fill fs-4 d-block"></i>
<span class="badge rounded-pill bg-secondary" data-count="private">0</span>
</button>
<button type="button" class="list-group-item list-group-item-action text-center py-3 chat-type-btn"
data-chat-type="group" title="Группы">
<i class="bi-people-fill fs-4 d-block"></i>
<span class="badge rounded-pill bg-secondary" data-count="group">0</span>
</button>
<button type="button" class="list-group-item list-group-item-action text-center py-3 chat-type-btn"
data-chat-type="channel" title="Каналы">
<i class="bi-broadcast fs-4 d-block"></i>
<span class="badge rounded-pill bg-secondary" data-count="channel">0</span>
</button>
<button type="button" class="list-group-item list-group-item-action text-center py-3 chat-type-btn"
data-chat-type="announcement" title="Объявления">
<i class="bi-megaphone fs-4 d-block"></i>
<span class="badge rounded-pill bg-secondary" data-count="announcement">0</span>
</button>
</div>
</div>
</div>
{# Правая колонка - список чатов #}
<div class="chat-list-main flex-grow-1">
{# ===== 1) ГЛОБАЛЬНЫЙ ===== #}
<div class="card chat-section mb-3" data-sec="global">
@ -33,7 +82,7 @@
{% for chat in chats %}
{% if chat.type == 'global' %}
{% with last=chat.last_message.0 %}
<div class="chat-row d-flex align-items-start gap-2 py-2 px-2 {% if not forloop.first %}border-top{% endif %}"
<div class="chat-row d-flex align-items-start gap-2 py-2 px-2"
data-chat-id="{{ chat.pk }}"
data-type="global"
data-last-ts="{% if last %}{{ last.created_at|date:'U' }}000{% else %}{{ chat.created_at|date:'U' }}000{% endif %}"
@ -104,7 +153,7 @@
{% for chat in chats %}
{% if chat.type == 'department' %}
{% with last=chat.last_message.0 %}
<div class="chat-row d-flex align-items-start gap-2 py-2 px-2 {% if not forloop.first %}border-top{% endif %}"
<div class="chat-row d-flex align-items-start gap-2 py-2 px-2"
data-chat-id="{{ chat.pk }}"
data-type="department"
data-last-ts="{% if last %}{{ last.created_at|date:'U' }}000{% else %}{{ chat.created_at|date:'U' }}000{% endif %}"
@ -166,7 +215,7 @@
{% for chat in chats %}
{% if chat.type == 'private' %}
{% with last=chat.last_message.0 %}
<div class="chat-row d-flex align-items-start gap-2 py-2 px-2 {% if not forloop.first %}border-top{% endif %}"
<div class="chat-row d-flex align-items-start gap-2 py-2 px-2"
data-chat-id="{{ chat.pk }}"
data-type="private"
data-last-ts="{% if last %}{{ last.created_at|date:'U' }}000{% else %}{{ chat.created_at|date:'U' }}000{% endif %}"
@ -242,7 +291,7 @@
{% for chat in chats %}
{% if chat.type == 'group' %}
{% with last=chat.last_message.0 %}
<div class="chat-row d-flex align-items-start gap-2 py-2 px-2 {% if not forloop.first %}border-top{% endif %}"
<div class="chat-row d-flex align-items-start gap-2 py-2 px-2"
data-chat-id="{{ chat.pk }}"
data-type="group"
data-last-ts="{% if last %}{{ last.created_at|date:'U' }}000{% else %}{{ chat.created_at|date:'U' }}000{% endif %}"
@ -316,7 +365,7 @@
{% for chat in chats %}
{% if chat.type == 'channel' %}
{% with last=chat.last_message.0 %}
<div class="chat-row d-flex align-items-start gap-2 py-2 px-2 {% if not forloop.first %}border-top{% endif %}"
<div class="chat-row d-flex align-items-start gap-2 py-2 px-2"
data-chat-id="{{ chat.pk }}"
data-type="channel"
data-last-ts="{% if last %}{{ last.created_at|date:'U' }}000{% else %}{{ chat.created_at|date:'U' }}000{% endif %}"
@ -390,7 +439,7 @@
{% for chat in chats %}
{% if chat.type == 'announcement' %}
{% with last=chat.last_message.0 %}
<div class="chat-row d-flex align-items-start gap-2 py-2 px-2 {% if not forloop.first %}border-top{% endif %}"
<div class="chat-row d-flex align-items-start gap-2 py-2 px-2"
data-chat-id="{{ chat.pk }}"
data-type="announcement"
data-last-ts="{% if last %}{{ last.created_at|date:'U' }}000{% else %}{{ chat.created_at|date:'U' }}000{% endif %}"
@ -456,6 +505,9 @@
</div>
{% endif %}
</div> {# конец chat-list-main #}
</div> {# конец d-flex #}
</div>
</div>
</div>
@ -523,6 +575,28 @@
const ACCESS = accessMeta ? accessMeta.content : '';
const headersAuth = ACCESS ? { 'Authorization': 'Bearer ' + ACCESS } : {};
// Динамически рассчитываем высоту page-header для смещения липкой панели
const chatListWrapper = document.querySelector('.chat-list-wrapper');
function updateSidebarOffset() {
const pageHeader = document.querySelector('.page-header');
if (!pageHeader) return;
const headerStyles = window.getComputedStyle(pageHeader);
const marginBottom = parseFloat(headerStyles.marginBottom) || 0;
const offset = pageHeader.offsetHeight + marginBottom;
document.documentElement.style.setProperty('--chat-sidebar-offset', `${offset}px`);
}
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(updateSidebarOffset, 150);
});
updateSidebarOffset();
// Инициализация RecipientPicker для создания чата
const chatPicker = new RecipientPicker(
document.getElementById('chatRecipients'),
@ -646,40 +720,109 @@
rowSelector: '.chat-row'
});
// Обработка селекта фильтра типа чата
const chatTypeFilter = document.getElementById('chatTypeFilter');
const applyBtn = document.getElementById('applyChatFilter');
const resetBtn = document.getElementById('resetChatFilter');
function applyTypeFilter() {
const selectedType = chatTypeFilter?.value || 'all';
const sections = document.querySelectorAll('.chat-section');
// ===== ФИЛЬТРАЦИЯ ПО ТИПАМ ЧАТОВ =====
let activeFilter = 'all';
const chatTypeButtons = document.querySelectorAll('.chat-type-btn');
const chatSections = document.querySelectorAll('.chat-section');
// Функция подсчета чатов по типам и управления видимостью
function updateChatCounts() {
const counts = {
all: 0,
global: 0,
department: 0,
private: 0,
group: 0,
channel: 0,
announcement: 0
};
sections.forEach(section => {
// Подсчитываем чаты в каждой секции
chatSections.forEach(section => {
const sectionType = section.dataset.sec;
if (selectedType === 'all' || selectedType === sectionType) {
section.style.display = '';
} else {
const chatRows = section.querySelectorAll('.chat-row');
// Считаем только видимые (не скрытые поиском) чаты
const visibleChats = Array.from(chatRows).filter(row => {
return row.style.display !== 'none';
}).length;
counts[sectionType] = visibleChats;
counts.all += visibleChats;
// Управление видимостью секции
// 1. Если чатов нет (скрыты поиском или отсутствуют) -> скрываем секцию
if (visibleChats === 0) {
section.style.display = 'none';
}
// 2. Если чаты есть, проверяем активный фильтр
else {
if (activeFilter === 'all' || activeFilter === sectionType) {
section.style.display = '';
} else {
section.style.display = 'none';
}
}
});
// Переприменяем поиск после изменения видимости секций
// Обновляем бейджи и кнопки
Object.keys(counts).forEach(type => {
const badge = document.querySelector(`[data-count="${type}"]`);
const button = document.querySelector(`[data-chat-type="${type}"]`);
if (badge) {
badge.textContent = counts[type];
// Скрываем бейдж если 0
badge.style.display = counts[type] === 0 ? 'none' : '';
}
// Скрываем кнопку категории, если в ней нет чатов (кроме 'all')
if (button && type !== 'all') {
button.style.display = counts[type] === 0 ? 'none' : '';
}
});
}
function applyTypeFilter(filterType) {
activeFilter = filterType;
// Обновляем активную кнопку
chatTypeButtons.forEach(btn => {
if (btn.dataset.chatType === filterType) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
// Переприменяем поиск (если есть), это обновит видимость строк
if (filterApi && filterApi.reapply) {
filterApi.reapply();
}
// Обновляем видимость секций и счетчики
updateChatCounts();
}
function resetFilter() {
if(chatTypeFilter) chatTypeFilter.value = 'all';
applyTypeFilter();
}
applyBtn?.addEventListener('click', applyTypeFilter);
resetBtn?.addEventListener('click', resetFilter);
// Применяем фильтр при загрузке
applyTypeFilter();
// Обработка кликов по кнопкам типов
chatTypeButtons.forEach(btn => {
btn.addEventListener('click', () => {
const filterType = btn.dataset.chatType;
applyTypeFilter(filterType);
});
});
// Применяем начальный фильтр и подсчитываем чаты
applyTypeFilter('all');
// Обновляем счетчики после поиска
const searchInput = document.getElementById('chatSearch');
if (searchInput) {
searchInput.addEventListener('input', () => {
setTimeout(updateChatCounts, 100);
});
}
</script>
{# Подключаем chat-list-enhanced.js для WebSocket и контекстного меню #}