mirror of
https://github.com/plankanban/planka.git
synced 2025-07-18 20:59:44 +02:00
ref: Refactoring
This commit is contained in:
parent
9a421b0b8a
commit
c29962174e
19 changed files with 234 additions and 247 deletions
|
@ -6,15 +6,15 @@
|
|||
import React, { useCallback, useState, useRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Mention, MentionsInput } from 'react-mentions';
|
||||
import { Button, Form } from 'semantic-ui-react';
|
||||
import { MentionsInput, Mention } from 'react-mentions';
|
||||
import { useClickAwayListener, useDidUpdate, useToggle } from '../../../lib/hooks';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
import { useEscapeInterceptor, useForm, useNestedRef } from '../../../hooks';
|
||||
import { isModifierKeyPressed } from '../../../utils/event-helpers';
|
||||
import UserAvatar, { Sizes } from '../../users/UserAvatar/UserAvatar';
|
||||
import UserAvatar from '../../users/UserAvatar';
|
||||
|
||||
import styles from './Add.module.scss';
|
||||
|
||||
|
@ -23,32 +23,18 @@ const DEFAULT_DATA = {
|
|||
};
|
||||
|
||||
const Add = React.memo(() => {
|
||||
const boardMemberships = useSelector(selectors.selectMembershipsForCurrentBoard);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
const [data, , setData] = useForm(DEFAULT_DATA);
|
||||
const [isOpened, setIsOpened] = useState(false);
|
||||
const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
|
||||
const [selectTextFieldState, selectTextField] = useToggle();
|
||||
|
||||
const textFieldRef = useRef(null);
|
||||
const mentionsInputRef = useRef(null);
|
||||
const textFieldRef = useRef(null);
|
||||
const [buttonRef, handleButtonRef] = useNestedRef();
|
||||
|
||||
const mentionsInputStyle = {
|
||||
control: {
|
||||
minHeight: isOpened ? '60px' : '36px',
|
||||
},
|
||||
};
|
||||
|
||||
const renderSuggestion = useCallback(
|
||||
(suggestion, search, highlightedDisplay) => (
|
||||
<div className={styles.suggestion}>
|
||||
<UserAvatar id={suggestion.id} size={Sizes.TINY} />
|
||||
<span className={styles.suggestionText}>{highlightedDisplay}</span>
|
||||
</div>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const submit = useCallback(() => {
|
||||
const cleanData = {
|
||||
...data,
|
||||
|
@ -56,7 +42,7 @@ const Add = React.memo(() => {
|
|||
};
|
||||
|
||||
if (!cleanData.text) {
|
||||
textFieldRef.current?.select();
|
||||
textFieldRef.current.select();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -66,12 +52,13 @@ const Add = React.memo(() => {
|
|||
}, [dispatch, data, setData, selectTextField, textFieldRef]);
|
||||
|
||||
const handleEscape = useCallback(() => {
|
||||
if (mentionsInputRef?.current?.isOpened()) {
|
||||
mentionsInputRef?.current.clearSuggestions();
|
||||
if (mentionsInputRef.current.isOpened()) {
|
||||
mentionsInputRef.current.clearSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
setIsOpened(false);
|
||||
textFieldRef.current?.blur();
|
||||
textFieldRef.current.blur();
|
||||
}, [textFieldRef]);
|
||||
|
||||
const [activateEscapeInterceptor, deactivateEscapeInterceptor] =
|
||||
|
@ -85,6 +72,15 @@ const Add = React.memo(() => {
|
|||
setIsOpened(true);
|
||||
}, []);
|
||||
|
||||
const handleFieldChange = useCallback(
|
||||
(_, text) => {
|
||||
setData({
|
||||
text,
|
||||
});
|
||||
},
|
||||
[setData],
|
||||
);
|
||||
|
||||
const handleFieldKeyDown = useCallback(
|
||||
(event) => {
|
||||
if (isModifierKeyPressed(event) && event.key === 'Enter') {
|
||||
|
@ -99,7 +95,7 @@ const Add = React.memo(() => {
|
|||
}, []);
|
||||
|
||||
const handleClickAwayCancel = useCallback(() => {
|
||||
textFieldRef.current?.focus();
|
||||
textFieldRef.current.focus();
|
||||
}, [textFieldRef]);
|
||||
|
||||
const clickAwayProps = useClickAwayListener(
|
||||
|
@ -108,16 +104,14 @@ const Add = React.memo(() => {
|
|||
handleClickAwayCancel,
|
||||
);
|
||||
|
||||
const users = useSelector(selectors.selectMembershipsForCurrentBoard);
|
||||
|
||||
const handleFormFieldChange = useCallback(
|
||||
(event, newValue) => {
|
||||
handleFieldChange(null, {
|
||||
name: 'text',
|
||||
value: newValue,
|
||||
});
|
||||
},
|
||||
[handleFieldChange],
|
||||
const suggestionRenderer = useCallback(
|
||||
(entry, _, highlightedDisplay) => (
|
||||
<div className={styles.suggestion}>
|
||||
<UserAvatar id={entry.id} size="tiny" />
|
||||
{highlightedDisplay}
|
||||
</div>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
useDidUpdate(() => {
|
||||
|
@ -129,39 +123,41 @@ const Add = React.memo(() => {
|
|||
}, [isOpened]);
|
||||
|
||||
useDidUpdate(() => {
|
||||
textFieldRef.current?.focus();
|
||||
textFieldRef.current.focus();
|
||||
}, [selectTextFieldState]);
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<div className={styles.field}>
|
||||
<MentionsInput
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...clickAwayProps}
|
||||
{...clickAwayProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||
allowSpaceInQuery
|
||||
allowSuggestionsAboveCursor
|
||||
ref={mentionsInputRef}
|
||||
inputRef={textFieldRef}
|
||||
value={data.text}
|
||||
placeholder={t('common.writeComment')}
|
||||
className="mentions-input"
|
||||
style={mentionsInputStyle}
|
||||
onFocus={handleFieldFocus}
|
||||
onChange={handleFormFieldChange}
|
||||
onKeyDown={handleFieldKeyDown}
|
||||
allowSpaceInQuery
|
||||
singleLine={false}
|
||||
rows={isOpened ? 3 : 1}
|
||||
maxLength={1048576}
|
||||
rows={isOpened ? 3 : 1}
|
||||
className="mentions-input"
|
||||
style={{
|
||||
control: {
|
||||
minHeight: isOpened ? '79px' : '37px',
|
||||
},
|
||||
}}
|
||||
onFocus={handleFieldFocus}
|
||||
onChange={handleFieldChange}
|
||||
onKeyDown={handleFieldKeyDown}
|
||||
>
|
||||
<Mention
|
||||
trigger="@"
|
||||
data={users.map((membership) => ({
|
||||
id: membership.user.id,
|
||||
display: membership.user.username || membership.user.name,
|
||||
}))}
|
||||
markup="@[__display__](__id__)"
|
||||
appendSpaceOnAdd
|
||||
displayTransform={(id, display) => `@${display}`}
|
||||
renderSuggestion={renderSuggestion}
|
||||
data={boardMemberships.map(({ user }) => ({
|
||||
id: user.id,
|
||||
display: user.username || user.name,
|
||||
}))}
|
||||
displayTransform={(_, display) => `@${display}`}
|
||||
renderSuggestion={suggestionRenderer}
|
||||
className={styles.mention}
|
||||
/>
|
||||
</MentionsInput>
|
||||
</div>
|
||||
|
|
|
@ -16,94 +16,36 @@
|
|||
}
|
||||
|
||||
.field {
|
||||
position: relative;
|
||||
margin-bottom: 8px !important;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
|
||||
:global(.mentions-input) {
|
||||
width: 100%;
|
||||
margin-bottom: 8px !important;
|
||||
|
||||
textarea {
|
||||
background: #fff;
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
border: 1px solid rgba(9, 30, 66, 0.13);
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
color: #333;
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
line-height: 1.4;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
padding: 8px 12px;
|
||||
resize: none;
|
||||
width: 100%;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.mentions-input {
|
||||
width: 100%;
|
||||
|
||||
&__suggestions {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
margin-top: 4px;
|
||||
background: white;
|
||||
border: 1px solid rgba(34, 36, 38, 0.15);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
&__list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
|
||||
&--focused {
|
||||
.mention {
|
||||
background-color: #f1f8ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__highlighter {
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
&__control {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__input {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.suggestion {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.suggestionText {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
overflow: hidden;
|
||||
padding: 8px 12px;
|
||||
resize: none;
|
||||
width: 100%;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
|
|
|
@ -13,6 +13,7 @@ import { Button } from 'semantic-ui-react';
|
|||
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
import { formatTextWithMentions } from '../../../utils/formatters';
|
||||
import Paths from '../../../constants/Paths';
|
||||
import { StaticUserIds } from '../../../constants/StaticUsers';
|
||||
import { NotificationTypes } from '../../../constants/Enums';
|
||||
|
@ -21,11 +22,6 @@ import UserAvatar from '../../users/UserAvatar';
|
|||
|
||||
import styles from './Item.module.scss';
|
||||
|
||||
const formatMentionText = (text) => {
|
||||
// Replace @[username](userId) with @username
|
||||
return text.replace(/@\[(.*?)\]\(.*?\)/g, '@$1');
|
||||
};
|
||||
|
||||
const Item = React.memo(({ id, onClose }) => {
|
||||
const selectNotificationById = useMemo(() => selectors.makeSelectNotificationById(), []);
|
||||
const selectCreatorUserById = useMemo(() => selectors.makeSelectUserById(), []);
|
||||
|
@ -88,7 +84,7 @@ const Item = React.memo(({ id, onClose }) => {
|
|||
break;
|
||||
}
|
||||
case NotificationTypes.COMMENT_CARD: {
|
||||
const commentText = truncate(notification.data.text);
|
||||
const commentText = truncate(formatTextWithMentions(notification.data.text));
|
||||
|
||||
contentNode = (
|
||||
<Trans
|
||||
|
@ -109,29 +105,6 @@ const Item = React.memo(({ id, onClose }) => {
|
|||
|
||||
break;
|
||||
}
|
||||
case NotificationTypes.COMMENT_MENTION: {
|
||||
const commentText = truncate(formatMentionText(notification.data.text));
|
||||
|
||||
contentNode = (
|
||||
<Trans
|
||||
i18nKey="common.userMentionedYouInCard"
|
||||
values={{
|
||||
user: creatorUserName,
|
||||
comment: commentText,
|
||||
card: cardName,
|
||||
}}
|
||||
>
|
||||
<span className={styles.author}>{creatorUserName}</span>
|
||||
{` mentioned you in `}
|
||||
<Link to={Paths.CARDS.replace(':id', notification.cardId)} onClick={onClose}>
|
||||
{cardName}
|
||||
</Link>
|
||||
{`: «${commentText}»`}
|
||||
</Trans>
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
case NotificationTypes.ADD_MEMBER_TO_CARD:
|
||||
contentNode = (
|
||||
<Trans
|
||||
|
@ -150,6 +123,28 @@ const Item = React.memo(({ id, onClose }) => {
|
|||
);
|
||||
|
||||
break;
|
||||
case NotificationTypes.MENTION_IN_COMMENT: {
|
||||
const commentText = truncate(formatTextWithMentions(notification.data.text));
|
||||
|
||||
contentNode = (
|
||||
<Trans
|
||||
i18nKey="common.userMentionedYouInCommentOnCard"
|
||||
values={{
|
||||
user: creatorUserName,
|
||||
comment: commentText,
|
||||
card: cardName,
|
||||
}}
|
||||
>
|
||||
<span className={styles.author}>{creatorUserName}</span>
|
||||
{` mentioned you in «${commentText}» on `}
|
||||
<Link to={Paths.CARDS.replace(':id', notification.cardId)} onClick={onClose}>
|
||||
{cardName}
|
||||
</Link>
|
||||
</Trans>
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
contentNode = null;
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import { StaticUserIds } from '../../../constants/StaticUsers';
|
|||
|
||||
import styles from './UserAvatar.module.scss';
|
||||
|
||||
export const Sizes = {
|
||||
const Sizes = {
|
||||
TINY: 'tiny',
|
||||
SMALL: 'small',
|
||||
MEDIUM: 'medium',
|
||||
|
|
|
@ -23,7 +23,7 @@ import { emojiDefs } from '@gravity-ui/markdown-editor/_/bundle/emoji';
|
|||
/* eslint-enable import/no-unresolved */
|
||||
|
||||
import link from './link';
|
||||
import mentions from './mentions';
|
||||
import mention from './mention';
|
||||
|
||||
export default [
|
||||
ins,
|
||||
|
@ -42,5 +42,5 @@ export default [
|
|||
meta,
|
||||
deflist,
|
||||
link,
|
||||
mentions,
|
||||
mention,
|
||||
];
|
||||
|
|
|
@ -3,23 +3,14 @@
|
|||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const mentionsPlugin = (md) => {
|
||||
const mentionRegex = /@\[(.*?)\]\((.*?)\)/g;
|
||||
const MENTION_REGEX = /@\[(.*?)\]\((.*?)\)/g;
|
||||
|
||||
const renderMention = (tokens, idx) => {
|
||||
const token = tokens[idx];
|
||||
const { display, userId } = token.meta;
|
||||
|
||||
return `<span class="mention" data-user-id="${userId}" style="color: #0366d6; background-color: #f1f8ff; border-radius: 3px; padding: 0 2px;">@${display}</span>`;
|
||||
};
|
||||
|
||||
md.core.ruler.push('mentions', (state) => {
|
||||
const { tokens } = state;
|
||||
|
||||
for (let i = 0; i < tokens.length; i += 1) {
|
||||
const token = tokens[i];
|
||||
export default (md) => {
|
||||
md.core.ruler.push('mention', ({ tokens }) => {
|
||||
tokens.forEach((token) => {
|
||||
if (token.type === 'inline' && token.content) {
|
||||
const matches = [...token.content.matchAll(mentionRegex)];
|
||||
const matches = [...token.content.matchAll(MENTION_REGEX)];
|
||||
|
||||
if (matches.length > 0) {
|
||||
const newChildren = [];
|
||||
let lastIndex = 0;
|
||||
|
@ -56,14 +47,15 @@ const mentionsPlugin = (md) => {
|
|||
});
|
||||
}
|
||||
|
||||
token.children = newChildren;
|
||||
}
|
||||
token.children = newChildren; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
md.renderer.rules.mention = renderMention;
|
||||
md.renderer.rules.mention = (tokens, index) => {
|
||||
const { display, userId } = tokens[index].meta;
|
||||
return `<span class="mention" data-user-id="${userId}">@${display}</span>`;
|
||||
};
|
||||
};
|
||||
|
||||
export default mentionsPlugin;
|
|
@ -99,8 +99,8 @@ export const ActivityTypes = {
|
|||
export const NotificationTypes = {
|
||||
MOVE_CARD: 'moveCard',
|
||||
COMMENT_CARD: 'commentCard',
|
||||
COMMENT_MENTION: 'commentMention',
|
||||
ADD_MEMBER_TO_CARD: 'addMemberToCard',
|
||||
MENTION_IN_COMMENT: 'mentionInComment',
|
||||
};
|
||||
|
||||
export const NotificationServiceFormats = {
|
||||
|
|
|
@ -303,6 +303,8 @@ export default {
|
|||
userMarkedTaskIncompleteOnCard:
|
||||
'<0>{{user}}</0> marked {{task}} incomplete on <4>{{card}}</4>',
|
||||
userMarkedTaskIncompleteOnThisCard: '<0>{{user}}</0> marked {{task}} incomplete on this card',
|
||||
userMentionedYouInCommentOnCard:
|
||||
'<0>{{user}}</0> mentioned you in a comment «{{comment}}» on <2>{{card}}</2>',
|
||||
userMovedCardFromListToList:
|
||||
'<0>{{user}}</0> moved <2>{{card}}</2> from {{fromList}} to {{toList}}',
|
||||
userMovedThisCardFromListToList:
|
||||
|
|
|
@ -293,12 +293,13 @@ export default {
|
|||
userJoinedThisCard: `<0>{{user}}</0> joined this card`,
|
||||
userLeftNewCommentToCard:
|
||||
'<0>{{user}}</0> left a new comment «{{comment}}» to <2>{{card}}</2>',
|
||||
userMentionedYouInCard: '<0>{{user}}</0> mentioned you in <2>{{card}}</2>: «{{comment}}»',
|
||||
userLeftCard: '<0>{{user}}</0> left <2>{{card}}</2>',
|
||||
userLeftThisCard: '<0>{{user}}</0> left this card',
|
||||
userMarkedTaskIncompleteOnCard:
|
||||
'<0>{{user}}</0> marked {{task}} incomplete on <4>{{card}}</4>',
|
||||
userMarkedTaskIncompleteOnThisCard: '<0>{{user}}</0> marked {{task}} incomplete on this card',
|
||||
userMentionedYouInCommentOnCard:
|
||||
'<0>{{user}}</0> mentioned you in a comment «{{comment}}» on <2>{{card}}</2>',
|
||||
userMovedCardFromListToList:
|
||||
'<0>{{user}}</0> moved <2>{{card}}</2> from {{fromList}} to {{toList}}',
|
||||
userMovedThisCardFromListToList:
|
||||
|
|
|
@ -12,6 +12,31 @@
|
|||
height: auto;
|
||||
}
|
||||
|
||||
.mentions-input {
|
||||
&__highlighter {
|
||||
line-height: 1.4;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
&__suggestions {
|
||||
border: 1px solid #d4d4d5;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 8px 16px -4px rgba(9, 45, 66, 0.25),
|
||||
0 0 0 1px rgba(9, 45, 66, 0.08);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
&__item {
|
||||
padding: 8px 12px;
|
||||
|
||||
&--focused {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
color: rgba(0, 0, 0, 0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.react-datepicker {
|
||||
border: 0;
|
||||
color: #444444;
|
||||
|
@ -192,6 +217,13 @@
|
|||
font-size: .85em !important;
|
||||
}
|
||||
|
||||
.mention {
|
||||
color: #0366d6;
|
||||
background-color: #f1f8ff;
|
||||
border-radius: 3px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.yfm-clipboard:hover {
|
||||
.yfm-clipboard-button {
|
||||
min-height: auto;
|
||||
|
|
4
client/src/utils/formatters.js
Normal file
4
client/src/utils/formatters.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
const MENTIONS_REGEX = /@\[(.*?)\]\(.*?\)/g;
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const formatTextWithMentions = (text) => text.replace(MENTIONS_REGEX, '@$1');
|
|
@ -6,6 +6,8 @@
|
|||
const escapeMarkdown = require('escape-markdown');
|
||||
const escapeHtml = require('escape-html');
|
||||
|
||||
const { formatTextWithMentions } = require('../../../utils/formatters');
|
||||
|
||||
const extractMentionedUserIds = (text) => {
|
||||
const mentionRegex = /@\[.*?\]\((.*?)\)/g;
|
||||
const matches = [...text.matchAll(mentionRegex)];
|
||||
|
@ -15,7 +17,7 @@ const extractMentionedUserIds = (text) => {
|
|||
const buildAndSendNotifications = async (services, board, card, comment, actorUser, t) => {
|
||||
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 commentText = _.truncate(comment.text);
|
||||
const commentText = _.truncate(formatTextWithMentions(comment.text));
|
||||
|
||||
await sails.helpers.utils.sendNotifications(services, t('New Comment'), {
|
||||
text: `${t(
|
||||
|
@ -97,6 +99,19 @@ module.exports = {
|
|||
user: values.user,
|
||||
});
|
||||
|
||||
let mentionedUserIds = extractMentionedUserIds(values.text);
|
||||
|
||||
if (mentionedUserIds.length > 0) {
|
||||
const boardMemberUserIds = await sails.helpers.boards.getMemberUserIds(inputs.board.id);
|
||||
|
||||
mentionedUserIds = _.difference(
|
||||
_.intersection(mentionedUserIds, boardMemberUserIds),
|
||||
comment.userId,
|
||||
);
|
||||
}
|
||||
|
||||
const mentionedUserIdsSet = new Set(mentionedUserIds);
|
||||
|
||||
const cardSubscriptionUserIds = await sails.helpers.cards.getSubscriptionUserIds(
|
||||
comment.cardId,
|
||||
comment.userId,
|
||||
|
@ -107,14 +122,11 @@ module.exports = {
|
|||
comment.userId,
|
||||
);
|
||||
|
||||
const mentionedUserIds = extractMentionedUserIds(values.text);
|
||||
|
||||
// Combine all user IDs, removing duplicates and the comment author
|
||||
const notifiableUserIds = [
|
||||
...cardSubscriptionUserIds,
|
||||
...boardSubscriptionUserIds,
|
||||
...mentionedUserIds,
|
||||
].filter((id) => id !== comment.userId);
|
||||
const notifiableUserIds = _.union(
|
||||
mentionedUserIds,
|
||||
cardSubscriptionUserIds,
|
||||
boardSubscriptionUserIds,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
notifiableUserIds.map((userId) =>
|
||||
|
@ -122,13 +134,12 @@ module.exports = {
|
|||
values: {
|
||||
userId,
|
||||
comment,
|
||||
type: mentionedUserIds.includes(userId)
|
||||
? Notification.Types.COMMENT_MENTION
|
||||
type: mentionedUserIdsSet.has(userId)
|
||||
? Notification.Types.MENTION_IN_COMMENT
|
||||
: Notification.Types.COMMENT_CARD,
|
||||
data: {
|
||||
card: _.pick(values.card, ['name']),
|
||||
text: comment.text,
|
||||
wasMentioned: mentionedUserIds.includes(userId),
|
||||
},
|
||||
creatorUser: values.user,
|
||||
card: values.card,
|
||||
|
|
|
@ -6,16 +6,18 @@
|
|||
const escapeMarkdown = require('escape-markdown');
|
||||
const escapeHtml = require('escape-html');
|
||||
|
||||
const { formatTextWithMentions } = require('../../../utils/formatters');
|
||||
|
||||
const buildTitle = (notification, t) => {
|
||||
switch (notification.type) {
|
||||
case Notification.Types.MOVE_CARD:
|
||||
return t('Card Moved');
|
||||
case Notification.Types.COMMENT_CARD:
|
||||
return t('New Comment');
|
||||
case Notification.Types.COMMENT_MENTION:
|
||||
return t('You Were Mentioned in a Comment');
|
||||
case Notification.Types.ADD_MEMBER_TO_CARD:
|
||||
return t('You Were Added to Card');
|
||||
case Notification.Types.MENTION_IN_COMMENT:
|
||||
return t('You Were Mentioned in Comment');
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -58,7 +60,7 @@ const buildBodyByFormat = (board, card, notification, actorUser, t) => {
|
|||
};
|
||||
}
|
||||
case Notification.Types.COMMENT_CARD: {
|
||||
const commentText = _.truncate(notification.data.text);
|
||||
const commentText = _.truncate(formatTextWithMentions(notification.data.text));
|
||||
|
||||
return {
|
||||
text: `${t(
|
||||
|
@ -81,30 +83,6 @@ const buildBodyByFormat = (board, card, notification, actorUser, t) => {
|
|||
)}:\n\n<i>${escapeHtml(commentText)}</i>`,
|
||||
};
|
||||
}
|
||||
case Notification.Types.COMMENT_MENTION: {
|
||||
const commentText = _.truncate(notification.data.text);
|
||||
|
||||
return {
|
||||
text: `${t(
|
||||
'%s mentioned you in %s on %s',
|
||||
actorUser.name,
|
||||
card.name,
|
||||
board.name,
|
||||
)}:\n${commentText}`,
|
||||
markdown: `${t(
|
||||
'%s mentioned you in %s on %s',
|
||||
escapeMarkdown(actorUser.name),
|
||||
markdownCardLink,
|
||||
escapeMarkdown(board.name),
|
||||
)}:\n\n*${escapeMarkdown(commentText)}*`,
|
||||
html: `${t(
|
||||
'%s mentioned you in %s on %s',
|
||||
escapeHtml(actorUser.name),
|
||||
htmlCardLink,
|
||||
escapeHtml(board.name),
|
||||
)}:\n\n<i>${escapeHtml(commentText)}</i>`,
|
||||
};
|
||||
}
|
||||
case Notification.Types.ADD_MEMBER_TO_CARD:
|
||||
return {
|
||||
text: t('%s added you to %s on %s', actorUser.name, card.name, board.name),
|
||||
|
@ -121,6 +99,30 @@ const buildBodyByFormat = (board, card, notification, actorUser, t) => {
|
|||
escapeHtml(board.name),
|
||||
),
|
||||
};
|
||||
case Notification.Types.MENTION_IN_COMMENT: {
|
||||
const commentText = _.truncate(formatTextWithMentions(notification.data.text));
|
||||
|
||||
return {
|
||||
text: `${t(
|
||||
'%s mentioned you in %s on %s',
|
||||
actorUser.name,
|
||||
card.name,
|
||||
board.name,
|
||||
)}:\n${commentText}`,
|
||||
markdown: `${t(
|
||||
'%s mentioned you in %s on %s',
|
||||
escapeMarkdown(actorUser.name),
|
||||
markdownCardLink,
|
||||
escapeMarkdown(board.name),
|
||||
)}:\n\n*${escapeMarkdown(commentText)}*`,
|
||||
html: `${t(
|
||||
'%s mentioned you in %s on %s',
|
||||
escapeHtml(actorUser.name),
|
||||
htmlCardLink,
|
||||
escapeHtml(board.name),
|
||||
)}:\n\n<i>${escapeHtml(commentText)}</i>`,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -164,15 +166,6 @@ const buildAndSendEmail = async (board, card, notification, actorUser, notifiabl
|
|||
boardLink,
|
||||
)}</p><p>${escapeHtml(notification.data.text)}</p>`;
|
||||
|
||||
break;
|
||||
case Notification.Types.COMMENT_MENTION:
|
||||
html = `<p>${t(
|
||||
'%s mentioned you in %s on %s',
|
||||
escapeHtml(actorUser.name),
|
||||
cardLink,
|
||||
boardLink,
|
||||
)}</p><p>${escapeHtml(notification.data.text)}</p>`;
|
||||
|
||||
break;
|
||||
case Notification.Types.ADD_MEMBER_TO_CARD:
|
||||
html = `<p>${t(
|
||||
|
@ -182,6 +175,15 @@ const buildAndSendEmail = async (board, card, notification, actorUser, notifiabl
|
|||
boardLink,
|
||||
)}</p>`;
|
||||
|
||||
break;
|
||||
case Notification.Types.MENTION_IN_COMMENT:
|
||||
html = `<p>${t(
|
||||
'%s mentioned you in %s on %s',
|
||||
escapeHtml(actorUser.name),
|
||||
cardLink,
|
||||
boardLink,
|
||||
)}</p><p>${escapeHtml(notification.data.text)}</p>`;
|
||||
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
|
@ -221,11 +223,11 @@ module.exports = {
|
|||
values.userId = values.user.id;
|
||||
}
|
||||
|
||||
const isCommentNotification =
|
||||
const isCommentRelated =
|
||||
values.type === Notification.Types.COMMENT_CARD ||
|
||||
values.type === Notification.Types.COMMENT_MENTION;
|
||||
values.type === Notification.Types.MENTION_IN_COMMENT;
|
||||
|
||||
if (isCommentNotification) {
|
||||
if (isCommentRelated) {
|
||||
values.commentId = values.comment.id;
|
||||
} else {
|
||||
values.actionId = values.action.id;
|
||||
|
@ -254,7 +256,7 @@ module.exports = {
|
|||
boards: [inputs.board],
|
||||
lists: [inputs.list],
|
||||
cards: [values.card],
|
||||
...(isCommentNotification
|
||||
...(isCommentRelated
|
||||
? {
|
||||
comments: [values.comment],
|
||||
}
|
||||
|
|
|
@ -13,8 +13,8 @@
|
|||
const Types = {
|
||||
MOVE_CARD: 'moveCard',
|
||||
COMMENT_CARD: 'commentCard',
|
||||
COMMENT_MENTION: 'commentMention',
|
||||
ADD_MEMBER_TO_CARD: 'addMemberToCard',
|
||||
MENTION_IN_COMMENT: 'mentionInComment',
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -7,8 +7,10 @@
|
|||
"This is a *test* **markdown** `message`!": "This is a *test* **markdown** `message`!",
|
||||
"This is a <i>test</i> <b>html</b> <code>message</code>": "This is a <i>test</i> <b>html</b> <code>message</code>",
|
||||
"You Were Added to Card": "Your Were Added to Card",
|
||||
"You Were Mentioned in Comment": "You Were Mentioned in Comment",
|
||||
"%s added you to %s on %s": "%s added you to %s on %s",
|
||||
"%s created %s in %s on %s": "%s created %s in %s on %s",
|
||||
"%s left a new comment to %s on %s": "%s left a new comment to %s on %s",
|
||||
"%s mentioned you in %s on %s": "%s mentioned you in %s on %s",
|
||||
"%s moved %s from %s to %s on %s": "%s moved %s from %s to %s on %s"
|
||||
}
|
||||
|
|
|
@ -2,15 +2,15 @@
|
|||
"Card Created": "Card Created",
|
||||
"Card Moved": "Card Moved",
|
||||
"New Comment": "New Comment",
|
||||
"You Were Mentioned in a Comment": "You Were Mentioned in a Comment",
|
||||
"Test Title": "Test Title",
|
||||
"This is a test text message!": "This is a test text message!",
|
||||
"This is a *test* **markdown** `message`!": "This is a *test* **markdown** `message`!",
|
||||
"This is a <i>test</i> <b>html</b> <code>message</code>": "This is a <i>test</i> <b>html</b> <code>message</code>",
|
||||
"You Were Added to Card": "Your Were Added to Card",
|
||||
"You Were Mentioned in Comment": "You Were Mentioned in Comment",
|
||||
"%s added you to %s on %s": "%s added you to %s on %s",
|
||||
"%s created %s in %s on %s": "%s created %s in %s on %s",
|
||||
"%s left a new comment to %s on %s": "%s left a new comment to %s on %s",
|
||||
"%s moved %s from %s to %s on %s": "%s moved %s from %s to %s on %s",
|
||||
"%s mentioned you in %s on %s": "%s mentioned you in %s on %s"
|
||||
"%s mentioned you in %s on %s": "%s mentioned you in %s on %s",
|
||||
"%s moved %s from %s to %s on %s": "%s moved %s from %s to %s on %s"
|
||||
}
|
||||
|
|
|
@ -7,8 +7,10 @@
|
|||
"This is a *test* **markdown** `message`!": "Это *тестовое* **markdown** `сообщение`!",
|
||||
"This is a <i>test</i> <b>html</b> <code>message</code>": "Это <i>тестовое</i> <b>html</b> <code>сообщение</code>",
|
||||
"You Were Added to Card": "Вы были добавлены к карточке",
|
||||
"You Were Mentioned in Comment": "Вы были упомянуты в комментарии",
|
||||
"%s added you to %s on %s": "%s добавил(а) вас к %s на %s",
|
||||
"%s created %s in %s on %s": "%s создал(а) %s в %s на %s",
|
||||
"%s left a new comment to %s on %s": "%s оставил(а) новый комментарий к %s на %s",
|
||||
"%s mentioned you in %s on %s": "%s упомянул(а) вас в %s на %s",
|
||||
"%s moved %s from %s to %s on %s": "%s переместил(а) %s из %s в %s на %s"
|
||||
}
|
||||
|
|
7
server/utils/formatters.js
Normal file
7
server/utils/formatters.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
const MENTIONS_REGEX = /@\[(.*?)\]\(.*?\)/g;
|
||||
|
||||
const formatTextWithMentions = (text) => text.replace(MENTIONS_REGEX, '@$1');
|
||||
|
||||
module.exports = {
|
||||
formatTextWithMentions,
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue