Список пользователей

1
Админ
Постов: 211
2
VIP
Постов: 72
3
Элита
Постов: 50
4
Проверенные
Постов: 35
5
VIP
Постов: 35
6
Проверенные
Постов: 32
7
Пользователи
Постов: 31
8
Проверенные
Постов: 29

  • Страница 1 из 1
  • 1
Мини-мессенджер для uCoz: новая эра личных сообщений V1.0
Дата: Среда, 25.02.2026, 20:49 | Сообщение # 1 | | Написал: Узнаваемый
Автор темы
Мурчанн не в сети
        Сообщений:211
         Регистрация:20.10.2016

Плавающий помощник сообщений получил логичное продолжение теперь он реализован в формате полноценного мини-мессенджера. Решение интегрировано с модулем личных сообщений, что позволяет как принимать входящие сообщения, так и отправлять их напрямую через интерфейс помощника.

Плавающая панель сообщений uCoz 1.0

На данный момент это первая версия, однако она уже полностью функциональна и готова к использованию. Новый формат можно считать достойным преемником прежнего Плавающего помощника сообщений. Предыдущая версия отличалась простотой и имела ограниченные возможности, поэтому было принято решение разработать более современный, удобный и технологически продвинутый вариант.

Результатом стала обновлённая система, сочетающая компактность, практичность и расширенный функционал.

Единственным недостатком текущей версии мини-мессенджера остаётся отсутствие мгновенного отображения отправленного сообщения в общем потоке диалога. Это связано с особенностями системы 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 , это серьёзный шаг вперёд, особенно учитывая ограничения системы.

Мурчанн

Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.
Дата: Среда, 25.02.2026, 20:49 | Сообщение # 2 | | Написал: Узнаваемый
Автор темы
Мурчанн не в сети
        Сообщений:211
         Регистрация:20.10.2016

Исходной код:

Код
<?if($USER_LOGGED_IN$)?>

<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;
}

.vk-btn:hover {
transform: translateY(5px);
box-shadow: 0 8px 24px rgba(42,88,133,0.45);
}
.vk-btn-inner {
width: 40px;
height: 40px;
background: white;
border-radius: 5%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 20px;
color: #2a5885;
}

/* Бейджик (без изменений, только чуть подвинул) */
.vk-badge {
position: absolute;
top: -6px;
right: -2px;
min-width: 20px;
height: 20px;
background: #e64646;
color: white;
font-size: 11px;
font-weight: bold;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(230,70,70,0.4);
border: 2px solid #fff; /* белая обводка для контраста */
}

@keyframes pop {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.35); }

}

.vk-badge.pop {
animation: pop 0.4s ease;
}

/* Панель чата */
.vk-panel {
position: fixed;
right: 5px;
bottom: 10px;
width: 380px;
height: 560px;
background: #fff;
border-radius: 18px;
box-shadow: 0 10px 40px rgba(0,0,0,0.22);
overflow: hidden;
display: none;
flex-direction: column;
z-index: 10001;
transform: scale(0.92) translateY(20px);
opacity: 0;
transition: all 0.28s cubic-bezier(0.34,1.56,0.64,1.2);
}
.vk-panel.show {
display: flex;
transform: none;
opacity: 1;
}

/* Заголовок панели */
.vk-header {
background: #2a5885;
color: white;
padding: 12px 16px;
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
}
.vk-header-back {
background: none;
border: none;
color: white;
font-size: 28px;
line-height: 1;
cursor: pointer;
padding: 4px 8px 4px 0;
}
.vk-header-ava {
width: 42px;
height: 42px;
border-radius: 50%;
object-fit: cover;
border: 2px solid rgba(255,255,255,0.4);
}
.vk-header-name {
flex: 1;
font-size: 17px;
font-weight: 600;
}
.vk-toggle {
background: none;
border: none;
color: white;
font-size: 26px;
cursor: pointer;
padding: 4px 8px;
}

/* Основная область */
.vk-body {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: #f0f2f5;
}
.vk-msg-list {
flex: 1;
overflow-y: auto;
padding: 16px 12px;
display: flex;
flex-direction: column;
gap: 12px;
}

/* Элемент списка чатов */
.vk-msg-item {
display: flex;
gap: 12px;
padding: 10px 12px;
border-radius: 12px;
cursor: pointer;
transition: background 0.14s;
}
.vk-msg-item:hover {
background: #e8f0fe;
}
.vk-msg-item img {
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
}
.vk-msg-title {
font-weight: 600;
font-size: 15.5px;
color: #000;
}
.vk-msg-text {
font-size: 13.5px;
color: #444;
line-height: 1.38;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-top: 1px;
}
.vk-msg-time {
font-size: 12px;
color: #888;
margin-top: 4px;
}
.vk-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: #aaa;
font-size: 15px;
text-align: center;
padding: 0 40px;
}

.msg-row {
display: flex;
align-items: flex-start;
margin-bottom: 14px;
max-width: 82%;
}

.msg-row.in .msg-avatar {
margin-right: 10px;
margin-left: 0;
}

.msg-row.in .msg-header {
display: flex;
justify-content: space-between; /* ник слева, дата справа */
align-items: center;
}

/* Исходящее  аватар слева, сообщение прижато вправо */
.msg-row.out {
flex-direction: row; /* аватар слева */
justify-content: flex-end; /* весь блок смещён вправо */
}

/* Отступ у исходящих аватарок */
.msg-row.out .msg-avatar {
margin-right: 10px; /* аватар слева от сообщения */
}

.msg-avatar {
width: 38px;
height: 38px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
margin-top: 4px;
}

/* Сообщение */
.msg {
padding: 9px 14px;
border-radius: 18px;
font-size: 15.5px;
line-height: 1.42;
word-break: break-word;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
max-width: 100%;
}

.msg.in {
background: white;
border-bottom-left-radius: 6px;
}

.msg.out {
background: #d0e4ff;
border-bottom-right-radius: 6px;
}

.msg-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 4px;
font-weight: 600;
font-size: 14px;
gap: 12px;
}
.msg-user { color: #000; }
.msg-time { font-size: 0.82em; color: #777; white-space: nowrap; }
.msg-body { white-space: pre-wrap; word-break: break-word; }

.msg-time {
font-size: 0.82em;
color: #777;
white-space: nowrap;
opacity: 0.85;
}

.msg-body {
white-space: pre-wrap;
word-break: break-word;
}

/* Футер  */
.msg-footer {
text-align: right;
font-size: 0.8em;
color: #888;
margin-top: 4px;
font-style: italic;
}

/* Поле ввода */
.chat-input-wrap {
padding: 12px 16px;
background: white;
border-top: 1px solid #e0e4e8;
display: flex;
gap: 10px;
flex-shrink: 0;
}

.chat-input {
flex: 1;
padding: 12px 16px;
border: none;
border-radius: 24px;
background: #f0f2f5;
font-size: 15.5px;
outline: none;
}

.chat-send {
width: 44px;
height: 44px;
border: none;
border-radius: 50%;
background: #2a5885;
color: white;
font-size: 20px;
cursor: pointer;
flex-shrink: 0;
}
</style>

<div class="vk-btn" id="vkBtn">
<div class="vk-btn-inner">PM</div>
<div class="vk-badge" id="vkBadge" style="display:none">0</div>
</div>

<div class="vk-panel" id="vkPanel">
<div class="vk-header">
<button class="vk-header-back" id="btnBack">←</button>
<img class="vk-header-ava" id="chatAva" src="" alt="" style="display:none">
<span class="vk-header-name" id="chatName">Чаты</span>
<button class="vk-toggle" id="vkToggle">×</button>
</div>
<div class="vk-body">
<div class="vk-msg-list" id="msgList">
<div class="vk-empty">Нет сообщений</div>
</div>
<div class="chat-input-wrap" id="chatInputWrap" style="display:none">
<input class="chat-input" id="chatInp" placeholder="Напишите сообщение..." autocomplete="off">
<button class="chat-send" id="chatSend">➤</button>
</div>
</div>
</div>

<!-- Скрытый список онлайн-пользователей от uCoz -->
<div id="uc-online-list" style="display:none;">
$ONLINE_USERS_LIST$
</div>

<!-- Скрытый iframe -->
<iframe id="hiddenPMFrame"
style="display:none; width:0; height:0; border:0; position:absolute; top:-9999px;"
src="about:blank"></iframe>

<script>
document.addEventListener('DOMContentLoaded', () => {
const elements = {
btn: document.getElementById('vkBtn'),
panel: document.getElementById('vkPanel'),
toggle: document.getElementById('vkToggle'),
msgList: document.getElementById('msgList'),
chatInput: document.getElementById('chatInputWrap'),
chatName: document.getElementById('chatName'),
chatAva: document.getElementById('chatAva'),
btnBack: document.getElementById('btnBack'),
vkBadge: document.getElementById('vkBadge'),
chatInp: document.getElementById('chatInp'),
chatSend: document.getElementById('chatSend'),
pmFrame: document.getElementById('hiddenPMFrame'),
};

if (!elements.btn || !elements.panel || !elements.msgList) {
console.warn('Не найдены ключевые элементы панели сообщений');
return;
}

let currentChat = null;
const avatarCache = new Map();
const CACHE = {
HTML: 'vk_pm_cache_html',
COUNT: 'vk_pm_cache_count',
TIME: 'vk_pm_cache_time',
};
const POLL_INTERVAL = 45000;
const defaultAvatar = 'https://bro.usite.pro/icon/oneava.png';

let MY_USERNAME = 'Гость';
let MY_AVA = defaultAvatar;
const userMenu = document.querySelector('#userMenu');
if (userMenu) {
const nameEl = userMenu.querySelector('.user-name');
if (nameEl && nameEl.textContent.trim()) MY_USERNAME = nameEl.textContent.trim();
const realAva = userMenu.querySelector('.real-avatar');
if (realAva && realAva.src && realAva.src !== defaultAvatar) MY_AVA = realAva.src;
}

// Кэш и бейджик - оригинальная логика сохранена
function getCachedData() {
try {
const html = localStorage.getItem(CACHE.HTML);
const count = Number(localStorage.getItem(CACHE.COUNT)) || 0;
return html ? { html, count } : null;
} catch {
return null;
}
}

function saveCache(html, count) {
try {
localStorage.setItem(CACHE.HTML, html);
localStorage.setItem(CACHE.COUNT, count);
localStorage.setItem(CACHE.TIME, Date.now());
} catch {}
}

function updateBadge(count) {
const el = elements.vkBadge;
if (!el) return;
if (count > 0) {
el.textContent = count > 99 ? '99+' : count;
el.style.display = 'flex';
} else {
el.style.display = 'none';
}
}

function extractIncomingCount(html) {
const m = html.match(/Принятые\s*\(<b>(\d+)<\/b>\)/i);
return m ? Number(m[1]) : 0;
}

// Аватары — без изменений
function getAvatarFromStorage(url) {
try {
const data = JSON.parse(localStorage.getItem('vk_avatar_cache') || '{}');
return data[url] || null;
} catch { return null; }
}

function saveAvatarToStorage(url, avatarUrl) {
try {
const data = JSON.parse(localStorage.getItem('vk_avatar_cache') || '{}');
data[url] = avatarUrl;
localStorage.setItem('vk_avatar_cache', JSON.stringify(data));
} catch {}
}

async function loadAvatar(profileUrl) {
if (avatarCache.has(profileUrl)) return avatarCache.get(profileUrl);
let ava = getAvatarFromStorage(profileUrl);
if (ava) {
avatarCache.set(profileUrl, ava);
return ava;
}
try {
const res = await fetch(profileUrl, { cache: 'no-store' });
if (!res.ok) throw new Error();
const html = await res.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
const photo = doc.querySelector('.profile-photo');
if (photo) {
const style = photo.getAttribute('style') || '';
const m = style.match(/url\(['"]?(.*?)['"]?\)/i);
if (m?.[1]) {
ava = m[1];
avatarCache.set(profileUrl, ava);
saveAvatarToStorage(profileUrl, ava);
return ava;
}
}
} catch {}
return defaultAvatar;
}

// Создание сообщения — без изменений
function createMessageBlock(contentHtml, senderName, isIncoming, time = ' ', avatarSrc = defaultAvatar) {
const row = document.createElement('div');
row.className = `msg-row ${isIncoming ? 'in' : 'out'}`;
const bubble = document.createElement('div');
bubble.className = `msg ${isIncoming ? 'in' : 'out'}`;
const header = document.createElement('div');
header.className = 'msg-header';
header.innerHTML = `
<span class="msg-user">${senderName}</span>
<span class="msg-time">${time || ' '}</span>
`;
const body = document.createElement('div');
body.className = 'msg-body';
body.innerHTML = contentHtml;
bubble.append(header, body);
const ava = document.createElement('img');
ava.className = 'msg-avatar';
ava.src = avatarSrc || defaultAvatar;
ava.alt = senderName;
ava.onerror = () => ava.src = defaultAvatar;
row.appendChild(ava);
row.appendChild(bubble);
return row;
}

// Парсинг списка — без изменений
function parseMessageList(html) {
const doc = new DOMParser().parseFromString(html, 'text/html');
const rows = doc.querySelectorAll('table.userpm-messages-table tr[id^="ent"]');
elements.msgList.innerHTML = rows.length === 0
? '<div class="vk-empty">Нет сообщений</div>'
: '';
rows.forEach(row => {
const subjA = row.querySelector('td:nth-child(2) a[href*="/index/14-"]');
const userA = row.querySelector('td:nth-child(2) a[href*="/index/8-"]');
const dateTd = row.querySelector('td:nth-child(3)');
if (!subjA || !userA) return;
const link = subjA.getAttribute('href');
const username = userA.textContent.trim();
const profile = userA.getAttribute('href');
const subject = subjA.textContent.trim();
const time = dateTd?.textContent.trim() || '';
const item = document.createElement('div');
item.className = 'vk-msg-item';
item.dataset.link = link;
item.innerHTML = `
<img src="${defaultAvatar}" alt="">
<div>
<div class="vk-msg-title">${username}</div>
<div class="vk-msg-text">${subject}</div>
<div class="vk-msg-time">${time}</div>
</div>
`;
item.addEventListener('click', () => openConversation(username, defaultAvatar, link, profile));
elements.msgList.appendChild(item);
loadAvatar(profile).then(src => {
const img = item.querySelector('img');
if (img) img.src = src;
});
});
}

// Загрузка списка — без изменений
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 // уже загруженная аватарка (если есть)
};

elements.btnBack.style.display = 'block';
elements.chatName.textContent = name;
elements.chatInput.style.display = 'none';
elements.msgList.innerHTML = '<div class="vk-empty">Загрузка...</div>';

let 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;
}

elements.chatAva.src = interlocutorAva;
elements.chatAva.style.display = 'block';

// ─── Онлайн-статус собеседника в заголовке чата ───
const onlineContainer = document.getElementById('uc-online-list');
let isOnline = false;

if (onlineContainer && currentChat?.name) {
const onlineText = onlineContainer.textContent.toLowerCase().trim();
isOnline = onlineText.includes(currentChat.name.toLowerCase().trim());
}

// Создаём/обновляем точку онлайн
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;';

// Перемещаем chatAva внутрь обёртки
elements.chatAva.parentNode.insertBefore(avaWrapper, elements.chatAva);
avaWrapper.appendChild(elements.chatAva);

// Создаём точку
statusDot = document.createElement('span');
statusDot.id = 'chat-online-dot';
statusDot.style.cssText = `
position: absolute;
bottom: 2px;
right: 2px;
width: 10px;
height: 10px;
border-radius: 50%;
border: 2.5px solid #ffffff;
box-shadow: 0 0 3px rgba(0,0,0,0.4);
background: ${isOnline ? '#00c853' : '#a0a0a0'};
z-index: 10;
pointer-events: none;
`;
statusDot.title = isOnline ? 'Онлайн' : 'Оффлайн';
avaWrapper.appendChild(statusDot);
} else {
// Просто обновляем цвет, если точка уже существует
statusDot.style.background = isOnline ? '#00c853' : '#a0a0a0';
statusDot.title = isOnline ? 'Онлайн' : 'Оффлайн';
}

const myAva = MY_AVA;

try {
const res = await fetch(pmLink, { cache: 'no-store', signal: AbortSignal.timeout(12000) });
if (!res.ok) throw new Error(`status ${res.status}`);

const html = await res.text();
const doc = new DOMParser().parseFromString(html, 'text/html');

const globalDate = doc.querySelector('.userpm-message-date')?.textContent.trim() || ' ';
const messageCells = doc.querySelectorAll('table.userpm-message-table .userpm-message-text, td.userpm-message-text');

if (!messageCells.length) {
elements.msgList.innerHTML = '<div class="vk-empty">Сообщений не найдено</div>';
return;
}

elements.msgList.innerHTML = '';
const recentMessages = new Set();

for (const cell of messageCells) {
// очистка мусора (оригинальный код без изменений)
cell.querySelectorAll('span[id^="dg"]').forEach(el => el.remove());
cell.querySelectorAll('img[src*="/.s/img/fr/"]').forEach(el => el.remove());
cell.querySelectorAll('img[src^="/.s/"]').forEach(img => {
img.src = location.origin + img.getAttribute('src');
});

const pmDivs = cell.querySelectorAll('div.inputPM, div.outputPM');
pmDivs.forEach(div => {
div.querySelectorAll('span[id^="dg"]').forEach(s => s.remove());
let content = div.innerHTML.trim();
if (!content) return;

const normalized = content.replace(/<[^>]+>/g, '').trim().toLowerCase();
if (recentMessages.has(normalized)) return;
recentMessages.add(normalized);

const isIncoming = div.classList.contains('inputPM');
const senderName = isIncoming ? name : MY_USERNAME;
const senderAva = isIncoming ? interlocutorAva : myAva;

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();

const normalizedCell = cellText.replace(/<[^>]+>/g, '').trim().toLowerCase();
if (cellText && !recentMessages.has(normalizedCell)) {
const sysBlock = createMessageBlock(cellText, name, false, globalDate, interlocutorAva);
sysBlock.style.background = '#f0f0f0';
sysBlock.style.color = '#333';
sysBlock.style.fontStyle = 'normal';
elements.msgList.appendChild(sysBlock);
recentMessages.add(normalizedCell);
}
}

if (elements.msgList.children.length === 0) {
elements.msgList.innerHTML = '<div class="vk-empty">Нет видимых сообщений</div>';
}

elements.chatInput.style.display = 'block';

} catch (err) {
console.error('Ошибка загрузки переписки:', err);
elements.msgList.innerHTML = '<div class="vk-empty">Не удалось загрузить переписку</div>';
}
}

// ─── КРАСИВОЕ ОКНО В СТИЛЕ ВКОНТАКТЕ ───
function showVKAlert(title, message, isSuccess = true, details = '', avatarUrl = '') {
const existing = document.getElementById('vk-custom-alert');
if (existing) existing.remove();

const alert = document.createElement('div');
alert.id = 'vk-custom-alert';
alert.style.cssText = `
position: fixed; inset: 0; display: flex; align-items: center; justify-content: center;
z-index: 1000000; background: rgba(0,0,0,0.6);
opacity: 0; transition: opacity 0.3s ease;
`;

const card = document.createElement('div');
card.style.cssText = `
width: 420px; max-width: 94%;
background: #f0f0f0;
border: 1px solid #c0cad5;
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0,0,0,0.4);
overflow: hidden;
transform: translateY(60px) scale(0.9);
transition: all 0.35s cubic-bezier(0.25, 0.8, 0.25, 1.2);
font-family: Tahoma, Arial, Verdana, sans-serif;
color: #000;
`;

const header = document.createElement('div');
header.style.cssText = `
padding: 12px 18px;
background: linear-gradient(to bottom, #5b8cc4, #4a76a8);
color: #ffffff;
font-size: 14px;
font-weight: bold;
text-align: left;
border-bottom: 1px solid #3b5e8c;
position: relative;
`;
header.textContent = title;

const closeBtn = document.createElement('span');
closeBtn.style.cssText = `
position: absolute; right: 14px; top: 10px;
font-size: 18px; color: #fff; cursor: pointer; opacity: 0.8;
`;
closeBtn.textContent = '×';
closeBtn.onclick = () => btn.click();
header.appendChild(closeBtn);

const contentWrap = document.createElement('div');
contentWrap.style.cssText = `
padding: 20px 24px 16px;
background: #fff;
`;

const iconRow = document.createElement('div');
iconRow.style.cssText = `
display: flex; align-items: flex-start; margin-bottom: 16px;
`;

// Иконка: либо аватарка собеседника, либо CSS-конверт
if (avatarUrl) {
// Аватарка собеседника (если передали)
const ava = document.createElement('img');
ava.src = avatarUrl;
ava.alt = 'Ава';
ava.style.cssText = `
width: 48px; height: 48px; border-radius: 50%;
object-fit: cover; margin-right: 16px; flex-shrink: 0;
border: 2px solid #c0cad5;
`;
ava.onerror = () => { ava.src = 'https://vk.com/images/camera_200.png'; }; // fallback старый ВК
iconRow.appendChild(ava);
} else {
// CSS-иконка письма (конверт в стиле старого ВК)
const icon = document.createElement('div');
icon.style.cssText = `
width: 48px; height: 48px; margin-right: 16px; flex-shrink: 0;
position: relative; background: #4a76a8; border-radius: 4px;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.3);
`;

// Основная часть конверта
const envelope = document.createElement('div');
envelope.style.cssText = `
position: absolute; top: 12px; left: 8px; right: 8px; bottom: 12px;
background: #fff; border: 2px solid #2f4f7a; border-radius: 3px;
clip-path: polygon(0 0, 100% 0, 100% 100%, 50% 70%, 0 100%);
`;
icon.appendChild(envelope);

// Клапан (верхний треугольник)
const flap = document.createElement('div');
flap.style.cssText = `
position: absolute; top: 0; left: 8px; right: 8px; height: 14px;
background: linear-gradient(to bottom, #5b8cc4, #4a76a8);
clip-path: polygon(0 100%, 50% 0, 100% 100%);
`;
icon.appendChild(flap);

iconRow.appendChild(icon);
}

const textBlock = document.createElement('div');
textBlock.style.cssText = `
flex: 1;
`;

const mainText = document.createElement('div');
mainText.style.cssText = `
font-size: 13px; line-height: 1.5; color: #000;
font-weight: bold; margin-bottom: 8px;
`;
mainText.innerHTML = message.replace(/\n/g, '<br>');

const detailText = document.createElement('div');
detailText.style.cssText = `
font-size: 12px; line-height: 1.4; color: #456;
margin-bottom: 20px;
`;
detailText.innerHTML = details || (isSuccess
? 'Сообщение доставлено получателю.<br>Время: ' + new Date().toLocaleString('ru-RU', {hour:'2-digit', minute:'2-digit', day:'numeric', month:'short'})
: 'Проверьте соединение или попробуйте позже.'
);

textBlock.append(mainText, detailText);

iconRow.append(textBlock);

const separator = document.createElement('div');
separator.style.cssText = `
height: 1px; background: #dae1e8; margin: 0 -24px 16px;
`;

const btn = document.createElement('button');
btn.style.cssText = `
display: block; margin: 0 auto; padding: 8px 32px;
background: linear-gradient(to bottom, #6a91c3, #4a76a8);
color: #ffffff; border: 1px solid #3b5e8c;
border-radius: 4px; font-size: 12px; font-weight: bold;
cursor: pointer; text-shadow: 0 -1px 0 rgba(0,0,0,0.3);
transition: all 0.18s;
`;
btn.textContent = isSuccess ? 'Закрыть' : 'Понятно';

btn.onmouseover = () => {
btn.style.background = 'linear-gradient(to bottom, #7aa3d5, #5b8cc4)';
btn.style.borderColor = '#5b8cc4';
};
btn.onmouseout = () => {
btn.style.background = 'linear-gradient(to bottom, #6a91c3, #4a76a8)';
btn.style.borderColor = '#3b5e8c';
};

btn.onclick = () => {
card.style.transform = 'translateY(60px) scale(0.9)';
alert.style.opacity = '0';
setTimeout(() => alert.remove(), 350);
};

contentWrap.append(iconRow, separator, btn);

card.append(header, contentWrap);
alert.appendChild(card);
document.body.appendChild(alert);

requestAnimationFrame(() => {
alert.style.opacity = '1';
card.style.transform = 'translateY(0) scale(1)';
});

setTimeout(() => btn.click(), 5000);
}

// ─── ОТПРАВКА СООБЩЕНИЙ ЧЕРЕЗ IFRAME ───
function sendMessage() {
if (!currentChat?.pmLink) {
showVKAlert('Ошибка', 'Чат не выбран или ссылка отсутствует', false);
return;
}

const messageText = elements.chatInp?.value?.trim();
if (!messageText) {
showVKAlert('Ошибка', 'Напишите текст сообщения!', false);
return;
}

elements.chatSend.disabled = true;
elements.chatSend.style.opacity = '0.5';
elements.chatSend.innerHTML = '⋯';

const iframe = elements.pmFrame;
iframe.dataset.phase = 'loading';
const ts = Date.now();
const url = currentChat.pmLink + (currentChat.pmLink.includes('?') ? '&' : '?') + '_=' + ts;
console.log('[SEND] Загружаем:', url);

iframe.src = url;

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 не найдено');

textarea.value = messageText;

const subj = form.querySelector('input[name="subject"]');
if (subj && !subj.value.trim()) subj.value = `Re: ${currentChat.name}`;

iframe.dataset.phase = 'sent';
form.submit();
}, 900);

} catch (err) {
console.error('[SEND ERROR]', err);
showVKAlert('Ошибка', err.message || 'Неизвестная ошибка', false);
resetSendUI();
}
};

setTimeout(() => {
if (iframe.dataset.phase === 'loading') {
showVKAlert('Ошибка', 'Не удалось загрузить страницу за 12 секунд', false);
resetSendUI();
}
}, 12000);
}

function resetSendUI() {
elements.chatSend.disabled = false;
elements.chatSend.style.opacity = '1';
elements.chatSend.innerHTML = '➤';
if (elements.pmFrame) delete elements.pmFrame.dataset.phase;
}

// События
elements.btn.addEventListener('click', () => {
elements.panel.classList.toggle('show');
if (elements.panel.classList.contains('show')) loadInbox();
});

elements.toggle?.addEventListener('click', () => elements.panel.classList.remove('show'));

elements.btnBack.addEventListener('click', () => {
currentChat = null;
elements.btnBack.style.display = 'none';
elements.chatInput.style.display = 'none';
loadInbox(true);
});

elements.chatSend.addEventListener('click', sendMessage);
elements.chatInp.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});

// Инициализация — оригинальная логика
const initialCache = getCachedData();
if (initialCache) updateBadge(initialCache.count);

(async () => {
try {
const res = await fetch('/index/14-0-0', { cache: 'no-store' });
const html = await res.text();
const cnt = extractIncomingCount(html);
const old = initialCache?.count ?? 0;
if (cnt !== old) {
saveCache(html, cnt);
updateBadge(cnt);
if (elements.panel.classList.contains('show')) {
parseMessageList(html);
}
}
} catch {}
})();
});
</script>

<?else?>
<!-- Гости ничего не видят -->
<?endif?>


Мурчанн

Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.
  • Страница 1 из 1
  • 1
Поиск: