)}
diff --git a/src/discussions/common/AuthorLabel.jsx b/src/discussions/common/AuthorLabel.jsx
index 6a4e67c1..45c2e03a 100644
--- a/src/discussions/common/AuthorLabel.jsx
+++ b/src/discussions/common/AuthorLabel.jsx
@@ -3,9 +3,10 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Link, useLocation } from 'react-router-dom';
+import * as timeago from 'timeago.js';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-import { Icon } from '@edx/paragon';
+import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon';
import { Institution, School } from '@edx/paragon/icons';
import { Routes } from '../../data/constants';
@@ -13,6 +14,7 @@ import { useShowLearnersTab } from '../data/hooks';
import messages from '../messages';
import { discussionsPath } from '../utils';
import { DiscussionContext } from './context';
+import timeLocale from './time-locale';
function AuthorLabel({
intl,
@@ -21,11 +23,15 @@ function AuthorLabel({
linkToProfile,
labelColor,
alert,
+ postCreatedAt,
+ authorToolTip,
+ postOrComment,
}) {
const location = useLocation();
const { courseId } = useContext(DiscussionContext);
let icon = null;
let authorLabelMessage = null;
+ timeago.register('time-locale', timeLocale);
if (authorLabel === 'Staff') {
icon = Institution;
@@ -37,37 +43,56 @@ function AuthorLabel({
}
const isRetiredUser = author ? author.startsWith('retired__user') : false;
+ const showTextPrimary = !authorLabelMessage && !isRetiredUser && !alert;
- 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);
+ && linkToProfile && author && author !== intl.formatMessage(messages.anonymous);
const labelContents = (
-
-
- {isRetiredUser ? '[Deactivated]' : author }
-
- {icon && (
-
+
+ {!alert && (
+
+ {isRetiredUser ? '[Deactivated]' : author}
+
)}
+
+
+ {author}
+
+ )}
+
+ trigger={['hover', 'focus']}
+ >
+
+
+
+
+
{authorLabelMessage && (
)}
+ {postCreatedAt && (
+
+ {timeago.format(postCreatedAt, 'time-locale')}
+
+ )}
+
);
@@ -100,6 +138,9 @@ AuthorLabel.propTypes = {
linkToProfile: PropTypes.bool,
labelColor: PropTypes.string,
alert: PropTypes.bool,
+ postCreatedAt: PropTypes.string,
+ authorToolTip: PropTypes.bool,
+ postOrComment: PropTypes.bool,
};
AuthorLabel.defaultProps = {
@@ -107,6 +148,9 @@ AuthorLabel.defaultProps = {
authorLabel: null,
labelColor: '',
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 3e830412..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,32 +27,35 @@ function EndorsedAlertBanner({
content.endorsed && (
-
{intl.formatMessage(
- isQuestion
- ? messages.answer
- : messages.endorsed,
- )}
-
-
-
- {intl.formatMessage(
- isQuestion
- ? messages.answeredLabel
- : messages.endorsedLabel,
- )}
-
+
+
+ {intl.formatMessage(
+ isQuestion
+ ? messages.answer
+ : messages.endorsed,
+ )}
+
+
+
- {intl.formatMessage(messages.time, { time: timeago.format(content.endorsedAt, 'time-locale') })}
diff --git a/src/discussions/common/EndorsedAlertBanner.test.jsx b/src/discussions/common/EndorsedAlertBanner.test.jsx
index b9ab9b5b..11a10595 100644
--- a/src/discussions/common/EndorsedAlertBanner.test.jsx
+++ b/src/discussions/common/EndorsedAlertBanner.test.jsx
@@ -46,21 +46,21 @@ describe.each([
type: 'comment',
postType: ThreadType.QUESTION,
props: { endorsed: true, endorsedBy: 'test-user', endorsedByLabel: 'Staff' },
- expectText: [messages.answer.defaultMessage, messages.answeredLabel.defaultMessage, 'test-user', 'Staff'],
+ expectText: [messages.answer.defaultMessage, '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'],
+ expectText: [messages.answer.defaultMessage, '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'],
+ expectText: [messages.endorsed.defaultMessage],
},
])('EndorsedAlertBanner', ({
label, type, postType, props, expectText,
diff --git a/src/discussions/common/HoverCard.jsx b/src/discussions/common/HoverCard.jsx
new file mode 100644
index 00000000..62901051
--- /dev/null
+++ b/src/discussions/common/HoverCard.jsx
@@ -0,0 +1,123 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+
+import classNames from 'classnames';
+
+import { injectIntl } from '@edx/frontend-platform/i18n';
+import {
+ Button,
+ Icon, IconButton,
+} from '@edx/paragon';
+
+import {
+ StarFilled, StarOutline, ThumbUpFilled, ThumbUpOutline,
+} from '../../components/icons';
+import { commentShape } from '../comments/comment/proptypes';
+import { useUserCanAddThreadInBlackoutDate } from '../data/hooks';
+import { postShape } from '../posts/post/proptypes';
+import ActionsDropdown from './ActionsDropdown';
+import { DiscussionContext } from './context';
+
+function HoverCard({
+ commentOrPost,
+ actionHandlers,
+ handleResponseCommentButton,
+ addResponseCommentButtonMessage,
+ onLike,
+ onFollow,
+ isClosedPost,
+ endorseIcons,
+}) {
+ const { enableInContextSidebar } = useContext(DiscussionContext);
+ const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
+
+ return (
+
+ {userCanAddThreadInBlackoutDate && (
+
+
+
+ )}
+
+ {endorseIcons && (
+
+ {
+ const actionFunction = actionHandlers[endorseIcons.action];
+ actionFunction();
+ }}
+ className={['endorse', 'unendorse'].includes(endorseIcons.id) ? 'text-dark-500' : 'text-success-500'}
+ size="sm"
+ alt="Endorse"
+ />
+
+ )}
+
+ {
+ e.preventDefault();
+ onLike();
+ }}
+ />
+
+ {commentOrPost.following !== undefined && (
+
+ {
+ e.preventDefault();
+ onFollow();
+ return true;
+ }}
+ />
+
+ )}
+
+
+ );
+}
+
+HoverCard.propTypes = {
+ commentOrPost: PropTypes.oneOfType([commentShape, postShape]).isRequired,
+ actionHandlers: PropTypes.objectOf(PropTypes.func).isRequired,
+ handleResponseCommentButton: PropTypes.func.isRequired,
+ onLike: PropTypes.func.isRequired,
+ onFollow: PropTypes.func,
+ addResponseCommentButtonMessage: PropTypes.string.isRequired,
+ isClosedPost: PropTypes.bool.isRequired,
+ endorseIcons: PropTypes.objectOf(PropTypes.any),
+};
+
+HoverCard.defaultProps = {
+ onFollow: () => null,
+ endorseIcons: null,
+};
+
+export default injectIntl(HoverCard);
diff --git a/src/discussions/common/HoverCard.test.jsx b/src/discussions/common/HoverCard.test.jsx
new file mode 100644
index 00000000..4976b1d5
--- /dev/null
+++ b/src/discussions/common/HoverCard.test.jsx
@@ -0,0 +1,194 @@
+import {
+ render, screen, waitFor, within,
+} from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+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 { getCommentsApiUrl } from '../comments/data/api';
+import DiscussionContent from '../discussions-home/DiscussionContent';
+import { getThreadsApiUrl } from '../posts/data/api';
+import { fetchThreads } from '../posts/data/thunks';
+import { DiscussionContext } from './context';
+
+import '../posts/data/__factories__';
+import '../comments/data/__factories__';
+
+const commentsApiUrl = getCommentsApiUrl();
+const threadsApiUrl = getThreadsApiUrl();
+const discussionPostId = 'thread-1';
+const questionPostId = 'thread-2';
+const courseId = 'course-v1:edX+TestX+Test_Course';
+let store;
+let axiosMock;
+let container;
+
+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) {
+ const wrapper = render(
+
+
+
+
+
+
+
+
+
+ ,
+ );
+ container = wrapper.container;
+ return container;
+}
+
+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();
+ });
+
+ test('it should show hover card when hovered on post', async () => {
+ renderComponent(discussionPostId);
+ const post = screen.getByTestId('post-thread-1');
+ userEvent.hover(post);
+ expect(screen.getByTestId('hover-card')).toBeInTheDocument();
+ });
+
+ test('it should show hover card when hovered on comment', async () => {
+ renderComponent(discussionPostId);
+ const comment = await waitFor(() => screen.findByTestId('comment-1'));
+ userEvent.hover(comment);
+ expect(screen.getByTestId('hover-card')).toBeInTheDocument();
+ });
+
+ test('it should not show hover card when post and comment not hovered', async () => {
+ renderComponent(discussionPostId);
+ expect(screen.queryByTestId('hover-card')).not.toBeInTheDocument();
+ });
+
+ test('it should show add response, like, follow and actions menu for hovered post', async () => {
+ renderComponent(discussionPostId);
+ const post = screen.getByTestId('post-thread-1');
+ userEvent.hover(post);
+ const view = screen.getByTestId('hover-card');
+ expect(within(view).queryByRole('button', { name: /Add response/i })).toBeInTheDocument();
+ expect(within(view).getByRole('button', { name: /like/i })).toBeInTheDocument();
+ expect(within(view).queryByRole('button', { name: /follow/i })).toBeInTheDocument();
+ expect(within(view).queryByRole('button', { name: /actions menu/i })).toBeInTheDocument();
+ });
+
+ test('it should show add comment, Endorse, like and actions menu Buttons for hovered comment', async () => {
+ renderComponent(questionPostId);
+ const comment = await waitFor(() => screen.findByTestId('comment-3'));
+ userEvent.hover(comment);
+ const view = screen.getByTestId('hover-card');
+ expect(screen.getByTestId('hover-card')).toBeInTheDocument();
+ expect(within(view).queryByRole('button', { name: /Add comment/i })).toBeInTheDocument();
+ expect(within(view).getByRole('button', { name: /Endorse/i })).toBeInTheDocument();
+ expect(within(view).queryByRole('button', { name: /like/i })).toBeInTheDocument();
+ expect(within(view).queryByRole('button', { name: /actions menu/i })).toBeInTheDocument();
+ });
+});
diff --git a/src/discussions/posts/post-editor/messages.js b/src/discussions/posts/post-editor/messages.js
index 9d06316d..c2691aeb 100644
--- a/src/discussions/posts/post-editor/messages.js
+++ b/src/discussions/posts/post-editor/messages.js
@@ -113,7 +113,7 @@ const messages = defineMessages({
},
showPreviewButton: {
id: 'discussions.editor.posts.showPreview.button',
- defaultMessage: 'Show Preview',
+ defaultMessage: 'Show preview',
description: 'show preview button text to allow user to see their post content.',
},
actionsAlt: {
diff --git a/src/discussions/posts/post/LikeButton.jsx b/src/discussions/posts/post/LikeButton.jsx
index 51bfaf06..fa9b9392 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();
@@ -23,20 +24,27 @@ function LikeButton({
};
return (
-
-
- {(count && count > 0) ? count : null}
+
+
+ {intl.formatMessage(voted ? messages.removeLike : messages.like)}
+
+ )}
+ >
+
+
+
+ {(count && count > 0) ? count : null}
+
+
);
}
@@ -46,13 +54,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 18e5f4e5..8b972a3b 100644
--- a/src/discussions/posts/post/Post.jsx
+++ b/src/discussions/posts/post/Post.jsx
@@ -1,4 +1,4 @@
-import React, { useContext } from 'react';
+import React, { useContext, useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
@@ -13,6 +13,7 @@ import { ContentActions } from '../../../data/constants';
import { selectorForUnitSubsection, selectTopicContext } from '../../../data/selectors';
import { AlertBanner, Confirmation } from '../../common';
import { DiscussionContext } from '../../common/context';
+import HoverCard from '../../common/HoverCard';
import { selectModerationSettings } from '../../data/selectors';
import { selectTopic } from '../../topics/data/selectors';
import { removeThread, updateExistingThread } from '../data/thunks';
@@ -26,6 +27,7 @@ function Post({
post,
preview,
intl,
+ handleAddResponseButton,
}) {
const location = useLocation();
const history = useHistory();
@@ -39,7 +41,7 @@ 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) {
dispatch(updateExistingThread(post.id, { flagged: !post.abuseFlagged }));
@@ -62,6 +64,12 @@ function Post({
hideReportConfirmation();
};
+ const handleHoverCardBlurEvent = (e) => {
+ if (!e.currentTarget.contains(e.relatedTarget)) {
+ setShowHoverCard(false);
+ }
+ };
+
const actionHandlers = {
[ContentActions.EDIT_CONTENT]: () => history.push({
...location,
@@ -87,7 +95,15 @@ function Post({
);
return (
-
+
setShowHoverCard(true)}
+ onMouseLeave={() => setShowHoverCard(false)}
+ onFocus={() => setShowHoverCard(true)}
+ onBlur={(e) => handleHoverCardBlurEvent(e)}
+ >
)}
+ {showHoverCard && (
+
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)}{' '}
)}
-
+
- dispatch(updateExistingThread(post.id, { voted: !post.voted }))}
- voted={post.voted}
- preview={preview}
- />
- {
- 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 && (
-
-
- {post.commentCount}
-
+
+ {post.voteCount !== 0 && (
+
dispatch(updateExistingThread(post.id, { voted: !post.voted }))}
+ voted={post.voted}
+ />
)}
- {showNewCountLabel && preview && post?.unreadCommentCount > 0 && post.commentCount > 1 && (
-
- {intl.formatMessage(messages.newLabel, { count: post.unreadCommentCount })}
-
+ {post.following && (
+
+ {intl.formatMessage(post.following ? messages.unFollow : messages.follow)}
+
+ )}
+ >
+ {
+ e.preventDefault();
+ dispatch(updateExistingThread(post.id, { following: !post.following }));
+ return true;
+ }}
+ iconAs={Icon}
+ iconClassNames="follow-icon-dimentions"
+ className="post-footer-icon-dimentions"
+ alt="Follow"
+ />
+
)}
{post.groupId && userHasModerationPrivileges && (
@@ -100,10 +78,8 @@ function PostFooter({
>
)}
-
- {timeago.format(post.createdAt, 'time-locale')}
-
- {!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();
@@ -89,18 +84,24 @@ describe('PostFooter', () => {
expect(screen.getByTestId('cohort-icon')).toBeTruthy();
});
- it.each([[true, /unfollow/i], [false, /follow/i]])('test follow button when following=%s', async (following, message) => {
- renderComponent({ ...mockPost, following });
+ it('test follow button when following=true', async () => {
+ renderComponent({ ...mockPost, following: true });
const followButton = screen.getByRole('button', { name: /follow/i });
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
await act(async () => {
fireEvent.mouseEnter(followButton);
});
- expect(screen.getByRole('tooltip')).toHaveTextContent(message);
+
+ expect(screen.getByRole('tooltip')).toHaveTextContent(/unfollow/i);
await act(async () => {
fireEvent.click(followButton);
});
// clicking on the button triggers thread update.
expect(store.getState().threads.status === RequestStatus.IN_PROGRESS).toBeTruthy();
});
+
+ it('test follow button when following=false', async () => {
+ renderComponent({ ...mockPost, following: false });
+ expect(screen.queryByRole('button', { name: /follow/i })).not.toBeInTheDocument();
+ });
});
diff --git a/src/discussions/posts/post/PostHeader.jsx b/src/discussions/posts/post/PostHeader.jsx
index e0915861..cf455872 100644
--- a/src/discussions/posts/post/PostHeader.jsx
+++ b/src/discussions/posts/post/PostHeader.jsx
@@ -9,7 +9,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 { AuthorLabel } from '../../common';
import { useAlertBannerVisible } from '../../data/hooks';
import { selectAuthorAvatars } from '../data/selectors';
import messages from './messages';
@@ -24,7 +24,7 @@ export function PostAvatar({
const avatarSize = useMemo(() => {
let size = '2rem';
if (post.type === ThreadType.DISCUSSION && !fromPostLink) {
- size = '2.375rem';
+ size = '2rem';
} else if (post.type === ThreadType.QUESTION) {
size = '1.5rem';
}
@@ -52,11 +52,11 @@ export function PostAvatar({
/>
)}
+
@@ -109,21 +108,17 @@ function PostHeader({
&&
{intl.formatMessage(messages.answered)}}
)
- : {post.title}
}
+ : {post.title}
}
- {!preview
- && (
-
- )}
);
}
@@ -132,7 +127,6 @@ PostHeader.propTypes = {
intl: intlShape.isRequired,
post: postShape.isRequired,
preview: PropTypes.bool,
- actionHandlers: PropTypes.objectOf(PropTypes.func).isRequired,
};
PostHeader.defaultProps = {
diff --git a/src/discussions/posts/post/messages.js b/src/discussions/posts/post/messages.js
index f47f1612..f095e1fb 100644
--- a/src/discussions/posts/post/messages.js
+++ b/src/discussions/posts/post/messages.js
@@ -6,6 +6,11 @@ const messages = defineMessages({
defaultMessage: 'anonymous',
description: 'Author name displayed when a post is anonymous',
},
+ addResponse: {
+ id: 'discussions.post.addResponse',
+ defaultMessage: 'Add response',
+ description: 'Button to add a response in a thread of forum posts',
+ },
lastResponse: {
id: 'discussions.post.lastResponse',
defaultMessage: 'Last response {time}',
diff --git a/src/discussions/utils.js b/src/discussions/utils.js
index bb1c5587..4c4f8cc0 100644
--- a/src/discussions/utils.js
+++ b/src/discussions/utils.js
@@ -185,7 +185,6 @@ export function useActions(content) {
.every(condition => condition === true)
: true
);
-
return ACTIONS_LIST.filter(
({
action,
@@ -295,3 +294,7 @@ export function handleKeyDown(event) {
selectedOption.focus();
}
}
+
+export function isLastElementOfList(list, element) {
+ return list[list.length - 1] === element;
+}
diff --git a/src/index.scss b/src/index.scss
index 9a9c6c74..cae205c0 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: 20px !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 {
@@ -77,6 +100,20 @@ $fa-font-path: "~font-awesome/fonts";
border-right-style: solid;
}
+.my-14px {
+ margin-top: 14px;
+ margin-bottom: 14px;
+}
+
+.my-10px {
+ margin-top: 10px;
+ margin-bottom: 10px;
+}
+
+.mb-14px {
+ margin-bottom: 14px;
+}
+
.mr-0\.5 {
margin-right: 2px;
}
@@ -93,6 +130,26 @@ $fa-font-path: "~font-awesome/fonts";
margin-left: 2px;
}
+.mt-14px {
+ margin-top: 14px;
+}
+
+.mb-10px {
+ margin-bottom: 10px;
+}
+
+.mt-10px {
+ margin-top: 10px;
+}
+
+.mt-17px {
+ margin-top: 17px !important;
+}
+
+.mr-36px {
+ margin-right: 36.6px;
+}
+
.badge-padding {
padding-top: 1px;
padding-bottom: 1px
@@ -102,7 +159,7 @@ $fa-font-path: "~font-awesome/fonts";
background-color: unset !important;
}
-.learner > a:hover {
+.learner>a:hover {
background-color: #F2F0EF;
}
@@ -111,14 +168,27 @@ $fa-font-path: "~font-awesome/fonts";
padding-bottom: 10px;
}
+.py-8px {
+ padding-top: 8px;
+ padding-bottom: 8px;
+}
+
+.pb-10px {
+ padding-bottom: 10px;
+}
+
+.pt-10px {
+ padding-top: 10px !important;
+}
+
.px-10px {
padding-left: 10px;
padding-right: 10px;
}
.question-icon-size {
- width: 1.625rem;
- height: 1.625rem;
+ width: 1.4581rem;
+ height: 1.4581rem;
}
.question-icon-position {
@@ -134,6 +204,7 @@ $fa-font-path: "~font-awesome/fonts";
header {
.logo {
margin-right: 1rem;
+
img {
height: 1.75rem;
}
@@ -142,6 +213,7 @@ header {
#learner-posts-link {
color: inherit;
+
span[role=heading]:hover {
text-decoration: underline;
}
@@ -170,11 +242,12 @@ header {
}
}
-.pointer-cursor-hover :hover{
+.pointer-cursor-hover :hover {
cursor: pointer;
}
-.filter-bar:focus-visible, .filter-bar:focus {
+.filter-bar:focus-visible,
+.filter-bar:focus {
outline: none;
}
@@ -198,9 +271,9 @@ header {
};
};
- .container-xl{
+ .container-xl {
.course-title-lockup {
- font-size: 1.125 rem;
+ font-size: 1.125rem;
};
.logo {
@@ -221,7 +294,7 @@ header {
.container-xl {
padding-left: 31px;
- font-size: 1.125 rem;
+ font-size: 1.125rem;
.nav {
line-height: 28px;
@@ -239,7 +312,7 @@ header {
.header-action-bar {
background-color: #fff;
- z-index: 1;
+ z-index: 2;
box-shadow: 0px 2px 4px rgb(0 0 0 / 15%), 0px 2px 8px rgb(0 0 0 / 15%);
position: sticky;
top: 0;
@@ -250,10 +323,10 @@ header {
}
.actions-dropdown {
- z-index: 0;
+ z-index: 1;
}
-.discussion-topic-group:last-of-type .divider{
+.discussion-topic-group:last-of-type .divider {
display: none;
}
@@ -269,6 +342,12 @@ header {
z-index: 0;
}
+.btn-icon.btn-icon-primary:hover {
+ background-color: #F2F0EF !important;
+ color: #00262B !important
+}
+
+
@media only screen and (max-width: 767px) {
body:not(.tox-force-desktop) .tox .tox-dialog {
align-self: center;
@@ -286,3 +365,66 @@ header {
.pgn__checkpoint {
max-width: 340px !important;
}
+
+.post-card-padding {
+ padding: 24px 24px 10px 24px;
+}
+
+.post-card-margin {
+ margin: 24px 24px 0px 24px;
+}
+
+.hover-card {
+ height: 36px;
+ box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.15), 0px 4px 10px rgba(0, 0, 0, 0.15);
+ border-radius: 3px;
+ background: #FFFFFF;
+ max-width: fit-content;
+ margin-left: auto;
+ margin-top: -2.063rem;
+ z-index: 1;
+ right: 32px;
+}
+
+.response-editor-position {
+ margin-top: 50px !important;
+}
+
+.hover-button:hover {
+ background-color: #F2F0EF !important;
+ height: 36px;
+ border: none;
+}
+
+.btn-tertiary:hover {
+ background-color: #F2F0EF;
+}
+
+.btn-tertiary:disabled {
+ color: #454545;
+ background-color: transparent;
+}
+
+.disable-div {
+ pointer-events: none;
+}
+
+.on-focus:focus-visible {
+ 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;
+}