From b60268a9a6d70ceab584f683a78d7b3d261084d2 Mon Sep 17 00:00:00 2001 From: rob Date: Tue, 28 Dec 2021 16:53:26 -0500 Subject: [PATCH] comment replies --- app/controllers/comment.js | 27 ++++++++++++ app/controllers/post.js | 11 ++++- app/services/comment.js | 21 ++++++++++ app/services/job-queue.js | 2 +- app/services/post.js | 2 +- .../components/comment-list-standalone.pug | 4 ++ .../comment/components/comment-standalone.pug | 1 + app/views/comment/components/comment.pug | 9 ++-- app/views/comment/components/composer.pug | 13 ++++-- app/views/post/view.pug | 12 +++++- client/js/site-app.js | 41 +++++++++++++++++++ 11 files changed, 132 insertions(+), 11 deletions(-) create mode 100644 app/views/comment/components/comment-list-standalone.pug diff --git a/app/controllers/comment.js b/app/controllers/comment.js index 70c6523..9890191 100644 --- a/app/controllers/comment.js +++ b/app/controllers/comment.js @@ -35,6 +35,8 @@ class CommentController extends SiteController { router.post('/:commentId/vote', authRequired, this.postVote.bind(this)); + router.get('/:commentId/replies', this.getCommentReplies.bind(this)); + router.delete('/:commentId', authRequired, limiterService.create(limiterService.config.comment.deleteComment), @@ -80,6 +82,31 @@ class CommentController extends SiteController { } } + async getCommentReplies (req, res) { + const { comment: commentService } = this.dtp.services; + try { + const displayList = this.createDisplayList('get-replies'); + + Object.assign(res.locals, req.app.locals); + + res.locals.pagination = this.getPaginationParameters(req, 20); + res.locals.comments = await commentService.getReplies(res.locals.comment, res.locals.pagination); + + const html = await commentService.renderTemplate('commentList', res.locals); + + const replyList = `ul.dtp-reply-list[data-comment-id="${res.locals.comment._id}"]`; + displayList.addElement(replyList, 'beforeEnd', html); + + const replyListContainer = `.dtp-reply-list-container[data-comment-id="${res.locals.comment._id}"]`; + displayList.removeAttribute(replyListContainer, 'hidden'); + + res.status(200).json({ success: true, displayList }); + } catch (error) { + res.status(error.statusCode || 500).json({ success: false, message: error.message }); + } + + } + async deleteComment (req, res) { const { comment: commentService } = this.dtp.services; try { diff --git a/app/controllers/post.js b/app/controllers/post.js index 28b5e3e..e6d0a3e 100644 --- a/app/controllers/post.js +++ b/app/controllers/post.js @@ -102,8 +102,15 @@ class PostController extends SiteController { let viewModel = Object.assign({ }, req.app.locals); viewModel = Object.assign(viewModel, res.locals); - const html = await commentService.templates.comment(viewModel); - displayList.addElement('ul#post-comment-list', 'afterBegin', html); + const html = await commentService.renderTemplate('comment', viewModel); + + if (req.body.replyTo) { + const replyListSelector = `.dtp-reply-list-container[data-comment-id="${req.body.replyTo}"]`; + displayList.addElement(replyListSelector, 'afterBegin', html); + displayList.removeAttribute(replyListSelector, 'hidden'); + } else { + displayList.addElement('ul#post-comment-list', 'afterBegin', html); + } displayList.showNotification( 'Comment created', diff --git a/app/services/comment.js b/app/services/comment.js index f07468d..8875968 100644 --- a/app/services/comment.js +++ b/app/services/comment.js @@ -51,6 +51,11 @@ class CommentService extends SiteService { async start ( ) { this.templates = { }; this.templates.comment = pug.compileFile(path.join(this.dtp.config.root, 'app', 'views', 'comment', 'components', 'comment-standalone.pug')); + this.templates.commentList = pug.compileFile(path.join(this.dtp.config.root, 'app', 'views', 'comment', 'components', 'comment-list-standalone.pug')); + } + + async renderTemplate (templateName, viewModel) { + return this.templates[templateName](viewModel); } async populateCommentId (req, res, next, commentId) { @@ -200,6 +205,22 @@ class CommentService extends SiteService { return comments; } + async getReplies (comment, pagination) { + const replies = await Comment + .find({ + replyTo: comment._id, + status: { + $in: ['published', 'mod-warn', 'mod-removed'] + } + }) + .sort({ created: 1 }) + .skip(pagination.skip) + .limit(pagination.cpp) + .populate(this.populateComment) + .lean(); + return replies; + } + async getContentHistory (comment, pagination) { /* * Extract a page from the contentHistory using $slice on the array diff --git a/app/services/job-queue.js b/app/services/job-queue.js index 3f93f6d..7e7b323 100644 --- a/app/services/job-queue.js +++ b/app/services/job-queue.js @@ -40,7 +40,7 @@ class JobQueueService extends SiteService { }, defaultJobOptions); queue = new BullQueue(name, { - prefix: process.env.DTP_BULL_PREFIX || 'dtp', + prefix: process.env.REDIS_KEY_PREFIX || 'dtp', redis: { host: process.env.REDIS_HOST, port: parseInt(process.env.REDIS_PORT || '6379', 10), diff --git a/app/services/post.js b/app/services/post.js index 69bad91..aad54eb 100644 --- a/app/services/post.js +++ b/app/services/post.js @@ -22,7 +22,7 @@ class PostService extends SiteService { this.populatePost = [ { path: 'author', - select: '_id username username_lc displayName picture', + select: '_id username username_lc displayName bio picture', }, ]; } diff --git a/app/views/comment/components/comment-list-standalone.pug b/app/views/comment/components/comment-list-standalone.pug new file mode 100644 index 0000000..e351613 --- /dev/null +++ b/app/views/comment/components/comment-list-standalone.pug @@ -0,0 +1,4 @@ +include ../../components/library +include comment-list +include composer ++renderCommentList(comments) \ No newline at end of file diff --git a/app/views/comment/components/comment-standalone.pug b/app/views/comment/components/comment-standalone.pug index a5a1f34..46bcb54 100644 --- a/app/views/comment/components/comment-standalone.pug +++ b/app/views/comment/components/comment-standalone.pug @@ -1,3 +1,4 @@ include ../../components/library include comment +include composer +renderComment(comment) \ No newline at end of file diff --git a/app/views/comment/components/comment.pug b/app/views/comment/components/comment.pug index 0ba7a4f..8fe6fac 100644 --- a/app/views/comment/components/comment.pug +++ b/app/views/comment/components/comment.pug @@ -108,7 +108,10 @@ mixin renderComment (comment) +renderLabeledIcon('fa-reply', 'reply') //- Comment replies and reply composer - div(data-comment-id= comment._id) - if user && user.flags.canComment + div(data-comment-id= comment._id, hidden).dtp-reply-composer.uk-margin + if user && user.permissions.canComment .uk-margin - +renderCommentComposer(`/post`, { replyTo: comment._id }) \ No newline at end of file + +renderCommentComposer(`/post/${comment.resource._id}/comment`, { showCancel: true, replyTo: comment._id }) + + div(data-comment-id= comment._id, hidden).dtp-reply-list-container.uk-margin + ul(data-comment-id= comment._id).dtp-reply-list.uk-list.uk-margin-medium-left \ No newline at end of file diff --git a/app/views/comment/components/composer.pug b/app/views/comment/components/composer.pug index c4bc6bc..2513657 100644 --- a/app/views/comment/components/composer.pug +++ b/app/views/comment/components/composer.pug @@ -1,6 +1,9 @@ -//- https://owenbenjamin.com/social-channels/ and https://gab.com/owenbenjamin -mixin renderCommentComposer (actionUrl) +mixin renderCommentComposer (actionUrl, options = { }) form(method="POST", action= actionUrl, onsubmit="return dtp.app.submitForm(event, 'create-comment');").uk-form + + if options.replyTo + input(type="hidden", name="replyTo", value= options.replyTo) + .uk-card.uk-card-secondary.uk-card-small .uk-card-body textarea( @@ -16,7 +19,7 @@ mixin renderCommentComposer (actionUrl) .uk-width-auto You are commenting as: #{user.username} .uk-width-auto #[span#comment-character-count 0] of 3,000 .uk-card-footer - div(uk-grid).uk-flex-between + div(uk-grid).uk-flex-between.uk-grid-small .uk-width-expand ul.uk-subnav li @@ -32,5 +35,9 @@ mixin renderCommentComposer (actionUrl) label input(id="is-nsfw", name="isNSFW", type="checkbox").uk-checkbox | NSFW + + if options.showCancel + .uk-width-auto + button(type="submit").uk-button.dtp-button-secondary Cancel .uk-width-auto button(type="submit").uk-button.dtp-button-primary Post \ No newline at end of file diff --git a/app/views/post/view.pug b/app/views/post/view.pug index 781d259..182ecea 100644 --- a/app/views/post/view.pug +++ b/app/views/post/view.pug @@ -31,8 +31,18 @@ block content if post.updated .uk-margin .uk-article-meta This post was updated on #{moment(post.updated).format('MMMM DD, YYYY, [at] hh:mm a')}. + + .uk-margin + h4 Post author + div(uk-grid).uk-grid-small + if post.author.picture.small + .uk-width-auto + img(src=`/image/${post.author.picture.small}`) + .uk-width-expand + .uk-text-bold= post.author.displayName || post.author.username + .uk-text-small= post.author.bio - if user && post.flags.enableComments + if user && post.flags.enableComments && user.permissions.canComment +renderSectionTitle('Add a comment') .uk-margin diff --git a/client/js/site-app.js b/client/js/site-app.js index f502f75..feaae1a 100644 --- a/client/js/site-app.js +++ b/client/js/site-app.js @@ -741,6 +741,47 @@ export default class DtpSiteApp extends DtpApp { UIkit.modal.alert(`Failed to render chart: ${error.message}`); } } + + async openReplies (event) { + event.preventDefault(); + event.stopPropagation(); + + const target = event.currentTarget || event.target; + const commentId = target.getAttribute('data-comment-id'); + + const container = document.querySelector(`.dtp-reply-list-container[data-comment-id="${commentId}"]`); + const replyList = document.querySelector(`ul.dtp-reply-list[data-comment-id="${commentId}"]`); + + const isOpen = !container.hasAttribute('hidden'); + if (isOpen) { + container.setAttribute('hidden', ''); + while (replyList.firstChild) { + replyList.removeChild(replyList.firstChild); + } + return; + } + + try { + const response = await fetch(`/comment/${commentId}/replies`); + this.processResponse(response); + } catch (error) { + UIkit.modal.alert(`Failed to load replies: ${error.message}`); + } + + return true; + } + + async openReplyComposer (event) { + event.preventDefault(); + event.stopPropagation(); + + const target = event.currentTarget || event.target; + const commentId = target.getAttribute('data-comment-id'); + const composer = document.querySelector(`.dtp-reply-composer[data-comment-id="${commentId}"]`); + composer.toggleAttribute('hidden'); + + return true; + } } dtp.DtpSiteApp = DtpSiteApp; \ No newline at end of file