@selfcommunity/react-ui
Version:
React UI Components to integrate a Community created with SelfCommunity Platform.
330 lines (321 loc) • 19.2 kB
JavaScript
import { __rest } from "tslib";
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
import React, { useContext, useState } from 'react';
import { styled } from '@mui/material/styles';
import Widget from '../Widget';
import { FormattedMessage } from 'react-intl';
import { Avatar, Box, Button, CardContent, Chip, Icon, IconButton, Typography } from '@mui/material';
import Bullet from '../../shared/Bullet';
import classNames from 'classnames';
import { SCOPE_SC_UI } from '../../constants/Errors';
import CommentObjectSkeleton from './Skeleton';
import { SCCommentsOrderBy } from '../../types/comments';
import CommentsObject from '../CommentsObject';
import CommentObjectReply from '../CommentObjectReply';
import ContributionActionsMenu from '../../shared/ContributionActionsMenu';
import DateTimeAgo from '../../shared/DateTimeAgo';
import { getCommentContributionHtml, getContributionType, getRouteData } from '../../utils/contribution';
import { useSnackbar } from 'notistack';
import { useThemeProps } from '@mui/system';
import BaseItem from '../../shared/BaseItem';
import { SCContributionType } from '@selfcommunity/types';
import { Endpoints, http } from '@selfcommunity/api-services';
import { CacheStrategies, Logger, LRUCache } from '@selfcommunity/utils';
import { Link, SCCache, SCRoutes, SCUserContext, UserUtils, useSCContext, useSCFetchCommentObject, useSCFetchCommentObjects, useSCRouting } from '@selfcommunity/react-core';
import VoteButton from '../VoteButton';
import VoteAudienceButton from '../VoteAudienceButton';
import UserDeletedSnackBar from '../../shared/UserDeletedSnackBar';
import UserAvatar from '../../shared/UserAvatar';
import { PREFIX } from './constants';
const classes = {
root: `${PREFIX}-root`,
comment: `${PREFIX}-comment`,
nestedComments: `${PREFIX}-nested-comments`,
avatar: `${PREFIX}-avatar`,
content: `${PREFIX}-content`,
showMoreContent: `${PREFIX}-show-more-content`,
author: `${PREFIX}-author`,
textContent: `${PREFIX}-text-content`,
commentActionsMenu: `${PREFIX}-comment-actions-menu`,
deleted: `${PREFIX}-deleted`,
activityAt: `${PREFIX}-activity-at`,
vote: `${PREFIX}-vote`,
voteAudience: `${PREFIX}-vote-audience`,
reply: `${PREFIX}-reply`,
contentSubSection: `${PREFIX}-comment-sub-section`,
collapsed: `${PREFIX}-collapsed`,
flagChip: `${PREFIX}-flag-chip`
};
const Root = styled(Box, {
name: PREFIX,
slot: 'Root'
})(() => ({}));
/**
* > API documentation for the Community-JS Comment Object component. Learn about the available props and the CSS API.
*
*
* This component renders a comment item.
* Take a look at our <strong>demo</strong> component [here](/docs/sdk/community-js/react-ui/Components/CommentObject)
#### Import
```jsx
import {CommentObject} from '@selfcommunity/react-ui';
```
#### Component Name
The name `SCCommentObject` can be used when providing style overrides in the theme.
#### CSS
|Rule Name|Global class|Description|
|---|---|---|
|root|.SCCommentObject-root|Styles applied to the root element.|
|comment|.SCCommentObject-comment|Styles applied to comment element.|
|nestedComments|.SCCommentObject-nestedComments|Styles applied to nested comments element wrapper.|
|avatar|.SCCommentObject-avatar|Styles applied to the avatar element.|
|author|.SCCommentObject-author|Styles applied to the author section.|
|content|.SCCommentObject-content|Styles applied to content section.|
|textContent|.SCCommentObject-text-content|Styles applied to text content section.|
|vote|.SCCommentObject-vote|Styles applied to the votes section.|
|btnVotes|.SCCommentObject-vote-audience|Styles applied to the votes audience section.|
|commentActionsMenu|.SCCommentObject-comment-actions-menu|Styles applied to comment action menu element.|
|deleted|.SCCommentObject-deleted|Styles applied to tdeleted element.|
|activityAt|.SCCommentObject-activity-at|Styles applied to activity at section.|
|reply|.SCCommentObject-reply|Styles applied to the reply element.|
|contentSubSection|.SCCommentObject-content-sub-section|Styles applied to the comment subsection|
* @param inProps
*/
export default function CommentObject(inProps) {
// PROPS
const props = useThemeProps({
props: inProps,
name: PREFIX
});
const { id = `comment_object_${props.commentObjectId ? props.commentObjectId : props.commentObject ? props.commentObject.id : ''}`, className, commentObjectId, commentObject, feedObjectId, feedObject, feedObjectType = SCContributionType.POST, commentReply, onOpenReply, onDelete, onCollapsed, onRestore, onVote, elevation = 0, truncateContent = false, CommentObjectSkeletonProps = { elevation, WidgetProps: { variant: 'outlined' } }, CommentObjectReplyProps = { elevation, WidgetProps: { variant: 'outlined' } }, linkableCommentDateTime = true, cacheStrategy = CacheStrategies.NETWORK_ONLY, CommentsObjectComponentProps = {} } = props, rest = __rest(props, ["id", "className", "commentObjectId", "commentObject", "feedObjectId", "feedObject", "feedObjectType", "commentReply", "onOpenReply", "onDelete", "onCollapsed", "onRestore", "onVote", "elevation", "truncateContent", "CommentObjectSkeletonProps", "CommentObjectReplyProps", "linkableCommentDateTime", "cacheStrategy", "CommentsObjectComponentProps"]);
// CONTEXT
const scContext = useSCContext();
const scUserContext = useContext(SCUserContext);
const scRoutingContext = useSCRouting();
const { enqueueSnackbar } = useSnackbar();
// STATE
const { obj, setObj } = useSCFetchCommentObject({ id: commentObjectId, commentObject, cacheStrategy });
const [collapsed, setCollapsed] = useState(obj === null || obj === void 0 ? void 0 : obj.collapsed);
const [replyComment, setReplyComment] = useState(commentReply);
const [isReplying, setIsReplying] = useState(false);
const [isSavingComment, setIsSavingComment] = useState(false);
const [editComment, setEditComment] = useState(null);
const commentsObject = useSCFetchCommentObjects({
id: feedObjectId,
feedObject,
feedObjectType,
orderBy: SCCommentsOrderBy.ADDED_AT_DESC,
parent: commentObject ? commentObject.id : commentObjectId,
cacheStrategy
});
const [openAlert, setOpenAlert] = useState(false);
// HANDLERS
const handleVoteSuccess = (contribution) => {
setObj(contribution);
onVote && onVote(contribution);
};
/**
* Update state object
* @param newObj
*/
function updateObject(newObj) {
LRUCache.set(SCCache.getCommentObjectCacheKey(obj.id), newObj);
setObj(newObj);
const contributionType = getContributionType(obj);
LRUCache.deleteKeysWithPrefix(SCCache.getCommentObjectsCachePrefixKeys(newObj[contributionType].id, contributionType));
}
/**
* Render added_at of the comment
* @param comment
*/
function renderTimeAgo(comment) {
return (_jsx(_Fragment, { children: linkableCommentDateTime ? (_jsx(Link, Object.assign({ to: scRoutingContext.url(SCRoutes.COMMENT_ROUTE_NAME, getRouteData(comment)), className: classes.activityAt }, { children: _jsx(DateTimeAgo, { date: comment.added_at }) }))) : (_jsx(DateTimeAgo, { date: comment.added_at })) }));
}
/**
* Render CommentObjectReply action
* @param comment
*/
function renderActionReply(comment) {
return (_jsx(Button, Object.assign({ className: classes.reply, variant: "text", onClick: () => reply(comment) }, { children: _jsx(FormattedMessage, { id: "ui.commentObject.reply", defaultMessage: "ui.commentObject.reply" }) })));
}
/**
* Handle reply: open Editor
* @param comment
*/
function reply(comment) {
if (!scUserContext.user) {
scContext.settings.handleAnonymousAction();
}
else {
setReplyComment(comment);
onOpenReply && onOpenReply(comment);
}
}
/**
* Perform reply
* Comment of second level
*/
const performReply = (comment) => {
return http
.request({
url: Endpoints.NewComment.url({}),
method: Endpoints.NewComment.method,
data: Object.assign(Object.assign({ [`${feedObject ? feedObject.type : feedObjectType}`]: feedObject ? feedObject.id : feedObjectId, parent: replyComment.parent ? replyComment.parent : replyComment.id }, (replyComment.parent ? { in_reply_to: replyComment.id } : {})), { text: comment })
})
.then((res) => {
if (res.status >= 300) {
return Promise.reject(res);
}
return Promise.resolve(res.data);
});
};
/**
* Handle comment of 2° level
*/
function handleReply(comment) {
if (UserUtils.isBlocked(scUserContext.user)) {
enqueueSnackbar(_jsx(FormattedMessage, { id: "ui.common.userBlocked", defaultMessage: "ui.common.userBlocked" }), {
variant: 'warning',
autoHideDuration: 3000
});
}
else {
setIsReplying(true);
performReply(comment)
.then((data) => {
// if add a comment -> the comment must be untruncated
const _data = data;
_data.summary_truncated = false;
updateObject(Object.assign(Object.assign({}, obj), { comment_count: obj.comment_count + 1, latest_comments: [...obj.latest_comments, _data] }));
setReplyComment(null);
setIsReplying(false);
})
.catch((error) => {
Logger.error(SCOPE_SC_UI, error);
enqueueSnackbar(_jsx(FormattedMessage, { id: "ui.common.error.action", defaultMessage: "ui.common.error.action" }), {
variant: 'error',
autoHideDuration: 3000
});
setIsReplying(false);
});
}
}
/**
* Handle comment delete
*/
function handleDelete(comment) {
updateObject(comment);
onDelete && onDelete(comment);
}
/**
* Handle comment delete
*/
function handleHide(comment) {
updateObject(Object.assign({}, obj, { collapsed: !obj.collapsed }));
onCollapsed && onCollapsed(comment);
}
/**
* Handle comment restore
*/
function handleRestore(comment) {
updateObject(Object.assign({}, obj, { deleted: false }));
onRestore && onRestore(comment);
}
/**
* Handle edit comment
*/
function handleEdit(comment) {
setEditComment(comment);
}
function handleCancel() {
setEditComment(null);
}
/**
* Perform save/update comment
*/
const performSave = (comment) => {
return http
.request({
url: Endpoints.UpdateComment.url({ id: editComment.id }),
method: Endpoints.UpdateComment.method,
data: { text: comment }
})
.then((res) => {
if (res.status >= 300) {
return Promise.reject(res);
}
return Promise.resolve(res.data);
});
};
/**
* Handle save comment
*/
function handleSave(comment) {
if (UserUtils.isBlocked(scUserContext.user)) {
enqueueSnackbar(_jsx(FormattedMessage, { id: "ui.common.userBlocked", defaultMessage: "ui.common.userBlocked" }), {
variant: 'warning',
autoHideDuration: 3000
});
}
else {
setIsSavingComment(true);
performSave(comment)
.then((data) => {
const newObj = Object.assign({}, obj, {
text: data.text,
html: data.html,
summary: data.summary,
added_at: data.added_at
});
updateObject(newObj);
setEditComment(null);
setIsSavingComment(false);
})
.catch((error) => {
Logger.error(SCOPE_SC_UI, error);
enqueueSnackbar(_jsx(FormattedMessage, { id: "ui.common.error.action", defaultMessage: "ui.common.error.action" }), {
variant: 'error',
autoHideDuration: 3000
});
});
}
}
/**
* Render comment & latest activities
* @param comment
*/
function renderComment(comment) {
if (comment.deleted &&
(!scUserContext.user || (scUserContext.user && !UserUtils.isStaff(scUserContext.user) && scUserContext.user.id !== comment.author.id))) {
// render the comment if user is logged and is staff (admin, moderator)
// or the comment author is the logged user
return null;
}
const summaryHtmlTruncated = 'summary_truncated' in comment ? comment.summary_truncated : false;
const commentHtml = 'summary_html' in comment && truncateContent && summaryHtmlTruncated ? comment.summary_html : comment.html;
const summaryHtml = getCommentContributionHtml(commentHtml, scRoutingContext.url);
return (_jsxs(React.Fragment, { children: [collapsed ? (_jsx(BaseItem, { elevation: 0, className: classes.comment, disableTypography: true, primary: _jsxs(Widget, Object.assign({ className: classNames(classes.content, classes.collapsed), elevation: elevation }, rest, { children: [_jsx(CardContent, Object.assign({ className: classNames({ [classes.deleted]: obj && obj.deleted }) }, { children: _jsx(FormattedMessage, { id: "ui.commentObject.collapsed", defaultMessage: "ui.commentObject.collapsed" }) })), _jsx(Box, Object.assign({ className: classes.commentActionsMenu }, { children: _jsx(IconButton, Object.assign({ onClick: () => setCollapsed(!collapsed) }, { children: _jsx(Icon, { children: "visibility" }) })) }))] })) })) : editComment && editComment.id === comment.id ? (_jsx(Box, Object.assign({ className: classes.comment }, { children: _jsx(CommentObjectReply, Object.assign({ text: comment.html, autoFocus: true, id: `edit-${comment.id}`, onSave: handleSave, onCancel: handleCancel, editable: !isReplying || !isSavingComment }, CommentObjectReplyProps)) }))) : (_jsx(BaseItem, { elevation: 0, className: classes.comment, image: _jsx(Link, Object.assign({}, (!comment.author.deleted && { to: scRoutingContext.url(SCRoutes.USER_PROFILE_ROUTE_NAME, comment.author) }), { onClick: comment.author.deleted ? () => setOpenAlert(true) : null }, { children: _jsx(UserAvatar, Object.assign({ hide: !obj.author.community_badge }, { children: _jsx(Avatar, { alt: obj.author.username, variant: "circular", src: comment.author.avatar, className: classes.avatar }) })) })), disableTypography: true, primary: _jsxs(_Fragment, { children: [_jsxs(Widget, Object.assign({ className: classes.content, elevation: elevation }, rest, { children: [_jsxs(CardContent, Object.assign({ className: classNames({ [classes.deleted]: obj && obj.deleted }) }, { children: [_jsx(Link, Object.assign({ className: classes.author }, (!comment.author.deleted && { to: scRoutingContext.url(SCRoutes.USER_PROFILE_ROUTE_NAME, comment.author) }), { onClick: comment.author.deleted ? () => setOpenAlert(true) : null }, { children: _jsx(Typography, Object.assign({ component: "span" }, { children: comment.author.username })) })), comment.collapsed && (_jsx(Chip, { className: classes.flagChip, color: "error", size: "small", label: _jsx(FormattedMessage, { id: "ui.commentObject.flag", defaultMessage: "ui.commentObject.flag" }) })), _jsx(Typography, { className: classes.textContent, variant: "body2", gutterBottom: true, dangerouslySetInnerHTML: { __html: summaryHtml } }), summaryHtmlTruncated && truncateContent && (_jsx(Link, Object.assign({ to: scRoutingContext.url(SCRoutes.COMMENT_ROUTE_NAME, getRouteData(comment)), className: classes.showMoreContent }, { children: _jsx(FormattedMessage, { id: "ui.commentObject.showMore", defaultMessage: "ui.commentObject.showMore" }) })))] })), scUserContext.user && (_jsx(Box, Object.assign({ className: classes.commentActionsMenu }, { children: _jsx(ContributionActionsMenu, { commentObject: comment, onRestoreContribution: handleRestore, onHideContribution: handleHide, onDeleteContribution: handleDelete, onEditContribution: handleEdit }) })))] })), _jsxs(Box, Object.assign({ component: "span", className: classes.contentSubSection }, { children: [renderTimeAgo(comment), _jsx(Bullet, {}), _jsx(VoteButton, { size: "small", className: classes.vote, contributionId: comment.id, contributionType: SCContributionType.COMMENT, contribution: comment, onVote: handleVoteSuccess }), _jsx(Bullet, {}), renderActionReply(comment), _jsx(VoteAudienceButton, { size: "small", className: classes.voteAudience, contributionId: comment.id, contributionType: SCContributionType.COMMENT, contribution: comment })] }))] }) })), comment.comment_count > 0 && _jsx(Box, Object.assign({ className: classes.nestedComments }, { children: renderLatestComment(comment) })), scUserContext.user && replyComment && (replyComment.id === comment.id || replyComment.parent === comment.id) && !comment.parent && (_jsx(Box, Object.assign({ className: classes.nestedComments }, { children: _jsx(CommentObjectReply, Object.assign({ text: `@${replyComment.author.username}, `, autoFocus: true, id: `reply-${replyComment.id}`, onReply: handleReply, editable: !isReplying }, CommentObjectReplyProps), `reply-${replyComment.id}`) })))] }, comment.id));
}
/**
* Render Latest Comment
* @param comment
*/
function renderLatestComment(comment) {
return (_jsx(CommentsObject, Object.assign({ feedObject: commentsObject.feedObject, feedObjectType: commentsObject.feedObject ? commentsObject.feedObject.type : feedObjectType, hideAdvertising: true, comments: [].concat(commentsObject.comments).reverse(), endComments: comment.latest_comments, previous: comment.comment_count > comment.latest_comments.length ? commentsObject.next : null, isLoadingPrevious: commentsObject.isLoadingNext, handlePrevious: commentsObject.getNextPage, CommentComponentProps: Object.assign(Object.assign({ onOpenReply: reply, CommentObjectSkeletonProps, elevation: elevation, linkableCommentDateTime: linkableCommentDateTime }, rest), { cacheStrategy,
truncateContent }), CommentsObjectSkeletonProps: { count: 1, CommentObjectSkeletonProps: CommentObjectSkeletonProps }, inPlaceLoadMoreContents: true }, CommentsObjectComponentProps, { cacheStrategy: cacheStrategy })));
}
/**
* Render comments
*/
let comment;
if (obj) {
comment = renderComment(obj);
}
else {
comment = _jsx(CommentObjectSkeleton, Object.assign({}, CommentObjectSkeletonProps));
}
/**
* Render object
*/
return (_jsxs(_Fragment, { children: [_jsx(Root, Object.assign({ id: id, className: classNames(classes.root, className) }, { children: comment })), openAlert && _jsx(UserDeletedSnackBar, { open: openAlert, handleClose: () => setOpenAlert(false) })] }));
}