Плавающий помощник сообщений получил логичное продолжение теперь он реализован в формате полноценного мини-мессенджера. Решение интегрировано с модулем личных сообщений, что позволяет как принимать входящие сообщения, так и отправлять их напрямую через интерфейс помощника.
На данный момент это первая версия, однако она уже полностью функциональна и готова к использованию. Новый формат можно считать достойным преемником прежнего Плавающего помощника сообщений. Предыдущая версия отличалась простотой и имела ограниченные возможности, поэтому было принято решение разработать более современный, удобный и технологически продвинутый вариант.
Результатом стала обновлённая система, сочетающая компактность, практичность и расширенный функционал.
Единственным недостатком текущей версии мини-мессенджера остаётся отсутствие мгновенного отображения отправленного сообщения в общем потоке диалога. Это связано с особенностями системы uCoz: входящие и исходящие сообщения хранятся в разных разделах, что затрудняет их объединение в единый интерфейс.
На данный момент механизм синхронизации этих двух потоков до конца не изучен. Для корректной реализации необходимо детально разобраться в архитектуре хранения и обработки сообщений внутри системы, понять различия между входящими и отправленными данными, а также определить возможные точки интеграции.
Я уже предпринимал попытки изучить этот вопрос, однако пока общая картина остаётся не до конца ясной. В перспективе хотелось бы реализовать работу мессенджера по принципу классического приложения с моментальным отображением сообщений и полноценной синхронизацией. Тем не менее, на данном этапе остаются технические вопросы, требующие дополнительного анализа и решения.
Мини-мессенджер на базе uCoz: как это работает изнутри
На первый взгляд , это компактная кнопка PM, аккуратно закреплённая на странице. Но за минималистичным интерфейсом скрывается полноценная система личных сообщений, интегрированная с модулем uCoz.
Плавающая точка входа
Всё начинается с небольшой кнопки:
Кнопка PM открывает панель сообщений.
Рядом отображается бейдж с количеством непрочитанных сообщений.
Если новых сообщений нет индикатор скрывается.
Система аккуратно использует localStorage для кэширования данных, чтобы не перегружать сервер лишними запросами и мгновенно показывать уже известную информацию.
Панель сообщений компактный центр управления
После открытия пользователю доступна:
Шапка с именем собеседника
Аватар
Кнопка возврата
Список диалогов
Поле ввода сообщения
Если диалог ещё не выбран ,отображается список переписок.
Если выбран собеседник , открывается полноценный чат.
Интеллектуальная загрузка сообщений
Скрипт не использует API (потому что в uCoz его попросту нет для таких задач). Вместо этого он:
Загружает HTML страницы личных сообщений.
Парсит её через DOMParser.
Извлекает:
имя отправителя,
тему,
дату,
ссылку на переписку.
Формирует собственный интерфейс сообщений.
Проще говоря , он аккуратно «разбирает» стандартную страницу uCoz и превращает её в современный чат.
Аватары с умным кэшированием
Отдельное внимание уделено аватарам:
Скрипт заходит на страницу профиля пользователя.
Извлекает фото.
Сохраняет его в кэш.
Повторно не запрашивает без необходимости.
Это снижает нагрузку и ускоряет открытие диалогов.
Онлайн-статус в реальном времени
В шапке чата отображается индикатор:
🟢 зелёная точка - пользователь онлайн
⚪ серая точка - оффлайн
Информация берётся из скрытого блока $ ONLINE_USERS_LIST $, встроенного в страницу.
Открытие диалога
При выборе переписки:
Загружается страница диалога
Парсятся входящие (inputPM) и исходящие (outputPM) сообщения
Удаляются служебные элементы
Формируется чистый чат с пузырями сообщений
Автоматически подставляются аватары
Восстанавливается дата каждого сообщения
Дополнительно реализована защита от дублирования сообщений через систему нормализации текста.
Отправка сообщений , инженерный обходной путь
Самая интересная часть отправка сообщения.
Так как uCoz не предоставляет прямого JS-API, используется скрытый iframe.
Алгоритм работы:
1. В iframe загружается страница переписки.
2. Программно вызывается функция new_message(1).
3. Находится форма отправки.
4.В поле textarea вставляется текст.
5. Форма отправляется.
6. После отправки чат перезагружается.
Это практически имитация действий реального пользователя но полностью автоматизированная.
Красивые уведомления в стиле старого VK
После отправки появляется кастомное окно:
1. Полупрозрачный затемнённый фон
2. Карточка с градиентной шапкой
3. Аватар собеседника или иконка письма
4. Кнопка закрытия
5. Автоматическое скрытие через 5 секунд
Визуально это напоминает интерфейсы социальных сетей 2010-х годов лёгкая ностальгия, но с современной реализацией.
Автообновление и синхронизация
Каждые 45 секунд:
1. Проверяется количество входящих сообщений
2. Сравнивается с кэшированным значением
3. При изменении обновляется бейдж
4. При открытой панели обновляется список
Это создаёт ощущение «живого» мессенджера.
Архитектурная особенность
Главная техническая сложность в архитектуре uCoz:
Входящие и исходящие сообщения хранятся в разных местах.
Система не предназначена для SPA-интерфейса.
Отсутствует полноценный API.
Тем не менее, скрипт превращает стандартный модуль личных сообщений в компактный, современный мини-мессенджер, работающий поверх существующей инфраструктуры.
Перед нами не просто кнопка «PM», а
мини-клиент личных сообщений,
с кэшированием,
аватарами,
онлайн-статусом,
красивыми уведомлениями,
и обходной системой отправки через iframe.
Для платформы uCoz , это серьёзный шаг вперёд, особенно учитывая ограничения системы.
<style> /* Кнопка FAB */ .vk-btn { position: fixed; right: 20px; /* чуть ближе к краю или дальше по вкусу */ bottom: 22px; /* ← вот здесь поднимаем/опускаем выше/ниже */ width: 46px; /* вернул классический размер Material Design */ height: 46px; background: #2a5885; border-radius: 50%; box-shadow: 0 4px 18px rgba(42,88,133,0.35); display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 10002; transition: all 0.22s; }
// Загрузка списка — без изменений async function loadInbox(force = false) { if (!elements.panel.classList.contains('show')) return; const cached = !force && getCachedData(); if (cached) { parseMessageList(cached.html); updateBadge(cached.count); } try { const res = await fetch('/index/14-0-0', { cache: 'no-store' }); if (!res.ok) throw new Error(); const html = await res.text(); const newCount = extractIncomingCount(html); const oldCount = cached?.count ?? 0; if (newCount !== oldCount || !cached) { saveCache(html, newCount); updateBadge(newCount); if (newCount > oldCount || force) { parseMessageList(html); } } } catch (err) { console.warn('Ошибка загрузки списка', err); if (!cached) elements.msgList.innerHTML = '<div class="vk-empty">Ошибка загрузки</div>'; } }
// Открытие переписки — без изменений async function openConversation(name, fallbackAva, pmLink, profileUrl = null) { // Если это повторный вызов после отправки — используем сохранённые данные if (currentChat && currentChat.name === name && currentChat.pmLink === pmLink) { // восстанавливаем profileUrl, если он был потерян в вызове profileUrl = profileUrl || currentChat.profileUrl; }
// Сохраняем все нужные данные в currentChat currentChat = { name, pmLink, profileUrl: profileUrl || currentChat?.profileUrl, // не теряем ссылку на профиль interlocutorAva: currentChat?.interlocutorAva // уже загруженная аватарка (если есть) };
// 1. Если аватар уже сохранён в currentChat — используем его сразу if (currentChat.interlocutorAva) { interlocutorAva = currentChat.interlocutorAva; } // 2. Если есть profileUrl — загружаем (и сохраняем) else if (profileUrl) { interlocutorAva = await loadAvatar(profileUrl); currentChat.interlocutorAva = interlocutorAva; // кэшируем для будущих вызовов } // 3. fallback — только если ничего нет else { interlocutorAva = fallbackAva || defaultAvatar; }
// Создаём/обновляем точку онлайн let statusDot = document.getElementById('chat-online-dot');
if (!statusDot) { // Создаём обёртку вокруг аватарки, если её ещё нет const avaWrapper = document.createElement('div'); avaWrapper.id = 'chat-ava-wrapper'; avaWrapper.style.cssText = 'position: relative; display: inline-block; width: 42px; height: 42px;';
let time = globalDate; let row = div.closest('tr'); while (row && row.previousElementSibling) { row = row.previousElementSibling; const dateCell = row.querySelector('.userpm-message-date'); if (dateCell) { time = dateCell.textContent.trim(); break; } }
const msgBlock = createMessageBlock(content, senderName, isIncoming, time, senderAva); elements.msgList.appendChild(msgBlock); });
// обработка hr и остального текста (код без изменений) const hrs = cell.getElementsByTagName('hr'); if (hrs.length) { const lastHr = hrs[hrs.length - 1]; let node = lastHr.nextSibling; let mainTextParts = []; while (node) { if (node.nodeType === Node.ELEMENT_NODE && node.classList?.contains('userpm-message-reply')) break; if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent.trim(); if (text && !/написать ответ|всего хорошего|добавить/i.test(text)) mainTextParts.push(text); } else if (node.nodeType === Node.ELEMENT_NODE) { mainTextParts.push(node.outerHTML); } node = node.nextSibling; } let mainText = mainTextParts.join(' ').trim().replace(/\s+/g, ' '); const normalizedMain = mainText.replace(/<[^>]+>/g, '').trim().toLowerCase(); if (mainText && !recentMessages.has(normalizedMain)) { const mainBlock = createMessageBlock(mainText, name, true, globalDate, interlocutorAva); elements.msgList.appendChild(mainBlock); recentMessages.add(normalizedMain); } }
// системный текст (код без изменений) let cellText = cell.innerHTML .replace(/<div class="inputPM"[^>]*>.*?<\/div>/gs, '') .replace(/<div class="outputPM"[^>]*>.*?<\/div>/gs, '') .replace(/\[\s*<a[^>]*>.*?<\/a>\s*\]/gi, '') .replace(/<span id="dg\d+">[+-]<\/span>/gi, '') .replace(/<img[^>]*src="\/\.s\/img\/fr\/(in|out)\.gif"[^>]*>/gi, '') .replace(/<div class="userpm-message-reply"[^>]*>[\s\S]*?<\/div>/gi, '') .replace(/<hr>/gi, '') .replace(/<br\s*\/?>/gi, ' ') .replace(/Написать ответ/gi, '') .replace(/Всего хорошего\.?/gi, '') .replace(/Вы также можете его добавить, перейдя по этой ссылке\.?/gi, '') .replace(/\s+/g, ' ') .trim();
// ─── ОТПРАВКА СООБЩЕНИЙ ЧЕРЕЗ IFRAME ─── function sendMessage() { if (!currentChat?.pmLink) { showVKAlert('Ошибка', 'Чат не выбран или ссылка отсутствует', false); return; }
const messageText = elements.chatInp?.value?.trim(); if (!messageText) { showVKAlert('Ошибка', 'Напишите текст сообщения!', false); return; }
iframe.onload = () => { try { const doc = iframe.contentDocument || iframe.contentWindow?.document; if (!doc) throw new Error('Нет доступа к iframe');
if (iframe.dataset.phase === 'sent') { showVKAlert('Готово', 'Сообщение успешно отправлено!', true);
setTimeout(() => { // Перезагружаем чат с сохранённым profileUrl openConversation( currentChat.name, '', // fallbackAva не нужен currentChat.pmLink, currentChat.profileUrl // ← вот ключевой момент ); elements.chatInp.value = ''; }, 1200);
resetSendUI(); iframe.onload = null; return; }
// Открываем форму отправки const win = iframe.contentWindow; if (win && typeof win.new_message === 'function') { win.new_message(1); } else { const newmess = doc.getElementById('newmess'); if (newmess) newmess.style.display = ''; else throw new Error('Форма не найдена'); }
setTimeout(() => { const form = doc.getElementById('addform') || doc.querySelector('form[name="addform"]'); if (!form) throw new Error('Форма #addform не появилась');
const textarea = doc.getElementById('message'); if (!textarea) throw new Error('Поле #message не найдено');