misuzu/assets/misuzu.js/comments/listing.jsx

260 lines
11 KiB
JavaScript

#include comments/api.js
#include comments/form.jsx
const MszCommentsEntry = function(userInfo, postInfo, 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}>
<i class="fas fa-chevron-up" />
</button>;
const dislikeAction = <button class="comments-entry-action comments-entry-action-vote-dislike" disabled={!userInfo.can_vote}>
<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 => {
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;
}
};
if(postInfo.deleted && userInfo.can_vote) {
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 listing = new MszCommentsListing({ hidden: !repliesIsArray });
if(repliesIsArray)
listing.addPosts(userInfo, postInfo.replies);
let form = null;
const repliesElem = <div class="comments-entry-replies">
{listing}
</div>;
const 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">
{replyToggleOpenElem}
{replyToggleClosedElem}
{replyCountElem}
</button>;
replyActionsGroup.appendChild(replyToggleElem);
const setReplyToggleState = visible => {
replyToggleOpenElem.classList.toggle('hidden', !visible);
replyToggleClosedElem.classList.toggle('hidden', visible);
};
setReplyToggleState(listing.visible);
const setReplyCount = count => {
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 = listing.loaded;
replyToggleElem.onclick = async () => {
setReplyToggleState(listing.visible = !listing.visible);
if(!replyLoaded) {
replyLoaded = true;
try {
listing.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">
<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(listing.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);
if(postInfo.can_delete || userInfo.can_pin) {
const misc = <div class="comments-entry-actions-group" />;
if(postInfo.can_delete) {
if(postInfo.deleted) {
misc.appendChild(<button class="comments-entry-action">
<i class="fas fa-trash-restore" />
</button>);
} else {
misc.appendChild(<button class="comments-entry-action">
<i class="fas fa-trash" />
</button>);
}
}
if(userInfo.can_pin)
misc.appendChild(<button class="comments-entry-action" disabled={!userInfo.can_pin}>
<i class="fas fa-thumbtack" />
</button>);
actions.appendChild(misc);
}
const created = typeof postInfo.created === 'string' ? new Date(postInfo.created) : null;
const edited = typeof postInfo.edited === 'string' ? new Date(postInfo.edited) : null;
const deleted = typeof postInfo.deleted === 'string' ? new Date(postInfo.deleted) : null;
const pinned = typeof postInfo.pinned === 'string' ? new Date(postInfo.pinned) : null;
const element = <div id={`comment-${postInfo.id}`} data-comment={postInfo.id} class={{ 'comments-entry': true, 'comments-entry-root': root, 'comments-entry-deleted': postInfo.deleted }} style={{ '--user-colour': postInfo.user?.colour }}>
<div class="comments-entry-main">
<div class="comments-entry-avatar">
<img src={postInfo.user?.avatar ?? '/images/no-avatar.png'} alt="" width="40" height="40" class="avatar" />
</div>
<div class="comments-entry-wrap">
<div class="comments-entry-meta">
<div class="comments-entry-user">
{postInfo.user
? <a class="comments-entry-user-link" href={postInfo.user.profile} style="color: var(--user-colour);">{postInfo.user.name}</a>
: <span class="comments-entry-user-dead">Deleted user</span>}
</div>
<div class="comments-entry-time">
<div class="comments-entry-time-icon">&mdash;</div>
<a href={`#comment-${postInfo.id}`} class="comments-entry-time-link">
{created !== null
? <time class="comments-entry-time-text" datetime={created.toISOString()} title={created.toString()}>{MszSakuya.formatTimeAgo(created)}</time>
: 'deleted'}
</a>
</div>
{edited !== null ? <div class="comments-entry-time comments-entry-time-edited">
<div class="comments-entry-time-icon"><i class="fas fa-pencil-alt" /></div>
<time class="comments-entry-time-text" datetime={edited.toISOString()} title={edited.toString()}>{MszSakuya.formatTimeAgo(edited)}</time>
</div> : null}
{pinned !== null ? <div class="comments-entry-time comments-entry-time-pinned">
<div class="comments-entry-time-icon"><i class="fas fa-thumbtack" /></div>
<time class="comments-entry-time-text" datetime={pinned.toISOString()} title={pinned.toString()}>{MszSakuya.formatTimeAgo(pinned)}</time>
</div> : null}
{deleted !== null ? <div class="comments-entry-time comments-entry-time-deleted">
<div class="comments-entry-time-icon"><i class="fas fa-trash" /></div>
<time class="comments-entry-time-text" datetime={deleted.toISOString()} title={deleted.toString()}>{MszSakuya.formatTimeAgo(deleted)}</time>
</div> : null}
</div>
<div class="comments-entry-body">{postInfo?.body ?? '[deleted]'}</div>
{actions.childElementCount > 0 ? actions : null}
</div>
</div>
{repliesElem}
</div>;
MszSakuya.trackElements(element.querySelectorAll('time'));
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 addPost = function(userInfo, postInfo, parentId=null) {
const entry = new MszCommentsEntry(userInfo ?? {}, postInfo, root);
entries.set(postInfo.id, entry);
element.appendChild(entry.element);
};
return {
get element() { return element; },
get visible() { return !element.classList.contains('hidden'); },
set visible(state) { element.classList.toggle('hidden', !state); },
get loaded() { return loading === null; },
addPost: addPost,
addPosts: function(userInfo, posts) {
try {
if(!Array.isArray(posts))
throw 'posts must be an array';
userInfo ??= {};
for(const postInfo of posts)
addPost(userInfo, postInfo);
} finally {
loading.element.remove();
loading = null;
}
},
};
};