Added more functionality, no posting yet though.
This commit is contained in:
parent
58e85cc67b
commit
6cf0348067
6 changed files with 495 additions and 159 deletions
assets
src/Comments
|
@ -72,7 +72,7 @@ const $create = function(info, attrs, child, created) {
|
||||||
let setFunc = null;
|
let setFunc = null;
|
||||||
if(elem[key] instanceof DOMTokenList)
|
if(elem[key] instanceof DOMTokenList)
|
||||||
setFunc = (ak, av) => { if(av) elem[key].add(ak); };
|
setFunc = (ak, av) => { if(av) elem[key].add(ak); };
|
||||||
else if(elem[key] instanceof CSS2Properties)
|
else if(elem[key] instanceof CSSStyleDeclaration)
|
||||||
setFunc = (ak, av) => { elem[key].setProperty(ak, av); }
|
setFunc = (ak, av) => { elem[key].setProperty(ak, av); }
|
||||||
else
|
else
|
||||||
setFunc = (ak, av) => { elem[key][ak] = av; };
|
setFunc = (ak, av) => { elem[key][ak] = av; };
|
||||||
|
|
|
@ -12,16 +12,6 @@
|
||||||
border-top: 1px solid var(--accent-colour);
|
border-top: 1px solid var(--accent-colour);
|
||||||
}
|
}
|
||||||
|
|
||||||
.comments-entry-deleted {
|
|
||||||
opacity: .5;
|
|
||||||
transition: opacity .1s;
|
|
||||||
}
|
|
||||||
.comments-entry-deleted:hover,
|
|
||||||
.comments-entry-deleted:focus,
|
|
||||||
.comments-entry-deleted:focus-within {
|
|
||||||
opacity: .8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comments-entry-replies {
|
.comments-entry-replies {
|
||||||
margin-left: 25px;
|
margin-left: 25px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,10 +84,48 @@ const MszCommentsApi = (() => {
|
||||||
return status;
|
return status;
|
||||||
},
|
},
|
||||||
updatePost: async (post, args) => {
|
updatePost: async (post, args) => {
|
||||||
//
|
if(typeof post !== 'string')
|
||||||
|
throw 'post id must be a string';
|
||||||
|
if(post.trim() === '')
|
||||||
|
throw 'post id may not be empty';
|
||||||
|
if(typeof args !== 'object' || args === null)
|
||||||
|
throw 'args must be a non-null object';
|
||||||
|
|
||||||
|
const { status, body } = await $xhr.post(
|
||||||
|
`/comments/posts/${post}`,
|
||||||
|
{ csrf: true, type: 'json' },
|
||||||
|
args
|
||||||
|
);
|
||||||
|
if(status === 400)
|
||||||
|
throw 'your update is not acceptable';
|
||||||
|
if(status === 401)
|
||||||
|
throw 'you must be logged in to do that';
|
||||||
|
if(status === 403)
|
||||||
|
throw 'you are not allowed to edit that part of the post';
|
||||||
|
if(status === 404)
|
||||||
|
throw 'that post does not exist';
|
||||||
|
if(status === 410)
|
||||||
|
throw 'that post disappeared while attempting to edit it';
|
||||||
|
if(status !== 200)
|
||||||
|
throw 'something went wrong';
|
||||||
|
|
||||||
|
return body;
|
||||||
},
|
},
|
||||||
deletePost: async post => {
|
deletePost: 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.delete(`/comments/posts/${post}`, { csrf: true });
|
||||||
|
if(status === 401)
|
||||||
|
throw 'you must be logged in to do that';
|
||||||
|
if(status === 403)
|
||||||
|
throw 'you are not allowed to delete that post';
|
||||||
|
if(status === 404)
|
||||||
|
throw 'that post does not exist';
|
||||||
|
if(status !== 204)
|
||||||
|
throw 'something went wrong';
|
||||||
},
|
},
|
||||||
restorePost: async post => {
|
restorePost: async post => {
|
||||||
if(typeof post !== 'string')
|
if(typeof post !== 'string')
|
||||||
|
@ -98,6 +136,8 @@ const MszCommentsApi = (() => {
|
||||||
const { status } = await $xhr.post(`/comments/posts/${post}/restore`, { csrf: true });
|
const { status } = await $xhr.post(`/comments/posts/${post}/restore`, { csrf: true });
|
||||||
if(status === 400)
|
if(status === 400)
|
||||||
throw 'that post is not deleted';
|
throw 'that post is not deleted';
|
||||||
|
if(status === 401)
|
||||||
|
throw 'you must be logged in to do that';
|
||||||
if(status === 403)
|
if(status === 403)
|
||||||
throw 'you are not allowed to restore posts';
|
throw 'you are not allowed to restore posts';
|
||||||
if(status === 404)
|
if(status === 404)
|
||||||
|
@ -105,6 +145,24 @@ const MszCommentsApi = (() => {
|
||||||
if(status !== 200)
|
if(status !== 200)
|
||||||
throw 'something went wrong';
|
throw 'something went wrong';
|
||||||
},
|
},
|
||||||
|
nukePost: 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}/nuke`, { csrf: true });
|
||||||
|
if(status === 400)
|
||||||
|
throw 'that post is not deleted';
|
||||||
|
if(status === 401)
|
||||||
|
throw 'you must be logged in to do that';
|
||||||
|
if(status === 403)
|
||||||
|
throw 'you are not allowed to nuke 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 'post id must be a string';
|
throw 'post id must be a string';
|
||||||
|
@ -122,6 +180,8 @@ const MszCommentsApi = (() => {
|
||||||
);
|
);
|
||||||
if(status === 400)
|
if(status === 400)
|
||||||
throw 'your vote is not acceptable';
|
throw 'your vote is not acceptable';
|
||||||
|
if(status === 401)
|
||||||
|
throw 'you must be logged in to do that';
|
||||||
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)
|
||||||
|
@ -141,6 +201,8 @@ const MszCommentsApi = (() => {
|
||||||
`/comments/posts/${post}/vote`,
|
`/comments/posts/${post}/vote`,
|
||||||
{ csrf: true, type: 'json' }
|
{ csrf: true, type: 'json' }
|
||||||
);
|
);
|
||||||
|
if(status === 401)
|
||||||
|
throw 'you must be logged in to do that';
|
||||||
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)
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
#include comments/api.js
|
#include comments/api.js
|
||||||
#include comments/form.jsx
|
#include comments/form.jsx
|
||||||
|
|
||||||
const MszCommentsEntry = function(userInfo, postInfo, root) {
|
const MszCommentsEntry = function(userInfo, postInfo, listing, root) {
|
||||||
userInfo ??= {};
|
userInfo ??= {};
|
||||||
|
|
||||||
const actions = <div class="comments-entry-actions" />;
|
const actions = <div class="comments-entry-actions" />;
|
||||||
|
|
||||||
const likeAction = <button class="comments-entry-action comments-entry-action-vote-like" disabled={!userInfo.can_vote}>
|
const likeAction = <button class="comments-entry-action comments-entry-action-vote-like" disabled={!userInfo.can_vote} title="Like">
|
||||||
<i class="fas fa-chevron-up" />
|
<i class="fas fa-chevron-up" />
|
||||||
</button>;
|
</button>;
|
||||||
const dislikeAction = <button class="comments-entry-action comments-entry-action-vote-dislike" disabled={!userInfo.can_vote}>
|
const dislikeAction = <button class="comments-entry-action comments-entry-action-vote-dislike" disabled={!userInfo.can_vote} title="Dislike">
|
||||||
<i class="fas fa-chevron-down" />
|
<i class="fas fa-chevron-down" />
|
||||||
</button>;
|
</button>;
|
||||||
const voteActions = <div class="comments-entry-actions-group comments-entry-actions-group-votes">
|
const voteActions = <div class="comments-entry-actions-group comments-entry-actions-group-votes">
|
||||||
|
@ -42,6 +42,9 @@ const MszCommentsEntry = function(userInfo, postInfo, root) {
|
||||||
updateVotes(postInfo);
|
updateVotes(postInfo);
|
||||||
|
|
||||||
const castVote = async vote => {
|
const castVote = async vote => {
|
||||||
|
if(postInfo.deleted)
|
||||||
|
return;
|
||||||
|
|
||||||
voteActions.classList.add('comments-entry-actions-group-disabled');
|
voteActions.classList.add('comments-entry-actions-group-disabled');
|
||||||
likeAction.disabled = dislikeAction.disabled = true;
|
likeAction.disabled = dislikeAction.disabled = true;
|
||||||
try {
|
try {
|
||||||
|
@ -56,29 +59,27 @@ const MszCommentsEntry = function(userInfo, postInfo, root) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if(postInfo.deleted && userInfo.can_vote) {
|
likeAction.onclick = () => { castVote(likeAction.classList.contains('comments-entry-action-vote-cast') ? 0 : 1); };
|
||||||
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); };
|
||||||
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 listing = new MszCommentsListing({ hidden: !repliesIsArray });
|
const replies = new MszCommentsListing({ hidden: !repliesIsArray });
|
||||||
if(repliesIsArray)
|
if(repliesIsArray)
|
||||||
listing.addPosts(userInfo, postInfo.replies);
|
replies.addPosts(userInfo, postInfo.replies);
|
||||||
|
|
||||||
let form = null;
|
let form = null;
|
||||||
const repliesElem = <div class="comments-entry-replies">
|
const repliesElem = <div class="comments-entry-replies">
|
||||||
{listing}
|
{replies}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
const replyCount = repliesIsArray ? postInfo.replies.length : (postInfo.replies ?? 0);
|
let replyCount = repliesIsArray ? postInfo.replies.length : (postInfo.replies ?? 0);
|
||||||
const replyActionsGroup = <div class="comments-entry-actions-group comments-entry-actions-group-replies" />;
|
const replyActionsGroup = <div class="comments-entry-actions-group comments-entry-actions-group-replies" />;
|
||||||
actions.appendChild(replyActionsGroup);
|
actions.appendChild(replyActionsGroup);
|
||||||
|
|
||||||
const replyToggleOpenElem = <span class="hidden"><i class="fas fa-minus" /></span>;
|
const replyToggleOpenElem = <span class="hidden"><i class="fas fa-minus" /></span>;
|
||||||
const replyToggleClosedElem = <span class="hidden"><i class="fas fa-plus" /></span>;
|
const replyToggleClosedElem = <span class="hidden"><i class="fas fa-plus" /></span>;
|
||||||
const replyCountElem = <span />;
|
const replyCountElem = <span />;
|
||||||
const replyToggleElem = <button class="comments-entry-action">
|
const replyToggleElem = <button class="comments-entry-action" title="Replies">
|
||||||
{replyToggleOpenElem}
|
{replyToggleOpenElem}
|
||||||
{replyToggleClosedElem}
|
{replyToggleClosedElem}
|
||||||
{replyCountElem}
|
{replyCountElem}
|
||||||
|
@ -89,10 +90,10 @@ const MszCommentsEntry = function(userInfo, postInfo, root) {
|
||||||
replyToggleOpenElem.classList.toggle('hidden', !visible);
|
replyToggleOpenElem.classList.toggle('hidden', !visible);
|
||||||
replyToggleClosedElem.classList.toggle('hidden', visible);
|
replyToggleClosedElem.classList.toggle('hidden', visible);
|
||||||
};
|
};
|
||||||
setReplyToggleState(listing.visible);
|
setReplyToggleState(replies.visible);
|
||||||
|
|
||||||
const setReplyCount = count => {
|
const setReplyCount = count => {
|
||||||
count ??= 0;
|
replyCount = count ??= 0;
|
||||||
if(count > 0) {
|
if(count > 0) {
|
||||||
replyCountElem.textContent = count.toLocaleString();
|
replyCountElem.textContent = count.toLocaleString();
|
||||||
replyToggleElem.classList.remove('hidden');
|
replyToggleElem.classList.remove('hidden');
|
||||||
|
@ -104,13 +105,13 @@ const MszCommentsEntry = function(userInfo, postInfo, root) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let replyLoaded = listing.loaded;
|
let replyLoaded = replies.loaded;
|
||||||
replyToggleElem.onclick = async () => {
|
replyToggleElem.onclick = async () => {
|
||||||
setReplyToggleState(listing.visible = !listing.visible);
|
setReplyToggleState(replies.visible = !replies.visible);
|
||||||
if(!replyLoaded) {
|
if(!replyLoaded) {
|
||||||
replyLoaded = true;
|
replyLoaded = true;
|
||||||
try {
|
try {
|
||||||
listing.addPosts(userInfo, await MszCommentsApi.getPostReplies(postInfo.id));
|
replies.addPosts(userInfo, await MszCommentsApi.getPostReplies(postInfo.id));
|
||||||
} catch(ex) {
|
} catch(ex) {
|
||||||
console.error(ex);
|
console.error(ex);
|
||||||
setReplyToggleState(false);
|
setReplyToggleState(false);
|
||||||
|
@ -124,7 +125,7 @@ const MszCommentsEntry = function(userInfo, postInfo, root) {
|
||||||
};
|
};
|
||||||
|
|
||||||
if(userInfo.can_create) {
|
if(userInfo.can_create) {
|
||||||
const replyElem = <button class="comments-entry-action">
|
const replyElem = <button class="comments-entry-action" title="Reply">
|
||||||
<span><i class="fas fa-reply" /></span>
|
<span><i class="fas fa-reply" /></span>
|
||||||
<span>Reply</span>
|
<span>Reply</span>
|
||||||
</button>;
|
</button>;
|
||||||
|
@ -134,7 +135,7 @@ const MszCommentsEntry = function(userInfo, postInfo, root) {
|
||||||
if(form === null) {
|
if(form === null) {
|
||||||
replyElem.classList.add('comments-entry-action-reply-active');
|
replyElem.classList.add('comments-entry-action-reply-active');
|
||||||
form = new MszCommentsForm(userInfo);
|
form = new MszCommentsForm(userInfo);
|
||||||
$insertBefore(listing.element, form.element);
|
$insertBefore(replies.element, form.element);
|
||||||
} else {
|
} else {
|
||||||
replyElem.classList.remove('comments-entry-action-reply-active');
|
replyElem.classList.remove('comments-entry-action-reply-active');
|
||||||
repliesElem.removeChild(form.element);
|
repliesElem.removeChild(form.element);
|
||||||
|
@ -146,63 +147,95 @@ const MszCommentsEntry = function(userInfo, postInfo, root) {
|
||||||
// this has to be called no earlier cus if there's less than 2 elements in the group it gets hidden on 0
|
// 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);
|
setReplyCount(replyCount);
|
||||||
|
|
||||||
if(postInfo.can_delete || userInfo.can_pin) {
|
const deleteButton = postInfo.can_delete
|
||||||
const misc = <div class="comments-entry-actions-group" />;
|
? <button class="comments-entry-action" title="Delete"><i class="fas fa-trash" /></button>
|
||||||
if(postInfo.can_delete) {
|
: null;
|
||||||
if(postInfo.deleted) {
|
const restoreButton = postInfo.can_delete_any
|
||||||
misc.appendChild(<button class="comments-entry-action">
|
? <button class="comments-entry-action" title="Restore"><i class="fas fa-trash-restore" /></button>
|
||||||
<i class="fas fa-trash-restore" />
|
: null;
|
||||||
</button>);
|
const nukeButton = postInfo.can_delete_any
|
||||||
} else {
|
? <button class="comments-entry-action" title="Permanently delete"><i class="fas fa-radiation-alt" /></button>
|
||||||
misc.appendChild(<button class="comments-entry-action">
|
: null;
|
||||||
<i class="fas fa-trash" />
|
const pinButton = root && userInfo.can_pin
|
||||||
</button>);
|
? <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);
|
||||||
}
|
}
|
||||||
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;
|
miscActions.classList.toggle('hidden', miscActions.querySelectorAll('.hidden').length === miscActions.childElementCount);
|
||||||
const edited = typeof postInfo.edited === 'string' ? new Date(postInfo.edited) : null;
|
};
|
||||||
const deleted = typeof postInfo.deleted === 'string' ? new Date(postInfo.deleted) : null;
|
const setMiscDisabled = state => {
|
||||||
const pinned = typeof postInfo.pinned === 'string' ? new Date(postInfo.pinned) : null;
|
miscActions.classList.toggle('comments-entry-actions-group-disabled', state);
|
||||||
|
for(const elem of miscActions.querySelectorAll('button'))
|
||||||
|
elem.disabled = state;
|
||||||
|
};
|
||||||
|
|
||||||
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 }}>
|
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-main">
|
||||||
<div class="comments-entry-avatar">
|
<div class="comments-entry-avatar">
|
||||||
<img src={postInfo.user?.avatar ?? '/images/no-avatar.png'} alt="" width="40" height="40" class="avatar" />
|
{userAvatarElem}
|
||||||
</div>
|
</div>
|
||||||
<div class="comments-entry-wrap">
|
<div class="comments-entry-wrap">
|
||||||
<div class="comments-entry-meta">
|
<div class="comments-entry-meta">
|
||||||
<div class="comments-entry-user">
|
<div class="comments-entry-user">
|
||||||
{postInfo.user
|
{userNameElem}
|
||||||
? <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>
|
||||||
<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">
|
{createdTimeElem}
|
||||||
{created !== null
|
|
||||||
? <time class="comments-entry-time-text" datetime={created.toISOString()} title={created.toString()}>{MszSakuya.formatTimeAgo(created)}</time>
|
|
||||||
: 'deleted'}
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
{edited !== null ? <div class="comments-entry-time comments-entry-time-edited">
|
{editedElem}
|
||||||
<div class="comments-entry-time-icon"><i class="fas fa-pencil-alt" /></div>
|
{pinnedElem}
|
||||||
<time class="comments-entry-time-text" datetime={edited.toISOString()} title={edited.toString()}>{MszSakuya.formatTimeAgo(edited)}</time>
|
{deletedElem}
|
||||||
</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>
|
||||||
<div class="comments-entry-body">{postInfo?.body ?? '[deleted]'}</div>
|
<div class="comments-entry-body">{postInfo?.body ?? '[deleted]'}</div>
|
||||||
{actions.childElementCount > 0 ? actions : null}
|
{actions.childElementCount > 0 ? actions : null}
|
||||||
|
@ -211,7 +244,168 @@ const MszCommentsEntry = function(userInfo, postInfo, root) {
|
||||||
{repliesElem}
|
{repliesElem}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
MszSakuya.trackElements(element.querySelectorAll('time'));
|
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 {
|
return {
|
||||||
get element() {
|
get element() {
|
||||||
|
@ -229,32 +423,40 @@ const MszCommentsListing = function(options) {
|
||||||
{loading}
|
{loading}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
const addPost = function(userInfo, postInfo, parentId=null) {
|
const listing = {
|
||||||
const entry = new MszCommentsEntry(userInfo ?? {}, postInfo, root);
|
|
||||||
entries.set(postInfo.id, entry);
|
|
||||||
element.appendChild(entry.element);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
get element() { return element; },
|
get element() { return element; },
|
||||||
|
get count() { return element.childElementCount; },
|
||||||
|
|
||||||
get visible() { return !element.classList.contains('hidden'); },
|
get visible() { return !element.classList.contains('hidden'); },
|
||||||
set visible(state) { element.classList.toggle('hidden', !state); },
|
set visible(state) { element.classList.toggle('hidden', !state); },
|
||||||
|
|
||||||
get loaded() { return loading === null; },
|
get loaded() { return loading === null; },
|
||||||
|
|
||||||
addPost: addPost,
|
reorder: () => {
|
||||||
addPosts: function(userInfo, posts) {
|
// 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 {
|
try {
|
||||||
if(!Array.isArray(posts))
|
if(!Array.isArray(posts))
|
||||||
throw 'posts must be an array';
|
throw 'posts must be an array';
|
||||||
userInfo ??= {};
|
userInfo ??= {};
|
||||||
for(const postInfo of posts)
|
for(const postInfo of posts)
|
||||||
addPost(userInfo, postInfo);
|
listing.addPost(userInfo, postInfo);
|
||||||
} finally {
|
} finally {
|
||||||
loading.element.remove();
|
loading.element.remove();
|
||||||
loading = null;
|
loading = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return listing;
|
||||||
};
|
};
|
||||||
|
|
|
@ -235,46 +235,38 @@ class CommentsPostsData {
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function editPost(CommentsPostInfo|string $infoOrId, string $body): void {
|
public function updatePost(
|
||||||
|
CommentsPostInfo|string $infoOrId,
|
||||||
|
?string $body,
|
||||||
|
?bool $pinned,
|
||||||
|
bool $edited = false
|
||||||
|
): void {
|
||||||
if($infoOrId instanceof CommentsPostInfo)
|
if($infoOrId instanceof CommentsPostInfo)
|
||||||
$infoOrId = $infoOrId->id;
|
$infoOrId = $infoOrId->id;
|
||||||
|
|
||||||
if(empty(trim($body)))
|
$fields = [];
|
||||||
throw new InvalidArgumentException('$body may not be empty.');
|
$values = [];
|
||||||
|
|
||||||
$stmt = $this->cache->get(<<<SQL
|
if($body !== null) {
|
||||||
UPDATE msz_comments_posts
|
if(trim($body) === '')
|
||||||
SET comment_text = ?,
|
throw new InvalidArgumentException('$body must be null or a non-empty string.');
|
||||||
comment_edited = NOW()
|
|
||||||
WHERE comment_id = ?
|
|
||||||
SQL);
|
|
||||||
$stmt->nextParameter($body);
|
|
||||||
$stmt->nextParameter($infoOrId);
|
|
||||||
$stmt->execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function pinPost(CommentsPostInfo|string $infoOrId): void {
|
$fields[] = 'comment_text = ?';
|
||||||
if($infoOrId instanceof CommentsPostInfo)
|
$values[] = $body;
|
||||||
$infoOrId = $infoOrId->id;
|
}
|
||||||
|
|
||||||
$stmt = $this->cache->get(<<<SQL
|
if($pinned !== null)
|
||||||
UPDATE msz_comments_posts
|
$fields[] = $pinned ? 'comment_pinned = COALESCE(comment_pinned, NOW())' : 'comment_pinned = NULL';
|
||||||
SET comment_pinned = COALESCE(comment_pinned, NOW())
|
|
||||||
WHERE comment_id = ?
|
|
||||||
SQL);
|
|
||||||
$stmt->nextParameter($infoOrId);
|
|
||||||
$stmt->execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function unpinPost(CommentsPostInfo|string $infoOrId): void {
|
if($edited)
|
||||||
if($infoOrId instanceof CommentsPostInfo)
|
$fields[] = 'comment_edited = NOW()';
|
||||||
$infoOrId = $infoOrId->id;
|
|
||||||
|
|
||||||
$stmt = $this->cache->get(<<<SQL
|
if(empty($fields))
|
||||||
UPDATE msz_comments_posts
|
return;
|
||||||
SET comment_pinned = NULL
|
|
||||||
WHERE comment_id = ?
|
$stmt = $this->cache->get(sprintf('UPDATE msz_comments_posts SET %s WHERE comment_id = ?', implode(', ', $fields)));
|
||||||
SQL);
|
foreach($values as $value)
|
||||||
|
$stmt->nextParameter($value);
|
||||||
$stmt->nextParameter($infoOrId);
|
$stmt->nextParameter($infoOrId);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,28 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function convertPosts(
|
||||||
|
IPermissionResult $perms,
|
||||||
|
iterable $postInfos,
|
||||||
|
bool $loadReplies = false
|
||||||
|
): array {
|
||||||
|
$posts = [];
|
||||||
|
|
||||||
|
foreach($postInfos as $postInfo) {
|
||||||
|
$post = $this->convertPost(
|
||||||
|
$perms,
|
||||||
|
$postInfo,
|
||||||
|
$loadReplies ? $this->commentsCtx->posts->getPosts(parentInfo: $postInfo) : null
|
||||||
|
);
|
||||||
|
if(isset($post['deleted']) && $post['deleted'] === true && empty($post['replies']))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$posts[] = $post;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $posts;
|
||||||
|
}
|
||||||
|
|
||||||
private function convertPost(
|
private function convertPost(
|
||||||
IPermissionResult $perms,
|
IPermissionResult $perms,
|
||||||
CommentsPostInfo $postInfo,
|
CommentsPostInfo $postInfo,
|
||||||
|
@ -50,10 +72,12 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
||||||
$canViewDeleted = $perms->check(Perm::G_COMMENTS_DELETE_ANY);
|
$canViewDeleted = $perms->check(Perm::G_COMMENTS_DELETE_ANY);
|
||||||
$isDeleted = $postInfo->deleted && !$canViewDeleted;
|
$isDeleted = $postInfo->deleted && !$canViewDeleted;
|
||||||
|
|
||||||
$post = ['id' => $postInfo->id];
|
$post = [
|
||||||
|
'id' => $postInfo->id,
|
||||||
|
'created' => $postInfo->createdAt->toIso8601ZuluString(),
|
||||||
|
];
|
||||||
if(!$isDeleted) {
|
if(!$isDeleted) {
|
||||||
$post['body'] = $postInfo->body;
|
$post['body'] = $postInfo->body;
|
||||||
$post['created'] = $postInfo->createdAt->toIso8601ZuluString();
|
|
||||||
if($postInfo->pinned)
|
if($postInfo->pinned)
|
||||||
$post['pinned'] = $postInfo->pinnedAt->toIso8601ZuluString();
|
$post['pinned'] = $postInfo->pinnedAt->toIso8601ZuluString();
|
||||||
if($postInfo->edited)
|
if($postInfo->edited)
|
||||||
|
@ -89,7 +113,7 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
||||||
if($perms->check(Perm::G_COMMENTS_EDIT_ANY))
|
if($perms->check(Perm::G_COMMENTS_EDIT_ANY))
|
||||||
$post['can_edit'] = true;
|
$post['can_edit'] = true;
|
||||||
if($perms->check(Perm::G_COMMENTS_DELETE_ANY))
|
if($perms->check(Perm::G_COMMENTS_DELETE_ANY))
|
||||||
$post['can_delete'] = true;
|
$post['can_delete'] = $post['can_delete_any'] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if($replyInfos === null) {
|
if($replyInfos === null) {
|
||||||
|
@ -97,9 +121,7 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
||||||
if($replies > 0)
|
if($replies > 0)
|
||||||
$post['replies'] = $replies;
|
$post['replies'] = $replies;
|
||||||
} else {
|
} else {
|
||||||
$replies = [];
|
$replies = $this->convertPosts($perms, $replyInfos);
|
||||||
foreach($replyInfos as $replyInfo)
|
|
||||||
$replies[] = $this->convertPost($perms, $replyInfo);
|
|
||||||
if(!empty($replies))
|
if(!empty($replies))
|
||||||
$post['replies'] = $replies;
|
$post['replies'] = $replies;
|
||||||
}
|
}
|
||||||
|
@ -111,12 +133,8 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
||||||
#[HttpMiddleware('/comments')]
|
#[HttpMiddleware('/comments')]
|
||||||
public function checkCsrf(HttpResponseBuilder $response, HttpRequest $request) {
|
public function checkCsrf(HttpResponseBuilder $response, HttpRequest $request) {
|
||||||
if(in_array($request->method, ['DELETE', 'PATCH', 'POST'])) {
|
if(in_array($request->method, ['DELETE', 'PATCH', 'POST'])) {
|
||||||
if($request->method !== 'DELETE' && !($request->content instanceof FormHttpContent))
|
|
||||||
return 400;
|
|
||||||
|
|
||||||
if(!$this->authInfo->loggedIn)
|
if(!$this->authInfo->loggedIn)
|
||||||
return 401;
|
return 401;
|
||||||
|
|
||||||
if(!CSRF::validate($request->getHeaderLine('x-csrf-token')))
|
if(!CSRF::validate($request->getHeaderLine('x-csrf-token')))
|
||||||
return 403;
|
return 403;
|
||||||
}
|
}
|
||||||
|
@ -166,17 +184,14 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
||||||
$result['user'] = $user;
|
$result['user'] = $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
$posts = [];
|
|
||||||
try {
|
try {
|
||||||
$postInfos = $this->commentsCtx->posts->getPosts(
|
$posts = $this->convertPosts($perms, $this->commentsCtx->posts->getPosts(
|
||||||
categoryInfo: $categoryInfo,
|
categoryInfo: $categoryInfo,
|
||||||
replies: false,
|
replies: false,
|
||||||
);
|
), true);
|
||||||
foreach($postInfos as $postInfo) {
|
} catch(RuntimeException $ex) {
|
||||||
$replyInfos = $this->commentsCtx->posts->getPosts(parentInfo: $postInfo);
|
$posts = [];
|
||||||
$posts[] = $this->convertPost($perms, $postInfo, $replyInfos);
|
}
|
||||||
}
|
|
||||||
} catch(RuntimeException $ex) {}
|
|
||||||
|
|
||||||
$result['posts'] = $posts;
|
$result['posts'] = $posts;
|
||||||
|
|
||||||
|
@ -208,9 +223,11 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
$perms = $this->getGlobalPerms();
|
$perms = $this->getGlobalPerms();
|
||||||
$replyInfos = $this->commentsCtx->posts->getPosts(parentInfo: $postInfo);
|
$post = $this->convertPost($perms, $postInfo, $this->commentsCtx->posts->getPosts(parentInfo: $postInfo));
|
||||||
|
if(isset($post['deleted']) && $post['deleted'] === true && empty($post['replies']))
|
||||||
|
return 404;
|
||||||
|
|
||||||
return $this->convertPost($perms, $postInfo, $replyInfos);
|
return $post;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[HttpGet('/comments/posts/([0-9]+)/replies')]
|
#[HttpGet('/comments/posts/([0-9]+)/replies')]
|
||||||
|
@ -221,26 +238,73 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
||||||
return 404;
|
return 404;
|
||||||
}
|
}
|
||||||
|
|
||||||
$perms = $this->getGlobalPerms();
|
return $this->convertPosts(
|
||||||
$replyInfos = $this->commentsCtx->posts->getPosts(parentInfo: $postInfo);
|
$this->getGlobalPerms(),
|
||||||
|
$this->commentsCtx->posts->getPosts(parentInfo: $postInfo)
|
||||||
$replies = [];
|
);
|
||||||
foreach($replyInfos as $replyInfo)
|
|
||||||
$replies[] = $this->convertPost($perms, $replyInfo);
|
|
||||||
|
|
||||||
return $replies;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[HttpPatch('/comments/posts/([0-9]+)')]
|
// this should be HttpPatch but PHP doesn't parse into $_POST for PATCH...
|
||||||
|
// fix this in the v3 router for index by just ignoring PHP's parsing altogether
|
||||||
|
#[HttpPost('/comments/posts/([0-9]+)')]
|
||||||
public function patchPost(HttpResponseBuilder $response, HttpRequest $request, string $commentId): int|array {
|
public function patchPost(HttpResponseBuilder $response, HttpRequest $request, string $commentId): int|array {
|
||||||
$perms = $this->getGlobalPerms();
|
if(!($request->content instanceof FormHttpContent))
|
||||||
$canEditAny = $perms->check(Perm::G_COMMENTS_EDIT_ANY);
|
return 400;
|
||||||
$canEditOwn = $perms->check(Perm::G_COMMENTS_EDIT_OWN);
|
|
||||||
$canPin = $perms->check(Perm::G_COMMENTS_PIN);
|
|
||||||
if(!$canEditAny && !$canEditOwn && !$canPin)
|
|
||||||
return 403;
|
|
||||||
|
|
||||||
return 501;
|
try {
|
||||||
|
$postInfo = $this->commentsCtx->posts->getPost($commentId);
|
||||||
|
} catch(RuntimeException $ex) {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
$perms = $this->getGlobalPerms();
|
||||||
|
|
||||||
|
if(!$perms->check(Perm::G_COMMENTS_DELETE_ANY) && $postInfo->deleted)
|
||||||
|
return 404;
|
||||||
|
|
||||||
|
$body = null;
|
||||||
|
$pinned = null;
|
||||||
|
$edited = false;
|
||||||
|
|
||||||
|
if($request->content->hasParam('pin')) {
|
||||||
|
if(!$perms->check(Perm::G_COMMENTS_PIN))
|
||||||
|
return 403;
|
||||||
|
|
||||||
|
$pinned = !empty($request->content->getParam('pin'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->content->hasParam('body')) {
|
||||||
|
if(!$perms->check(Perm::G_COMMENTS_EDIT_ANY) && !($perms->check(Perm::G_COMMENTS_EDIT_OWN) && $this->authInfo->userId === $postInfo->userId))
|
||||||
|
return 403;
|
||||||
|
|
||||||
|
$body = (string)$request->content->getParam('body');
|
||||||
|
$edited = $body !== $postInfo->body;
|
||||||
|
if(!$edited)
|
||||||
|
$body = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->commentsCtx->posts->updatePost(
|
||||||
|
$postInfo,
|
||||||
|
body: $body,
|
||||||
|
pinned: $pinned,
|
||||||
|
edited: $edited,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$postInfo = $this->commentsCtx->posts->getPost($postInfo->id);
|
||||||
|
} catch(RuntimeException $ex) {
|
||||||
|
return 410;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = ['id' => $postInfo->id];
|
||||||
|
if($body !== null)
|
||||||
|
$result['body'] = $postInfo->body;
|
||||||
|
if($pinned !== null)
|
||||||
|
$result['pinned'] = $postInfo->pinned ? $postInfo->pinnedAt->toIso8601ZuluString() : false;
|
||||||
|
if($edited)
|
||||||
|
$result['edited'] = $postInfo->editedAt->toIso8601ZuluString();
|
||||||
|
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[HttpDelete('/comments/posts/([0-9]+)')]
|
#[HttpDelete('/comments/posts/([0-9]+)')]
|
||||||
|
@ -251,12 +315,17 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
||||||
return 404;
|
return 404;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if($postInfo->deleted)
|
||||||
|
return 404;
|
||||||
|
|
||||||
$perms = $this->getGlobalPerms();
|
$perms = $this->getGlobalPerms();
|
||||||
$canDeleteAny = $perms->check(Perm::G_COMMENTS_DELETE_ANY);
|
if(!$perms->check(Perm::G_COMMENTS_DELETE_ANY)
|
||||||
if(!$canDeleteAny && !$perms->check(Perm::G_COMMENTS_DELETE_OWN))
|
&& !($postInfo->userId === $this->authInfo->userId && $perms->check(Perm::G_COMMENTS_DELETE_OWN)))
|
||||||
return 403;
|
return 403;
|
||||||
|
|
||||||
return 501;
|
$this->commentsCtx->posts->deletePost($postInfo);
|
||||||
|
|
||||||
|
return 204;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[HttpPost('/comments/posts/([0-9]+)/restore')]
|
#[HttpPost('/comments/posts/([0-9]+)/restore')]
|
||||||
|
@ -278,8 +347,30 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
||||||
return 200;
|
return 200;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[HttpPost('/comments/posts/([0-9]+)/nuke')]
|
||||||
|
public function postPostNuke(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->nukePost($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|array {
|
public function postPostVote(HttpResponseBuilder $response, HttpRequest $request, string $commentId): int|array {
|
||||||
|
if(!($request->content instanceof FormHttpContent))
|
||||||
|
return 400;
|
||||||
|
|
||||||
$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;
|
||||||
|
@ -302,7 +393,6 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
||||||
$voteInfo = $this->commentsCtx->votes->getVote($postInfo, $this->authInfo->userInfo);
|
$voteInfo = $this->commentsCtx->votes->getVote($postInfo, $this->authInfo->userInfo);
|
||||||
$votes = $this->commentsCtx->votes->getVotesAggregate($postInfo);
|
$votes = $this->commentsCtx->votes->getVotesAggregate($postInfo);
|
||||||
|
|
||||||
$response->statusCode = 200;
|
|
||||||
return [
|
return [
|
||||||
'vote' => $voteInfo->weight,
|
'vote' => $voteInfo->weight,
|
||||||
'positive' => $votes->positive,
|
'positive' => $votes->positive,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue