⚡Мгновенные "Быстроплюсы" и ⚡Мгновенные "Сказали спасибо"
Предыстория
Есть скрипт "Быстроплюсы от Deff" https://forum.mybb.ru/viewtopic.php?id= … 29#p982958 , он позволяет ставить положительные "лайки" однократным нажатием на "сердечко".
И есть скрипт "Поблагодарили" от Reysler https://forum.mybb.ru/viewtopic.php?id= … 78#p995185 и "Список поддержавших пост" от Виплич https://forum.mybb.ru/viewtopic.php?id= … 29#p995548 . Смысл этих скриптов - вывести в сообщениях, под подписью, список поставивших "лайки".
Эти скрипты, "Быстроплюсы" и список их поставивших, органично дополняют друг друга.
В моём варианте список поставивших положительные "лайки" называется "Сказали спасибо", поэтому далее я буду называть эти функции "Быстроплюсы" и "Сказали спасибо".
Вышеприведённые скрипты выполняют свою работу, но у них есть свои функциональные ограничения:
● "Быстроплюсы" не работают при включении бесплатного скрипта "Живых тем" от Alex_63 https://forum.mybb.ru/viewtopic.php?id=41002#p993848
● "Сказали спасибо" для отображения в обычном режиме требуют перезагрузки страницы.
● "Сказали спасибо" для отображения в режиме "Живые темы" тоже требуют перезагрузки страницы.
● При использовании платного дополнения "Мгновенные уведомления" от Alex_63 https://forum.mybb.ru/viewtopic.php?id=38567#p939931 , "Сказали спасибо" тоже требуют перезагрузки страницы.
А чего мы хотим? Мы хотим, чтобы всё работало 🚀быстрее, чем лайки в Телеграме!
Далее будет приведён скрипт, при котором "Быстроплюсы" и "Сказали спасибо" отлично работают и отображаются мгновенно в ЛЮБОМ варианте работы форума!
"Быстроплюсы" и "Сказали спасибо" используют единое логическое ядро и непосредственно связаны друг с другом через вызов функции.
Кому не нужны "Сказали спасибо", а нужны лишь всегда быстро и чётко работающие сердечки "Быстроплюсов", для тех выложим сокращённую версию скрипта без "Сказали спасибо".
Подготовительная работа.
1. В панели администрирования форума Администрирование - Права поставьте "Разрешить ajax для предпросмотра и отправки сообщения", затем нажмите кнопку "Сохранить".
Скриншот (кликабельно)

2. В панели администрирования форума Администрирование - Настройки в блоке "Система отношений" поставьте "Тип оценок - Только плюсы", затем нажмите кнопку "Сохранить".
Скриншот

🛠️Установка скрипта
🔀Самый простой способ:
В Администрирование - Формы - HTML низ вставить код:
Недостаток самого простого способа: картинка с "Сердечком" может не подходить под стиль Вашего форума (у меня стоит предустановленный "Mybb vBulletin mix").
🔀Рекомендуемый способ:
В Администрирование - Формы - HTML низ вставить код:
Ссылку замените на файл c Вашей картинкой сердечка (с нужным Вам фоном или на прозрачном фоне).
🔀Способ для опытных администраторов:
Исходные коды CSS и JS вставляете в HTML Низ, а ещё лучше - сохраните коды в файлы (без строчек с <style> и </style> в файле с CSS и без <script> и </script> для файла с JS) и подключите как внешние ресурсы через link rel и script src.
🧩Исходные коды🧩
⚡Мгновенные "Быстроплюсы" и ⚡мгновенные "Сказали спасибо"
CSS (⚡Мгновенные" Быстроплюсы" и ⚡Мгновенные "Сказали спасибо")
JS (⚡Мгновенные" Быстроплюсы" и ⚡Мгновенные "Сказали спасибо")
Код:<script>
'use strict';
// --- ОБЩИЕ ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---
// Получает ID постов из указанного DOM-контекста
function getPostIds(scope = document) {
const postElements = $(scope).find('div.post');
return postElements.map(function() {
const id = $(this).attr('id');
if (id && id.startsWith('p')) {
return id.slice(1);
}
}).get();
}
// --- ЧАСТЬ 1: БЫСТРОПЛЮСЫ ---
// Отслеживание уже обработанных постов (для избежания дублирования)
const processedPostsForPlus = new Set();
// Обновляет отображение цифры рейтинга и управляет классом noNull
function setDigit(th) {
var d = parseInt(th.innerHTML);
if (d > 0) $(th).addClass('noNull');
else $(th).removeClass('noNull');
th.innerHTML = d;
}
// Сохранение оригинального alert для восстановления после голосования
var Busy = window.alert;
// Обработка клика по кнопке "плюса": отправка запроса, обновление рейтинга и мини-профиля
function setPlus(sel) {
window.alert = null;
var a = sel.prop('href');
var pid = a.match(/\?id=(\d+)/)[1];
var uid = sel.parents('.post').find('.pl-email a[href*="profile.php?"]').prop('href');
if (uid) uid = uid.match(/\?id=(\d+)$/)[1];
var v = a.match(/&v=(\d+)/)[1] == 0 ? -1 : 1;
$('#post-' + pid + '-vote').hide();
$.get(a + '&format=json', function(data) {
if (data.error && data.error.message) {
// Ошибка обрабатывается без уведомлений — только внутренняя логика
} else if (data.delta) {
if(data.response !== undefined) {
$('#p' + pid + ' .post-rating a').text(data.response);
setDigit($('#p' + pid + ' .post-rating a')[0]);
} else {
var oldRating = parseInt($('#p' + pid + ' .post-rating a').text()) || 0;
var newRating = oldRating + data.delta;
$('#p' + pid + ' .post-rating a').text(newRating);
setDigit($('#p' + pid + ' .post-rating a')[0]);
}
// Обновление мини-профиля автора и текущего пользователя
var $res = $('.pl-email a[href$="profile.php?id=' + uid + '"]').parents('.post').find('.pa-respect');
var $pos = $('.pl-email a[href$="profile.php?id=' + UserID + '"]').parents('.post').find('.pa-positive');
function replaceRating(sel, v, revert) {
var html = $(sel).html(),
delta = v;
if (revert) delta = delta > 0 ? -1 : 1;
if (v > 0) {
html = html.replace(/\[\+(\d+)\//g, function(str, p1) {
return '[+' + (parseInt(p1) + delta) + '/';
});
} else {
html = html.replace(/\/-(\d+)\]/g, function(str, p1) {
return '/-' + (parseInt(p1) - delta) + ']';
});
}
$(sel).html(html);
}
if ($res.html().indexOf('[') != -1) {
$res.each(function() { replaceRating(this, v); });
$pos.each(function() { replaceRating(this, v); });
if (Math.abs(data.delta) == 2) {
v = v > 0 ? -1 : 1;
$res.each(function() { replaceRating(this, v, 1); });
$pos.each(function() { replaceRating(this, v, 1); });
}
} else {
var d0 = $res.find('span:not(.fld-name)').html(),
p0 = $pos.find('span:not(.fld-name)').html();
var d1 = parseInt(d0) + v;
if (p0) {
var p1 = parseInt(p0) + v;
}
if (d1 && d1 > 0) {
d1 = '+' + d1;
}
if (p1 && p1 > 0) {
p1 = '+' + p1;
}
$res.find('span:not(.fld-name)').html(d1);
if (p0) $pos.find('span:not(.fld-name)').html(p1);
}
// Генерация события для обновления списка "Спасибо" при лайке
if(v === 1) {
document.dispatchEvent(new CustomEvent('vote:happened', {
detail: { pid: pid, type: 'thank' }
}));
}
}
setTimeout(function() {window.alert = Busy;},1300);
})
.fail(function(xhr, status, error) {
setTimeout(function() {window.alert = Busy;},1300);
});
}
// Обёртка для вызова setPlus из onclick
window.BR = function(th) {
var lnk = $(th).parents('.post-box').find('.post-vote>p>a');
setPlus(lnk);
};
// Скрывает все вновь добавленные элементы .post-vote
function hideAllNewPostVotes(scope = document) {
$(scope).find('.post-vote[id^="post-"]').each(function() {
var postId = this.id.replace('post-', '');
var postRatingLink = $(`.post#p${postId} .post-rating p a`);
if(postRatingLink.length) {
$(this).hide();
}
});
}
// Назначает обработчики для кнопок "плюса" в указанной области
function applyPlusHandlers(scope = document) {
$(scope).find('.post .post-rating p a').each(function() {
if (!this.hasAttribute('data-plus-handler')) {
this.setAttribute("onclick", "BR(this)");
setDigit(this);
var postVoteLinkSelector = $(this).parents('.post').attr('id');
if(postVoteLinkSelector) {
var pid = postVoteLinkSelector.slice(1);
var postVoteLink = $('#post-' + pid + '-vote');
if(postVoteLink.length) {
postVoteLink.hide();
}
}
this.setAttribute('data-plus-handler', 'true');
}
});
hideAllNewPostVotes(scope);
}
// --- ЧАСТЬ 2: СПИСОК ПОБЛАГОДАРИВШИХ (API-ЛОГИКА) ---
// Отслеживание постов, для которых уже запрашивались "спасибо"
const processedPostsForThanks = new Set();
// Отслеживание ID постов, для которых выполняется запрос (для дедупликации)
const pendingUpdates = new Set();
// Вставка списка благодаривших в пост
function insertThanksList(postId, thankersListHtml) {
if (!thankersListHtml) {
return;
}
var postBox = $('.post#p' + postId + ' .post-box');
if (postBox.length && postBox.find('.postVoters').length === 0) {
var html = '<div class="postVoters"><strong><i><font color ="#13355e">Сказали спасибо</font></i>:</strong> ' + thankersListHtml + '</div>';
postBox.append(html);
processedPostsForThanks.add(postId);
} else {
if(postBox.length === 0) {
var postElement = $('.post#p' + postId);
if(postElement.length && postElement.find('.postVoters').length === 0) {
var html = '<div class="postVoters"><strong><i><font color ="#13355e">Сказали спасибо</font></i>:</strong> ' + thankersListHtml + '</div>';
postElement.append(html);
processedPostsForThanks.add(postId);
}
} else {
postBox.find('.postVoters').html('<strong><i><font color ="#13355e">Сказали спасибо</font></i>:</strong> ' + thankersListHtml);
}
}
}
// Обработка ответа API: группировка и вставка благодаривших
function processVotes(data) {
if (!data || !data.response) {
const postIdsFromRequest = currentFetchRequestIds || [];
postIdsFromRequest.forEach(id => processedPostsForThanks.add(id));
currentFetchRequestIds = null;
return;
}
const votesByPost = {};
data.response.forEach(item => {
const postId = item.post_id;
if (item.votes && Array.isArray(item.votes)) {
item.votes.forEach(vote => {
if (vote.value === '1') {
if (!votesByPost[postId]) {
votesByPost[postId] = [];
}
votesByPost[postId].push(vote);
}
});
}
});
Object.keys(votesByPost).forEach(postId => {
const votesForThisPost = votesByPost[postId];
let thankersHtml = '';
votesForThisPost.forEach((vote, index) => {
thankersHtml += '<a href="/profile.php?id=' + vote.user_id + '">' + vote.username + '</a>';
if (index < votesForThisPost.length - 1) {
thankersHtml += ', ';
}
});
insertThanksList(postId, thankersHtml);
});
Object.keys(votesByPost).forEach(id => processedPostsForThanks.add(id));
data.response.forEach(item => {
if (!processedPostsForThanks.has(item.post_id)) {
processedPostsForThanks.add(item.post_id);
}
});
}
// Выполнение API-запроса для получения списка благодаривших
let currentFetchRequestIds = null;
function fetchVotesForPosts(postIds) {
if (!postIds || postIds.length === 0) {
postIds.forEach(id => pendingUpdates.delete(id));
return;
}
const apiParams = {
method: 'post.getVotesByPosts',
post_id: postIds.join(','),
fields: 'post_id,user_id,username,value,datetime',
sort_dir: 'desc'
};
currentFetchRequestIds = [...postIds];
return $.get('/api.php', apiParams, function(data) {
processVotes(data);
}, 'json')
.fail(function(xhr, status, error) {
postIds.forEach(id => processedPostsForThanks.add(id));
currentFetchRequestIds = null;
});
}
// Планирование обновления списка "спасибо" для новых постов
function scheduleUpdate(scope = document) {
requestAnimationFrame(() => {
const newPostIds = getPostIds(scope).filter(id => !processedPostsForThanks.has(id));
if (newPostIds.length > 0) {
const idsToRequest = newPostIds.filter(id => !pendingUpdates.has(id));
if (idsToRequest.length > 0) {
idsToRequest.forEach(id => pendingUpdates.add(id));
fetchVotesForPosts(idsToRequest).always(() => {
idsToRequest.forEach(id => pendingUpdates.delete(id));
});
}
}
});
}
// Объединённая функция резервного обновления (раз в секунду)
function combinedBackupUpdate() {
applyPlusHandlers(document);
scheduleUpdate(document);
}
// --- ИНИЦИАЛИЗАЦИЯ И ПОДПИСКИ НА СОБЫТИЯ ---
// Инициализация при загрузке DOM
$(function() {
$('.post .post-rating p a').each(function() {
this.setAttribute("onclick", "BR(this)");
setDigit(this);
var postVoteLinkSelector = $(this).parents('.post').attr('id');
if(postVoteLinkSelector) {
var pid = postVoteLinkSelector.slice(1);
var postVoteLink = $('#post-' + pid + '-vote');
if(postVoteLink.length && $(this).hasClass('noNull')) {
postVoteLink.hide();
}
}
this.setAttribute('data-plus-handler', 'true');
});
scheduleUpdate(document);
});
// Подписка на событие добавления нового поста (например, через AJAX)
document.addEventListener('pun_post', function(e) {
var scope = e.detail?.post || document.body;
applyPlusHandlers(scope);
scheduleUpdate(scope);
});
// Подписка на собственное событие после успешного лайка
document.addEventListener('vote:happened', function(e) {
if (e.detail && e.detail.pid && e.detail.type === 'thank') {
var pid = e.detail.pid;
var specificPostScope = $('#p' + pid);
if(specificPostScope.length) {
if (!pendingUpdates.has(pid)) {
pendingUpdates.add(pid);
fetchVotesForPosts([pid]).always(() => {
pendingUpdates.delete(pid);
});
}
}
}
});
// Подписка на глобальное AJAX-событие для отслеживания лайков от любых пользователей
$(document).on('ajaxSuccess', function(e, xhr, data) {
if (data && data.url && /relation\.php.*[&?]v=(\d+)/.test(data.url)) {
const match = data.url.match(/id=(\d+)/);
const pid = match ? match[1] : null;
const voteType = data.url.match(/v=(\d+)/)?.[1];
if (pid && voteType === '1') {
if (!pendingUpdates.has(pid)) {
pendingUpdates.add(pid);
fetchVotesForPosts([pid]).always(() => {
pendingUpdates.delete(pid);
});
}
}
}
});
// Наблюдатель за изменениями DOM для динамических постов
const observer = new MutationObserver(function(mutationsList) {
let shouldUpdate = false;
let relevantScope = document.body;
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
for (let node of mutation.addedNodes) {
if (node.nodeType === 1) {
if (node.classList && node.classList.contains('post')) {
relevantScope = node;
shouldUpdate = true;
break;
} else if (node.querySelector && node.querySelector('div.post')) {
relevantScope = node;
shouldUpdate = true;
break;
} else {
const nestedPosts = node.querySelectorAll && node.querySelectorAll('div.post');
if (nestedPosts && nestedPosts.length > 0) {
relevantScope = node;
shouldUpdate = true;
break;
}
}
}
}
if (shouldUpdate) break;
}
}
if (shouldUpdate) {
requestAnimationFrame(() => {
applyPlusHandlers(relevantScope);
hideAllNewPostVotes(relevantScope);
scheduleUpdate(relevantScope);
});
}
});
observer.observe(document.body, { childList: true, subtree: true });
// Резервный таймер для обновления раз в секунду
const backupInterval = setInterval(combinedBackupUpdate, 1000);
</script>
Если вам нужны лишь "Быстроплюсы", то вы берёте вышеприведённый CSS, а JS будет такой:
JS (⚡Мгновенные "Быстроплюсы")
При разработке использовался ChatGPT и т.п. нейросети, затем код был проверен и протестирован, сейчас всё работает на реальных форумах.
(указано по требованию администрации сервиса)
Отредактировано Merlin777 (Пн, 1 Дек 2025 22:48:20)