462 lines
17 KiB
JavaScript
462 lines
17 KiB
JavaScript
#include comments/api.js
|
|
#include comments/form.jsx
|
|
|
|
const MszCommentsEntry = function(userInfo, postInfo, listing, root) {
|
|
userInfo ??= {};
|
|
|
|
const actions = <div class="comments-entry-actions" />;
|
|
|
|
const likeAction = <button class="comments-entry-action comments-entry-action-vote-like" disabled={!userInfo.can_vote} title="Like">
|
|
<i class="fas fa-chevron-up" />
|
|
</button>;
|
|
const dislikeAction = <button class="comments-entry-action comments-entry-action-vote-dislike" disabled={!userInfo.can_vote} title="Dislike">
|
|
<i class="fas fa-chevron-down" />
|
|
</button>;
|
|
const voteActions = <div class="comments-entry-actions-group comments-entry-actions-group-votes">
|
|
{likeAction}
|
|
{dislikeAction}
|
|
</div>;
|
|
actions.appendChild(voteActions);
|
|
|
|
const updateVoteElem = (elem, count, cast) => {
|
|
elem.classList.toggle('comments-entry-action-vote-cast', cast);
|
|
|
|
let counter = elem.querySelector('.js-votes');
|
|
if(!counter) {
|
|
if(count === 0)
|
|
return;
|
|
|
|
elem.appendChild(counter = <span class="js-votes" />);
|
|
}
|
|
|
|
if(count === 0)
|
|
elem.removeChild(counter);
|
|
else
|
|
counter.textContent = count.toLocaleString();
|
|
};
|
|
const updateVotes = votes => {
|
|
updateVoteElem(likeAction, votes?.positive ?? 0, votes?.vote > 0);
|
|
updateVoteElem(dislikeAction, Math.abs(votes?.negative ?? 0), votes?.vote < 0);
|
|
};
|
|
|
|
updateVotes(postInfo);
|
|
|
|
const castVote = async vote => {
|
|
if(postInfo.deleted)
|
|
return;
|
|
|
|
voteActions.classList.add('comments-entry-actions-group-disabled');
|
|
likeAction.disabled = dislikeAction.disabled = true;
|
|
try {
|
|
updateVotes(vote === 0
|
|
? await MszCommentsApi.deleteVote(postInfo.id)
|
|
: await MszCommentsApi.createVote(postInfo.id, vote));
|
|
} catch(ex) {
|
|
console.error(ex);
|
|
} finally {
|
|
voteActions.classList.remove('comments-entry-actions-group-disabled');
|
|
likeAction.disabled = dislikeAction.disabled = false;
|
|
}
|
|
};
|
|
|
|
likeAction.onclick = () => { castVote(likeAction.classList.contains('comments-entry-action-vote-cast') ? 0 : 1); };
|
|
dislikeAction.onclick = () => { castVote(dislikeAction.classList.contains('comments-entry-action-vote-cast') ? 0 : -1); };
|
|
|
|
const repliesIsArray = Array.isArray(postInfo.replies);
|
|
const replies = new MszCommentsListing({ hidden: !repliesIsArray });
|
|
if(repliesIsArray)
|
|
replies.addPosts(userInfo, postInfo.replies);
|
|
|
|
let form = null;
|
|
const repliesElem = <div class="comments-entry-replies">
|
|
{replies}
|
|
</div>;
|
|
|
|
let replyCount = repliesIsArray ? postInfo.replies.length : (postInfo.replies ?? 0);
|
|
const replyActionsGroup = <div class="comments-entry-actions-group comments-entry-actions-group-replies" />;
|
|
actions.appendChild(replyActionsGroup);
|
|
|
|
const replyToggleOpenElem = <span class="hidden"><i class="fas fa-minus" /></span>;
|
|
const replyToggleClosedElem = <span class="hidden"><i class="fas fa-plus" /></span>;
|
|
const replyCountElem = <span />;
|
|
const replyToggleElem = <button class="comments-entry-action" title="Replies">
|
|
{replyToggleOpenElem}
|
|
{replyToggleClosedElem}
|
|
{replyCountElem}
|
|
</button>;
|
|
replyActionsGroup.appendChild(replyToggleElem);
|
|
|
|
const setReplyToggleState = visible => {
|
|
replyToggleOpenElem.classList.toggle('hidden', !visible);
|
|
replyToggleClosedElem.classList.toggle('hidden', visible);
|
|
};
|
|
setReplyToggleState(replies.visible);
|
|
|
|
const setReplyCount = count => {
|
|
replyCount = count ??= 0;
|
|
if(count > 0) {
|
|
replyCountElem.textContent = count.toLocaleString();
|
|
replyToggleElem.classList.remove('hidden');
|
|
replyActionsGroup.classList.remove('hidden');
|
|
} else {
|
|
replyToggleElem.classList.add('hidden');
|
|
if(replyActionsGroup.childElementCount < 2)
|
|
replyActionsGroup.classList.add('hidden');
|
|
}
|
|
};
|
|
|
|
let replyLoaded = replies.loaded;
|
|
replyToggleElem.onclick = async () => {
|
|
setReplyToggleState(replies.visible = !replies.visible);
|
|
if(!replyLoaded) {
|
|
replyLoaded = true;
|
|
try {
|
|
replies.addPosts(userInfo, await MszCommentsApi.getPostReplies(postInfo.id));
|
|
} catch(ex) {
|
|
console.error(ex);
|
|
setReplyToggleState(false);
|
|
replyLoaded = false;
|
|
|
|
// THIS IS NOT FINAL DO NOT PUSH THIS TO PUBLIC THIS WOULD BE HORRIBLE
|
|
if(typeof ex === 'string')
|
|
MszShowMessageBox(ex);
|
|
}
|
|
}
|
|
};
|
|
|
|
if(userInfo.can_create) {
|
|
const replyElem = <button class="comments-entry-action" title="Reply">
|
|
<span><i class="fas fa-reply" /></span>
|
|
<span>Reply</span>
|
|
</button>;
|
|
replyActionsGroup.appendChild(replyElem);
|
|
|
|
replyElem.onclick = () => {
|
|
if(form === null) {
|
|
replyElem.classList.add('comments-entry-action-reply-active');
|
|
form = new MszCommentsForm(userInfo);
|
|
$insertBefore(replies.element, form.element);
|
|
} else {
|
|
replyElem.classList.remove('comments-entry-action-reply-active');
|
|
repliesElem.removeChild(form.element);
|
|
form = null;
|
|
}
|
|
};
|
|
}
|
|
|
|
// this has to be called no earlier cus if there's less than 2 elements in the group it gets hidden on 0
|
|
setReplyCount(replyCount);
|
|
|
|
const deleteButton = postInfo.can_delete
|
|
? <button class="comments-entry-action" title="Delete"><i class="fas fa-trash" /></button>
|
|
: null;
|
|
const restoreButton = postInfo.can_delete_any
|
|
? <button class="comments-entry-action" title="Restore"><i class="fas fa-trash-restore" /></button>
|
|
: null;
|
|
const nukeButton = postInfo.can_delete_any
|
|
? <button class="comments-entry-action" title="Permanently delete"><i class="fas fa-radiation-alt" /></button>
|
|
: null;
|
|
const pinButton = root && userInfo.can_pin
|
|
? <button class="comments-entry-action" title="Pin"><i class="fas fa-thumbtack" /></button>
|
|
: null;
|
|
const unpinButton = root && userInfo.can_pin
|
|
? <button class="comments-entry-action" title="Unpin">
|
|
{/*<i class="fas fa-crow" />
|
|
<i class="fas fa-bars" />*/}
|
|
<img src="https://mikoto.misaka.nl/u/1I0KnhRO/crowbar.png" width="11" height="12" alt="crowbar" />
|
|
</button> : null;
|
|
|
|
const miscActions = <div class="comments-entry-actions-group hidden">
|
|
{deleteButton}
|
|
{restoreButton}
|
|
{nukeButton}
|
|
{pinButton}
|
|
{unpinButton}
|
|
</div>;
|
|
actions.appendChild(miscActions);
|
|
|
|
const setMiscVisible = (deleted=null, pinned=null) => {
|
|
if(deleted !== null) {
|
|
if(deleteButton)
|
|
deleteButton.classList.toggle('hidden', deleted);
|
|
if(restoreButton)
|
|
restoreButton.classList.toggle('hidden', !deleted);
|
|
if(nukeButton)
|
|
nukeButton.classList.toggle('hidden', !deleted);
|
|
}
|
|
if(pinned !== null) {
|
|
if(pinButton)
|
|
pinButton.classList.toggle('hidden', pinned);
|
|
if(unpinButton)
|
|
unpinButton.classList.toggle('hidden', !pinned);
|
|
}
|
|
|
|
miscActions.classList.toggle('hidden', miscActions.querySelectorAll('.hidden').length === miscActions.childElementCount);
|
|
};
|
|
const setMiscDisabled = state => {
|
|
miscActions.classList.toggle('comments-entry-actions-group-disabled', state);
|
|
for(const elem of miscActions.querySelectorAll('button'))
|
|
elem.disabled = state;
|
|
};
|
|
|
|
setMiscVisible(!!postInfo.deleted, !!postInfo.pinned);
|
|
|
|
const userAvatarElem = <img alt="" width="40" height="40" class="avatar" />;
|
|
const userNameElem = <div class="comments-entry-user" />;
|
|
|
|
const createdTimeElem = <a href={`#comment-${postInfo.id}`} class="comments-entry-time-link" />;
|
|
const editedElem = <div class="comments-entry-time comments-entry-time-edited">
|
|
<div class="comments-entry-time-icon"><i class="fas fa-pencil-alt" /></div>
|
|
</div>;
|
|
const pinnedElem = <div class="comments-entry-time comments-entry-time-pinned">
|
|
<div class="comments-entry-time-icon"><i class="fas fa-thumbtack" /></div>
|
|
</div>;
|
|
const deletedElem = <div class="comments-entry-time comments-entry-time-deleted">
|
|
<div class="comments-entry-time-icon"><i class="fas fa-trash" /></div>
|
|
</div>;
|
|
|
|
const bodyElem = <div class="comments-entry-body" />;
|
|
const setBody = body => { bodyElem.textContent = body ?? '[deleted]'; };
|
|
setBody(postInfo?.body);
|
|
|
|
const element = <div id={`comment-${postInfo.id}`} data-comment={postInfo.id} class={{ 'comments-entry': true, 'comments-entry-root': root }}>
|
|
<div class="comments-entry-main">
|
|
<div class="comments-entry-avatar">
|
|
{userAvatarElem}
|
|
</div>
|
|
<div class="comments-entry-wrap">
|
|
<div class="comments-entry-meta">
|
|
<div class="comments-entry-user">
|
|
{userNameElem}
|
|
</div>
|
|
<div class="comments-entry-time">
|
|
<div class="comments-entry-time-icon">—</div>
|
|
{createdTimeElem}
|
|
</div>
|
|
{editedElem}
|
|
{pinnedElem}
|
|
{deletedElem}
|
|
</div>
|
|
<div class="comments-entry-body">{postInfo?.body ?? '[deleted]'}</div>
|
|
{actions.childElementCount > 0 ? actions : null}
|
|
</div>
|
|
</div>
|
|
{repliesElem}
|
|
</div>;
|
|
|
|
const setUserInfo = userInfo => {
|
|
$removeChildren(userNameElem);
|
|
if(userInfo) {
|
|
if(typeof userInfo.colour === 'string')
|
|
element.style.setProperty('--user-colour', userInfo.colour);
|
|
userAvatarElem.src = userInfo.avatar;
|
|
userNameElem.appendChild(<a class="comments-entry-user-link" href={userInfo.profile} style="color: var(--user-colour);">{userInfo.name}</a>);
|
|
} else {
|
|
element.style.removeProperty('--user-colour');
|
|
userAvatarElem.src = '/images/no-avatar.png';
|
|
userNameElem.appendChild(<span class="comments-entry-user-dead">Deleted user</span>);
|
|
}
|
|
};
|
|
setUserInfo(postInfo.user);
|
|
|
|
const setCreatedTime = date => {
|
|
if(typeof date === 'string')
|
|
date = new Date(date);
|
|
|
|
$removeChildren(createdTimeElem);
|
|
element.dataset.commentCreated = date.getTime();
|
|
const time = <time class="comments-entry-time-text" datetime={date.toISOString()} title={date.toString()}>{MszSakuya.formatTimeAgo(date)}</time>;
|
|
createdTimeElem.appendChild(time);
|
|
MszSakuya.trackElement(time);
|
|
};
|
|
setCreatedTime(postInfo.created);
|
|
|
|
const updateOrderValue = () => {
|
|
let order = parseInt(element.dataset.commentCreated ?? 0);
|
|
|
|
if(element.dataset.commentDeleted !== undefined)
|
|
order -= parseInt(element.dataset.commentDeleted);
|
|
else if(element.dataset.commentPinned !== undefined)
|
|
order += parseInt(element.dataset.commentPinned);
|
|
|
|
element.dataset.commentOrder = order;
|
|
};
|
|
|
|
const setOptionalTime = (elem, date, name, reorder=true, textIfTrue=null) => {
|
|
if(typeof date === 'string')
|
|
date = new Date(date);
|
|
|
|
while(!(elem.lastChild instanceof HTMLDivElement))
|
|
elem.removeChild(elem.lastChild);
|
|
|
|
if(date) {
|
|
if(date instanceof Date) {
|
|
if(name)
|
|
element.dataset[name] = date.getTime();
|
|
|
|
const timeElem = <time class="comments-entry-time-text" datetime={date.toISOString()} title={date.toString()}>{MszSakuya.formatTimeAgo(date)}</time>
|
|
elem.appendChild(timeElem);
|
|
MszSakuya.trackElement(timeElem);
|
|
} else {
|
|
// this is kiiiind of a hack but commentCreated isn't updated through this function so who cares lol !
|
|
if(name)
|
|
element.dataset[name] = element.dataset.commentCreated;
|
|
|
|
if(typeof textIfTrue === 'string')
|
|
elem.appendChild(<span>{textIfTrue}</span>);
|
|
}
|
|
|
|
elem.classList.remove('hidden');
|
|
} else {
|
|
if(name)
|
|
delete element.dataset[name];
|
|
elem.classList.add('hidden');
|
|
}
|
|
|
|
if(reorder)
|
|
updateOrderValue();
|
|
};
|
|
|
|
setOptionalTime(editedElem, postInfo.edited, 'commentEdited', false);
|
|
setOptionalTime(pinnedElem, postInfo.pinned, 'commentPinned', false);
|
|
setOptionalTime(deletedElem, postInfo.deleted, 'commentDeleted', false, 'deleted');
|
|
updateOrderValue();
|
|
|
|
const nukeThePost = () => {
|
|
if(replies.count < 1 && replyCount < 1)
|
|
listing.element.removeChild(element);
|
|
else {
|
|
miscActions.classList.add('hidden');
|
|
setMiscDisabled(true);
|
|
setUserInfo(null);
|
|
setBody(null);
|
|
}
|
|
};
|
|
|
|
if(deleteButton)
|
|
deleteButton.onclick = async () => {
|
|
setMiscDisabled(true);
|
|
try {
|
|
await MszCommentsApi.deletePost(postInfo.id);
|
|
if(restoreButton) {
|
|
setOptionalTime(deletedElem, new Date, 'commentDeleted');
|
|
listing.reorder();
|
|
deleteButton.classList.add('hidden');
|
|
restoreButton.classList.remove('hidden');
|
|
nukeButton.classList.remove('hidden');
|
|
} else
|
|
nukeThePost();
|
|
} catch(ex) {
|
|
console.error(ex);
|
|
} finally {
|
|
setMiscDisabled(false);
|
|
}
|
|
};
|
|
if(restoreButton)
|
|
restoreButton.onclick = async () => {
|
|
setMiscDisabled(true);
|
|
try {
|
|
await MszCommentsApi.restorePost(postInfo.id);
|
|
setMiscVisible(false, null);
|
|
setOptionalTime(deletedElem, null, 'commentDeleted');
|
|
listing.reorder();
|
|
} catch(ex) {
|
|
console.error(ex);
|
|
} finally {
|
|
setMiscDisabled(false);
|
|
}
|
|
};
|
|
if(nukeButton)
|
|
nukeButton.onclick = async () => {
|
|
setMiscDisabled(true);
|
|
try {
|
|
await MszCommentsApi.nukePost(postInfo.id);
|
|
nukeThePost();
|
|
} catch(ex) {
|
|
console.error(ex);
|
|
} finally {
|
|
setMiscDisabled(false);
|
|
}
|
|
};
|
|
if(pinButton)
|
|
pinButton.onclick = async () => {
|
|
setMiscDisabled(true);
|
|
try {
|
|
const result = await MszCommentsApi.updatePost(postInfo.id, { pin: '1' });
|
|
setMiscVisible(null, !!result.pinned);
|
|
setOptionalTime(pinnedElem, result.pinned, 'commentPinned');
|
|
listing.reorder();
|
|
} catch(ex) {
|
|
console.error(ex);
|
|
} finally {
|
|
setMiscDisabled(false);
|
|
}
|
|
};
|
|
if(unpinButton)
|
|
unpinButton.onclick = async () => {
|
|
setMiscDisabled(true);
|
|
try {
|
|
const result = await MszCommentsApi.updatePost(postInfo.id, { pin: '0' });
|
|
setMiscVisible(null, !!result.pinned);
|
|
setOptionalTime(pinnedElem, result.pinned, 'commentPinned');
|
|
listing.reorder();
|
|
} catch(ex) {
|
|
console.error(ex);
|
|
} finally {
|
|
setMiscDisabled(false);
|
|
}
|
|
};
|
|
|
|
return {
|
|
get element() {
|
|
return element;
|
|
},
|
|
};
|
|
};
|
|
|
|
const MszCommentsListing = function(options) {
|
|
let { hidden, root } = options ?? {};
|
|
|
|
let loading = new MszLoading;
|
|
const entries = new Map;
|
|
const element = <div class={{ 'comments-listing': true, 'comments-listing-root': root, 'hidden': hidden }}>
|
|
{loading}
|
|
</div>;
|
|
|
|
const listing = {
|
|
get element() { return element; },
|
|
get count() { return element.childElementCount; },
|
|
|
|
get visible() { return !element.classList.contains('hidden'); },
|
|
set visible(state) { element.classList.toggle('hidden', !state); },
|
|
|
|
get loaded() { return loading === null; },
|
|
|
|
reorder: () => {
|
|
// this feels yucky but it works
|
|
const items = Array.from(element.children).sort((a, b) => parseInt(b.dataset.commentOrder - a.dataset.commentOrder));
|
|
for(const item of items)
|
|
element.appendChild(item);
|
|
},
|
|
|
|
addPost: (userInfo, postInfo, parentId=null) => {
|
|
const entry = new MszCommentsEntry(userInfo ?? {}, postInfo, listing, root);
|
|
entries.set(postInfo.id, entry);
|
|
element.appendChild(entry.element);
|
|
},
|
|
addPosts: (userInfo, posts) => {
|
|
try {
|
|
if(!Array.isArray(posts))
|
|
throw 'posts must be an array';
|
|
userInfo ??= {};
|
|
for(const postInfo of posts)
|
|
listing.addPost(userInfo, postInfo);
|
|
} finally {
|
|
loading.element.remove();
|
|
loading = null;
|
|
}
|
|
},
|
|
};
|
|
|
|
return listing;
|
|
};
|