I suppose it's kinda ready to push to master rn? not sure yet though

This commit is contained in:
flash 2018-08-10 06:20:54 +02:00
parent 9f97f38b7c
commit 6a7edd84a7
10 changed files with 290 additions and 117 deletions

View file

@ -67,6 +67,10 @@
transition: opacity .2s; transition: opacity .2s;
} }
&--voted {
font-weight: 700;
}
&__checkbox { &__checkbox {
vertical-align: text-top; vertical-align: text-top;
margin-right: 2px; margin-right: 2px;

View file

@ -70,10 +70,40 @@ function migrate_up(PDO $conn): void
ON DELETE CASCADE ON DELETE CASCADE
); );
"); ");
$conn->exec("
ALTER TABLE `msz_news_posts`
ADD COLUMN `comment_section_id` INT UNSIGNED NULL DEFAULT NULL AFTER `deleted_at`,
ADD INDEX `news_posts_comment_section` (`comment_section_id`),
ADD CONSTRAINT `news_posts_comment_section`
FOREIGN KEY (`comment_section_id`)
REFERENCES `msz_comments_categories` (`category_id`)
ON UPDATE CASCADE
ON DELETE SET NULL;
");
// create a comment section for all news posts
$getNews = $conn->query('SELECT `post_id` FROM `msz_news_posts` WHERE `comment_section_id` IS NULL')
->fetchAll(PDO::FETCH_ASSOC);
$setNews = $conn->prepare('UPDATE `msz_news_posts` SET `comment_section_id` = :c WHERE `post_id` = :p');
foreach ($getNews as $post) {
$info = comments_category_create("news-{$post['post_id']}");
$setNews->execute([
'p' => $post['post_id'],
'c' => $info['category_id'],
]);
}
} }
function migrate_down(PDO $conn): void function migrate_down(PDO $conn): void
{ {
$conn->exec('
ALTER TABLE `msz_news_posts`
DROP COLUMN `comment_section_id`,
DROP INDEX `news_posts_comment_section`,
DROP FOREIGN KEY `news_posts_comment_section`;
');
$conn->exec('DROP TABLE `msz_comments_votes`'); $conn->exec('DROP TABLE `msz_comments_votes`');
$conn->exec('DROP TABLE `msz_comments_posts`'); $conn->exec('DROP TABLE `msz_comments_posts`');
$conn->exec('DROP TABLE `msz_comments_categories`'); $conn->exec('DROP TABLE `msz_comments_categories`');

View file

@ -64,7 +64,7 @@ if ($changelogChange > 0) {
"changelog-date-{$change['change_date']}", "changelog-date-{$change['change_date']}",
true true
), ),
'comments' => comments_category_get($commentsCategory['category_id']), 'comments' => comments_category_get($commentsCategory['category_id'], $app->getUserId()),
]); ]);
return; return;
} }
@ -90,7 +90,7 @@ if (!$changes) {
if (!empty($changelogDate)) { if (!empty($changelogDate)) {
$tpl->vars([ $tpl->vars([
'comments_category' => $commentsCategory = comments_category_info("changelog-date-{$changelogDate}", true), 'comments_category' => $commentsCategory = comments_category_info("changelog-date-{$changelogDate}", true),
'comments' => comments_category_get($commentsCategory['category_id']), 'comments' => comments_category_get($commentsCategory['category_id'], $app->getUserId()),
]); ]);
} }

View file

@ -3,11 +3,16 @@ use Misuzu\Database;
require_once __DIR__ . '/../misuzu.php'; require_once __DIR__ . '/../misuzu.php';
// if false, display informational pages instead of outputting json. // basing whether or not this is an xhr request on whether a referrer header is present
$isXHR = !empty($_SERVER['HTTP_MISUZU_XHR_REQUEST']); // this page is never directy accessed, under normal circumstances
$redirect = !empty($_SERVER['HTTP_REFERER']) && empty($_SERVER['HTTP_X_MISUZU_XHR']) ? $_SERVER['HTTP_REFERER'] : '';
$isXHR = !$redirect;
if ($isXHR || $_SERVER['REQUEST_METHOD'] === 'GET') { if ($isXHR) {
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');
} elseif (!is_local_url($redirect)) {
echo render_info('Possible request forgery detected.', 403);
return;
} }
if ($app->getUserId() < 1) { if ($app->getUserId() < 1) {
@ -15,38 +20,95 @@ if ($app->getUserId() < 1) {
return; return;
} }
$redirect = !$isXHR && !empty($_SERVER['HTTP_REFERER'])
&& is_local_url($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
$commentPerms = comments_get_perms($app->getUserId()); $commentPerms = comments_get_perms($app->getUserId());
$commentId = (int)($_REQUEST['comment_id'] ?? 0);
if (isset($_POST['vote']) && array_key_exists((int)$_POST['vote'], MSZ_COMMENTS_VOTE_TYPES)) { switch ($_GET['m'] ?? null) {
echo comments_vote_add( case 'vote':
$commentId, $comment = (int)($_GET['c'] ?? 0);
if ($comment < 1) {
echo render_info_or_json($isXHR, 'Missing data.', 400);
break;
}
$vote = (int)($_GET['v'] ?? 0);
if (!array_key_exists($vote, MSZ_COMMENTS_VOTE_TYPES)) {
echo render_info_or_json($isXHR, 'Invalid vote action.', 400);
break;
}
$vote = MSZ_COMMENTS_VOTE_TYPES[(int)($_GET['v'] ?? 0)];
$voteResult = comments_vote_add(
$comment,
$app->getUserId(), $app->getUserId(),
MSZ_COMMENTS_VOTE_TYPES[(int)$_POST['vote']] $vote
); );
return;
}
switch ($_SERVER['REQUEST_METHOD']) { if (!$isXHR) {
case 'GET': header('Location: ' . $redirect . '#comment-' . $comment);
break;
}
echo '[]'; // we don't really need a answer for this, the client implicitly does all
break;
/*
case 'delete':
if ($commentId < 1) { if ($commentId < 1) {
echo render_info_or_json(true, 'Missing data.', 400); echo render_info_or_json($isXHR, 'Missing data.', 400);
break; break;
} }
switch ($_GET['fetch'] ?? '') { if (!$commentPerms['can_delete']) {
case 'replies': echo render_info_or_json($isXHR, "You're not allowed to delete comments.", 403);
echo json_encode(comments_post_replies($commentId));
break; break;
default:
echo json_encode(comments_post_get($commentId));
} }
if (!$commentPerms['can_delete_any']
&& !comments_post_check_ownership($commentId, $app->getUserId())) {
echo render_info_or_json($isXHR, "You're not allowed to delete comments made by others.", 403);
break;
}
if (!comments_post_delete($commentId)) {
echo render_info_or_json($isXHR, 'Failed to delete comment.', 500);
break;
}
if ($redirect) {
header('Location: ' . $redirect);
break;
}
echo render_info_or_json($isXHR, 'Comment deleted.');
break; break;
case 'POST': case 'edit':
if ($commentId < 1) {
echo render_info_or_json($isXHR, 'Missing data.', 400);
break;
}
if (!$commentPerms['can_edit']) {
echo render_info_or_json($isXHR, "You're not allowed to edit comments.", 403);
break;
}
if (!$commentPerms['can_edit_any']
&& !comments_post_check_ownership($commentId, $app->getUserId())) {
echo render_info_or_json($isXHR, "You're not allowed to delete comments made by others.", 403);
break;
}
if ($redirect) {
header('Location: ' . $redirect . '#comment-' . $commentId);
break;
}
var_dump($_POST);
break;
*/
case 'create':
if (!$commentPerms['can_comment']) { if (!$commentPerms['can_comment']) {
echo render_info_or_json($isXHR, "You're not allowed to post comments.", 403); echo render_info_or_json($isXHR, "You're not allowed to post comments.", 403);
break; break;
@ -119,63 +181,6 @@ switch ($_SERVER['REQUEST_METHOD']) {
echo json_encode(comments_post_get($commentId)); echo json_encode(comments_post_get($commentId));
break; break;
case 'PATCH':
method_patch:
if ($commentId < 1) {
echo render_info_or_json($isXHR, 'Missing data.', 400);
break;
}
if (!$commentPerms['can_edit']) {
echo render_info_or_json($isXHR, "You're not allowed to edit comments.", 403);
break;
}
if (!$commentPerms['can_edit_any']
&& !comments_post_check_ownership($commentId, $app->getUserId())) {
echo render_info_or_json($isXHR, "You're not allowed to delete comments made by others.", 403);
break;
}
if ($redirect) {
header('Location: ' . $redirect . '#comment-' . $commentId);
break;
}
var_dump($_POST);
break;
case 'DELETE':
method_delete:
if ($commentId < 1) {
echo render_info_or_json($isXHR, 'Missing data.', 400);
break;
}
if (!$commentPerms['can_delete']) {
echo render_info_or_json($isXHR, "You're not allowed to delete comments.", 403);
break;
}
if (!$commentPerms['can_delete_any']
&& !comments_post_check_ownership($commentId, $app->getUserId())) {
echo render_info_or_json($isXHR, "You're not allowed to delete comments made by others.", 403);
break;
}
if (!comments_post_delete($commentId)) {
echo render_info_or_json($isXHR, 'Failed to delete comment.', 500);
break;
}
if ($redirect) {
header('Location: ' . $redirect);
break;
}
echo render_info_or_json($isXHR, 'Comment deleted.');
break;
default: default:
echo render_info_or_json($isXHR, 'Invalid request method.', 405); echo render_info_or_json($isXHR, 'Not found.', 404);
} }

View file

@ -22,7 +22,12 @@ $news = Database::query('
SELECT SELECT
p.`post_id`, p.`post_title`, p.`post_text`, p.`created_at`, p.`post_id`, p.`post_title`, p.`post_text`, p.`created_at`,
u.`user_id`, u.`username`, u.`user_id`, u.`username`,
COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour` COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour`,
(
SELECT COUNT(`comment_id`)
FROM `msz_comments_posts`
WHERE `category_id` = `comment_section_id`
) as `post_comments`
FROM `msz_news_posts` as p FROM `msz_news_posts` as p
LEFT JOIN `msz_users` as u LEFT JOIN `msz_users` as u
ON p.`user_id` = u.`user_id` ON p.`user_id` = u.`user_id`

View file

@ -18,7 +18,7 @@ $templating->vars([
if ($postId !== null) { if ($postId !== null) {
$getPost = Database::prepare(' $getPost = Database::prepare('
SELECT SELECT
p.`post_id`, p.`post_title`, p.`post_text`, p.`created_at`, p.`post_id`, p.`post_title`, p.`post_text`, p.`created_at`, p.`comment_section_id`,
c.`category_id`, c.`category_name`, c.`category_id`, c.`category_name`,
u.`user_id`, u.`username`, u.`user_id`, u.`username`,
COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour` COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour`
@ -39,7 +39,30 @@ if ($postId !== null) {
return; return;
} }
echo $templating->render('news.post', compact('post')); if ($post['comment_section_id'] === null) {
$commentsInfo = comments_category_create("news-{$post['post_id']}");
if ($commentsInfo) {
$post['comment_section_id'] = $commentsInfo['category_id'];
Database::prepare('
UPDATE `msz_news_posts`
SET `comment_section_id` = :comment_section_id
WHERE `post_id` = :post_id
')->execute([
'comment_section_id' => $post['comment_section_id'],
'post_id' => $post['post_id'],
]);
}
} else {
$commentsInfo = comments_category_info($post['comment_section_id']);
}
echo $templating->render('news.post', [
'post' => $post,
'comments_perms' => comments_get_perms($app->getUserId()),
'comments_category' => $commentsInfo,
'comments' => comments_category_get($commentsInfo['category_id'], $app->getUserId()),
]);
return; return;
} }

View file

@ -125,7 +125,25 @@ define('MSZ_COMMENTS_CATEGORY_QUERY', '
p.`comment_id`, p.`comment_text`, p.`comment_reply_to`, p.`comment_id`, p.`comment_text`, p.`comment_reply_to`,
p.`comment_created`, p.`comment_pinned`, p.`comment_created`, p.`comment_pinned`,
u.`user_id`, u.`username`, u.`user_id`, u.`username`,
COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour` COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour`,
(
SELECT COUNT(`comment_id`)
FROM `msz_comments_votes`
WHERE `comment_id` = p.`comment_id`
AND `comment_vote` = \'Like\'
) as `comment_likes`,
(
SELECT COUNT(`comment_id`)
FROM `msz_comments_votes`
WHERE `comment_id` = p.`comment_id`
AND `comment_vote` = \'Dislike\'
) as `comment_dislikes`,
(
SELECT `comment_vote`
FROM `msz_comments_votes`
WHERE `comment_id` = p.`comment_id`
AND `user_id` = :user
) as `comment_user_vote`
FROM `msz_comments_posts` as p FROM `msz_comments_posts` as p
LEFT JOIN `msz_users` as u LEFT JOIN `msz_users` as u
ON u.`user_id` = p.`user_id` ON u.`user_id` = p.`user_id`
@ -147,7 +165,7 @@ define('MSZ_COMMENTS_CATEGORY_QUERY_REPLIES', sprintf(
)); ));
// heavily recursive // heavily recursive
function comments_category_get(int $category, ?int $parent = null): array function comments_category_get(int $category, int $user, ?int $parent = null): array
{ {
if ($parent !== null) { if ($parent !== null) {
$getComments = Database::prepare(MSZ_COMMENTS_CATEGORY_QUERY_REPLIES); $getComments = Database::prepare(MSZ_COMMENTS_CATEGORY_QUERY_REPLIES);
@ -156,12 +174,13 @@ function comments_category_get(int $category, ?int $parent = null): array
$getComments = Database::prepare(MSZ_COMMENTS_CATEGORY_QUERY_ROOT); $getComments = Database::prepare(MSZ_COMMENTS_CATEGORY_QUERY_ROOT);
} }
$getComments->bindValue('user', $user);
$getComments->bindValue('category', $category); $getComments->bindValue('category', $category);
$comments = $getComments->execute() ? $getComments->fetchAll(PDO::FETCH_ASSOC) : []; $comments = $getComments->execute() ? $getComments->fetchAll(PDO::FETCH_ASSOC) : [];
$commentsCount = count($comments); $commentsCount = count($comments);
for ($i = 0; $i < $commentsCount; $i++) { for ($i = 0; $i < $commentsCount; $i++) {
$comments[$i]['comment_replies'] = comments_category_get($category, $comments[$i]['comment_id']); $comments[$i]['comment_replies'] = comments_category_get($category, $user, $comments[$i]['comment_id']);
} }
return $comments; return $comments;

View file

@ -2,7 +2,7 @@
{% set reply_mode = reply_to is not null %} {% set reply_mode = reply_to is not null %}
<form class="comment comment--input{% if reply_mode %} comment--reply{% endif %}" <form class="comment comment--input{% if reply_mode %} comment--reply{% endif %}"
method="post" action="/comments.php" method="post" action="/comments.php?m=create"
id="comment-{{ reply_mode ? 'reply-' ~ reply_to.comment_id : 'create-' ~ category.category_id }}"> id="comment-{{ reply_mode ? 'reply-' ~ reply_to.comment_id : 'create-' ~ category.category_id }}">
<input type="hidden" name="comment[category]" value="{{ category.category_id }}"> <input type="hidden" name="comment[category]" value="{{ category.category_id }}">
@ -85,6 +85,23 @@
{% endif %} {% endif %}
</div> </div>
<script>
window.addEventListener('load', function () {
if (typeof commentVote === 'function') { // if this exists, the user is allowed to vote
var likeButtons = document.getElementsByClassName('comment__action--like'),
dislikeButtons = document.getElementsByClassName('comment__action--dislike');
for (var i = 0; i < likeButtons.length; i++) // there's gonna be an equal amount of like and dislike buttons
{
likeButtons[i].href = 'javascript:void(0);';
likeButtons[i].onclick = commentVote;
dislikeButtons[i].href = 'javascript:void(0);';
dislikeButtons[i].onclick = commentVote;
}
}
});
</script>
{% if perms.can_comment %} {% if perms.can_comment %}
<script> <script>
var commentPostLock = false; var commentPostLock = false;
@ -98,34 +115,79 @@
{% if perms.can_vote %} {% if perms.can_vote %}
<script> <script>
var commentVoteLock = false; var commentVoteLock = false,
commentLikeClass = 'comment__action--like',
commentDislikeClass = 'comment__action--dislike',
commentVotedClass = 'comment__action--voted';
// this is kinda temporary // DEBUG THIS IF YOU MAKE MAJOR DOM CHANGES TO COMMENTS
// needs to be able to change the state of both the dislike and like buttons function commentVote(ev)
function commentVote(elem, id, vote)
{ {
var elem = ev.target,
id = elem.parentNode.parentNode.parentNode.parentNode.id.substr(8); // STACK UP
// the moment we find the id we engage vote lock
if (id < 1 || commentVoteLock) if (id < 1 || commentVoteLock)
return; return;
commentVoteLock = true; commentVoteLock = true;
var originalText = elem.textContent; var originalText = elem.textContent;
elem.textContent = '...'; elem.textContent = '.';
var formData = new FormData(); var isLike = elem.classList.contains(commentLikeClass),
formData.append('comment_id', id); isDislike = elem.classList.contains(commentDislikeClass),
formData.append('vote', vote); isIndifferent = elem.classList.contains(commentVotedClass),
formData.append('csrf', '{{ csrf_token() }}'); vote = isIndifferent ? 0 : (isLike ? 1 : -1);
elem.textContent += '.';
// find friendo (the other vote button), this'll fuck up if the parent element is fucked with
for (var i = 0; i < elem.parentNode.childNodes.length; i++) {
var current = elem.parentNode.childNodes[i];
if (current.nodeName.toLowerCase() === 'a' && current !== elem) {
var friend = current;
break;
}
}
if (typeof friend !== 'object') {
console.error('something happened');
return;
}
friend.classList.remove(commentVotedClass);
var friendText = friend.textContent;
friend.textContent = '';
elem.textContent += '.';
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() { xhr.onreadystatechange = function() {
console.log(this.readyState + ' ' + this.status + ': ' + this.responseText); //console.log(this.readyState + ' ' + this.status + ': ' + this.responseText);
if (this.readyState === 4) { if (this.readyState !== 4)
return;
elem.textContent = originalText; elem.textContent = originalText;
friend.textContent = friendText;
commentVoteLock = false; commentVoteLock = false;
}
if (vote)
elem.classList.add(commentVotedClass);
else
elem.classList.remove(commentVotedClass);
var json = JSON.parse(this.responseText),
message = json.error || json.message;
if (message)
alert(message);
console.log(json);
}; };
xhr.open('POST', '/comments.php'); xhr.open('GET', '/comments.php?m=vote&c={0}&v={1}&h={{ csrf_token() }}'.replace('{0}', id).replace('{1}', vote));
xhr.send(formData); xhr.setRequestHeader('X-Misuzu-XHR', 'comments');
xhr.send();
} }
</script> </script>
{% endif %} {% endif %}
@ -167,15 +229,27 @@
</div> </div>
<div class="comment__actions"> <div class="comment__actions">
{% if perms.can_vote %} {% if perms.can_vote %}
<a class="comment__action comment__action--link" href="javascript:void(0)" onclick="commentVote(this, {{ comment.comment_id }}, 1)">Like</a> <a class="comment__action comment__action--link comment__action--like{% if comment.comment_user_vote == 'Like' %} comment__action--voted{% endif %}"
<a class="comment__action comment__action--link" href="javascript:void(0)" onclick="commentVote(this, {{ comment.comment_id }}, -1)">Dislike</a> href="/comments.php?m=vote&amp;c={{ comment.comment_id }}&amp;v={{ comment.comment_user_vote ? '0' : '1' }}">
Like
{% if comment.comment_likes > 0 %}
({{ comment.comment_likes|number_format }})
{% endif %}
</a>
<a class="comment__action comment__action--link comment__action--dislike{% if comment.comment_user_vote == 'Dislike' %} comment__action--voted{% endif %}"
href="/comments.php?m=vote&amp;c={{ comment.comment_id }}&amp;v={{ comment.comment_user_vote ? '0' : '-1' }}">
Dislike
{% if comment.comment_dislikes > 0 %}
({{ comment.comment_dislikes|number_format }})
{% endif %}
</a>
{% endif %} {% endif %}
{% if perms.can_comment %} {% if perms.can_comment %}
<label class="comment__action comment__action--link" for="comment-reply-toggle-{{ comment.comment_id }}">Reply</label> <label class="comment__action comment__action--link" for="comment-reply-toggle-{{ comment.comment_id }}">Reply</label>
{% endif %} {% endif %}
{% if user is not null %} {# if user is not null %}
<a class="comment__action comment__action--link comment__action--hide" href="#">Report</a> <a class="comment__action comment__action--link comment__action--hide" href="#">Report</a>
{% endif %} {% endif #}
</div> </div>
</div> </div>
</div> </div>

View file

@ -22,6 +22,16 @@
<div class="avatar news__preview__user__avatar" style="background-image:url('/profile.php?u={{ post.user_id }}&amp;m=avatar')"></div> <div class="avatar news__preview__user__avatar" style="background-image:url('/profile.php?u={{ post.user_id }}&amp;m=avatar')"></div>
</a> </a>
{% endif %} {% endif %}
<a href="/news.php?p={{ post.post_id }}#comments" style="color: inherit; text-decoration: none; display: block">
{% if post.post_comments < 1 %}
No comments
{% elseif post.post_comments == 1 %}
1 comment
{% else %}
{{ post.post_comments }} comments
{% endif %}
</a>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,4 +1,5 @@
{% extends '@mio/news/master.twig' %} {% extends '@mio/news/master.twig' %}
{% from '@mio/_layout/comments.twig' import comments_section %}
{% set title = post.post_title ~ ' :: News' %} {% set title = post.post_title ~ ' :: News' %}
{% set canonical_url = '/news.php?p=' ~ post.post_id %} {% set canonical_url = '/news.php?p=' ~ post.post_id %}
@ -54,10 +55,12 @@
</div> </div>
</div> </div>
{% if comments is defined %}
<div class="container"> <div class="container">
<div class="container__title">Comments</div> <div class="container__title">
<div class="container__content"> Comments
Eventually&trade;
</div> </div>
{{ comments_section(comments, comments_category, current_user|default(null), comments_perms) }}
</div> </div>
{% endif %}
{% endblock %} {% endblock %}