diff --git a/README.md b/README.md index 7801e87..5976817 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,15 @@ * Комментарии родители (имеющие ответы/ветку ответов) получают два варианта одиночного удаления: * Мягкое удаление (удалится все , что относится к данному комментарию (Рейтинги, файлы , текст) и вместо этого будет выводится специальный аватар и текст: Комментарий удален администратором. Восстановить данные такого комментария невозможно, * Жесткое удаление самого комментария и всей ветки ответов на этот комментарий. Администратор выбирает сам какой вариант ему необходим. - * Массовое удаление - одиночные комментарии удаляются сразу, комментарии-родители удаляются Мягким вариантом (с подстановкой текста Комментарий удален Администратором). + * Массовое удаление - одиночные комментарии удаляются сразу, комментарии-родители удаляются Мягким вариантом (с подстановкой текста Комментарий удален Администратором). +* Статус Скрыт/Опубликован (иконка глаз) + * Если комментарий родитель и на него есть ответы: + * При клике по иконке Скрыть (как в публичной части сайта так и в Админке) - автоматически будет скрыт как сам комментарий так и все ответы на него (ветка комментариев), при этом у ответов возможность скрытия/публикации будет заблокирована до тех пор, пока родительский комментарий не будет разблокирован (опубликован). Восстановление в ветке статусов - опубликован, происходит персонально, кликом Опубликовать по каждому комментарию в ветке. + * Если комментарий одиночный (на него нет ответов): + * При клике по иконке Скрыть - комментарий будет скрыт. При клике опубликовать - будет опубликован. +* Видимость скрытых комментариев: + * Если комментарий родитель и на него есть ответы и он скрыт - будут также скрыты все ответы на него. Такой комментарий будет видеть только Администратор и Автор комментария с пометкой "На модерации", однако если этот комментарий будет в свою очередь потомком вышестояшего в иерархии комментария-родителя другого Автора и тот в свою очередь будет скрыт - то тогда комментарий будет виден только Администратору. + * Если комментарий одиночный и на него нет ответов и он, в свою очередь, сам не является ответом: если статус Скрыт - то его видит только Администратор и сам Автор. Другие посетители его не видят. * Контроль Имен Анонима * Если Анонимный пользователь, в течении жизни куки, сменит имя под которым он опубликовал свой первый комментарий, рядом с именем появится плашка с тултипом в котором будут перечислены все его имена. * Пагинация diff --git a/class/comment.php b/class/comment.php index de1d71e..a8aca97 100644 --- a/class/comment.php +++ b/class/comment.php @@ -1326,20 +1326,48 @@ function commentAdminDelete($comment_id) * @param int $comment_id - идентификатор комментария * @param string $comment_status - {lock|unlock} признак запрета/разрешения */ - function commentReplyStatusSet($comment_id, $comment_status = 'lock') - { - global $AVE_DB; +function commentReplyStatusSet($comment_id, $comment_status = 'lock') +{ + global $AVE_DB; + $comment_id = (int)$comment_id; + + // Определяем цифровой статус: lock -> 0, unlock -> 1 + $status_numeric = ($comment_status == 'lock') ? 0 : 1; - $comment_id = (int)$comment_id; + if ($status_numeric == 0) { + // --- ТУДА (Скрываем всю ветку) --- + $all_ids = [$comment_id]; + $stack = [$comment_id]; + while (!empty($stack)) { + $curr_id = array_shift($stack); + $res = $AVE_DB->Query("SELECT Id FROM " . PREFIX . "_module_comment_info WHERE parent_id = '$curr_id'"); + while ($res && $row = $res->FetchAssocArray()) { + $all_ids[] = (int)$row['Id']; + $stack[] = (int)$row['Id']; + } + } + $ids_string = implode(',', $all_ids); + $AVE_DB->Query("UPDATE " . PREFIX . "_module_comment_info SET comment_status = '0' WHERE Id IN ($ids_string)"); + echo "OK_LOCKED"; + } +else { + // --- ОБРАТНО (Открываем ТОЛЬКО один коммент) --- + + // Магия SQL: Обновляем только если (родителя нет) ИЛИ (у родителя статус не 0) + $AVE_DB->Query(" + UPDATE " . PREFIX . "_module_comment_info AS child + LEFT JOIN " . PREFIX . "_module_comment_info AS parent ON child.parent_id = parent.Id + SET child.comment_status = '1' + WHERE child.Id = '$comment_id' + AND (child.parent_id = 0 OR parent.comment_status != '0') + "); - $AVE_DB->Query(" - UPDATE " . PREFIX . "_module_comment_info - SET comment_status = '" . (($comment_status == 'lock') ? 0 : 1) . "' - WHERE Id = '" . $comment_id . "' - "); + // Чтобы JS не тупил, всегда отвечаем OK (так как в базе либо уже 1, либо стала 1) + echo "OK_UNLOCKED"; + } - exit; - } + exit; +} /** * Метод, предназначенный для управления запретом или разрешением комментировать документ @@ -1387,51 +1415,70 @@ function commentAdminListShow($tpl_dir) $items = (is_array($_REQUEST['id'])) ? $_REQUEST['id'] : [$_REQUEST['id']]; } - if (!empty($action) && !empty($items)) { - $ids = array_map('intval', $items); +// Проверяем наличие действия И (наличие массива галочек ИЛИ наличие одиночного ID в ссылке) + if (!empty($action) && (!empty($items) || !empty($_REQUEST['id']))) { - // --- СОБИРАЕМ ВСЕ ID ПОТОМКОВ (ЛЮБАЯ ГЛУБИНА) --- - $all_ids_to_process = $ids; - $current_parents = $ids; - - while (!empty($current_parents)) { - $parent_list = implode(',', $current_parents); - - // Получаем результат запроса - $res = $AVE_DB->Query("SELECT Id FROM " . PREFIX . "_module_comment_info WHERE parent_id IN ($parent_list)"); - - $found_children = []; - // Перебираем результат построчно - if ($res) { - while ($row_child = $res->FetchAssocArray()) { - $found_children[] = (int)$row_child['Id']; - } - } - - if (!empty($found_children)) { - $all_ids_to_process = array_merge($all_ids_to_process, $found_children); - $current_parents = $found_children; - } else { - $current_parents = []; - } + // Формируем единый массив $ids для обработки + if (!empty($items)) { + $ids = array_map('intval', $items); + } else { + // Если пришел одиночный ID (может быть как числом, так и массивом из одного элемента) + $single_id = $_REQUEST['id']; + $ids = is_array($single_id) ? array_map('intval', $single_id) : [(int)$single_id]; } - $all_ids_to_process = array_unique($all_ids_to_process); - $final_id_list = implode(',', $all_ids_to_process); - // ------------------------------------------------ + + // 1. Для СКРЫТИЯ собираем "паровоз" (всех детей на любую глубину) + $all_related_for_hiding = $ids; + $current_p = $ids; + while (!empty($current_p)) { + $p_str = implode(',', $current_p); + $res = $AVE_DB->Query("SELECT Id FROM " . PREFIX . "_module_comment_info WHERE parent_id IN ($p_str)"); + $found = []; + if ($res) { + while ($r = $res->FetchAssocArray()) { $found[] = (int)$r['Id']; } + } + if (!empty($found)) { + $all_related_for_hiding = array_merge($all_related_for_hiding, $found); + $current_p = $found; + } else { $current_p = []; } + } + $final_hide_list = implode(',', array_unique($all_related_for_hiding)); switch ($action) { case 'approve': case 'set_status_1': - $AVE_DB->Query("UPDATE " . PREFIX . "_module_comment_info SET comment_status = '1' WHERE Id IN ($final_id_list)"); + // ПУБЛИКАЦИЯ: Работаем СТРОГО с теми ID, на которых кликнули или поставили галки + $active_db = []; + $res_act = $AVE_DB->Query("SELECT Id FROM " . PREFIX . "_module_comment_info WHERE comment_status = '1'"); + if ($res_act) { + while ($row = $res_act->FetchAssocArray()) { $active_db[] = (int)$row['Id']; } + } + + $to_publish = []; + foreach ($ids as $curr_id) { + $p_id = (int)$AVE_DB->Query("SELECT parent_id FROM " . PREFIX . "_module_comment_info WHERE Id = $curr_id")->GetCell(); + + // Разрешаем включить, только если родитель УЖЕ активен в базе + if ($p_id == 0 || in_array($p_id, $active_db)) { + $to_publish[] = $curr_id; + } + } + + if (!empty($to_publish)) { + $pub_str = implode(',', $to_publish); + $AVE_DB->Query("UPDATE " . PREFIX . "_module_comment_info SET comment_status = '1' WHERE Id IN ($pub_str)"); + } break; + case 'unapprove': case 'set_status_0': - $AVE_DB->Query("UPDATE " . PREFIX . "_module_comment_info SET comment_status = '0' WHERE Id IN ($final_id_list)"); + // СКРЫТИЕ: Используем полный список (родители + все дети) + if (!empty($final_hide_list)) { + $AVE_DB->Query("UPDATE " . PREFIX . "_module_comment_info SET comment_status = '0' WHERE Id IN ($final_hide_list)"); + } break; - //case 'delete': - // $AVE_DB->Query("DELETE FROM " . PREFIX . "_module_comment_info WHERE Id IN ($final_id_list)"); - //break; } + header("Location: index.php?do=modules&action=modedit&mod=comment&moduleaction=1&cp=" . $session_id); exit; } @@ -1521,71 +1568,53 @@ function commentAdminListShow($tpl_dir) if ($p_id > 0) $child_map[$p_id][] = $id; } - // Проходим по всем элементам и помечаем тех, у кого есть дети в child_map foreach ($all_items as $id => &$item_for_check) { $item_for_check['has_children'] = isset($child_map[$id]) ? 1 : 0; } - unset($item_for_check); // очистка ссылки + unset($item_for_check); $docs = []; $processed = []; - $buildTree = function($parent_id, $level) use (&$buildTree, &$docs, &$processed, &$all_items, &$child_map) { + // Универсальная функция с наследованием блокировки parent_locked + $buildTree = function($parent_id, $level, $is_parent_hidden) use (&$buildTree, &$docs, &$processed, &$all_items, &$child_map) { if (isset($child_map[$parent_id])) { foreach ($child_map[$parent_id] as $child_id) { if (!in_array($child_id, $processed)) { $item = $all_items[$child_id]; $item['depth_level'] = $level; - // has_children уже записан в $all_items выше + + // Блокируем ребенка, если скрыт его родитель или кто-то выше по дереву + $item['parent_locked'] = ($is_parent_hidden || $all_items[$parent_id]['comment_status'] == '0') ? 1 : 0; + $docs[] = $item; $processed[] = $child_id; - $buildTree($child_id, $level + 1); + $buildTree($child_id, $level + 1, $item['parent_locked']); } } } }; + // Собираем дерево, начиная с корней foreach ($all_items as $id => $item) { if ($item['parent_id'] == 0 && !in_array($id, $processed)) { $item['depth_level'] = 0; + $item['parent_locked'] = 0; $docs[] = $item; $processed[] = $id; - $buildTree($id, 1); - } - } - - $docs = []; - $processed = []; - - $buildTree = function($parent_id, $level) use (&$buildTree, &$docs, &$processed, &$all_items, &$child_map) { - if (isset($child_map[$parent_id])) { - foreach ($child_map[$parent_id] as $child_id) { - if (!in_array($child_id, $processed)) { - $item = $all_items[$child_id]; - $item['depth_level'] = $level; - $docs[] = $item; - $processed[] = $child_id; - $buildTree($child_id, $level + 1); - } - } - } - }; - - foreach ($all_items as $id => $item) { - if ($item['parent_id'] == 0 && !in_array($id, $processed)) { - $item['depth_level'] = 0; - $docs[] = $item; - $processed[] = $id; - $buildTree($id, 1); + $buildTree($id, 1, false); } } + // Добираем то, что не попало в иерархию (битые связи) foreach ($all_items as $id => $item) { if (!in_array($id, $processed)) { $item['depth_level'] = 0; + $item['parent_locked'] = 0; $docs[] = $item; } } +// --- КОНЕЦ ПОСТРОЕНИЯ ДЕРЕВА --- $comment_rating_type = $AVE_DB->Query(" diff --git a/js/comment.js b/js/comment.js index 7a417c9..eba9b22 100644 --- a/js/comment.js +++ b/js/comment.js @@ -177,7 +177,10 @@ if ($fileInput.length && $fileInput[0].files.length > 0) { return isValid; } - /* --- ДЕЙСТВИЯ (DELETE, LOCK, OPEN/CLOSE, VOTE) --- */ + + + +/* --- ДЕЙСТВИЯ (DELETE, LOCK, OPEN/CLOSE, VOTE) --- */ function cAction(obj, action) { var $btn = $(obj); var cid = $btn.data('id'); @@ -186,75 +189,86 @@ function cAction(obj, action) { $.get(aveabspath + 'index.php', { module: 'comment', - action: action, + action: action, // Это для роутинга (какой метод вызвать) + comment_status: action, // что именно сделать: lock или unlock docid: typeof DOC_ID !== 'undefined' ? DOC_ID : '', Id: cid ? cid : '' - }, function(data) { // Добавляем аргумент data для получения ответа от PHP + }, function(data) { - if (action === 'close') { - $btn.attr('id', 'mod_comment_open') - .removeClass('btn-outline-danger').addClass('btn-outline-success') - .html(' ' + COMMENT_SITE_OPEN); - $('#mod_comment_new').fadeOut(300); - } - else if (action === 'open') { - $btn.attr('id', 'mod_comment_close') - .removeClass('btn-outline-success').addClass('btn-outline-danger') - .html(' ' + COMMENT_SITE_CLOSE); - $('#mod_comment_new').fadeIn(300); + if (action === 'close' || action === 'open') { + location.reload(); // Для глобального закрытия проще обновить + return; } -// --- ОБНОВЛЕННЫЙ БЛОК УДАЛЕНИЯ --- if (action === 'delete') { var response = data ? data.trim() : ''; - var $commentBlock = $btn.closest('.mod_comment_comment'); // Вся ветка (включая детей) - var $commentBox = $btn.closest('.mod_comment_box'); // Только текущая карточка - + var $commentBlock = $btn.closest('.mod_comment_comment'); if (response === 'OK_SOFT') { - // обновляем страницу location.reload(); } else { - // удаляем совсем комментарий $commentBlock.fadeOut(300, function() { $(this).remove(); }); } } - // показать / скрыть комментарий - if (action === 'unlock' || action === 'lock') { - var $icon = $btn.find('i'); - var $commentBox = $btn.closest('.mod_comment_box'); - var $card = $btn.closest('.mod_comment_comment'); - - if (action === 'lock') { - $icon.attr('class', 'bi bi-eye-slash'); - $btn.removeClass('text-success text-muted').addClass('text-danger').attr('title', COMMENT_ICON_SHOW); - $commentBox.addClass('opacity-75 border border-warning rounded'); +// --- БЛОК LOCK / UNLOCK --- +if (action === 'unlock' || action === 'lock') { + var $mainWrapper = $btn.closest('.mod_comment_comment'); + var $currentBox = $btn.closest('.mod_comment_box'); - var $alert = $commentBox.find('.alert-warning'); - if ($alert.length === 0) { - var alertHtml = '
'; - $commentBox.find('.flex-grow-1').first().prepend(alertHtml); - $alert = $commentBox.find('.alert-warning'); - } - $alert.stop(true, true).fadeIn(300); - } else { - $icon.attr('class', 'bi bi-eye'); - $btn.removeClass('text-danger text-muted').addClass('text-success').attr('title', COMMENT_ICON_HIDE); - var classesToRemove = 'opacity-75 border border-warning rounded'; - $commentBox.removeClass(classesToRemove); - $card.removeClass(classesToRemove + ' border-warning'); - - $commentBox.find('.alert-warning').first().stop(true, true).fadeOut(300, function() { - $(this).remove(); - }); + if (action === 'lock') { + // --- СКРЫТИЕ (lock) --- + // 1. МЕНЯЕМ ПОДСКАЗКУ НА "ПОКАЗАТЬ" + $btn.attr('title', 'Показать комментарий'); + + $mainWrapper.find('.mod_comment_box').each(function() { + var $box = $(this); + var $tgl = $box.find('.mod_comment_toggle'); + + $box.addClass('opacity-75 border border-warning rounded'); + $tgl.removeClass('text-success text-muted').addClass('text-danger'); + $tgl.find('i').attr('class', 'bi bi-eye-slash'); + + if ($box.find('.alert-warning').length === 0) { + var alertHtml = '