You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
265 lines
8.6 KiB
265 lines
8.6 KiB
// 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) {
|
|
this.app = app;
|
|
this.log = new DtpLog({ logId: 'site-comments', index: 'siteComments', className: 'SiteComments' });
|
|
this.createEmojiPickers();
|
|
}
|
|
|
|
createEmojiPickers ( ) {
|
|
const pickerContainers = document.querySelectorAll('li.comment-emoji-picker:not([data-initialized])');
|
|
for (const container of pickerContainers) {
|
|
const picker = { };
|
|
|
|
picker.drop = container.querySelector('.comment-emoji-picker-drop');
|
|
picker.ui = picker.drop.querySelector('.comment-emoji-picker-ui');
|
|
|
|
const formId = picker.drop.getAttribute('data-form-id');
|
|
picker.form = document.querySelector(`form#${formId}`);
|
|
picker.input = picker.form.querySelector(`textarea[data-form-id=${formId}]`);
|
|
picker.characterCount = picker.form.querySelector('span.comment-character-count');
|
|
|
|
picker.picmo = picmo.createPicker({
|
|
emojisPerRow: 7,
|
|
rootElement: picker.ui,
|
|
theme: picmo.darkTheme,
|
|
});
|
|
|
|
picker.picmo.addEventListener('emoji:select', this.onEmojiSelected.bind(this, picker));
|
|
|
|
picker.emojiPickerDrop = UIkit.drop(picker.drop);
|
|
UIkit.util.on(picker.drop, 'show', ( ) => {
|
|
this.log.info('SiteComments', 'showing emoji picker');
|
|
picker.picmo.reset();
|
|
});
|
|
|
|
container.setAttribute('data-initialized', true);
|
|
}
|
|
}
|
|
|
|
async onCommentInput (event) {
|
|
const target = event.currentTarget || event.target;
|
|
|
|
const formId = target.getAttribute('data-form-id');
|
|
if (!formId) { return; }
|
|
|
|
const form = document.getElementById(formId);
|
|
if (!form) { return; }
|
|
|
|
const label = form.querySelector('span.comment-character-count');
|
|
if (!label) { return; }
|
|
|
|
label.textContent = numeral(event.target.value.charCount()).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);
|
|
this.createEmojiPickers();
|
|
} 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);
|
|
this.createEmojiPickers();
|
|
} catch (error) {
|
|
UIkit.modal.alert(`Failed to load more comments: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
async onEmojiSelected (picker, event) {
|
|
picker.emojiPickerDrop.hide(false);
|
|
await this.insertContentAtCursor(picker, event.emoji);
|
|
picker.characterCount.textContent = numeral(picker.input.value.charCount()).format('0,0');
|
|
}
|
|
|
|
async insertContentAtCursor (picker, content) {
|
|
picker.input.focus();
|
|
|
|
if (document.selection) {
|
|
let sel = document.selection.createRange();
|
|
sel.text = content;
|
|
} else if (picker.input.selectionStart || (picker.input.selectionStart === 0)) {
|
|
let startPos = picker.input.selectionStart;
|
|
let endPos = picker.input.selectionEnd;
|
|
|
|
let oldLength = picker.input.value.length;
|
|
picker.input.value =
|
|
picker.input.value.substring(0, startPos) +
|
|
content +
|
|
picker.input.value.substring(endPos, picker.input.value.length);
|
|
|
|
picker.input.selectionStart = startPos + (picker.input.value.length - oldLength);
|
|
picker.input.selectionEnd = picker.input.selectionStart;
|
|
} else {
|
|
picker.input.value += content;
|
|
}
|
|
}
|
|
}
|