@@ -188,4 +202,14 @@ const Add = React.memo(() => {
);
});
+Add.propTypes = {
+ initialText: PropTypes.string,
+ onInitialTextConsumed: PropTypes.func,
+};
+
+Add.defaultProps = {
+ initialText: undefined,
+ onInitialTextConsumed: () => {},
+};
+
export default Add;
diff --git a/client/src/components/comments/Comments/Comments.jsx b/client/src/components/comments/Comments/Comments.jsx
index 6ee36f24..f64f6708 100755
--- a/client/src/components/comments/Comments/Comments.jsx
+++ b/client/src/components/comments/Comments/Comments.jsx
@@ -3,7 +3,7 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
-import React, { useMemo } from 'react';
+import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useInView } from 'react-intersection-observer';
import { Comment, Loader } from 'semantic-ui-react';
@@ -45,6 +45,15 @@ const Comments = React.memo(() => {
});
const dispatch = useDispatch();
+ const [replyText, setReplyText] = useState();
+
+ const handleReply = useCallback((username) => {
+ setReplyText(`@${username} `);
+ }, []);
+
+ const handleReplyTextConsumed = useCallback(() => {
+ setReplyText(undefined);
+ }, []);
const [inViewRef] = useInView({
threshold: 1,
@@ -57,11 +66,11 @@ const Comments = React.memo(() => {
return (
<>
- {cadAdd &&
}
+ {cadAdd &&
}
{commentIds.map((commentId) => (
-
+
))}
diff --git a/client/src/components/comments/Comments/Item.jsx b/client/src/components/comments/Comments/Item.jsx
index 56f32139..bd656289 100755
--- a/client/src/components/comments/Comments/Item.jsx
+++ b/client/src/components/comments/Comments/Item.jsx
@@ -25,7 +25,7 @@ import UserAvatar from '../../users/UserAvatar';
import styles from './Item.module.scss';
-const Item = React.memo(({ id }) => {
+const Item = React.memo(({ id, canReply, onReply }) => {
const selectCommentById = useMemo(() => selectors.makeSelectCommentById(), []);
const selectUserById = useMemo(() => selectors.makeSelectUserById(), []);
const selectListById = useMemo(() => selectors.makeSelectListById(), []);
@@ -83,6 +83,12 @@ const Item = React.memo(({ id }) => {
setIsEditOpened(true);
}, []);
+ const handleReplyClick = useCallback(() => {
+ if (user.username) {
+ onReply(user.username);
+ }
+ }, [onReply, user.username]);
+
const handleEditClose = useCallback(() => {
setIsEditOpened(false);
}, []);
@@ -117,8 +123,18 @@ const Item = React.memo(({ id }) => {
- {(canEdit || canDelete) && (
+ {(canReply || canEdit || canDelete) && (
+ {canReply && user.username && (
+
+ )}
{canEdit && (
{
Item.propTypes = {
id: PropTypes.string.isRequired,
+ canReply: PropTypes.bool,
+ onReply: PropTypes.func,
+};
+
+Item.defaultProps = {
+ canReply: false,
+ onReply: () => {},
};
export default Item;
diff --git a/client/src/components/common/EditMarkdown/EditMarkdown.jsx b/client/src/components/common/EditMarkdown/EditMarkdown.jsx
index f5028dfd..c4c586eb 100755
--- a/client/src/components/common/EditMarkdown/EditMarkdown.jsx
+++ b/client/src/components/common/EditMarkdown/EditMarkdown.jsx
@@ -3,8 +3,9 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
-import React, { useCallback, useRef, useState } from 'react';
+import React, { useCallback, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types';
+import keyBy from 'lodash/keyBy';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Button, Form } from 'semantic-ui-react';
@@ -13,6 +14,7 @@ import { useClickAwayListener } from '../../../lib/hooks';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
import { useNestedRef } from '../../../hooks';
+import { mentionMarkupToText, mentionTextToMarkup } from '../../../utils/mentions';
import MarkdownEditor from '../MarkdownEditor';
import styles from './EditMarkdown.module.scss';
@@ -21,10 +23,20 @@ const MAX_LENGTH = 1048576;
const EditMarkdown = React.memo(({ defaultValue, draftValue, onUpdate, onClose }) => {
const defaultMode = useSelector((state) => selectors.selectCurrentUser(state).defaultEditorMode);
+ const boardMemberships = useSelector(selectors.selectMembershipsForCurrentBoard);
+
+ const userByUsername = useMemo(
+ () =>
+ keyBy(
+ boardMemberships.map(({ user }) => user),
+ (user) => user.username.toLowerCase(),
+ ),
+ [boardMemberships],
+ );
const dispatch = useDispatch();
const [t] = useTranslation();
- const [value, setValue] = useState(() => draftValue || defaultValue || '');
+ const [value, setValue] = useState(() => mentionMarkupToText(draftValue || defaultValue || ''));
const fieldRef = useRef(null);
const [submitButtonRef, handleSubmitButtonRef] = useNestedRef();
@@ -44,14 +56,15 @@ const EditMarkdown = React.memo(({ defaultValue, draftValue, onUpdate, onClose }
const isExceeded = value.length > MAX_LENGTH;
const submit = useCallback(() => {
- const cleanValue = value.trim() || null;
+ const valueWithMarkup = mentionTextToMarkup(value, userByUsername);
+ const cleanValue = valueWithMarkup.trim() || null;
if (!isExceeded && cleanValue !== defaultValue) {
onUpdate(cleanValue);
}
onClose(isExceeded ? cleanValue : null);
- }, [onUpdate, onClose, defaultValue, value, isExceeded]);
+ }, [onUpdate, onClose, defaultValue, value, isExceeded, userByUsername]);
const handleChange = useCallback((nextValue) => {
setValue(nextValue);