+
{intl.formatMessage(messages.closedBy)}
-
+
-
+
+ {intl.formatMessage(messages.fullStop)}
+
+
{content.closeReason && (`${intl.formatMessage(messages.reason)}: ${content.closeReason}`)}
+
)}
diff --git a/src/discussions/common/AuthorLabel.jsx b/src/discussions/common/AuthorLabel.jsx
index 25f9fd26..7beb46f7 100644
--- a/src/discussions/common/AuthorLabel.jsx
+++ b/src/discussions/common/AuthorLabel.jsx
@@ -25,6 +25,7 @@ function AuthorLabel({
alert,
postCreatedAt,
authorToolTip,
+ postOrComment,
}) {
const location = useLocation();
const { courseId } = useContext(DiscussionContext);
@@ -43,13 +44,13 @@ function AuthorLabel({
const isRetiredUser = author ? author.startsWith('retired__user') : false;
- const className = classNames('d-flex align-items-center mb-0.5', labelColor);
+ const className = classNames('d-flex align-items-center', { 'mb-0.5': !postOrComment }, labelColor);
const showUserNameAsLink = useShowLearnersTab()
&& linkToProfile && author && author !== intl.formatMessage(messages.anonymous);
const labelContents = (
-
+
{!alert && (
- {authorLabelMessage && (
-
- {authorLabelMessage}
-
- )}
+
- {
- postCreatedAt && (
-
- {timeago.format(postCreatedAt, 'time-locale')}
-
- )
- }
+ {authorLabelMessage && (
+
+ {authorLabelMessage}
+
+ )}
+ {postCreatedAt && (
+
+ {timeago.format(postCreatedAt, 'time-locale')}
+
+ )}
);
@@ -139,6 +139,7 @@ AuthorLabel.propTypes = {
alert: PropTypes.bool,
postCreatedAt: PropTypes.string,
authorToolTip: PropTypes.bool,
+ postOrComment: PropTypes.bool,
};
AuthorLabel.defaultProps = {
@@ -148,6 +149,7 @@ AuthorLabel.defaultProps = {
alert: false,
postCreatedAt: null,
authorToolTip: false,
+ postOrComment: false,
};
export default injectIntl(AuthorLabel);
diff --git a/src/discussions/common/EndorsedAlertBanner.jsx b/src/discussions/common/EndorsedAlertBanner.jsx
index 56382f29..0d6320be 100644
--- a/src/discussions/common/EndorsedAlertBanner.jsx
+++ b/src/discussions/common/EndorsedAlertBanner.jsx
@@ -4,7 +4,7 @@ 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 { Alert, Icon } from '@edx/paragon';
import { CheckCircle, Verified } from '@edx/paragon/icons';
import { ThreadType } from '../../data/constants';
@@ -27,18 +27,26 @@ function EndorsedAlertBanner({
content.endorsed && (
-
{intl.formatMessage(
- isQuestion
- ? messages.answer
- : messages.endorsed,
- )}
-
-
+
+
+ {intl.formatMessage(
+ isQuestion
+ ? messages.answer
+ : messages.endorsed,
+ )}
+
+
+
diff --git a/src/discussions/common/HoverCard.jsx b/src/discussions/common/HoverCard.jsx
index 708d4f9e..925c689d 100644
--- a/src/discussions/common/HoverCard.jsx
+++ b/src/discussions/common/HoverCard.jsx
@@ -33,14 +33,15 @@ function HoverCard({
return (
{userCanAddThreadInBlackoutDate && (
)}
-
+
+ {
+ e.preventDefault();
+ onLike();
+ }}
+ />
+
{commentOrPost.following !== undefined && (
{
e.preventDefault();
onFollow();
@@ -84,20 +98,9 @@ function HoverCard({
/>
)}
-
- {
- e.preventDefault();
- onLike();
- }}
- />
-
+
);
diff --git a/src/discussions/common/HoverCard.test.jsx b/src/discussions/common/HoverCard.test.jsx
new file mode 100644
index 00000000..532e9385
--- /dev/null
+++ b/src/discussions/common/HoverCard.test.jsx
@@ -0,0 +1,157 @@
+// import {
+// act, fireEvent, render, screen, waitFor, within,
+// } from '@testing-library/react';
+// import MockAdapter from 'axios-mock-adapter';
+// import { IntlProvider } from 'react-intl';
+// import { MemoryRouter, Route } from 'react-router';
+// import { Factory } from 'rosie';
+
+// import { camelCaseObject, initializeMockApp } from '@edx/frontend-platform';
+// import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+// import { AppProvider } from '@edx/frontend-platform/react';
+
+// import { initializeStore } from '../../store';
+// import { executeThunk } from '../../test-utils';
+// import { getCourseConfigApiUrl } from '../data/api';
+// import { fetchCourseConfig } from '../data/thunks';
+// import DiscussionContent from '../discussions-home/DiscussionContent';
+// import { getThreadsApiUrl } from '../posts/data/api';
+// import { fetchThreads } from '../posts/data/thunks';
+// import { getCommentsApiUrl } from './data/api';
+// import { DiscussionContext } from './context';
+
+// import '../posts/data/__factories__';
+// import './data/__factories__';
+
+// const courseConfigApiUrl = getCourseConfigApiUrl();
+// const commentsApiUrl = getCommentsApiUrl();
+// const threadsApiUrl = getThreadsApiUrl();
+// const discussionPostId = 'thread-1';
+// const questionPostId = 'thread-2';
+// const closedPostId = 'thread-2';
+// const courseId = 'course-v1:edX+TestX+Test_Course';
+// let store;
+// let axiosMock;
+// let testLocation;
+
+// function mockAxiosReturnPagedComments() {
+// [null, false, true].forEach(endorsed => {
+// const postId = endorsed === null ? discussionPostId : questionPostId;
+// [1, 2].forEach(page => {
+// axiosMock
+// .onGet(commentsApiUrl, {
+// params: {
+// thread_id: postId,
+// page,
+// page_size: undefined,
+// requested_fields: 'profile_image',
+// endorsed,
+// },
+// })
+// .reply(200, Factory.build('commentsResult', { can_delete: true }, {
+// threadId: postId,
+// page,
+// pageSize: 1,
+// count: 2,
+// endorsed,
+// childCount: page === 1 ? 2 : 0,
+// }));
+// });
+// });
+// }
+
+// function mockAxiosReturnPagedCommentsResponses() {
+// const parentId = 'comment-1';
+// const commentsResponsesApiUrl = `${commentsApiUrl}${parentId}/`;
+// const paramsTemplate = {
+// page: undefined,
+// page_size: undefined,
+// requested_fields: 'profile_image',
+// };
+
+// for (let page = 1; page <= 2; page++) {
+// axiosMock
+// .onGet(commentsResponsesApiUrl, { params: { ...paramsTemplate, page } })
+// .reply(200, Factory.build('commentsResult', null, {
+// parentId,
+// page,
+// pageSize: 1,
+// count: 2,
+// }));
+// }
+// }
+
+// function renderComponent(postId) {
+// render(
+//
+//
+//
+//
+//
+// {
+// testLocation = location;
+// return null;
+// }}
+// />
+//
+//
+//
+// ,
+// );
+// }
+
+// describe('HoverCard', () => {
+// beforeEach(() => {
+// initializeMockApp({
+// authenticatedUser: {
+// userId: 3,
+// username: 'abc123',
+// administrator: true,
+// roles: [],
+// },
+// });
+
+// store = initializeStore();
+// Factory.resetAll();
+// axiosMock = new MockAdapter(getAuthenticatedHttpClient());
+// axiosMock.onGet(threadsApiUrl)
+// .reply(200, Factory.build('threadsResult'));
+// axiosMock.onPatch(new RegExp(`${commentsApiUrl}*`)).reply(({
+// url,
+// data,
+// }) => {
+// const commentId = url.match(/comments\/(?[a-z1-9-]+)\//).groups.id;
+// const {
+// rawBody,
+// } = camelCaseObject(JSON.parse(data));
+// return [200, Factory.build('comment', {
+// id: commentId,
+// rendered_body: rawBody,
+// raw_body: rawBody,
+// })];
+// });
+// axiosMock.onPost(commentsApiUrl)
+// .reply(({ data }) => {
+// const {
+// rawBody,
+// threadId,
+// } = camelCaseObject(JSON.parse(data));
+// return [200, Factory.build(
+// 'comment',
+// {
+// rendered_body: rawBody,
+// raw_body: rawBody,
+// thread_id: threadId,
+// },
+// )];
+// });
+
+// executeThunk(fetchThreads(courseId), store.dispatch, store.getState);
+// mockAxiosReturnPagedComments();
+// mockAxiosReturnPagedCommentsResponses();
+// });
+// });
diff --git a/src/discussions/posts/post/LikeButton.jsx b/src/discussions/posts/post/LikeButton.jsx
index 30cb66ad..70b9ef37 100644
--- a/src/discussions/posts/post/LikeButton.jsx
+++ b/src/discussions/posts/post/LikeButton.jsx
@@ -2,7 +2,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-import { Icon, IconButtonWithTooltip } from '@edx/paragon';
+import {
+ Icon, IconButton, OverlayTrigger, Tooltip,
+} from '@edx/paragon';
import { ThumbUpFilled, ThumbUpOutline } from '../../../components/icons';
import messages from './messages';
@@ -12,7 +14,6 @@ function LikeButton({
intl,
onClick,
voted,
- preview,
}) {
const handleClick = (e) => {
e.preventDefault();
@@ -24,19 +25,27 @@ function LikeButton({
return (
-
- {(count && count > 0) ? count : null}
+
+ {intl.formatMessage(voted ? messages.removeLike : messages.like)}
+
+ )}
+ >
+
+
+
+ {(count && count > 0) ? count : null}
+
+
);
}
@@ -46,13 +55,11 @@ LikeButton.propTypes = {
intl: intlShape.isRequired,
onClick: PropTypes.func,
voted: PropTypes.bool,
- preview: PropTypes.bool,
};
LikeButton.defaultProps = {
voted: false,
onClick: undefined,
- preview: false,
};
export default injectIntl(LikeButton);
diff --git a/src/discussions/posts/post/Post.jsx b/src/discussions/posts/post/Post.jsx
index b7a5a9ea..d2470838 100644
--- a/src/discussions/posts/post/Post.jsx
+++ b/src/discussions/posts/post/Post.jsx
@@ -1,4 +1,4 @@
-import React, { useContext, useState } from 'react';
+import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
@@ -41,7 +41,6 @@ function Post({
const [isDeleting, showDeleteConfirmation, hideDeleteConfirmation] = useToggle(false);
const [isReporting, showReportConfirmation, hideReportConfirmation] = useToggle(false);
const [isClosing, showClosePostModal, hideClosePostModal] = useToggle(false);
- const [showHoverCard, setShowHoverCard] = useState(false);
const handleAbusedFlag = () => {
if (post.abuseFlagged) {
@@ -91,10 +90,8 @@ function Post({
return (
setShowHoverCard(true)}
- onMouseLeave={() => setShowHoverCard(false)}
+ className="d-flex flex-column w-100 mw-100 post-card-comment"
+ aria-level={5}
>
)}
- {showHoverCard && (
-
dispatch(updateExistingThread(post.id, { voted: !post.voted }))}
- onFollow={() => dispatch(updateExistingThread(post.id, { following: !post.following }))}
- isClosedPost={post.closed}
- />
- )}
+
+ dispatch(updateExistingThread(post.id, { voted: !post.voted }))}
+ onFollow={() => dispatch(updateExistingThread(post.id, { following: !post.following }))}
+ isClosedPost={post.closed}
+ />
+
-
-
+
+
{topicContext && topic && (
-
{intl.formatMessage(messages.relatedTo)}{' '}
+
{intl.formatMessage(messages.relatedTo)}{' '}
+
{post.voteCount !== 0 && (
dispatch(updateExistingThread(post.id, { voted: !post.voted }))}
voted={post.voted}
- preview={preview}
/>
)}
{post.following && (
- {
- e.preventDefault();
- dispatch(updateExistingThread(post.id, { following: !post.following }));
- return true;
- }}
- size={preview ? 'inline' : 'sm'}
- className={preview && 'p-3'}
- iconClassNames={preview && 'icon-size'}
- />
- )}
- {preview && post.commentCount > 1 && (
-
-
+ {intl.formatMessage(post.following ? messages.unFollow : messages.follow)}
+
+ )}
+ >
+ {
+ e.preventDefault();
+ dispatch(updateExistingThread(post.id, { following: !post.following }));
+ return true;
+ }}
iconAs={Icon}
- alt="Comment Count"
- size="inline"
- className="p-3 mr-0.5"
- iconClassNames="icon-size"
+ iconClassNames="follow-icon-dimentions"
+ className="post-footer-icon-dimentions"
+ alt="Follow"
/>
- {post.commentCount}
-
- )}
- {showNewCountLabel && preview && post?.unreadCommentCount > 0 && post.commentCount > 1 && (
-
- {intl.formatMessage(messages.newLabel, { count: post.unreadCommentCount })}
-
+
)}
{post.groupId && userHasModerationPrivileges && (
@@ -101,7 +79,7 @@ function PostFooter({
>
)}
- {!preview && post.closed
+ {post.closed
&& (
{
});
});
- it("shows 'x new' badge for new comments in case of read post only", () => {
- renderComponent(mockPost, true, true);
- expect(screen.getByText('2 New')).toBeTruthy();
- });
-
it("doesn't have 'new' badge when there are 0 new comments", () => {
renderComponent({ ...mockPost, unreadCommentCount: 0 });
expect(screen.queryByText('2 New')).toBeFalsy();
diff --git a/src/discussions/posts/post/PostHeader.jsx b/src/discussions/posts/post/PostHeader.jsx
index 2f2976c6..cf455872 100644
--- a/src/discussions/posts/post/PostHeader.jsx
+++ b/src/discussions/posts/post/PostHeader.jsx
@@ -108,13 +108,14 @@ function PostHeader({
&& {intl.formatMessage(messages.answered)}}
)
- : {post.title}
}
+ : {post.title}
}
diff --git a/src/index.scss b/src/index.scss
index 4703c01a..61e8a444 100755
--- a/src/index.scss
+++ b/src/index.scss
@@ -45,6 +45,14 @@ $fa-font-path: "~font-awesome/fonts";
font-size: 14px;
}
+.font-size-12 {
+ font-size: 12px;
+}
+
+.font-size-8 {
+ font-size: 8px;
+}
+
.font-weight-500 {
font-weight: 500;
}
@@ -57,9 +65,24 @@ $fa-font-path: "~font-awesome/fonts";
font-family: "Inter";
}
-.icon-size {
- height: 15px !important;
- width: 15px !important;
+.post-footer-icon-dimentions {
+ width: 32px !important;
+ height: 32px !important;
+}
+
+.like-icon-dimentions {
+ width: 21px !important;
+ height: 23px !important;
+}
+
+.follow-icon-dimentions {
+ width: 21px !important;
+ height: 24px !important;
+}
+
+.dropdown-icon-dimentions {
+ width: 20px !important;
+ height: 21px !important;
}
.post-summary-icons-dimensions {
@@ -67,11 +90,6 @@ $fa-font-path: "~font-awesome/fonts";
width: 16px !important;
}
-.footer-icons-dimensions {
- height: 16px !important;
- width: 16px !important;
-}
-
.post-summary-timestamp {
font-size: 12px !important;
line-height: 20px !important;
@@ -155,6 +173,14 @@ $fa-font-path: "~font-awesome/fonts";
padding-bottom: 8px;
}
+.pb-10px {
+ padding-bottom: 10px;
+}
+
+.pt-10px {
+ padding-top: 10px !important;
+}
+
.px-10px {
padding-left: 10px;
padding-right: 10px;
@@ -357,7 +383,7 @@ header {
}
.post-card-padding {
- padding: 24px 24px 6px 24px;
+ padding: 24px 24px 10px 24px;
}
.post-card-margin {
@@ -381,7 +407,9 @@ header {
}
.hover-button:hover {
- background-color: #F2F0EF;
+ background-color: #F2F0EF !important;
+ height: 36px;
+ border: none;
}
.btn-tertiary:hover {
@@ -393,14 +421,26 @@ header {
background-color: transparent;
}
-.comment-card-padding {
- margin: 24px 24px 0px 24px;
-}
-
.disable-div {
pointer-events: none;
}
-[role=listitem]:focus {
- border: 2px solid black;
+.on-focus:focus {
+ outline: 2px solid black;
+}
+
+.html-loader p:last-child {
+ margin-bottom: 0px;
+}
+
+.post-card-comment:hover,
+.post-card-comment:focus {
+ .hover-card {
+ display: flex !important;
+ }
+}
+
+.spinner-dimentions {
+ height: 24px;
+ width: 24px;
}