Things are taking shape.
This commit is contained in:
parent
f0c9854a94
commit
58e85cc67b
5 changed files with 306 additions and 113 deletions
assets
src/Comments
|
@ -91,11 +91,15 @@
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 1px;
|
padding: 1px;
|
||||||
gap: 1px;
|
gap: 1px;
|
||||||
|
transition: opacity .1s;
|
||||||
}
|
}
|
||||||
.comments-entry-actions-group-votes,
|
.comments-entry-actions-group-votes,
|
||||||
.comments-entry-actions-group-replies {
|
.comments-entry-actions-group-replies {
|
||||||
border: 1px solid var(--accent-colour);
|
border: 1px solid var(--accent-colour);
|
||||||
}
|
}
|
||||||
|
.comments-entry-actions-group-disabled {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
.comments-entry-action {
|
.comments-entry-action {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
@ -108,12 +112,14 @@
|
||||||
padding: 3px 6px;
|
padding: 3px 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color .2s;
|
transition: background-color .2s;
|
||||||
|
min-width: 24px;
|
||||||
|
min-height: 22px;
|
||||||
}
|
}
|
||||||
.comments-entry-action:hover,
|
.comments-entry-action:hover,
|
||||||
.comments-entry-action:focus {
|
.comments-entry-action:focus {
|
||||||
background: var(--comments-entry-action-background-hover, #fff4);
|
background: var(--comments-entry-action-background-hover, #fff4);
|
||||||
}
|
}
|
||||||
.comments-entry-action-replies-open {
|
.comments-entry-action-reply-active {
|
||||||
background: #fff2;
|
background: #fff2;
|
||||||
}
|
}
|
||||||
.comments-entry-action-vote-like.comments-entry-action-vote-cast {
|
.comments-entry-action-vote-like.comments-entry-action-vote-cast {
|
||||||
|
|
|
@ -3,3 +3,6 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
.comments-listing-root {
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,10 @@ const MszCommentsApi = (() => {
|
||||||
if(name.trim() === '')
|
if(name.trim() === '')
|
||||||
throw 'name may not be empty';
|
throw 'name may not be empty';
|
||||||
|
|
||||||
const { status, body } = await $xhr.get(`/comments/categories/${name}`, { type: 'json' });
|
const { status, body } = await $xhr.get(
|
||||||
|
`/comments/categories/${name}`,
|
||||||
|
{ type: 'json' }
|
||||||
|
);
|
||||||
if(status === 404)
|
if(status === 404)
|
||||||
throw 'that category does not exist';
|
throw 'that category does not exist';
|
||||||
if(status !== 200)
|
if(status !== 200)
|
||||||
|
@ -22,7 +25,11 @@ const MszCommentsApi = (() => {
|
||||||
if(typeof args !== 'object' || args === null)
|
if(typeof args !== 'object' || args === null)
|
||||||
throw 'args must be a non-null object';
|
throw 'args must be a non-null object';
|
||||||
|
|
||||||
const { status } = await $xhr.patch(`/comments/categories/${name}`, { csrf: true }, args);
|
const { status } = await $xhr.patch(
|
||||||
|
`/comments/categories/${name}`,
|
||||||
|
{ csrf: true },
|
||||||
|
args
|
||||||
|
);
|
||||||
|
|
||||||
return status;
|
return status;
|
||||||
},
|
},
|
||||||
|
@ -32,7 +39,10 @@ const MszCommentsApi = (() => {
|
||||||
if(post.trim() === '')
|
if(post.trim() === '')
|
||||||
throw 'post id may not be empty';
|
throw 'post id may not be empty';
|
||||||
|
|
||||||
const { status, body } = await $xhr.get(`/comments/posts/${post}`, { type: 'json' });
|
const { status, body } = await $xhr.get(
|
||||||
|
`/comments/posts/${post}`,
|
||||||
|
{ type: 'json' }
|
||||||
|
);
|
||||||
if(status === 404)
|
if(status === 404)
|
||||||
throw 'that post does not exist';
|
throw 'that post does not exist';
|
||||||
if(status !== 200)
|
if(status !== 200)
|
||||||
|
@ -46,7 +56,10 @@ const MszCommentsApi = (() => {
|
||||||
if(post.trim() === '')
|
if(post.trim() === '')
|
||||||
throw 'post id may not be empty';
|
throw 'post id may not be empty';
|
||||||
|
|
||||||
const { status, body } = await $xhr.get(`/comments/posts/${post}/replies`, { type: 'json' });
|
const { status, body } = await $xhr.get(
|
||||||
|
`/comments/posts/${post}/replies`,
|
||||||
|
{ type: 'json' }
|
||||||
|
);
|
||||||
if(status === 404)
|
if(status === 404)
|
||||||
throw 'that post does not exist';
|
throw 'that post does not exist';
|
||||||
if(status !== 200)
|
if(status !== 200)
|
||||||
|
@ -62,7 +75,11 @@ const MszCommentsApi = (() => {
|
||||||
if(typeof args !== 'object' || args === null)
|
if(typeof args !== 'object' || args === null)
|
||||||
throw 'args must be a non-null object';
|
throw 'args must be a non-null object';
|
||||||
|
|
||||||
const { status, body } = await $xhr.post('/comments/posts', { csrf: true }, args);
|
const { status, body } = await $xhr.post(
|
||||||
|
'/comments/posts',
|
||||||
|
{ csrf: true },
|
||||||
|
args
|
||||||
|
);
|
||||||
|
|
||||||
return status;
|
return status;
|
||||||
},
|
},
|
||||||
|
@ -72,17 +89,37 @@ const MszCommentsApi = (() => {
|
||||||
deletePost: async post => {
|
deletePost: async post => {
|
||||||
//
|
//
|
||||||
},
|
},
|
||||||
|
restorePost: async post => {
|
||||||
|
if(typeof post !== 'string')
|
||||||
|
throw 'post id must be a string';
|
||||||
|
if(post.trim() === '')
|
||||||
|
throw 'post id may not be empty';
|
||||||
|
|
||||||
|
const { status } = await $xhr.post(`/comments/posts/${post}/restore`, { csrf: true });
|
||||||
|
if(status === 400)
|
||||||
|
throw 'that post is not deleted';
|
||||||
|
if(status === 403)
|
||||||
|
throw 'you are not allowed to restore posts';
|
||||||
|
if(status === 404)
|
||||||
|
throw 'that post does not exist';
|
||||||
|
if(status !== 200)
|
||||||
|
throw 'something went wrong';
|
||||||
|
},
|
||||||
createVote: async (post, vote) => {
|
createVote: async (post, vote) => {
|
||||||
if(typeof post !== 'string')
|
if(typeof post !== 'string')
|
||||||
throw 'name must be a string';
|
throw 'post id must be a string';
|
||||||
if(post.trim() === '')
|
if(post.trim() === '')
|
||||||
throw 'name may not be empty';
|
throw 'post id may not be empty';
|
||||||
if(typeof vote === 'string')
|
if(typeof vote === 'string')
|
||||||
vote = parseInt(vote);
|
vote = parseInt(vote);
|
||||||
if(typeof vote !== 'number' || isNaN(vote))
|
if(typeof vote !== 'number' || isNaN(vote))
|
||||||
throw 'vote must be a number';
|
throw 'vote must be a number';
|
||||||
|
|
||||||
const { status } = await $xhr.post(`/comments/posts/${post}/vote`, { csrf: true }, { vote });
|
const { status, body } = await $xhr.post(
|
||||||
|
`/comments/posts/${post}/vote`,
|
||||||
|
{ csrf: true, type: 'json' },
|
||||||
|
{ vote }
|
||||||
|
);
|
||||||
if(status === 400)
|
if(status === 400)
|
||||||
throw 'your vote is not acceptable';
|
throw 'your vote is not acceptable';
|
||||||
if(status === 403)
|
if(status === 403)
|
||||||
|
@ -91,20 +128,27 @@ const MszCommentsApi = (() => {
|
||||||
throw 'that post does not exist';
|
throw 'that post does not exist';
|
||||||
if(status !== 200)
|
if(status !== 200)
|
||||||
throw 'something went wrong';
|
throw 'something went wrong';
|
||||||
|
|
||||||
|
return body;
|
||||||
},
|
},
|
||||||
deleteVote: async post => {
|
deleteVote: async post => {
|
||||||
if(typeof post !== 'string')
|
if(typeof post !== 'string')
|
||||||
throw 'name must be a string';
|
throw 'post id must be a string';
|
||||||
if(post.trim() === '')
|
if(post.trim() === '')
|
||||||
throw 'name may not be empty';
|
throw 'post id may not be empty';
|
||||||
|
|
||||||
const { status } = await $xhr.delete(`/comments/posts/${post}/vote`, { csrf: true });
|
const { status, body } = await $xhr.delete(
|
||||||
|
`/comments/posts/${post}/vote`,
|
||||||
|
{ csrf: true, type: 'json' }
|
||||||
|
);
|
||||||
if(status === 403)
|
if(status === 403)
|
||||||
throw 'you are not allowed to like or dislike comments';
|
throw 'you are not allowed to like or dislike comments';
|
||||||
if(status === 404)
|
if(status === 404)
|
||||||
throw 'that post does not exist';
|
throw 'that post does not exist';
|
||||||
if(status !== 204)
|
if(status !== 200)
|
||||||
throw 'something went wrong';
|
throw 'something went wrong';
|
||||||
|
|
||||||
|
return body;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -5,59 +5,160 @@ const MszCommentsEntry = function(userInfo, postInfo, root) {
|
||||||
userInfo ??= {};
|
userInfo ??= {};
|
||||||
|
|
||||||
const actions = <div class="comments-entry-actions" />;
|
const actions = <div class="comments-entry-actions" />;
|
||||||
actions.appendChild(<div class="comments-entry-actions-group comments-entry-actions-group-votes">
|
|
||||||
<button class={{ 'comments-entry-action': true, 'comments-entry-action-vote-like': true, 'comments-entry-action-vote-cast': postInfo.vote > 0 }} disabled={!userInfo.can_vote}>
|
const likeAction = <button class="comments-entry-action comments-entry-action-vote-like" disabled={!userInfo.can_vote}>
|
||||||
<i class="fas fa-chevron-up" />
|
<i class="fas fa-chevron-up" />
|
||||||
{postInfo.positive > 0 ? <span>{postInfo.positive.toLocaleString()}</span> : null}
|
</button>;
|
||||||
</button>
|
const dislikeAction = <button class="comments-entry-action comments-entry-action-vote-dislike" disabled={!userInfo.can_vote}>
|
||||||
<button class={{ 'comments-entry-action': true, 'comments-entry-action-vote-dislike': true, 'comments-entry-action-vote-cast': postInfo.vote < 0 }} disabled={!userInfo.can_vote}>
|
<i class="fas fa-chevron-down" />
|
||||||
<i class="fas fa-chevron-down" />
|
</button>;
|
||||||
{postInfo.negative < 0 ? <span>{Math.abs(postInfo.negative).toLocaleString()}</span> : null}
|
const voteActions = <div class="comments-entry-actions-group comments-entry-actions-group-votes">
|
||||||
</button>
|
{likeAction}
|
||||||
</div>);
|
{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 repliesIsArray = Array.isArray(postInfo.replies);
|
||||||
const form = userInfo?.can_create ? new MszCommentsForm(userInfo) : null;
|
|
||||||
const listing = new MszCommentsListing({ hidden: !repliesIsArray });
|
const listing = new MszCommentsListing({ hidden: !repliesIsArray });
|
||||||
if(repliesIsArray)
|
if(repliesIsArray)
|
||||||
listing.addPosts(userInfo, postInfo.replies);
|
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 replyCount = repliesIsArray ? postInfo.replies.length : (postInfo.replies ?? 0);
|
||||||
if(replyCount > 0 || userInfo.can_create) {
|
const replyActionsGroup = <div class="comments-entry-actions-group comments-entry-actions-group-replies" />;
|
||||||
const replyElem = <button class={{ 'comments-entry-action': true, 'comments-entry-action-replies-open': listing.visible }}>
|
actions.appendChild(replyActionsGroup);
|
||||||
<i class="fas fa-reply" />
|
|
||||||
{replyCount > 0 ? <span>{replyCount.toLocaleString()}</span> : null}
|
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>;
|
</button>;
|
||||||
|
replyActionsGroup.appendChild(replyElem);
|
||||||
|
|
||||||
let loaded = listing.loaded;
|
replyElem.onclick = () => {
|
||||||
replyElem.onclick = async () => {
|
if(form === null) {
|
||||||
replyElem.classList.toggle('comments-entry-action-replies-open', listing.visible = !listing.visible);
|
replyElem.classList.add('comments-entry-action-reply-active');
|
||||||
if(!loaded) {
|
form = new MszCommentsForm(userInfo);
|
||||||
loaded = true;
|
$insertBefore(listing.element, form.element);
|
||||||
try {
|
} else {
|
||||||
listing.addPosts(userInfo, await MszCommentsApi.getPostReplies(postInfo.id));
|
replyElem.classList.remove('comments-entry-action-reply-active');
|
||||||
} catch(ex) {
|
repliesElem.removeChild(form.element);
|
||||||
loaded = false;
|
form = null;
|
||||||
console.error(ex);
|
|
||||||
|
|
||||||
// THIS IS NOT FINAL DO NOT PUSH THIS TO PUBLIC THIS WOULD BE HORRIBLE
|
|
||||||
if(typeof ex === 'string')
|
|
||||||
MszShowMessageBox(ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
actions.appendChild(<div class="comments-entry-actions-group comments-entry-actions-group-replies">
|
|
||||||
{replyElem}
|
|
||||||
</div>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
if(postInfo.can_delete || userInfo.can_pin) {
|
||||||
const misc = <div class="comments-entry-actions-group" />;
|
const misc = <div class="comments-entry-actions-group" />;
|
||||||
if(postInfo.can_delete)
|
if(postInfo.can_delete) {
|
||||||
misc.appendChild(<button class="comments-entry-action">
|
if(postInfo.deleted) {
|
||||||
<i class="fas fa-trash" />
|
misc.appendChild(<button class="comments-entry-action">
|
||||||
</button>);
|
<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)
|
if(userInfo.can_pin)
|
||||||
misc.appendChild(<button class="comments-entry-action" disabled={!userInfo.can_pin}>
|
misc.appendChild(<button class="comments-entry-action" disabled={!userInfo.can_pin}>
|
||||||
<i class="fas fa-thumbtack" />
|
<i class="fas fa-thumbtack" />
|
||||||
|
@ -65,10 +166,10 @@ const MszCommentsEntry = function(userInfo, postInfo, root) {
|
||||||
actions.appendChild(misc);
|
actions.appendChild(misc);
|
||||||
}
|
}
|
||||||
|
|
||||||
const created = new Date(postInfo.created);
|
const created = typeof postInfo.created === 'string' ? new Date(postInfo.created) : null;
|
||||||
const edited = postInfo.edited ? new Date(postInfo.edited) : null;
|
const edited = typeof postInfo.edited === 'string' ? new Date(postInfo.edited) : null;
|
||||||
const deleted = postInfo.deleted ? new Date(postInfo.deleted) : null;
|
const deleted = typeof postInfo.deleted === 'string' ? new Date(postInfo.deleted) : null;
|
||||||
const pinned = postInfo.pinned ? new Date(postInfo.pinned) : 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 }}>
|
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-main">
|
||||||
|
@ -85,7 +186,9 @@ const MszCommentsEntry = function(userInfo, postInfo, root) {
|
||||||
<div class="comments-entry-time">
|
<div class="comments-entry-time">
|
||||||
<div class="comments-entry-time-icon">—</div>
|
<div class="comments-entry-time-icon">—</div>
|
||||||
<a href={`#comment-${postInfo.id}`} class="comments-entry-time-link">
|
<a href={`#comment-${postInfo.id}`} class="comments-entry-time-link">
|
||||||
<time class="comments-entry-time-text" datetime={created.toISOString()} title={created.toString()}>{MszSakuya.formatTimeAgo(created)}</time>
|
{created !== null
|
||||||
|
? <time class="comments-entry-time-text" datetime={created.toISOString()} title={created.toString()}>{MszSakuya.formatTimeAgo(created)}</time>
|
||||||
|
: 'deleted'}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{edited !== null ? <div class="comments-entry-time comments-entry-time-edited">
|
{edited !== null ? <div class="comments-entry-time comments-entry-time-edited">
|
||||||
|
@ -101,14 +204,11 @@ const MszCommentsEntry = function(userInfo, postInfo, root) {
|
||||||
<time class="comments-entry-time-text" datetime={deleted.toISOString()} title={deleted.toString()}>{MszSakuya.formatTimeAgo(deleted)}</time>
|
<time class="comments-entry-time-text" datetime={deleted.toISOString()} title={deleted.toString()}>{MszSakuya.formatTimeAgo(deleted)}</time>
|
||||||
</div> : null}
|
</div> : null}
|
||||||
</div>
|
</div>
|
||||||
<div class="comments-entry-body">{postInfo.body}</div>
|
<div class="comments-entry-body">{postInfo?.body ?? '[deleted]'}</div>
|
||||||
{actions.childElementCount > 0 ? actions : null}
|
{actions.childElementCount > 0 ? actions : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="comments-entry-replies">
|
{repliesElem}
|
||||||
{form}
|
|
||||||
{listing}
|
|
||||||
</div>
|
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
MszSakuya.trackElements(element.querySelectorAll('time'));
|
MszSakuya.trackElements(element.querySelectorAll('time'));
|
||||||
|
@ -125,7 +225,7 @@ const MszCommentsListing = function(options) {
|
||||||
|
|
||||||
let loading = new MszLoading;
|
let loading = new MszLoading;
|
||||||
const entries = new Map;
|
const entries = new Map;
|
||||||
const element = <div class={{ 'comments-listing': true, 'hidden': hidden }}>
|
const element = <div class={{ 'comments-listing': true, 'comments-listing-root': root, 'hidden': hidden }}>
|
||||||
{loading}
|
{loading}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
|
|
|
@ -47,46 +47,53 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
||||||
CommentsPostInfo $postInfo,
|
CommentsPostInfo $postInfo,
|
||||||
?iterable $replyInfos = null
|
?iterable $replyInfos = null
|
||||||
): array {
|
): array {
|
||||||
$post = [
|
$canViewDeleted = $perms->check(Perm::G_COMMENTS_DELETE_ANY);
|
||||||
'id' => $postInfo->id,
|
$isDeleted = $postInfo->deleted && !$canViewDeleted;
|
||||||
'body' => $postInfo->body,
|
|
||||||
'created' => $postInfo->createdAt->toIso8601ZuluString(),
|
$post = ['id' => $postInfo->id];
|
||||||
];
|
if(!$isDeleted) {
|
||||||
if($postInfo->pinned)
|
$post['body'] = $postInfo->body;
|
||||||
$post['pinned'] = $postInfo->pinnedAt->toIso8601ZuluString();
|
$post['created'] = $postInfo->createdAt->toIso8601ZuluString();
|
||||||
if($postInfo->edited)
|
if($postInfo->pinned)
|
||||||
$post['edited'] = $postInfo->editedAt->toIso8601ZuluString();
|
$post['pinned'] = $postInfo->pinnedAt->toIso8601ZuluString();
|
||||||
|
if($postInfo->edited)
|
||||||
|
$post['edited'] = $postInfo->editedAt->toIso8601ZuluString();
|
||||||
|
|
||||||
|
if(!$isDeleted && $postInfo->userId !== null)
|
||||||
|
try {
|
||||||
|
$post['user'] = $this->convertUser(
|
||||||
|
$this->usersCtx->getUserInfo($postInfo->userId)
|
||||||
|
);
|
||||||
|
} catch(RuntimeException $ex) {}
|
||||||
|
|
||||||
|
$votes = $this->commentsCtx->votes->getVotesAggregate($postInfo);
|
||||||
|
$post['positive'] = $votes->positive;
|
||||||
|
$post['negative'] = $votes->negative;
|
||||||
|
|
||||||
|
if($this->authInfo->loggedIn) {
|
||||||
|
$voteInfo = $this->commentsCtx->votes->getVote($postInfo, $this->authInfo->userInfo);
|
||||||
|
if($voteInfo->weight !== 0)
|
||||||
|
$post['vote'] = $voteInfo->weight;
|
||||||
|
|
||||||
|
$isAuthor = $this->authInfo->userId === $postInfo->userId;
|
||||||
|
if($isAuthor && $perms->check(Perm::G_COMMENTS_EDIT_OWN))
|
||||||
|
$post['can_edit'] = true;
|
||||||
|
if($isAuthor && $perms->check(Perm::G_COMMENTS_DELETE_OWN))
|
||||||
|
$post['can_delete'] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
if($postInfo->deleted)
|
if($postInfo->deleted)
|
||||||
$post['deleted'] = $postInfo->deletedAt->toIso8601ZuluString();
|
$post['deleted'] = $canViewDeleted ? $postInfo->deletedAt->toIso8601ZuluString() : true;
|
||||||
|
|
||||||
if($postInfo->userId !== null)
|
|
||||||
try {
|
|
||||||
$post['user'] = $this->convertUser(
|
|
||||||
$this->usersCtx->getUserInfo($postInfo->userId)
|
|
||||||
);
|
|
||||||
} catch(RuntimeException $ex) {}
|
|
||||||
|
|
||||||
$votes = $this->commentsCtx->votes->getVotesAggregate($postInfo);
|
|
||||||
$post['positive'] = $votes->positive;
|
|
||||||
$post['negative'] = $votes->negative;
|
|
||||||
|
|
||||||
if($this->authInfo->loggedIn) {
|
if($this->authInfo->loggedIn) {
|
||||||
$voteInfo = $this->commentsCtx->votes->getVote($postInfo, $this->authInfo->userInfo);
|
if($perms->check(Perm::G_COMMENTS_EDIT_ANY))
|
||||||
if($voteInfo->weight !== 0)
|
|
||||||
$post['vote'] = $voteInfo->weight;
|
|
||||||
|
|
||||||
$isAuthor = $this->authInfo->userId === $postInfo->userId;
|
|
||||||
if($perms->check(Perm::G_COMMENTS_EDIT_ANY) || ($isAuthor && $perms->check(Perm::G_COMMENTS_EDIT_OWN)))
|
|
||||||
$post['can_edit'] = true;
|
$post['can_edit'] = true;
|
||||||
if($perms->check(Perm::G_COMMENTS_DELETE_ANY) || ($isAuthor && $perms->check(Perm::G_COMMENTS_DELETE_OWN)))
|
if($perms->check(Perm::G_COMMENTS_DELETE_ANY))
|
||||||
$post['can_delete'] = true;
|
$post['can_delete'] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if($replyInfos === null) {
|
if($replyInfos === null) {
|
||||||
$replies = $this->commentsCtx->posts->countPosts(
|
$replies = $this->commentsCtx->posts->countPosts(parentInfo: $postInfo);
|
||||||
parentInfo: $postInfo,
|
|
||||||
deleted: $perms->check(Perm::G_COMMENTS_DELETE_ANY) ? null : false,
|
|
||||||
);
|
|
||||||
if($replies > 0)
|
if($replies > 0)
|
||||||
$post['replies'] = $replies;
|
$post['replies'] = $replies;
|
||||||
} else {
|
} else {
|
||||||
|
@ -164,14 +171,9 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
||||||
$postInfos = $this->commentsCtx->posts->getPosts(
|
$postInfos = $this->commentsCtx->posts->getPosts(
|
||||||
categoryInfo: $categoryInfo,
|
categoryInfo: $categoryInfo,
|
||||||
replies: false,
|
replies: false,
|
||||||
deleted: $perms->check(Perm::G_COMMENTS_DELETE_ANY) ? null : false,
|
|
||||||
);
|
);
|
||||||
foreach($postInfos as $postInfo) {
|
foreach($postInfos as $postInfo) {
|
||||||
$replyInfos = $this->commentsCtx->posts->getPosts(
|
$replyInfos = $this->commentsCtx->posts->getPosts(parentInfo: $postInfo);
|
||||||
parentInfo: $postInfo,
|
|
||||||
deleted: $perms->check(Perm::G_COMMENTS_DELETE_ANY) ? null : false,
|
|
||||||
);
|
|
||||||
|
|
||||||
$posts[] = $this->convertPost($perms, $postInfo, $replyInfos);
|
$posts[] = $this->convertPost($perms, $postInfo, $replyInfos);
|
||||||
}
|
}
|
||||||
} catch(RuntimeException $ex) {}
|
} catch(RuntimeException $ex) {}
|
||||||
|
@ -206,10 +208,7 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
$perms = $this->getGlobalPerms();
|
$perms = $this->getGlobalPerms();
|
||||||
$replyInfos = $this->commentsCtx->posts->getPosts(
|
$replyInfos = $this->commentsCtx->posts->getPosts(parentInfo: $postInfo);
|
||||||
parentInfo: $postInfo,
|
|
||||||
deleted: $perms->check(Perm::G_COMMENTS_DELETE_ANY) ? null : false,
|
|
||||||
);
|
|
||||||
|
|
||||||
return $this->convertPost($perms, $postInfo, $replyInfos);
|
return $this->convertPost($perms, $postInfo, $replyInfos);
|
||||||
}
|
}
|
||||||
|
@ -223,10 +222,7 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
$perms = $this->getGlobalPerms();
|
$perms = $this->getGlobalPerms();
|
||||||
$replyInfos = $this->commentsCtx->posts->getPosts(
|
$replyInfos = $this->commentsCtx->posts->getPosts(parentInfo: $postInfo);
|
||||||
parentInfo: $postInfo,
|
|
||||||
deleted: $perms->check(Perm::G_COMMENTS_DELETE_ANY) ? null : false,
|
|
||||||
);
|
|
||||||
|
|
||||||
$replies = [];
|
$replies = [];
|
||||||
foreach($replyInfos as $replyInfo)
|
foreach($replyInfos as $replyInfo)
|
||||||
|
@ -249,6 +245,12 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
||||||
|
|
||||||
#[HttpDelete('/comments/posts/([0-9]+)')]
|
#[HttpDelete('/comments/posts/([0-9]+)')]
|
||||||
public function deletePost(HttpResponseBuilder $response, HttpRequest $request, string $commentId): int|array {
|
public function deletePost(HttpResponseBuilder $response, HttpRequest $request, string $commentId): int|array {
|
||||||
|
try {
|
||||||
|
$postInfo = $this->commentsCtx->posts->getPost($commentId);
|
||||||
|
} catch(RuntimeException $ex) {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
$perms = $this->getGlobalPerms();
|
$perms = $this->getGlobalPerms();
|
||||||
$canDeleteAny = $perms->check(Perm::G_COMMENTS_DELETE_ANY);
|
$canDeleteAny = $perms->check(Perm::G_COMMENTS_DELETE_ANY);
|
||||||
if(!$canDeleteAny && !$perms->check(Perm::G_COMMENTS_DELETE_OWN))
|
if(!$canDeleteAny && !$perms->check(Perm::G_COMMENTS_DELETE_OWN))
|
||||||
|
@ -257,8 +259,27 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
||||||
return 501;
|
return 501;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[HttpPost('/comments/posts/([0-9]+)/restore')]
|
||||||
|
public function postPostRestore(HttpResponseBuilder $response, HttpRequest $request, string $commentId): int {
|
||||||
|
if(!$this->getGlobalPerms()->check(Perm::G_COMMENTS_DELETE_ANY))
|
||||||
|
return 403;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$postInfo = $this->commentsCtx->posts->getPost($commentId);
|
||||||
|
} catch(RuntimeException $ex) {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$postInfo->deleted)
|
||||||
|
return 400;
|
||||||
|
|
||||||
|
$this->commentsCtx->posts->restorePost($postInfo);
|
||||||
|
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
#[HttpPost('/comments/posts/([0-9]+)/vote')]
|
#[HttpPost('/comments/posts/([0-9]+)/vote')]
|
||||||
public function postPostVote(HttpResponseBuilder $response, HttpRequest $request, string $commentId): int {
|
public function postPostVote(HttpResponseBuilder $response, HttpRequest $request, string $commentId): int|array {
|
||||||
$vote = (int)$request->content->getParam('vote', FILTER_SANITIZE_NUMBER_INT);
|
$vote = (int)$request->content->getParam('vote', FILTER_SANITIZE_NUMBER_INT);
|
||||||
if($vote === 0)
|
if($vote === 0)
|
||||||
return 400;
|
return 400;
|
||||||
|
@ -278,11 +299,19 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
||||||
max(-1, min(1, $vote))
|
max(-1, min(1, $vote))
|
||||||
);
|
);
|
||||||
|
|
||||||
return 200;
|
$voteInfo = $this->commentsCtx->votes->getVote($postInfo, $this->authInfo->userInfo);
|
||||||
|
$votes = $this->commentsCtx->votes->getVotesAggregate($postInfo);
|
||||||
|
|
||||||
|
$response->statusCode = 200;
|
||||||
|
return [
|
||||||
|
'vote' => $voteInfo->weight,
|
||||||
|
'positive' => $votes->positive,
|
||||||
|
'negative' => $votes->negative,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
#[HttpDelete('/comments/posts/([0-9]+)/vote')]
|
#[HttpDelete('/comments/posts/([0-9]+)/vote')]
|
||||||
public function deletePostVote(HttpResponseBuilder $response, HttpRequest $request, string $commentId): int {
|
public function deletePostVote(HttpResponseBuilder $response, HttpRequest $request, string $commentId): int|array {
|
||||||
if(!$this->getGlobalPerms()->check(Perm::G_COMMENTS_VOTE))
|
if(!$this->getGlobalPerms()->check(Perm::G_COMMENTS_VOTE))
|
||||||
return 403;
|
return 403;
|
||||||
|
|
||||||
|
@ -292,8 +321,19 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
||||||
return 404;
|
return 404;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->commentsCtx->votes->removeVote($postInfo, $this->authInfo->userInfo);
|
$this->commentsCtx->votes->removeVote(
|
||||||
|
$postInfo,
|
||||||
|
$this->authInfo->userInfo
|
||||||
|
);
|
||||||
|
|
||||||
return 204;
|
$voteInfo = $this->commentsCtx->votes->getVote($postInfo, $this->authInfo->userInfo);
|
||||||
|
$votes = $this->commentsCtx->votes->getVotesAggregate($postInfo);
|
||||||
|
|
||||||
|
$response->statusCode = 200;
|
||||||
|
return [
|
||||||
|
'vote' => $voteInfo->weight,
|
||||||
|
'positive' => $votes->positive,
|
||||||
|
'negative' => $votes->negative,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue