// site-comments.js // Copyright (C) 2022 DTP Technologies, LLC // License: Apache-2.0 'use strict'; import DtpLog from 'dtp/dtp-log.js'; import UIkit from 'uikit'; import * as picmo from 'picmo'; export default class SiteComments { constructor (app, rootElement) { this.app = app; this.log = new DtpLog({ name: 'Site Comments', slug: 'comments' }); this.ui = { input: rootElement.querySelector('#comment-content'), emojiPicker: rootElement.querySelector('#comment-emoji-picker'), characterCount: rootElement.querySelector('.comment-character-count'), }; if (this.ui.emojiPicker) { this.ui.emojiPickerUI = this.ui.emojiPicker.querySelector('.comment-emoji-picker'); this.ui.picmo = picmo.createPicker({ emojisPerRow: 7, rootElement: this.ui.emojiPickerUI, theme: picmo.darkTheme, }); this.ui.picmo.addEventListener('emoji:select', this.onEmojiSelected.bind(this)); this.ui.emojiPickerDrop = UIkit.drop(this.ui.emojiPicker); UIkit.util.on(this.ui.emojiPicker, 'show', ( ) => { this.log.info('SiteComments', 'showing emoji picker'); this.ui.picmo.reset(); }); } else { UIkit.modal.alert('Comment section without an emoji picker defined'); } } async onCommentInput (event) { this.ui.characterCount.textContent = numeral(event.target.value.length).format('0,0'); } async showReportCommentForm (event) { event.preventDefault(); event.stopPropagation(); const resourceType = event.currentTarget.getAttribute('data-resource-type'); const resourceId = event.currentTarget.getAttribute('data-resource-id'); const commentId = event.currentTarget.getAttribute('data-comment-id'); this.closeCommentDropdownMenu(commentId); try { const response = await fetch('/content-report/comment/form', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ resourceType, resourceId, commentId }), }); if (!response.ok) { throw new Error('failed to load report form'); } const html = await response.text(); this.currentDialog = UIkit.modal.dialog(html); } catch (error) { this.log.error('reportComment', 'failed to process comment request', { resourceType, resourceId, commentId, error }); UIkit.modal.alert(`Failed to report comment: ${error.message}`); } return true; } async deleteComment (event) { event.preventDefault(); event.stopPropagation(); const commentId = (event.currentTarget || event.target).getAttribute('data-comment-id'); try { const response = fetch(`/comment/${commentId}`, { method: 'DELETE' }); if (!response.ok) { throw new Error('Server error'); } this.app.processResponse(response); } catch (error) { UIkit.modal.alert(`Failed to delete comment: ${error.message}`); } } async blockCommentAuthor (event) { event.preventDefault(); event.stopPropagation(); const resourceType = event.currentTarget.getAttribute('data-resource-type'); const resourceId = event.currentTarget.getAttribute('data-resource-id'); const commentId = event.currentTarget.getAttribute('data-comment-id'); const actionUrl = this.getCommentActionUrl(resourceType, resourceId, commentId, 'block-author'); this.closeCommentDropdownMenu(commentId); try { this.log.info('blockCommentAuthor', 'blocking comment author', { resourceType, resourceId, commentId }); const response = await fetch(actionUrl, { method: 'POST'}); await this.app.processResponse(response); } catch (error) { this.log.error('reportComment', 'failed to process comment request', { resourceType, resourceId, commentId, error }); UIkit.modal.alert(`Failed to block comment author: ${error.message}`); } return true; } closeCommentDropdownMenu (commentId) { const dropdown = document.querySelector(`[data-comment-id="${commentId}"][uk-dropdown]`); UIkit.dropdown(dropdown).hide(false); } getCommentActionUrl (resourceType, resourceId, commentId, action) { switch (resourceType) { case 'Newsletter': return `/newsletter/${resourceId}/comment/${commentId}/${action}`; case 'Page': return `/page/${resourceId}/comment/${commentId}/${action}`; case 'Post': return `/post/${resourceId}/comment/${commentId}/${action}`; default: break; } throw new Error('Invalid resource type for comment operation'); } async submitCommentVote (event) { const target = (event.currentTarget || event.target); const commentId = target.getAttribute('data-comment-id'); const vote = target.getAttribute('data-vote'); try { const response = await fetch(`/comment/${commentId}/vote`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ vote }), }); await this.app.processResponse(response); } catch (error) { UIkit.modal.alert(`Failed to submit vote: ${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.app.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; } async loadMoreComments (event) { event.preventDefault(); event.stopPropagation(); const target = event.currentTarget || event.target; const buttonId = target.getAttribute('data-button-id'); const rootUrl = target.getAttribute('data-root-url'); const nextPage = target.getAttribute('data-next-page'); try { const response = await fetch(`${rootUrl}?p=${nextPage}&buttonId=${buttonId}`); await this.app.processResponse(response); } catch (error) { UIkit.modal.alert(`Failed to load more comments: ${error.message}`); } } async onEmojiSelected (event) { this.ui.emojiPickerDrop.hide(false); return this.insertContentAtCursor(event.emoji); } async insertContentAtCursor (content) { this.ui.input.focus(); if (document.selection) { let sel = document.selection.createRange(); sel.text = content; } else if (this.ui.input.selectionStart || (this.ui.input.selectionStart === 0)) { let startPos = this.ui.input.selectionStart; let endPos = this.ui.input.selectionEnd; let oldLength = this.ui.input.value.length; this.ui.input.value = this.ui.input.value.substring(0, startPos) + content + this.ui.input.value.substring(endPos, this.ui.input.value.length); this.ui.input.selectionStart = startPos + (this.ui.input.value.length - oldLength); this.ui.input.selectionEnd = this.ui.input.selectionStart; } else { this.ui.input.value += content; } } }