))}
diff --git a/src/discussions/comments/comment-icons/CommentIcons.jsx b/src/discussions/comments/comment-icons/CommentIcons.jsx
index efabc22f..1a87d284 100644
--- a/src/discussions/comments/comment-icons/CommentIcons.jsx
+++ b/src/discussions/comments/comment-icons/CommentIcons.jsx
@@ -4,16 +4,18 @@ import PropTypes from 'prop-types';
import { useDispatch } from 'react-redux';
import * as timeago from 'timeago.js';
-import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { injectIntl } from '@edx/frontend-platform/i18n';
+import timeLocale from '../../common/time-locale';
import LikeButton from '../../posts/post/LikeButton';
import { editComment } from '../data/thunks';
function CommentIcons({
comment,
- intl,
}) {
const dispatch = useDispatch();
+ timeago.register('time-locale', timeLocale);
+
const handleLike = () => dispatch(editComment(comment.id, { voted: !comment.voted }));
return (
@@ -22,8 +24,8 @@ function CommentIcons({
onClick={handleLike}
voted={comment.voted}
/>
-
- {timeago.format(comment.createdAt, intl.locale)}
+
+ {timeago.format(comment.createdAt, 'time-locale')}
);
@@ -37,7 +39,6 @@ CommentIcons.propTypes = {
voted: PropTypes.bool,
createdAt: PropTypes.string,
}).isRequired,
- intl: intlShape.isRequired,
};
export default injectIntl(CommentIcons);
diff --git a/src/discussions/comments/comment/Comment.jsx b/src/discussions/comments/comment/Comment.jsx
index 1c70b544..650d71d4 100644
--- a/src/discussions/comments/comment/Comment.jsx
+++ b/src/discussions/comments/comment/Comment.jsx
@@ -9,7 +9,7 @@ import { Button, useToggle } from '@edx/paragon';
import HTMLLoader from '../../../components/HTMLLoader';
import { ContentActions } from '../../../data/constants';
-import { AlertBanner, DeleteConfirmation } from '../../common';
+import { AlertBanner, DeleteConfirmation, EndorsedAlertBanner } from '../../common';
import { fetchThread } from '../../posts/data/thunks';
import CommentIcons from '../comment-icons/CommentIcons';
import { selectCommentCurrentPage, selectCommentHasMorePages, selectCommentResponses } from '../data/selectors';
@@ -35,12 +35,14 @@ function Comment({
const [isReplying, setReplying] = useState(false);
const hasMorePages = useSelector(selectCommentHasMorePages(comment.id));
const currentPage = useSelector(selectCommentCurrentPage(comment.id));
+
useEffect(() => {
// If the comment has a parent comment, it won't have any children, so don't fetch them.
if (hasChildren && !currentPage && showFullThread) {
dispatch(fetchCommentResponses(comment.id, { page: 1 }));
}
}, [comment.id]);
+
const actionHandlers = {
[ContentActions.EDIT_CONTENT]: () => setEditing(true),
[ContentActions.ENDORSE]: async () => {
@@ -50,79 +52,82 @@ function Comment({
[ContentActions.DELETE]: showDeleteConfirmation,
[ContentActions.REPORT]: () => dispatch(editComment(comment.id, { flagged: !comment.abuseFlagged })),
};
+
const handleLoadMoreComments = () => (
dispatch(fetchCommentResponses(comment.id, { page: currentPage + 1 }))
);
- const commentClasses = classNames('d-flex flex-column card', { 'my-3': showFullThread });
return (
-
-
{
- dispatch(removeComment(comment.id));
- hideDeleteConfirmation();
- }}
- />
-
-
-
- {isEditing
- ? (
-
setEditing(false)} />
- )
- : }
- dispatch(editComment(comment.id, { voted: !comment.voted }))}
- createdAt={comment.createdAt}
+
+
+
{
+ dispatch(removeComment(comment.id));
+ hideDeleteConfirmation();
+ }}
/>
- {intl.formatMessage(messages.replies, { count: inlineReplies.length })}
-
- {/* Pass along intl since component used here is the one before it's injected with `injectIntl` */}
- {inlineReplies.map(inlineReply => (
-
- ))}
-
- {hasMorePages && (
+
+
+
+
+ {isEditing
+ ? (
+
setEditing(false)} />
+ )
+ : }
+ dispatch(editComment(comment.id, { voted: !comment.voted }))}
+ createdAt={comment.createdAt}
+ />
+ {intl.formatMessage(messages.replies, { count: inlineReplies.length })}
+
+ {/* Pass along intl since component used here is the one before it's injected with `injectIntl` */}
+ {inlineReplies.map(inlineReply => (
+
+ ))}
+
+ {hasMorePages && (
- )}
- {!isNested && showFullThread
- && (
- isReplying
- ? (
- setReplying(false)}
- />
- )
- : (
-
- )
)}
+ {!isNested && showFullThread && (
+ isReplying ? (
+ setReplying(false)}
+ />
+ ) : (
+
+ )
+ )}
+
);
diff --git a/src/discussions/comments/comment/CommentHeader.jsx b/src/discussions/comments/comment/CommentHeader.jsx
index d3388717..cada8c17 100644
--- a/src/discussions/comments/comment/CommentHeader.jsx
+++ b/src/discussions/comments/comment/CommentHeader.jsx
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
+import classNames from 'classnames';
import { useSelector } from 'react-redux';
import { injectIntl } from '@edx/frontend-platform/i18n';
@@ -10,6 +11,7 @@ import { CheckCircle, Verified } from '@edx/paragon/icons';
import { AvatarOutlineAndLabelColors, ThreadType } from '../../../data/constants';
import { AuthorLabel } from '../../common';
import ActionsDropdown from '../../common/ActionsDropdown';
+import { useAlertBannerVisible } from '../../data/hooks';
import { selectAuthorAvatars } from '../../posts/data/selectors';
import { commentShape } from './proptypes';
@@ -20,20 +22,31 @@ function CommentHeader({
}) {
const authorAvatars = useSelector(selectAuthorAvatars(comment.author));
const colorClass = AvatarOutlineAndLabelColors[comment.authorLabel];
+ const hasAnyAlert = useAlertBannerVisible(comment);
+
return (
-
+
- {comment.endorsed && (postType === 'question'
- ?
- :
)}
+
+ {comment.endorsed && (postType === 'question'
+ ?
+ : )}
+
+
-
-
-
+ {hasAnyAlert && (
+
-
-
+ )}
-
+
-
-
+
+
{isEditing
? setEditing(false)} />
- : }
+ : }
- {timeago.format(reply.createdAt, intl.locale)}
+ {timeago.format(reply.createdAt, 'time-locale')}
);
diff --git a/src/discussions/common/ActionsDropdown.jsx b/src/discussions/common/ActionsDropdown.jsx
index c0fa2ce6..4a03d843 100644
--- a/src/discussions/common/ActionsDropdown.jsx
+++ b/src/discussions/common/ActionsDropdown.jsx
@@ -33,16 +33,15 @@ function ActionsDropdown({
};
return (
<>
-
- setOpen(!isOpen)}
- alt={intl.formatMessage(messages.actionsAlt)}
- src={MoreHoriz}
- iconAs={Icon}
- disabled={disabled}
- size="sm"
- />
-
+
setOpen(!isOpen)}
+ alt={intl.formatMessage(messages.actionsAlt)}
+ src={MoreHoriz}
+ iconAs={Icon}
+ disabled={disabled}
+ size="sm"
+ ref={dropdownIconRef}
+ />
setOpen(false)}
positionRef={dropdownIconRef}
diff --git a/src/discussions/common/AlertBanner.jsx b/src/discussions/common/AlertBanner.jsx
index fbcd4f18..f66a65d0 100644
--- a/src/discussions/common/AlertBanner.jsx
+++ b/src/discussions/common/AlertBanner.jsx
@@ -2,13 +2,11 @@ import React from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
-import * as timeago from 'timeago.js';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Alert } from '@edx/paragon';
-import { CheckCircle, Error, Verified } from '@edx/paragon/icons';
+import { Error } from '@edx/paragon/icons';
-import { ThreadType } from '../../data/constants';
import { commentShape } from '../comments/comment/proptypes';
import messages from '../comments/messages';
import { selectModerationSettings, selectUserIsPrivileged } from '../data/selectors';
@@ -18,50 +16,19 @@ import AuthorLabel from './AuthorLabel';
function AlertBanner({
intl,
content,
- postType,
}) {
- const isQuestion = postType === ThreadType.QUESTION;
- const classes = isQuestion ? 'bg-success-500 text-white' : 'bg-dark-500 text-white';
- const iconClass = isQuestion ? CheckCircle : Verified;
const userIsPrivileged = useSelector(selectUserIsPrivileged);
const { reasonCodesEnabled } = useSelector(selectModerationSettings);
+
return (
<>
- {content.endorsed && (
-
-
-
{intl.formatMessage(
- isQuestion
- ? messages.answer
- : messages.endorsed,
- )}
-
-
-
- {intl.formatMessage(
- isQuestion
- ? messages.answeredLabel
- : messages.endorsedLabel,
- )}
-
-
- {timeago.format(content.endorsedAt, intl.locale)}
-
-
-
- )}
{content.abuseFlagged && (
-
+
{intl.formatMessage(messages.abuseFlaggedMessage)}
)}
{reasonCodesEnabled && userIsPrivileged && content.lastEdit?.reason && (
-
+
{intl.formatMessage(messages.editedBy)}
@@ -72,7 +39,7 @@ function AlertBanner({
)}
{reasonCodesEnabled && content.closed && (
-
+
{intl.formatMessage(messages.closedBy)}
@@ -90,11 +57,6 @@ function AlertBanner({
AlertBanner.propTypes = {
intl: intlShape.isRequired,
content: PropTypes.oneOfType([commentShape.isRequired, postShape.isRequired]).isRequired,
- postType: PropTypes.string,
-};
-
-AlertBanner.defaultProps = {
- postType: null,
};
export default injectIntl(AlertBanner);
diff --git a/src/discussions/common/AlertBanner.test.jsx b/src/discussions/common/AlertBanner.test.jsx
index 1ec499e8..1928d11a 100644
--- a/src/discussions/common/AlertBanner.test.jsx
+++ b/src/discussions/common/AlertBanner.test.jsx
@@ -22,14 +22,12 @@ function buildTestContent(type, buildParams) {
function renderComponent(
content,
- postType,
) {
render(
,
@@ -44,27 +42,6 @@ describe.each([
props: { abuseFlagged: true },
expectText: [messages.abuseFlaggedMessage.defaultMessage],
},
- {
- label: 'Staff endorsed comment in a question thread',
- type: 'comment',
- postType: ThreadType.QUESTION,
- props: { endorsed: true, endorsedBy: 'test-user', endorsedByLabel: 'Staff' },
- expectText: [messages.answer.defaultMessage, messages.answeredLabel.defaultMessage, 'test-user', 'Staff'],
- },
- {
- label: 'TA endorsed comment in a question thread',
- type: 'comment',
- postType: ThreadType.QUESTION,
- props: { endorsed: true, endorsedBy: 'test-user', endorsedByLabel: 'Community TA' },
- expectText: [messages.answer.defaultMessage, messages.answeredLabel.defaultMessage, 'test-user', 'TA'],
- },
- {
- label: 'endorsed comment in a discussion thread',
- type: 'comment',
- postType: ThreadType.DISCUSSION,
- props: { endorsed: true, endorsedBy: 'test-user' },
- expectText: [messages.endorsed.defaultMessage, messages.endorsedLabel.defaultMessage, 'test-user'],
- },
{
label: 'flagged thread',
type: 'thread',
@@ -87,7 +64,7 @@ describe.each([
expectText: [messages.closedBy.defaultMessage, 'closing-user', 'test-close-reason'],
},
])('AlertBanner', ({
- label, type, postType, props, expectText,
+ label, type, props, expectText,
}) => {
beforeEach(async () => {
initializeMockApp({
@@ -105,7 +82,7 @@ describe.each([
},
});
const content = buildTestContent(type, props);
- renderComponent(content, postType);
+ renderComponent(content);
});
it(`should show correct banner for a ${label}`, async () => {
diff --git a/src/discussions/common/AuthorLabel.jsx b/src/discussions/common/AuthorLabel.jsx
index 83826df3..06d9ee52 100644
--- a/src/discussions/common/AuthorLabel.jsx
+++ b/src/discussions/common/AuthorLabel.jsx
@@ -34,13 +34,14 @@ function AuthorLabel({
authorLabelMessage = intl.formatMessage(messages.authorLabelTA);
}
- const fontWeight = authorLabelMessage ? 'font-weight-500' : 'font-weight-normal text-primary-500';
const className = classNames('d-flex align-items-center', labelColor);
const labelContents = (
@@ -57,7 +58,9 @@ function AuthorLabel({
)}
{authorLabelMessage && (
{authorLabelMessage}
diff --git a/src/discussions/common/EndorsedAlertBanner.jsx b/src/discussions/common/EndorsedAlertBanner.jsx
new file mode 100644
index 00000000..789634a7
--- /dev/null
+++ b/src/discussions/common/EndorsedAlertBanner.jsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import * as timeago from 'timeago.js';
+
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { Alert } from '@edx/paragon';
+import { CheckCircle, Verified } from '@edx/paragon/icons';
+
+import { ThreadType } from '../../data/constants';
+import { commentShape } from '../comments/comment/proptypes';
+import messages from '../comments/messages';
+import AuthorLabel from './AuthorLabel';
+
+function EndorsedAlertBanner({
+ intl,
+ content,
+ postType,
+}) {
+ const isQuestion = postType === ThreadType.QUESTION;
+ const classes = isQuestion ? 'bg-success-500 text-white' : 'bg-dark-500 text-white';
+ const iconClass = isQuestion ? CheckCircle : Verified;
+
+ return (
+ content.endorsed && (
+
+
+
{intl.formatMessage(
+ isQuestion
+ ? messages.answer
+ : messages.endorsed,
+ )}
+
+
+
+ {intl.formatMessage(
+ isQuestion
+ ? messages.answeredLabel
+ : messages.endorsedLabel,
+ )}
+
+
+ {timeago.format(content.endorsedAt, intl.locale)}
+
+
+
+ )
+ );
+}
+
+EndorsedAlertBanner.propTypes = {
+ intl: intlShape.isRequired,
+ content: PropTypes.oneOfType([commentShape.isRequired]).isRequired,
+ postType: PropTypes.string,
+};
+
+EndorsedAlertBanner.defaultProps = {
+ postType: null,
+};
+
+export default injectIntl(EndorsedAlertBanner);
diff --git a/src/discussions/common/EndorsedAlertBanner.test.jsx b/src/discussions/common/EndorsedAlertBanner.test.jsx
new file mode 100644
index 00000000..4248249f
--- /dev/null
+++ b/src/discussions/common/EndorsedAlertBanner.test.jsx
@@ -0,0 +1,87 @@
+import { render, screen } from '@testing-library/react';
+import { IntlProvider } from 'react-intl';
+import { Factory } from 'rosie';
+
+import { camelCaseObject, initializeMockApp, snakeCaseObject } from '@edx/frontend-platform';
+import { AppProvider } from '@edx/frontend-platform/react';
+
+import { ThreadType } from '../../data/constants';
+import { initializeStore } from '../../store';
+import messages from '../comments/messages';
+import EndorsedAlertBanner from './EndorsedAlertBanner';
+
+import '../comments/data/__factories__';
+import '../posts/data/__factories__';
+
+let store;
+
+function buildTestContent(type, buildParams) {
+ const buildParamsSnakeCase = snakeCaseObject(buildParams);
+ return camelCaseObject(Factory.build(type, { ...buildParamsSnakeCase }, null));
+}
+
+function renderComponent(
+ content, postType,
+) {
+ render(
+
+
+
+
+ ,
+ );
+}
+
+describe.each([
+ {
+ label: 'Staff endorsed comment in a question thread',
+ type: 'comment',
+ postType: ThreadType.QUESTION,
+ props: { endorsed: true, endorsedBy: 'test-user', endorsedByLabel: 'Staff' },
+ expectText: [messages.answer.defaultMessage, messages.answeredLabel.defaultMessage, 'test-user', 'Staff'],
+ },
+ {
+ label: 'TA endorsed comment in a question thread',
+ type: 'comment',
+ postType: ThreadType.QUESTION,
+ props: { endorsed: true, endorsedBy: 'test-user', endorsedByLabel: 'Community TA' },
+ expectText: [messages.answer.defaultMessage, messages.answeredLabel.defaultMessage, 'test-user', 'TA'],
+ },
+ {
+ label: 'endorsed comment in a discussion thread',
+ type: 'comment',
+ postType: ThreadType.DISCUSSION,
+ props: { endorsed: true, endorsedBy: 'test-user' },
+ expectText: [messages.endorsed.defaultMessage, messages.endorsedLabel.defaultMessage, 'test-user'],
+ },
+])('EndorsedAlertBanner', ({
+ label, type, postType, props, expectText,
+}) => {
+ beforeEach(async () => {
+ initializeMockApp({
+ authenticatedUser: {
+ userId: 3,
+ username: 'abc123',
+ administrator: false,
+ roles: [],
+ },
+ });
+ store = initializeStore({
+ config: {
+ userIsPrivileged: true,
+ reasonCodesEnabled: true,
+ },
+ });
+ const content = buildTestContent(type, props);
+ renderComponent(content, postType);
+ });
+
+ it(`should show correct banner for a ${label}`, async () => {
+ expectText.forEach(message => {
+ expect(screen.queryAllByText(message, { exact: false }).length).toBeGreaterThan(0);
+ });
+ });
+});
diff --git a/src/discussions/common/index.js b/src/discussions/common/index.js
index e80c85a7..4bee420d 100644
--- a/src/discussions/common/index.js
+++ b/src/discussions/common/index.js
@@ -2,3 +2,4 @@ export { default as ActionsDropdown } from './ActionsDropdown';
export { default as AlertBanner } from './AlertBanner';
export { default as AuthorLabel } from './AuthorLabel';
export { default as DeleteConfirmation } from './DeleteConfirmation';
+export { default as EndorsedAlertBanner } from './EndorsedAlertBanner';
diff --git a/src/discussions/data/hooks.js b/src/discussions/data/hooks.js
index 443547c0..64039a81 100644
--- a/src/discussions/data/hooks.js
+++ b/src/discussions/data/hooks.js
@@ -13,7 +13,9 @@ import { clearRedirect } from '../posts/data';
import { selectTopics } from '../topics/data/selectors';
import { fetchCourseTopics } from '../topics/data/thunks';
import { discussionsPath, postMessageToParent } from '../utils';
-import { selectAreThreadsFiltered, selectPostThreadCount } from './selectors';
+import {
+ selectAreThreadsFiltered, selectModerationSettings, selectPostThreadCount, selectUserIsPrivileged,
+} from './selectors';
import { fetchCourseConfig } from './thunks';
export function useTotalTopicThreadCount() {
@@ -141,3 +143,14 @@ export function useContainerSizeForParent(refContainer) {
};
}, [refContainer, resizeObserver, location]);
}
+
+export const useAlertBannerVisible = (content) => {
+ const userIsPrivileged = useSelector(selectUserIsPrivileged);
+ const { reasonCodesEnabled } = useSelector(selectModerationSettings);
+
+ return (
+ (content.abuseFlagged)
+ || (reasonCodesEnabled && userIsPrivileged && content.lastEdit?.reason)
+ || (reasonCodesEnabled && content.closed)
+ );
+};
diff --git a/src/discussions/posts/post/Post.jsx b/src/discussions/posts/post/Post.jsx
index e6830200..bc148d8c 100644
--- a/src/discussions/posts/post/Post.jsx
+++ b/src/discussions/posts/post/Post.jsx
@@ -70,7 +70,7 @@ function Post({
hideDeleteConfirmation();
}}
/>
-
+
diff --git a/src/discussions/posts/post/PostHeader.jsx b/src/discussions/posts/post/PostHeader.jsx
index b5378cb3..b5c7ddb5 100644
--- a/src/discussions/posts/post/PostHeader.jsx
+++ b/src/discussions/posts/post/PostHeader.jsx
@@ -10,6 +10,7 @@ import { Avatar, Badge, Icon } from '@edx/paragon';
import { Issue, Question } from '../../../components/icons';
import { AvatarOutlineAndLabelColors, ThreadType } from '../../../data/constants';
import { ActionsDropdown, AuthorLabel } from '../../common';
+import { useAlertBannerVisible } from '../../data/hooks';
import { selectAuthorAvatars } from '../data/selectors';
import messages from './messages';
import { postShape } from './proptypes';
@@ -89,8 +90,10 @@ function PostHeader({
}) {
const showAnsweredBadge = preview && post.hasEndorsed && post.type === ThreadType.QUESTION;
const authorLabelColor = AvatarOutlineAndLabelColors[post.authorLabel];
+ const hasAnyAlert = useAlertBannerVisible(post);
+
return (
-
+
diff --git a/src/index.scss b/src/index.scss
index 7231b402..54d200b0 100755
--- a/src/index.scss
+++ b/src/index.scss
@@ -63,6 +63,10 @@ $fa-font-path: "~font-awesome/fonts";
margin-right: 2px;
}
+.mb-0\.5 {
+ margin-bottom: 2px;
+}
+
.mt-0\.5 {
margin-top: 2px;
}
@@ -100,6 +104,6 @@ $fa-font-path: "~font-awesome/fonts";
}
.avarat-img-position {
-margin-top: 17px;
-margin-left: 17px;
+ margin-top: 17px;
+ margin-left: 17px;
}