misuzu/assets/typescript/Comments.tsx

434 lines
17 KiB
TypeScript

/// <reference path="FormUtilities.ts" />
enum CommentVoteType {
Indifferent = 0,
Like = 1,
Dislike = -1,
}
interface CommentNotice {
error: string;
message: string;
}
interface CommentDeletionInfo extends CommentNotice {
id: number; // minor inconsistency, deal with it
}
interface CommentPostInfo extends CommentNotice {
comment_id: number;
category_id: number;
comment_text: string;
comment_html: string;
comment_created: Date;
comment_edited: Date | null;
comment_deleted: Date | null;
comment_reply_to: number;
comment_pinned: Date | null;
user_id: number;
username: string;
user_colour: number;
}
interface CommentVotesInfo extends CommentNotice {
comment_id: number;
likes: number;
dislikes: number;
}
function commentDeleteEventHandler(ev: Event): void {
const target: HTMLAnchorElement = ev.target as HTMLAnchorElement,
commentId: number = parseInt(target.dataset.commentId);
commentDelete(
commentId,
info => {
let elem = document.getElementById('comment-' + info.id);
if(elem)
elem.parentNode.removeChild(elem);
},
message => messageBox(message)
);
}
function commentDelete(commentId: number, onSuccess: (info: CommentDeletionInfo) => void = null, onFail: (message: string) => void = null): void
{
if(!checkUserPerm('comments', CommentPermission.Delete)) {
if(onFail)
onFail("You aren't allowed to delete comments.");
return;
}
const xhr: XMLHttpRequest = new XMLHttpRequest;
xhr.addEventListener('readystatechange', () => {
if(xhr.readyState !== 4)
return;
updateCSRF(xhr.getResponseHeader('X-Misuzu-CSRF'));
let json: CommentDeletionInfo = JSON.parse(xhr.responseText) as CommentDeletionInfo,
message = json.error || json.message;
if(message && onFail)
onFail(message);
else if(!message && onSuccess)
onSuccess(json);
});
xhr.open('GET', urlFormat('comments-delete', [{name:'comment',value:commentId}]));
xhr.setRequestHeader('X-Misuzu-XHR', 'comments');
xhr.send();
}
function commentPostEventHandler(ev: Event): void
{
const form: HTMLFormElement = ev.target as HTMLFormElement;
if(form.dataset.disabled)
return;
form.dataset.disabled = '1';
form.style.opacity = '0.5';
commentPost(
extractFormData(form, true),
info => commentPostSuccess(form, info),
message => commentPostFail(form, message)
);
}
function commentPost(formData: FormData, onSuccess: (comment: CommentPostInfo) => void = null, onFail: (message: string) => void = null): void
{
if(!checkUserPerm('comments', CommentPermission.Create)) {
if(onFail)
onFail("You aren't allowed to post comments.");
return;
}
const xhr = new XMLHttpRequest();
xhr.addEventListener('readystatechange', () => {
if(xhr.readyState !== 4)
return;
updateCSRF(xhr.getResponseHeader('X-Misuzu-CSRF'));
const json: CommentPostInfo = JSON.parse(xhr.responseText) as CommentPostInfo,
message: string = json.error || json.message;
if(message && onFail)
onFail(message);
else if(!message && onSuccess)
onSuccess(json);
});
xhr.open('POST', urlFormat('comment-create'));
xhr.setRequestHeader('X-Misuzu-XHR', 'comments');
xhr.send(formData);
}
function commentPostSuccess(form: HTMLFormElement, comment: CommentPostInfo): void {
if(form.classList.contains('comment--reply'))
(form.parentNode.parentNode.querySelector('label.comment__action') as HTMLLabelElement).click();
commentInsert(comment, form);
form.style.opacity = '1';
form.dataset.disabled = '';
}
function commentPostFail(form: HTMLFormElement, message: string): void {
messageBox(message);
form.style.opacity = '1';
form.dataset.disabled = '';
}
function commentsInit(): void {
const commentDeletes: HTMLCollectionOf<HTMLAnchorElement> = document.getElementsByClassName('comment__action--delete') as HTMLCollectionOf<HTMLAnchorElement>;
for(let i = 0; i < commentDeletes.length; i++) {
commentDeletes[i].addEventListener('click', commentDeleteEventHandler);
commentDeletes[i].dataset.href = commentDeletes[i].href;
commentDeletes[i].href = 'javascript:void(0);';
}
const commentInputs: HTMLCollectionOf<HTMLTextAreaElement> = document.getElementsByClassName('comment__text--input') as HTMLCollectionOf<HTMLTextAreaElement>;
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', commentInputEventHandler);
}
const voteButtons: HTMLCollectionOf<HTMLAnchorElement> = document.getElementsByClassName('comment__action--vote') as HTMLCollectionOf<HTMLAnchorElement>;
for(let i = 0; i < voteButtons.length; i++)
{
voteButtons[i].href = 'javascript:void(0);';
voteButtons[i].addEventListener('click', commentVoteEventHandler);
}
const pinButtons: HTMLCollectionOf<HTMLAnchorElement> = document.getElementsByClassName('comment__action--pin') as HTMLCollectionOf<HTMLAnchorElement>;
for(let i = 0; i < pinButtons.length; i++) {
pinButtons[i].href = 'javascript:void(0);';
pinButtons[i].addEventListener('click', commentPinEventHandler);
}
}
function commentInputEventHandler(ev: KeyboardEvent): void {
if(ev.code === 'Enter' && ev.ctrlKey && !ev.altKey && !ev.shiftKey && !ev.metaKey) {
const form: HTMLFormElement = (ev.target as HTMLTextAreaElement).form;
commentPost(
extractFormData(form, true),
info => commentPostSuccess(form, info),
message => commentPostFail
);
}
}
function commentConstruct(comment: CommentPostInfo, layer: number = 0): HTMLElement {
const commentDate = new Date(comment.comment_created + 'Z'),
commentTime: HTMLElement = <time class="comment__date" title={commentDate.toLocaleString()} dateTime={commentDate.toISOString()}>{timeago.format(commentDate)}</time>;
let actions: HTMLElement[] = [];
if(checkUserPerm('comments', CommentPermission.Vote)) {
actions.push(<a class="comment__action comment__action--link comment__action--vote comment__action--like"
data-comment-id={comment.comment_id} data-comment-vote={CommentVoteType.Like}
href="javascript:void(0);" onClick={commentVoteEventHandler}>Like</a>);
actions.push(<a class="comment__action comment__action--link comment__action--vote comment__action--dislike"
data-comment-id={comment.comment_id} data-comment-vote={CommentVoteType.Dislike}
href="javascript:void(0);" onClick={commentVoteEventHandler}>Dislike</a>);
}
const commentText: HTMLDivElement = <div class="comment__text"></div>;
if(comment.comment_html)
commentText.innerHTML = comment.comment_html;
else
commentText.textContent = comment.comment_text;
const commentElement: HTMLDivElement = <div class="comment" id={"comment-" + comment.comment_id}>
<div class="comment__container">
<a class="comment__avatar" href={urlFormat('user-profile', [{name:'user',value:comment.user_id}])}>
<img class="avatar" alt={comment.username} width={layer <= 1 ? 50 : 40} height={layer <= 1 ? 50 : 40} src={urlFormat('user-avatar', [
{ name: 'user', value: comment.user_id },
{ name: 'res', value: layer <= 1 ? 100 : 80 }
])}/>
</a>
<div class="comment__content">
<div class="comment__info">
<a class="comment__user comment__user--link" href={urlFormat('user-profile', [{name:'user',value:comment.user_id}])}
style={"--user-colour: " + colourGetCSS(comment.user_colour)}>{comment.username}</a>
<a class="comment__link" href={"#comment-" + comment.comment_id}>{commentTime}</a>
</div>
{commentText}
<div class="comment__actions">
{actions}
<label class="comment__action comment__action--link" for={"comment-reply-toggle-" + comment.comment_id}>Reply</label>
</div>
</div>
</div>
<div class={"comment__replies comment__replies--indent-" + layer} id={"comment-" + comment.comment_id + "-replies"}>
<input type="checkbox" id={"comment-reply-toggle-" + comment.comment_id} class="comment__reply-toggle" />
<form id={"comment-reply-" + comment.comment_id} class="comment comment--input comment--reply" method="post"
action="javascript:void(0);" onSubmit={commentPostEventHandler}>
<input type="hidden" name="csrf" value={getCSRFToken()} />
<input type="hidden" name="comment[category]" value={comment.category_id} />
<input type="hidden" name="comment[reply]" value={comment.comment_id} />
<div class="comment__container">
<div class="avatar comment__avatar">
<img class="avatar" width="40" height="40" src={urlFormat('user-avatar', [{name:'user',value:comment.user_id},{name:'res',value:80}])}/>
</div>
<div class="comment__content">
<div class="comment__info"></div>
<textarea class="comment__text input__textarea comment__text--input" name="comment[text]" placeholder="Share your extensive insights..." onKeyDown={commentInputEventHandler}></textarea>
<div class="comment__actions">
<button class="input__button comment__action comment__action--button comment__action--post">Reply</button>
</div>
</div>
</div>
</form>
</div>
</div>;
timeago.render(commentTime);
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)
parent.appendChild(commentElement);
else
parent.insertBefore(commentElement, parent.firstElementChild);
const placeholder: HTMLElement = document.getElementById('_no_comments_notice_' + comment.category_id);
if(placeholder)
placeholder.parentNode.removeChild(placeholder);
}
function commentVoteEventHandler(ev: Event): void {
const target: HTMLAnchorElement = this as HTMLAnchorElement,
commentId: number = parseInt(target.dataset.commentId),
voteType: CommentVoteType = parseInt(target.dataset.commentVote),
buttons: NodeListOf<HTMLAnchorElement> = document.querySelectorAll(`.comment__action--vote[data-comment-id="${commentId}"]`),
likeButton: HTMLAnchorElement = document.querySelector(`.comment__action--like[data-comment-id="${commentId}"]`),
dislikeButton: HTMLAnchorElement = document.querySelector(`.comment__action--dislike[data-comment-id="${commentId}"]`),
classVoted: string = 'comment__action--voted';
for(let i = 0; i < buttons.length; i++) {
let button: HTMLAnchorElement = buttons[i];
button.textContent = button === target ? '...' : '';
button.classList.remove(classVoted);
if(button === likeButton) {
button.dataset.commentVote = (voteType === CommentVoteType.Like ? CommentVoteType.Indifferent : CommentVoteType.Like).toString();
} else if(button === dislikeButton) {
button.dataset.commentVote = (voteType === CommentVoteType.Dislike ? CommentVoteType.Indifferent : CommentVoteType.Dislike).toString();
}
}
commentVote(
commentId,
voteType,
info => {
switch(voteType) {
case CommentVoteType.Like:
likeButton.classList.add(classVoted);
break;
case CommentVoteType.Dislike:
dislikeButton.classList.add(classVoted);
break;
}
likeButton.textContent = info.likes > 0 ? `Like (${info.likes.toLocaleString()})` : 'Like';
dislikeButton.textContent = info.dislikes > 0 ? `Dislike (${info.dislikes.toLocaleString()})` : 'Dislike';
},
message => {
likeButton.textContent = 'Like';
dislikeButton.textContent = 'Dislike';
messageBox(message);
}
);
}
function commentVote(
commentId: number,
vote: CommentVoteType,
onSuccess: (voteInfo: CommentVotesInfo) => void = null,
onFail: (message: string) => void = null
): void {
if(!checkUserPerm('comments', CommentPermission.Vote)) {
if(onFail)
onFail("You aren't allowed to vote on comments.");
return;
}
const xhr: XMLHttpRequest = new XMLHttpRequest;
xhr.onreadystatechange = () => {
if(xhr.readyState !== 4)
return;
updateCSRF(xhr.getResponseHeader('X-Misuzu-CSRF'));
const json: CommentVotesInfo = JSON.parse(xhr.responseText),
message: string = json.error || json.message;
if(message && onFail)
onFail(message);
else if(!message && onSuccess)
onSuccess(json);
};
xhr.open('GET', urlFormat('comment-vote', [{name: 'comment', value: commentId}, {name: 'vote', value: vote}]));
xhr.setRequestHeader('X-Misuzu-XHR', 'comments');
xhr.send();
}
function commentPinEventHandler(ev: Event): void {
const target: HTMLAnchorElement = this as HTMLAnchorElement,
commentId: number = parseInt(target.dataset.commentId),
isPinned: boolean = target.dataset.commentPinned !== '0';
target.textContent = '...';
commentPin(
commentId,
!isPinned,
info => {
if(info.comment_pinned === null) {
target.textContent = 'Pin';
target.dataset.commentPinned = '0';
const pinElement: HTMLDivElement = document.querySelector(`#comment-${info.comment_id} .comment__pin`);
pinElement.parentElement.removeChild(pinElement);
} else {
target.textContent = 'Unpin';
target.dataset.commentPinned = '1';
const pinInfo: HTMLDivElement = document.querySelector(`#comment-${info.comment_id} .comment__info`),
pinElement: HTMLDivElement = document.createElement('div'),
pinTime: HTMLTimeElement = document.createElement('time'),
pinDateTime = new Date(info.comment_pinned + 'Z');
pinTime.title = pinDateTime.toLocaleString();
pinTime.dateTime = pinDateTime.toISOString();
pinTime.textContent = timeago().format(pinDateTime);
timeago().render(pinTime);
pinElement.className = 'comment__pin';
pinElement.appendChild(document.createTextNode('Pinned '));
pinElement.appendChild(pinTime);
pinInfo.appendChild(pinElement);
}
},
message => {
target.textContent = isPinned ? 'Unpin' : 'Pin';
messageBox(message);
}
);
}
function commentPin(
commentId: number,
pin: boolean,
onSuccess: (commentInfo: CommentPostInfo) => void = null,
onFail: (message: string) => void = null
): void {
if(!checkUserPerm('comments', CommentPermission.Pin)) {
if(onFail)
onFail("You aren't allowed to pin comments.");
return;
}
const mode: string = pin ? 'pin' : 'unpin';
const xhr: XMLHttpRequest = new XMLHttpRequest;
xhr.onreadystatechange = () => {
if(xhr.readyState !== 4)
return;
updateCSRF(xhr.getResponseHeader('X-Misuzu-CSRF'));
const json: CommentPostInfo = JSON.parse(xhr.responseText),
message: string = json.error || json.message;
if(message && onFail)
onFail(message);
else if(!message && onSuccess)
onSuccess(json);
};
xhr.open('GET', urlFormat(`comment-${mode}`, [{name: 'comment', value: commentId}]));
xhr.setRequestHeader('X-Misuzu-XHR', 'comments');
xhr.send();
}