mirror of
https://github.com/plankanban/planka.git
synced 2025-12-19 17:23:27 +03:00
fix: Improve mentions behavior
This commit is contained in:
24
client/patches/react-mentions+4.4.10.patch
Normal file
24
client/patches/react-mentions+4.4.10.patch
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
diff --git a/node_modules/react-mentions/dist/react-mentions.esm.js b/node_modules/react-mentions/dist/react-mentions.esm.js
|
||||||
|
index 2efebba..b244446 100644
|
||||||
|
--- a/node_modules/react-mentions/dist/react-mentions.esm.js
|
||||||
|
+++ b/node_modules/react-mentions/dist/react-mentions.esm.js
|
||||||
|
@@ -1426,7 +1426,7 @@ var MentionsInput = /*#__PURE__*/function (_React$Component) {
|
||||||
|
|
||||||
|
var mentions = getMentions(newValue, config);
|
||||||
|
|
||||||
|
- if (ev.nativeEvent.isComposing && selectionStart === selectionEnd) {
|
||||||
|
+ if ((ev.nativeEvent.isComposing || newValue.length < value.length) && selectionStart === selectionEnd) {
|
||||||
|
_this.updateMentionsQueries(_this.inputElement.value, selectionStart);
|
||||||
|
} // Propagate change
|
||||||
|
// let handleChange = this.getOnChange(this.props) || emptyFunction;
|
||||||
|
@@ -1454,7 +1454,9 @@ var MentionsInput = /*#__PURE__*/function (_React$Component) {
|
||||||
|
var el = _this.inputElement;
|
||||||
|
|
||||||
|
if (ev.target.selectionStart === ev.target.selectionEnd) {
|
||||||
|
- _this.updateMentionsQueries(el.value, ev.target.selectionStart);
|
||||||
|
+ requestAnimationFrame(function () {
|
||||||
|
+ _this.updateMentionsQueries(el.value, ev.target.selectionStart);
|
||||||
|
+ });
|
||||||
|
} else {
|
||||||
|
_this.clearSuggestions();
|
||||||
|
} // sync highlighters scroll position
|
||||||
@@ -3,7 +3,8 @@
|
|||||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useState, useRef } from 'react';
|
import keyBy from 'lodash/keyBy';
|
||||||
|
import React, { useCallback, useState, useRef, useMemo } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Mention, MentionsInput } from 'react-mentions';
|
import { Mention, MentionsInput } from 'react-mentions';
|
||||||
@@ -13,6 +14,7 @@ import { useClickAwayListener, useDidUpdate, useToggle } from '../../../lib/hook
|
|||||||
import selectors from '../../../selectors';
|
import selectors from '../../../selectors';
|
||||||
import entryActions from '../../../entry-actions';
|
import entryActions from '../../../entry-actions';
|
||||||
import { useEscapeInterceptor, useForm, useNestedRef } from '../../../hooks';
|
import { useEscapeInterceptor, useForm, useNestedRef } from '../../../hooks';
|
||||||
|
import { isUsernameChar, mentionTextToMarkup } from '../../../utils/mentions';
|
||||||
import { isModifierKeyPressed } from '../../../utils/event-helpers';
|
import { isModifierKeyPressed } from '../../../utils/event-helpers';
|
||||||
import UserAvatar from '../../users/UserAvatar';
|
import UserAvatar from '../../users/UserAvatar';
|
||||||
|
|
||||||
@@ -36,10 +38,19 @@ const Add = React.memo(() => {
|
|||||||
const textInputRef = useRef(null);
|
const textInputRef = useRef(null);
|
||||||
const [buttonRef, handleButtonRef] = useNestedRef();
|
const [buttonRef, handleButtonRef] = useNestedRef();
|
||||||
|
|
||||||
|
const userByUsername = useMemo(
|
||||||
|
() =>
|
||||||
|
keyBy(
|
||||||
|
boardMemberships.flatMap(({ user }) => (user.username ? user : [])),
|
||||||
|
'username',
|
||||||
|
),
|
||||||
|
[boardMemberships],
|
||||||
|
);
|
||||||
|
|
||||||
const submit = useCallback(() => {
|
const submit = useCallback(() => {
|
||||||
const cleanData = {
|
const cleanData = {
|
||||||
...data,
|
...data,
|
||||||
text: data.text.trim(),
|
text: mentionTextToMarkup(data.text.trim(), userByUsername),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!cleanData.text) {
|
if (!cleanData.text) {
|
||||||
@@ -50,7 +61,7 @@ const Add = React.memo(() => {
|
|||||||
dispatch(entryActions.createCommentInCurrentCard(cleanData));
|
dispatch(entryActions.createCommentInCurrentCard(cleanData));
|
||||||
setData(DEFAULT_DATA);
|
setData(DEFAULT_DATA);
|
||||||
selectTextField();
|
selectTextField();
|
||||||
}, [dispatch, data, setData, selectTextField]);
|
}, [dispatch, data, setData, selectTextField, userByUsername]);
|
||||||
|
|
||||||
const handleEscape = useCallback(() => {
|
const handleEscape = useCallback(() => {
|
||||||
if (textMentionsRef.current.isOpened()) {
|
if (textMentionsRef.current.isOpened()) {
|
||||||
@@ -76,10 +87,10 @@ const Add = React.memo(() => {
|
|||||||
const handleFieldChange = useCallback(
|
const handleFieldChange = useCallback(
|
||||||
(_, text) => {
|
(_, text) => {
|
||||||
setData({
|
setData({
|
||||||
text,
|
text: !isUsernameChar(text.slice(-1)) ? mentionTextToMarkup(text, userByUsername) : text,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setData],
|
[setData, userByUsername],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFieldKeyDown = useCallback(
|
const handleFieldKeyDown = useCallback(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { dequal } from 'dequal';
|
import { dequal } from 'dequal';
|
||||||
|
import { keyBy } from 'lodash';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
@@ -15,6 +16,7 @@ import { useClickAwayListener } from '../../../lib/hooks';
|
|||||||
import selectors from '../../../selectors';
|
import selectors from '../../../selectors';
|
||||||
import entryActions from '../../../entry-actions';
|
import entryActions from '../../../entry-actions';
|
||||||
import { useForm, useNestedRef } from '../../../hooks';
|
import { useForm, useNestedRef } from '../../../hooks';
|
||||||
|
import { isUsernameChar, mentionTextToMarkup } from '../../../utils/mentions';
|
||||||
import { focusEnd } from '../../../utils/element-helpers';
|
import { focusEnd } from '../../../utils/element-helpers';
|
||||||
import { isModifierKeyPressed } from '../../../utils/event-helpers';
|
import { isModifierKeyPressed } from '../../../utils/event-helpers';
|
||||||
import UserAvatar from '../../users/UserAvatar';
|
import UserAvatar from '../../users/UserAvatar';
|
||||||
@@ -48,10 +50,19 @@ const Edit = React.memo(({ commentId, onClose }) => {
|
|||||||
const [submitButtonRef, handleSubmitButtonRef] = useNestedRef();
|
const [submitButtonRef, handleSubmitButtonRef] = useNestedRef();
|
||||||
const [cancelButtonRef, handleCancelButtonRef] = useNestedRef();
|
const [cancelButtonRef, handleCancelButtonRef] = useNestedRef();
|
||||||
|
|
||||||
|
const userByUsername = useMemo(
|
||||||
|
() =>
|
||||||
|
keyBy(
|
||||||
|
boardMemberships.flatMap(({ user }) => (user.username ? user : [])),
|
||||||
|
'username',
|
||||||
|
),
|
||||||
|
[boardMemberships],
|
||||||
|
);
|
||||||
|
|
||||||
const submit = useCallback(() => {
|
const submit = useCallback(() => {
|
||||||
const cleanData = {
|
const cleanData = {
|
||||||
...data,
|
...data,
|
||||||
text: data.text.trim(),
|
text: mentionTextToMarkup(data.text.trim(), userByUsername),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (cleanData.text && !dequal(cleanData, defaultData)) {
|
if (cleanData.text && !dequal(cleanData, defaultData)) {
|
||||||
@@ -59,7 +70,7 @@ const Edit = React.memo(({ commentId, onClose }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
}, [commentId, onClose, dispatch, defaultData, data]);
|
}, [commentId, onClose, dispatch, defaultData, data, userByUsername]);
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
submit();
|
submit();
|
||||||
@@ -68,10 +79,10 @@ const Edit = React.memo(({ commentId, onClose }) => {
|
|||||||
const handleFieldChange = useCallback(
|
const handleFieldChange = useCallback(
|
||||||
(_, text) => {
|
(_, text) => {
|
||||||
setData({
|
setData({
|
||||||
text,
|
text: !isUsernameChar(text.slice(-1)) ? mentionTextToMarkup(text, userByUsername) : text,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setData],
|
[setData, userByUsername],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFieldKeyDown = useCallback(
|
const handleFieldKeyDown = useCallback(
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { Button } from 'semantic-ui-react';
|
|||||||
|
|
||||||
import selectors from '../../../selectors';
|
import selectors from '../../../selectors';
|
||||||
import entryActions from '../../../entry-actions';
|
import entryActions from '../../../entry-actions';
|
||||||
import { formatTextWithMentions } from '../../../utils/mentions';
|
import { mentionMarkupToText } from '../../../utils/mentions';
|
||||||
import Paths from '../../../constants/Paths';
|
import Paths from '../../../constants/Paths';
|
||||||
import { StaticUserIds } from '../../../constants/StaticUsers';
|
import { StaticUserIds } from '../../../constants/StaticUsers';
|
||||||
import { NotificationTypes } from '../../../constants/Enums';
|
import { NotificationTypes } from '../../../constants/Enums';
|
||||||
@@ -84,7 +84,7 @@ const Item = React.memo(({ id, onClose }) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case NotificationTypes.COMMENT_CARD: {
|
case NotificationTypes.COMMENT_CARD: {
|
||||||
const commentText = truncate(formatTextWithMentions(notification.data.text));
|
const commentText = truncate(mentionMarkupToText(notification.data.text));
|
||||||
|
|
||||||
contentNode = (
|
contentNode = (
|
||||||
<Trans
|
<Trans
|
||||||
@@ -124,7 +124,7 @@ const Item = React.memo(({ id, onClose }) => {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
case NotificationTypes.MENTION_IN_COMMENT: {
|
case NotificationTypes.MENTION_IN_COMMENT: {
|
||||||
const commentText = truncate(formatTextWithMentions(notification.data.text));
|
const commentText = truncate(mentionMarkupToText(notification.data.text));
|
||||||
|
|
||||||
contentNode = (
|
contentNode = (
|
||||||
<Trans
|
<Trans
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { orderBy } from 'lodash';
|
import orderBy from 'lodash/orderBy';
|
||||||
import { attr } from 'redux-orm';
|
import { attr } from 'redux-orm';
|
||||||
|
|
||||||
import BaseModel from './BaseModel';
|
import BaseModel from './BaseModel';
|
||||||
|
|||||||
@@ -3,7 +3,23 @@
|
|||||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const MENTION_REGEX = /@\[(.*?)\]\((.*?)\)/g;
|
const USERNAME_CHAR_CLASS = 'a-zA-Z0-9._';
|
||||||
export const MENTION_NAME_REGEX = /@\[(.*?)\]\(.*?\)/g;
|
const USERNAME_CHAR_REGEX = new RegExp(`^[${USERNAME_CHAR_CLASS}]$`);
|
||||||
|
|
||||||
export const formatTextWithMentions = (text) => text.replace(MENTION_NAME_REGEX, '@$1');
|
const MENTION_TEXT_REGEX = new RegExp(
|
||||||
|
`(^|[^${USERNAME_CHAR_CLASS}])@([${USERNAME_CHAR_CLASS}]+)`,
|
||||||
|
'gi',
|
||||||
|
);
|
||||||
|
|
||||||
|
const MENTION_MARKUP_REGEX = /@\[(.*?)\]\((.*?)\)/g;
|
||||||
|
|
||||||
|
export const mentionTextToMarkup = (text, userByUsername) =>
|
||||||
|
text.replace(MENTION_TEXT_REGEX, (match, before, username) => {
|
||||||
|
const user = userByUsername[username.toLowerCase()];
|
||||||
|
return user ? `${before}@[${user.username}](${user.id})` : match;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mentionMarkupToText = (markup) =>
|
||||||
|
markup.replace(MENTION_MARKUP_REGEX, (_, username) => `@${username}`);
|
||||||
|
|
||||||
|
export const isUsernameChar = (char) => USERNAME_CHAR_REGEX.test(char);
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
const escapeMarkdown = require('escape-markdown');
|
const escapeMarkdown = require('escape-markdown');
|
||||||
const escapeHtml = require('escape-html');
|
const escapeHtml = require('escape-html');
|
||||||
|
|
||||||
const { extractMentionIds, formatTextWithMentions } = require('../../../utils/mentions');
|
const { extractMentionIds, mentionMarkupToText } = require('../../../utils/mentions');
|
||||||
|
|
||||||
const buildAndSendNotifications = async (services, board, card, comment, actorUser, t) => {
|
const buildAndSendNotifications = async (services, board, card, comment, actorUser, t) => {
|
||||||
const markdownCardLink = `[${escapeMarkdown(card.name)}](${sails.config.custom.baseUrl}/cards/${card.id})`;
|
const markdownCardLink = `[${escapeMarkdown(card.name)}](${sails.config.custom.baseUrl}/cards/${card.id})`;
|
||||||
const htmlCardLink = `<a href="${sails.config.custom.baseUrl}/cards/${card.id}}">${escapeHtml(card.name)}</a>`;
|
const htmlCardLink = `<a href="${sails.config.custom.baseUrl}/cards/${card.id}}">${escapeHtml(card.name)}</a>`;
|
||||||
const commentText = _.truncate(formatTextWithMentions(comment.text));
|
const commentText = _.truncate(mentionMarkupToText(comment.text));
|
||||||
|
|
||||||
await sails.helpers.utils.sendNotifications(services, t('New Comment'), {
|
await sails.helpers.utils.sendNotifications(services, t('New Comment'), {
|
||||||
text: `${t(
|
text: `${t(
|
||||||
@@ -101,10 +101,9 @@ module.exports = {
|
|||||||
if (mentionUserIds.length > 0) {
|
if (mentionUserIds.length > 0) {
|
||||||
const boardMemberUserIds = await sails.helpers.boards.getMemberUserIds(inputs.board.id);
|
const boardMemberUserIds = await sails.helpers.boards.getMemberUserIds(inputs.board.id);
|
||||||
|
|
||||||
mentionUserIds = _.difference(
|
mentionUserIds = _.difference(_.intersection(mentionUserIds, boardMemberUserIds), [
|
||||||
_.intersection(mentionUserIds, boardMemberUserIds),
|
|
||||||
comment.userId,
|
comment.userId,
|
||||||
);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mentionUserIdsSet = new Set(mentionUserIds);
|
const mentionUserIdsSet = new Set(mentionUserIds);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
const escapeMarkdown = require('escape-markdown');
|
const escapeMarkdown = require('escape-markdown');
|
||||||
const escapeHtml = require('escape-html');
|
const escapeHtml = require('escape-html');
|
||||||
|
|
||||||
const { formatTextWithMentions } = require('../../../utils/mentions');
|
const { mentionMarkupToText } = require('../../../utils/mentions');
|
||||||
|
|
||||||
const buildTitle = (notification, t) => {
|
const buildTitle = (notification, t) => {
|
||||||
switch (notification.type) {
|
switch (notification.type) {
|
||||||
@@ -60,7 +60,7 @@ const buildBodyByFormat = (board, card, notification, actorUser, t) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
case Notification.Types.COMMENT_CARD: {
|
case Notification.Types.COMMENT_CARD: {
|
||||||
const commentText = _.truncate(formatTextWithMentions(notification.data.text));
|
const commentText = _.truncate(mentionMarkupToText(notification.data.text));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: `${t(
|
text: `${t(
|
||||||
@@ -100,7 +100,7 @@ const buildBodyByFormat = (board, card, notification, actorUser, t) => {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
case Notification.Types.MENTION_IN_COMMENT: {
|
case Notification.Types.MENTION_IN_COMMENT: {
|
||||||
const commentText = _.truncate(formatTextWithMentions(notification.data.text));
|
const commentText = _.truncate(mentionMarkupToText(notification.data.text));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: `${t(
|
text: `${t(
|
||||||
|
|||||||
@@ -4,19 +4,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const MENTION_ID_REGEX = /@\[.*?\]\((.*?)\)/g;
|
const MENTION_ID_REGEX = /@\[.*?\]\((.*?)\)/g;
|
||||||
const MENTION_NAME_REGEX = /@\[(.*?)\]\(.*?\)/g;
|
const MENTION_USERNAME_REGEX = /@\[(.*?)\]\(.*?\)/g;
|
||||||
|
|
||||||
const extractMentionIds = (text) => {
|
const extractMentionIds = (text) => {
|
||||||
const matches = [...text.matchAll(MENTION_ID_REGEX)];
|
const matches = [...text.matchAll(MENTION_ID_REGEX)];
|
||||||
return matches.map((match) => match[1]);
|
return matches.map((match) => match[1]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTextWithMentions = (text) => text.replace(MENTION_NAME_REGEX, '@$1');
|
const mentionMarkupToText = (markup) =>
|
||||||
|
markup.replace(MENTION_USERNAME_REGEX, (_, username) => `@${username}`);
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
MENTION_ID_REGEX,
|
|
||||||
MENTION_NAME_REGEX,
|
|
||||||
|
|
||||||
extractMentionIds,
|
extractMentionIds,
|
||||||
formatTextWithMentions,
|
mentionMarkupToText,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user