mediacms/frontend/src/static/js/components/comments/Comments.jsx

484 wiersze
17 KiB
React
Czysty Zwykły widok Historia

import React, { useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import { format } from 'timeago.js';
import { usePopup } from '../../utils/hooks/';
import { PageStore, MediaPageStore } from '../../utils/stores/';
import { PageActions, MediaPageActions } from '../../utils/actions/';
import { LinksContext, MemberContext, SiteContext } from '../../utils/contexts/';
import { PopupMain, UserThumbnail } from '../_shared';
import './Comments.scss';
const commentsText = {
single: 'comment',
uppercaseSingle: 'COMMENT',
ucfirstSingle: 'Comment',
ucfirstPlural: 'Comments',
submitCommentText: 'SUBMIT',
disabledCommentsMsg: 'Comments are disabled',
};
function CommentForm(props) {
const textareaRef = useRef(null);
const [value, setValue] = useState('');
const [madeChanges, setMadeChanges] = useState(false);
const [textareaFocused, setTextareaFocused] = useState(false);
const [textareaLineHeight, setTextareaLineHeight] = useState(-1);
const [loginUrl] = useState(
!MemberContext._currentValue.is.anonymous
? null
: LinksContext._currentValue.signin +
'?next=/' +
window.location.href.replace(SiteContext._currentValue.url, '').replace(/^\//g, '')
);
function onFocus() {
setTextareaFocused(true);
}
function onBlur() {
setTextareaFocused(false);
}
function onCommentSubmit() {
textareaRef.current.style.height = '';
const contentHeight = textareaRef.current.scrollHeight;
const contentLineHeight =
0 < textareaLineHeight ? textareaLineHeight : parseFloat(window.getComputedStyle(textareaRef.current).lineHeight);
setValue('');
setMadeChanges(false);
setTextareaLineHeight(contentLineHeight);
textareaRef.current.style.height =
Math.max(20, textareaLineHeight * Math.ceil(contentHeight / contentLineHeight)) + 'px';
}
function onCommentSubmitFail() {
setMadeChanges(false);
}
function onChange(event) {
textareaRef.current.style.height = '';
const contentHeight = textareaRef.current.scrollHeight;
const contentLineHeight =
0 < textareaLineHeight ? textareaLineHeight : parseFloat(window.getComputedStyle(textareaRef.current).lineHeight);
setValue(textareaRef.current.value);
setMadeChanges(true);
setTextareaLineHeight(contentLineHeight);
textareaRef.current.style.height =
Math.max(20, textareaLineHeight * Math.ceil(contentHeight / contentLineHeight)) + 'px';
}
function submitComment() {
if (!madeChanges) {
return;
}
const val = textareaRef.current.value.trim();
if ('' !== val) {
MediaPageActions.submitComment(val);
}
}
useEffect(() => {
MediaPageStore.on('comment_submit', onCommentSubmit);
MediaPageStore.on('comment_submit_fail', onCommentSubmitFail);
return () => {
MediaPageStore.removeListener('comment_submit', onCommentSubmit);
MediaPageStore.removeListener('comment_submit_fail', onCommentSubmitFail);
};
});
return !MemberContext._currentValue.is.anonymous ? (
<div className="comments-form">
<div className="comments-form-inner">
<UserThumbnail />
<div className="form">
<div className={'form-textarea-wrap' + (textareaFocused ? ' focused' : '')}>
<textarea
ref={textareaRef}
className="form-textarea"
rows="1"
placeholder={'Add a ' + commentsText.single + '...'}
value={value}
onChange={onChange}
onFocus={onFocus}
onBlur={onBlur}
></textarea>
</div>
<div className="form-buttons">
<button className={'' === value.trim() ? 'disabled' : ''} onClick={submitComment}>
{commentsText.submitCommentText}
</button>
</div>
</div>
</div>
</div>
) : (
<div className="comments-form">
<div className="comments-form-inner">
<UserThumbnail />
<div className="form">
<a
href={loginUrl}
rel="noffolow"
className="form-textarea-wrap"
title={'Add a ' + commentsText.single + '...'}
>
<span className="form-textarea">{'Add a ' + commentsText.single + '...'}</span>
</a>
<div className="form-buttons">
<a href={loginUrl} rel="noffolow" className="disabled">
{commentsText.submitCommentText}
</a>
</div>
</div>
</div>
</div>
);
}
CommentForm.propTypes = {
comment_type: PropTypes.oneOf(['new', 'reply']),
media_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
reply_comment_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
CommentForm.defaultProps = {
comment_type: 'new',
};
const ENABLED_COMMENTS_READ_MORE = false;
function CommentActions(props) {
const [popupContentRef, PopupContent, PopupTrigger] = usePopup();
function cancelCommentRemoval() {
popupContentRef.current.toggle();
}
function proceedCommentRemoval() {
popupContentRef.current.toggle();
MediaPageActions.deleteComment(props.comment_id);
}
return (
<div className="comment-actions">
{/*<div className="comment-action like-action"><CircleIconButton><MaterialIcon type="thumb_up" /></CircleIconButton><span className="likes-num">145</span></div>*/}
{/*<div className="comment-action dislike-action"><CircleIconButton><MaterialIcon type="thumb_down" /></CircleIconButton><span className="dislikes-num">19</span></div>*/}
{/*<div className="comment-action replay-comment"><button>REPLY</button></div>*/}
{MemberContext._currentValue.can.deleteComment ? (
<div className="comment-action remove-comment">
<PopupTrigger contentRef={popupContentRef}>
<button>DELETE {commentsText.uppercaseSingle}</button>
</PopupTrigger>
<PopupContent contentRef={popupContentRef}>
<PopupMain>
<div className="popup-message">
<span className="popup-message-title">{commentsText.ucfirstSingle} removal</span>
<span className="popup-message-main">You're willing to remove {commentsText.single} permanently?</span>
</div>
<hr />
<span className="popup-message-bottom">
<button className="button-link cancel-comment-removal" onClick={cancelCommentRemoval}>
CANCEL
</button>
<button className="button-link proceed-comment-removal" onClick={proceedCommentRemoval}>
PROCEED
</button>
</span>
</PopupMain>
</PopupContent>
</div>
) : null}
</div>
);
}
function Comment(props) {
const commentTextRef = useRef(null);
const commentTextInnerRef = useRef(null);
const [viewMoreContent, setViewMoreContent] = useState(!ENABLED_COMMENTS_READ_MORE || false);
const [enabledViewMoreContent, setEnabledViewMoreContent] = useState(false);
function onWindowResize() {
const newval = enabledViewMoreContent || commentTextInnerRef.offsetHeight > commentTextRef.offsetHeight;
setEnabledViewMoreContent(newval);
setViewMoreContent(newval || false);
}
function toggleMore() {
setViewMoreContent(!viewMoreContent);
}
useEffect(() => {
if (ENABLED_COMMENTS_READ_MORE) {
PageStore.on('window_resize', onWindowResize);
setEnabledViewMoreContent(commentTextInnerRef.offsetHeight > commentTextRef.offsetHeight);
}
return () => {
if (ENABLED_COMMENTS_READ_MORE) {
PageStore.removeListener('window_resize', onWindowResize);
}
};
}, []);
return (
<div className="comment">
<div className="comment-inner">
<a className="comment-author-thumb" href={props.author_link} title={props.author_name}>
<img src={props.author_thumb} alt={props.author_name} />
</a>
<div className="comment-content">
<div className="comment-meta">
<div className="comment-author">
<a href={props.author_link} title={props.author_name}>
{props.author_name}
</a>
</div>
<div className="comment-date">{format(new Date(props.publish_date))}</div>
</div>
<div ref={commentTextRef} className={'comment-text' + (viewMoreContent ? ' show-all' : '')}>
<div
ref={commentTextInnerRef}
className="comment-text-inner"
dangerouslySetInnerHTML={{ __html: props.text }}
></div>
</div>
{enabledViewMoreContent ? (
<button className="toggle-more" onClick={toggleMore}>
{viewMoreContent ? 'Show less' : 'Read more'}
</button>
) : null}
{MemberContext._currentValue.can.deleteComment ? <CommentActions comment_id={props.comment_id} /> : null}
</div>
</div>
</div>
);
}
Comment.propTypes = {
comment_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
media_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
text: PropTypes.string,
author_name: PropTypes.string,
author_link: PropTypes.string,
author_thumb: PropTypes.string,
publish_date: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
likes: PropTypes.number,
dislikes: PropTypes.number,
};
Comment.defaultProps = {
author_name: '',
author_link: '#',
publish_date: 0,
likes: 0,
dislikes: 0,
};
function displayCommentsRelatedAlert() {
// TODO: Improve this and move it into Media Page code.
var pageMainEl = document.querySelector('.page-main');
var noCommentDiv = pageMainEl.querySelector('.no-comment');
const postUploadMessage = PageStore.get('config-contents').uploader.postUploadMessage;
if ('' === postUploadMessage) {
if (noCommentDiv && 0 === comm.length) {
noCommentDiv.parentNode.removeChild(noCommentDiv);
}
} else if (0 === comm.length && 'unlisted' === MediaPageStore.get('media-data').state) {
if (-1 < LinksContext._currentValue.profile.media.indexOf(MediaPageStore.get('media-data').author_profile)) {
if (!noCommentDiv) {
const missingCommentariesUnlistedMsgElem = document.createElement('div');
missingCommentariesUnlistedMsgElem.setAttribute('role', 'alert');
missingCommentariesUnlistedMsgElem.setAttribute('class', 'alert info alert-dismissible no-comment');
missingCommentariesUnlistedMsgElem.innerHTML =
'<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>' +
postUploadMessage;
if (pageMainEl.firstChild) {
pageMainEl.insertBefore(missingCommentariesUnlistedMsgElem, pageMainEl.firstChild);
} else {
pageMainEl.appendChild(missingCommentariesUnlistedMsgElem);
}
missingCommentariesUnlistedMsgElem.querySelector('button.close').addEventListener('click', function (ev) {
missingCommentariesUnlistedMsgElem.setAttribute('class', 'alert info alert-dismissible hiding');
setTimeout(function () {
missingCommentariesUnlistedMsgElem.parentNode.removeChild(missingCommentariesUnlistedMsgElem);
}, 400);
ev.preventDefault();
ev.stopPropagation();
return false;
});
}
}
} else if (noCommentDiv && 0 < comm.length) {
noCommentDiv.parentNode.removeChild(noCommentDiv);
}
}
const CommentsListHeader = ({ commentsLength }) => {
return (
<>
{!MemberContext._currentValue.can.readComment || MediaPageStore.get('media-data').enable_comments ? null : (
<span className="disabled-comments-msg">{commentsText.disabledCommentsMsg}</span>
)}
{MemberContext._currentValue.can.readComment &&
(MediaPageStore.get('media-data').enable_comments || MemberContext._currentValue.can.editMedia) ? (
<h2>
{commentsLength
? 1 < commentsLength
? commentsLength + ' ' + commentsText.ucfirstPlural
: commentsLength + ' ' + commentsText.ucfirstSingle
: MediaPageStore.get('media-data').enable_comments
? 'No ' + commentsText.single + ' yet'
: ''}
</h2>
) : null}
</>
);
};
export default function CommentsList(props) {
const [mediaId, setMediaId] = useState(MediaPageStore.get('media-id'));
const [comments, setComments] = useState(
MemberContext._currentValue.can.readComment ? MediaPageStore.get('media-comments') : []
);
const [displayComments, setDisplayComments] = useState(false);
function onCommentsLoad() {
const retrievedComments = [...MediaPageStore.get('media-comments')];
retrievedComments.forEach(comment => {
comment.text = setTimestampAnchors(comment.text);
});
displayCommentsRelatedAlert();
setComments(retrievedComments);
}
function setTimestampAnchors(text)
{
function wrapTimestampWithAnchor(match, string)
{
let split = match.split(':'), s = 0, m = 1;
let searchParameters = new URLSearchParams(window.location.search);
while (split.length > 0)
{
s += m * parseInt(split.pop(), 10);
m *= 60;
}
searchParameters.set('t', s)
const wrapped = "<a href=\"" + MediaPageStore.get('media-url').split('?')[0] + "?" + searchParameters + "\">" + match + "</a>";
return wrapped;
}
const timeRegex = new RegExp('((\\d)?\\d:)?(\\d)?\\d:\\d\\d', 'g');
return text.replace(timeRegex , wrapTimestampWithAnchor);
}
function onCommentSubmit(commentId) {
onCommentsLoad();
// FIXME: Without delay creates conflict [ Uncaught Error: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. ].
setTimeout(() => PageActions.addNotification(commentsText.ucfirstSingle + ' added', 'commentSubmit'), 100);
}
function onCommentSubmitFail() {
// FIXME: Without delay creates conflict [ Uncaught Error: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. ].
setTimeout(
() => PageActions.addNotification(commentsText.ucfirstSingle + ' submition failed', 'commentSubmitFail'),
100
);
}
function onCommentDelete(commentId) {
onCommentsLoad();
// FIXME: Without delay creates conflict [ Uncaught Error: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. ].
setTimeout(() => PageActions.addNotification(commentsText.ucfirstSingle + ' removed', 'commentDelete'), 100);
}
function onCommentDeleteFail(commentId) {
// FIXME: Without delay creates conflict [ Uncaught Error: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. ].
setTimeout(
() => PageActions.addNotification(commentsText.ucfirstSingle + ' removal failed', 'commentDeleteFail'),
100
);
}
useEffect(() => {
setDisplayComments(
comments.length &&
MemberContext._currentValue.can.readComment &&
(MediaPageStore.get('media-data').enable_comments || MemberContext._currentValue.can.editMedia)
);
}, [comments]);
useEffect(() => {
MediaPageStore.on('comments_load', onCommentsLoad);
MediaPageStore.on('comment_submit', onCommentSubmit);
MediaPageStore.on('comment_submit_fail', onCommentSubmitFail);
MediaPageStore.on('comment_delete', onCommentDelete);
MediaPageStore.on('comment_delete_fail', onCommentDeleteFail);
return () => {
MediaPageStore.removeListener('comments_load', onCommentsLoad);
MediaPageStore.removeListener('comment_submit', onCommentSubmit);
MediaPageStore.removeListener('comment_submit_fail', onCommentSubmitFail);
MediaPageStore.removeListener('comment_delete', onCommentDelete);
MediaPageStore.removeListener('comment_delete_fail', onCommentDeleteFail);
};
}, []);
return (
<div className="comments-list">
<div className="comments-list-inner">
<CommentsListHeader commentsLength={comments.length} />
{MediaPageStore.get('media-data').enable_comments ? <CommentForm media_id={mediaId} /> : null}
{displayComments
? comments.map((c) => {
return (
<Comment
key={c.uid}
comment_id={c.uid}
media_id={mediaId}
text={c.text}
author_name={c.author_name}
author_link={c.author_profile}
author_thumb={SiteContext._currentValue.url + '/' + c.author_thumbnail_url.replace(/^\//g, '')}
publish_date={c.add_date}
likes={0}
dislikes={0}
/>
);
})
: null}
</div>
</div>
);
}