// More comments system updates.

This commit is contained in:
flash 2018-12-10 00:56:36 +01:00
parent b4abea015a
commit d3531fc2c1
5 changed files with 159 additions and 150 deletions

View file

@ -21,6 +21,12 @@ function commentsFreeLock(): void
globalCommentLock = false; globalCommentLock = false;
} }
enum CommentVoteType {
Indifferent = 0,
Like = 1,
Dislike = -1,
}
interface CommentNotice { interface CommentNotice {
error: string; error: string;
message: string; message: string;
@ -45,6 +51,12 @@ interface CommentPostInfo extends CommentNotice {
user_colour: number; user_colour: number;
} }
interface CommentVotesInfo extends CommentNotice {
comment_id: number;
likes: number;
dislikes: number;
}
function commentDelete(ev: Event): void function commentDelete(ev: Event): void
{ {
if (!checkUserPerm('comments', CommentPermission.Delete) || !commentsRequestLock()) if (!checkUserPerm('comments', CommentPermission.Delete) || !commentsRequestLock())
@ -77,19 +89,18 @@ function commentDelete(ev: Event): void
function commentPostEventHandler(ev: Event): void function commentPostEventHandler(ev: Event): void
{ {
const form: HTMLFormElement = ev.target as HTMLFormElement, const form: HTMLFormElement = ev.target as HTMLFormElement;
formData: FormData = ExtractFormData(form, true);
commentPost( commentPost(
formData, ExtractFormData(form, true),
info => commentPostSuccess(form, info), info => commentPostSuccess(form, info),
message => commentPostFail commentPostFail
); );
} }
function commentPost(formData: FormData, onSuccess: (comment: CommentPostInfo) => void = null, onFail: (message: string) => void = null): void function commentPost(formData: FormData, onSuccess: (comment: CommentPostInfo) => void = null, onFail: (message: string) => void = null): void
{ {
if (!commentsRequestLock()) if (!checkUserPerm('comments', CommentPermission.Create) || !commentsRequestLock())
return; return;
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
@ -100,6 +111,8 @@ function commentPost(formData: FormData, onSuccess: (comment: CommentPostInfo) =
commentsFreeLock(); commentsFreeLock();
console.log(xhr.getResponseHeader('X-Misuzu-CSRF'));
const json: CommentPostInfo = JSON.parse(xhr.responseText) as CommentPostInfo, const json: CommentPostInfo = JSON.parse(xhr.responseText) as CommentPostInfo,
message: string = json.error || json.message; message: string = json.error || json.message;
@ -118,7 +131,7 @@ function commentPostSuccess(form: HTMLFormElement, comment: CommentPostInfo): vo
if (form.classList.contains('comment--reply')) if (form.classList.contains('comment--reply'))
(form.parentNode.parentNode.querySelector('label.comment__action') as HTMLLabelElement).click(); (form.parentNode.parentNode.querySelector('label.comment__action') as HTMLLabelElement).click();
//commentInsert(info, form); commentInsert(comment, form);
} }
function commentPostFail(message: string): void { function commentPostFail(message: string): void {
@ -137,6 +150,8 @@ function commentsInit(): void {
const commentInputs: HTMLCollectionOf<HTMLTextAreaElement> = document.getElementsByClassName('comment__text--input') as HTMLCollectionOf<HTMLTextAreaElement>; const commentInputs: HTMLCollectionOf<HTMLTextAreaElement> = document.getElementsByClassName('comment__text--input') as HTMLCollectionOf<HTMLTextAreaElement>;
for (let i = 0; i < commentInputs.length; i++) { for (let i = 0; i < commentInputs.length; i++) {
commentInputs[i].form.action = 'javascript:void(0);';
commentInputs[i].form.addEventListener('submit', commentPostEventHandler);
commentInputs[i].addEventListener('keydown', ev => { commentInputs[i].addEventListener('keydown', ev => {
if (ev.keyCode === 13 && ev.ctrlKey && !ev.altKey && !ev.shiftKey) { if (ev.keyCode === 13 && ev.ctrlKey && !ev.altKey && !ev.shiftKey) {
let form = commentInputs[i].form; let form = commentInputs[i].form;
@ -151,8 +166,6 @@ function commentsInit(): void {
} }
function commentConstruct(comment: CommentPostInfo, layer: number = 0): HTMLElement { function commentConstruct(comment: CommentPostInfo, layer: number = 0): HTMLElement {
const isReply = comment.comment_reply_to > 0;
const commentElement: HTMLDivElement = document.createElement('div'); const commentElement: HTMLDivElement = document.createElement('div');
commentElement.className = 'comment'; commentElement.className = 'comment';
commentElement.id = 'comment-' + comment.comment_id; commentElement.id = 'comment-' + comment.comment_id;
@ -189,120 +202,94 @@ function commentConstruct(comment: CommentPostInfo, layer: number = 0): HTMLElem
const commentActions = commentContent.appendChild(document.createElement('div')); const commentActions = commentContent.appendChild(document.createElement('div'));
commentActions.className = 'comment__actions'; commentActions.className = 'comment__actions';
}
function commentInsert(comment, form): void
{
var isReply = form.classList.contains('comment--reply'),
parent = isReply
? form.parentNode
: form.parentNode.parentNode.getElementsByClassName('comments__listing')[0],
repliesIndent = isReply
? (parseInt(parent.classList[1].substr(25)) + 1)
: 1;
// info // info
var commentUser = document.createElement('a'); const commentUser: HTMLAnchorElement = commentInfo.appendChild(document.createElement('a'));
commentUser.className = 'comment__user comment__user--link'; commentUser.className = 'comment__user comment__user--link';
commentUser.textContent = comment.username; commentUser.textContent = comment.username;
commentUser.href = '/profile?u=' + comment.user_id; commentUser.href = '/profile?u=' + comment.user_id;
commentUser.style.color = comment.user_colour == null || (comment.user_colour & 0x40000000) > 0 commentUser.style.color = comment.user_colour == null || (comment.user_colour & 0x40000000) > 0
? 'inherit' ? 'inherit'
: '#' + (comment.user_colour & 0xFFFFFF).toString(16); : '#' + (comment.user_colour & 0xFFFFFF).toString(16);
commentInfo.appendChild(commentUser);
var commentLink = document.createElement('a'); const commentLink: HTMLAnchorElement = commentInfo.appendChild(document.createElement('a'));
commentLink.className = 'comment__link'; commentLink.className = 'comment__link';
commentLink.href = '#' + commentElement.id; commentLink.href = '#' + commentElement.id;
commentInfo.appendChild(commentLink);
var commentTime = document.createElement('time'), const commentTime: HTMLTimeElement = commentLink.appendChild(document.createElement('time')),
commentDate = new Date(comment.comment_created + 'Z'); commentDate = new Date(comment.comment_created + 'Z');
commentTime.className = 'comment__date'; commentTime.className = 'comment__date';
commentTime.title = commentDate.toLocaleString(); commentTime.title = commentDate.toLocaleString();
commentTime.dateTime = commentDate.toISOString(); commentTime.dateTime = commentDate.toISOString();
commentTime.textContent = timeago().format(commentDate); commentTime.textContent = timeago().format(commentDate);
commentLink.appendChild(commentTime); timeago().render(commentTime);
// actions // actions
if (typeof commentVote === 'function') { if (checkUserPerm('comments', CommentPermission.Vote)) {
var commentLike = document.createElement('a'); const commentLike: HTMLAnchorElement = commentActions.appendChild(document.createElement('a'));
commentLike.className = 'comment__action comment__action--link comment__action--like'; commentLike.className = 'comment__action comment__action--link comment__action--like';
commentLike.href = 'javascript:void(0);'; commentLike.href = 'javascript:void(0);';
commentLike.textContent = 'Like'; commentLike.textContent = 'Like';
commentLike.onclick = commentVote; commentLike.addEventListener('click', commentVoteEventHandler);
commentActions.appendChild(commentLike);
var commentDislike = document.createElement('a'); const commentDislike: HTMLAnchorElement = commentActions.appendChild(document.createElement('a'));
commentDislike.className = 'comment__action comment__action--link comment__action--dislike'; commentDislike.className = 'comment__action comment__action--link comment__action--dislike';
commentDislike.href = 'javascript:void(0);'; commentDislike.href = 'javascript:void(0);';
commentDislike.textContent = 'Dislike'; commentDislike.textContent = 'Dislike';
commentDislike.onclick = commentVote; commentLike.addEventListener('click', commentVoteEventHandler);
commentActions.appendChild(commentDislike);
} }
// if we're executing this it's fairly obvious that we can reply, // if we're executing this it's fairly obvious that we can reply,
// so no need to have a permission check on it here // so no need to have a permission check on it here
var commentReply = document.createElement('label'); const commentReply: HTMLLabelElement = commentActions.appendChild(document.createElement('label'));
commentReply.className = 'comment__action comment__action--link'; commentReply.className = 'comment__action comment__action--link';
commentReply.htmlFor = 'comment-reply-toggle-' + comment.comment_id; commentReply.htmlFor = 'comment-reply-toggle-' + comment.comment_id;
commentReply.textContent = 'Reply'; commentReply.textContent = 'Reply';
commentActions.appendChild(commentReply);
// reply section // reply section
var commentReplyState = document.createElement('input'); const commentReplyState: HTMLInputElement = commentReplies.appendChild(document.createElement('input'));
commentReplyState.id = commentReply.htmlFor; commentReplyState.id = commentReply.htmlFor;
commentReplyState.type = 'checkbox'; commentReplyState.type = 'checkbox';
commentReplyState.className = 'comment__reply-toggle'; commentReplyState.className = 'comment__reply-toggle';
commentReplies.appendChild(commentReplyState);
var commentReplyInput = document.createElement('form'); const commentReplyInput: HTMLFormElement = commentReplies.appendChild(document.createElement('form'));
commentReplyInput.id = 'comment-reply-' + comment.comment_id; commentReplyInput.id = 'comment-reply-' + comment.comment_id;
commentReplyInput.className = 'comment comment--input comment--reply'; commentReplyInput.className = 'comment comment--input comment--reply';
commentReplyInput.method = 'post'; commentReplyInput.method = 'post';
commentReplyInput.action = 'javascript:void(0);'; commentReplyInput.action = 'javascript:void(0);';
commentReplyInput.onsubmit = commentPostEventHandler; commentReplyInput.addEventListener('submit', commentPostEventHandler);
commentReplies.appendChild(commentReplyInput);
// reply attributes // reply attributes
var replyCategory = document.createElement('input'); const replyCategory: HTMLInputElement = commentReplyInput.appendChild(document.createElement('input'));
replyCategory.name = 'comment[category]'; replyCategory.name = 'comment[category]';
replyCategory.value = comment.category_id; replyCategory.value = comment.category_id.toString();
replyCategory.type = 'hidden'; replyCategory.type = 'hidden';
commentReplyInput.appendChild(replyCategory);
var replyCsrf = document.createElement('input'); const replyCsrf: HTMLInputElement = commentReplyInput.appendChild(document.createElement('input'));
replyCsrf.name = 'csrf'; replyCsrf.name = 'csrf';
replyCsrf.value = '{{ csrf_token("comments") }}'; replyCsrf.value = '{{ csrf_token("comments") }}';
replyCsrf.type = 'hidden'; replyCsrf.type = 'hidden';
commentReplyInput.appendChild(replyCsrf);
var replyId = document.createElement('input'); const replyId: HTMLInputElement = commentReplyInput.appendChild(document.createElement('input'));
replyId.name = 'comment[reply]'; replyId.name = 'comment[reply]';
replyId.value = comment.comment_id; replyId.value = comment.comment_id.toString();
replyId.type = 'hidden'; replyId.type = 'hidden';
commentReplyInput.appendChild(replyId);
var replyContainer = document.createElement('div'); const replyContainer: HTMLDivElement = commentReplyInput.appendChild(document.createElement('div'));
replyContainer.className = 'comment__container'; replyContainer.className = 'comment__container';
commentReplyInput.appendChild(replyContainer);
// reply container // reply container
var replyAvatar = document.createElement('div'); const replyAvatar: HTMLDivElement = replyContainer.appendChild(document.createElement('div'));
replyAvatar.className = 'avatar comment__avatar'; replyAvatar.className = 'avatar comment__avatar';
replyAvatar.style.backgroundImage = 'url(\'/profile.php?m=avatar&u={0}\')'.replace('{0}', comment.user_id); replyAvatar.style.backgroundImage = `url('/profile.php?m=avatar&u=${comment.user_id}')`;
replyContainer.appendChild(replyAvatar);
var replyContent = document.createElement('div'); const replyContent: HTMLDivElement = replyContainer.appendChild(document.createElement('div'));
replyContent.className = 'comment__content'; replyContent.className = 'comment__content';
replyContainer.appendChild(replyContent);
// reply content // reply content
var replyInfo = document.createElement('div'); const replyInfo: HTMLDivElement = replyContent.appendChild(document.createElement('div'));
replyInfo.className = 'comment__info'; replyInfo.className = 'comment__info';
replyContent.appendChild(replyInfo);
var replyUser = document.createElement('div'); const replyUser: HTMLDivElement = document.createElement('div');
replyUser.className = 'comment__user'; replyUser.className = 'comment__user';
replyUser.textContent = comment.username; replyUser.textContent = comment.username;
replyUser.style.color = comment.user_colour == null || (comment.user_colour & 0x40000000) > 0 replyUser.style.color = comment.user_colour == null || (comment.user_colour & 0x40000000) > 0
@ -310,30 +297,72 @@ function commentInsert(comment, form): void
: '#' + (comment.user_colour & 0xFFFFFF).toString(16); : '#' + (comment.user_colour & 0xFFFFFF).toString(16);
replyInfo.appendChild(replyUser); replyInfo.appendChild(replyUser);
var replyText = document.createElement('textarea'); const replyText: HTMLTextAreaElement = replyContent.appendChild(document.createElement('textarea'));
replyText.className = 'comment__text input__textarea comment__text--input'; replyText.className = 'comment__text input__textarea comment__text--input';
replyText.name = 'comment[text]'; replyText.name = 'comment[text]';
replyText.placeholder = 'Share your extensive insights...'; replyText.placeholder = 'Share your extensive insights...';
replyContent.appendChild(replyText);
var replyActions = document.createElement('div'); const replyActions: HTMLDivElement = replyContent.appendChild(document.createElement('div'));
replyActions.className = 'comment__actions'; replyActions.className = 'comment__actions';
replyContent.appendChild(replyActions);
var replyButton = document.createElement('button'); const replyButton: HTMLButtonElement = replyActions.appendChild(document.createElement('button'));
replyButton.className = 'input__button comment__action comment__action--button comment__action--post'; replyButton.className = 'input__button comment__action comment__action--button comment__action--post';
replyButton.textContent = 'Reply'; replyButton.textContent = 'Reply';
replyActions.appendChild(replyButton);
return commentElement;
}
function commentInsert(comment: CommentPostInfo, form: HTMLFormElement): void
{
const isReply: boolean = form.classList.contains('comment--reply'),
parent: Element = isReply
? form.parentElement
: form.parentElement.parentElement.getElementsByClassName('comments__listing')[0],
repliesIndent: number = isReply
? (parseInt(parent.classList[1].substr(25)) + 1)
: 1,
commentElement: HTMLElement = commentConstruct(comment, repliesIndent);
if (isReply) if (isReply)
parent.appendChild(commentElement); parent.appendChild(commentElement);
else else
parent.insertBefore(commentElement, parent.firstElementChild); parent.insertBefore(commentElement, parent.firstElementChild);
timeago().render(commentTime); const placeholder: HTMLElement = document.getElementById('_no_comments_notice_' + comment.category_id);
var placeholder = document.getElementById('_no_comments_notice_' + comment.category_id);
if (placeholder) if (placeholder)
placeholder.parentNode.removeChild(placeholder); placeholder.parentNode.removeChild(placeholder);
} }
function commentVoteEventHandler(ev: Event): void {
//
}
function commentVoteV2(
commentId: number,
vote: CommentVoteType,
onSuccess: (voteInfo: CommentVotesInfo, userVote: CommentVoteType) => void,
onFail: (message: string) => void
): void {
if (!checkUserPerm('comments', CommentPermission.Vote) || !commentsRequestLock())
return;
const xhr: XMLHttpRequest = new XMLHttpRequest;
xhr.onreadystatechange = () => {
if (xhr.readyState !== 4)
return;
commentsFreeLock();
const json: CommentVotesInfo = JSON.parse(xhr.responseText),
message: string = json.error || json.message;
if (message && onFail)
onFail(message);
else if (!message && onSuccess)
onSuccess(json, vote);
};
xhr.open('GET', `/comments.php?m=vote&c=${commentId}&v=${vote}&csrf={{ csrf_token("comments") }}`);
xhr.setRequestHeader('X-Misuzu-XHR', 'comments');
xhr.send();
}

View file

@ -23,6 +23,7 @@ if (!user_session_active()) {
return; return;
} }
header(csrf_http_header('comments'));
$commentPerms = comments_get_perms(user_session_current('user_id', 0)); $commentPerms = comments_get_perms(user_session_current('user_id', 0));
switch ($_GET['m'] ?? null) { switch ($_GET['m'] ?? null) {

View file

@ -101,17 +101,17 @@ function comments_vote_add(int $comment, int $user, ?string $vote): bool
function comments_votes_get(int $commentId): array function comments_votes_get(int $commentId): array
{ {
$getVotes = db_prepare(' $getVotes = db_prepare('
SELECT :id as `id`, SELECT :id as `comment_id`,
( (
SELECT COUNT(`user_id`) SELECT COUNT(`user_id`)
FROM `msz_comments_votes` FROM `msz_comments_votes`
WHERE `comment_id` = `id` WHERE `comment_id` = `comment_id`
AND `comment_vote` = \'Like\' AND `comment_vote` = \'Like\'
) as `likes`, ) as `likes`,
( (
SELECT COUNT(`user_id`) SELECT COUNT(`user_id`)
FROM `msz_comments_votes` FROM `msz_comments_votes`
WHERE `comment_id` = `id` WHERE `comment_id` = `comment_id`
AND `comment_vote` = \'Dislike\' AND `comment_vote` = \'Dislike\'
) as `dislikes` ) as `dislikes`
'); ');

View file

@ -112,3 +112,8 @@ function csrf_html(string $realm, string $name = 'csrf'): string
{ {
return sprintf(MSZ_CSRF_HTML, $name, csrf_token($realm)); return sprintf(MSZ_CSRF_HTML, $name, csrf_token($realm));
} }
function csrf_http_header(string $realm, string $name = 'X-Misuzu-CSRF'): string
{
return "{$name}: {$realm};" . csrf_token($realm);
}

View file

@ -182,7 +182,6 @@
<script> <script>
window.addEventListener('load', function () { window.addEventListener('load', function () {
if (typeof commentVote === 'function') { // if this exists, the user is allowed to vote
var likeButtons = document.getElementsByClassName('comment__action--like'), var likeButtons = document.getElementsByClassName('comment__action--like'),
dislikeButtons = document.getElementsByClassName('comment__action--dislike'); dislikeButtons = document.getElementsByClassName('comment__action--dislike');
@ -193,21 +192,8 @@
dislikeButtons[i].href = 'javascript:void(0);'; dislikeButtons[i].href = 'javascript:void(0);';
dislikeButtons[i].onclick = commentVote; dislikeButtons[i].onclick = commentVote;
} }
}
if (typeof commentPostEventHandler === 'function') { // can comment
var commentForms = document.getElementsByClassName('comment--input');
for (var i = 0; i < commentForms.length; i++) {
commentForms[i].action = 'javascript:void(0);';
commentForms[i].onsubmit = commentPostEventHandler;
}
}
}); });
</script>
{% if perms.can_vote %}
<script>
var commentVoteLock = false, var commentVoteLock = false,
commentLikeClass = 'comment__action--like', commentLikeClass = 'comment__action--like',
commentDislikeClass = 'comment__action--dislike', commentDislikeClass = 'comment__action--dislike',
@ -255,24 +241,16 @@
elem.textContent += '.'; elem.textContent += '.';
var xhr = new XMLHttpRequest(); commentVoteV2(
xhr.onreadystatechange = function () { id, vote,
if (this.readyState !== 4) (vInfo, uVote) => {
return;
if (vote) if (vote)
elem.classList.add(commentVotedClass); elem.classList.add(commentVotedClass);
else else
elem.classList.remove(commentVotedClass); elem.classList.remove(commentVotedClass);
var json = JSON.parse(this.responseText), var likes = vInfo.likes || 0,
message = json.error || json.message; dislikes = vInfo.dislikes || 0;
if (message)
alert(message);
var likes = json.likes || 0,
dislikes = json.dislikes || 0;
if (isLike) { // somewhat implicitly defined, like will always come before dislike if (isLike) { // somewhat implicitly defined, like will always come before dislike
elem.textContent = commentLikeText + (likes > 0 ? commentVoteCountSuffix.replace('{0}', likes.toLocaleString()) : ''); elem.textContent = commentLikeText + (likes > 0 ? commentVoteCountSuffix.replace('{0}', likes.toLocaleString()) : '');
@ -281,13 +259,9 @@
elem.textContent = commentDislikeText + (dislikes > 0 ? commentVoteCountSuffix.replace('{0}', dislikes.toLocaleString()) : ''); elem.textContent = commentDislikeText + (dislikes > 0 ? commentVoteCountSuffix.replace('{0}', dislikes.toLocaleString()) : '');
friend.textContent = commentLikeText + (likes > 0 ? commentVoteCountSuffix.replace('{0}', likes.toLocaleString()) : ''); friend.textContent = commentLikeText + (likes > 0 ? commentVoteCountSuffix.replace('{0}', likes.toLocaleString()) : '');
} }
},
commentVoteLock = false; alert
}; );
xhr.open('GET', '/comments.php?m=vote&c={0}&v={1}&csrf={{ csrf_token("comments") }}'.replace('{0}', id).replace('{1}', vote));
xhr.setRequestHeader('X-Misuzu-XHR', 'comments');
xhr.send();
} }
</script> </script>
{% endif %}
{% endmacro %} {% endmacro %}