1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-19 05:09:43 +02:00

ref: Refactoring

This commit is contained in:
Maksim Eltyshev 2025-05-30 21:57:15 +02:00
parent 9a421b0b8a
commit c29962174e
19 changed files with 234 additions and 247 deletions

View file

@ -6,15 +6,15 @@
import React, { useCallback, useState, useRef } from 'react'; import React, { useCallback, useState, useRef } 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 { Button, Form } from 'semantic-ui-react'; import { Button, Form } from 'semantic-ui-react';
import { MentionsInput, Mention } from 'react-mentions';
import { useClickAwayListener, useDidUpdate, useToggle } from '../../../lib/hooks'; import { useClickAwayListener, useDidUpdate, useToggle } from '../../../lib/hooks';
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 { isModifierKeyPressed } from '../../../utils/event-helpers'; import { isModifierKeyPressed } from '../../../utils/event-helpers';
import UserAvatar, { Sizes } from '../../users/UserAvatar/UserAvatar'; import UserAvatar from '../../users/UserAvatar';
import styles from './Add.module.scss'; import styles from './Add.module.scss';
@ -23,32 +23,18 @@ const DEFAULT_DATA = {
}; };
const Add = React.memo(() => { const Add = React.memo(() => {
const boardMemberships = useSelector(selectors.selectMembershipsForCurrentBoard);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [t] = useTranslation(); const [t] = useTranslation();
const [data, , setData] = useForm(DEFAULT_DATA);
const [isOpened, setIsOpened] = useState(false); const [isOpened, setIsOpened] = useState(false);
const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
const [selectTextFieldState, selectTextField] = useToggle(); const [selectTextFieldState, selectTextField] = useToggle();
const textFieldRef = useRef(null);
const mentionsInputRef = useRef(null); const mentionsInputRef = useRef(null);
const textFieldRef = useRef(null);
const [buttonRef, handleButtonRef] = useNestedRef(); 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 submit = useCallback(() => {
const cleanData = { const cleanData = {
...data, ...data,
@ -56,7 +42,7 @@ const Add = React.memo(() => {
}; };
if (!cleanData.text) { if (!cleanData.text) {
textFieldRef.current?.select(); textFieldRef.current.select();
return; return;
} }
@ -66,12 +52,13 @@ const Add = React.memo(() => {
}, [dispatch, data, setData, selectTextField, textFieldRef]); }, [dispatch, data, setData, selectTextField, textFieldRef]);
const handleEscape = useCallback(() => { const handleEscape = useCallback(() => {
if (mentionsInputRef?.current?.isOpened()) { if (mentionsInputRef.current.isOpened()) {
mentionsInputRef?.current.clearSuggestions(); mentionsInputRef.current.clearSuggestions();
return; return;
} }
setIsOpened(false); setIsOpened(false);
textFieldRef.current?.blur(); textFieldRef.current.blur();
}, [textFieldRef]); }, [textFieldRef]);
const [activateEscapeInterceptor, deactivateEscapeInterceptor] = const [activateEscapeInterceptor, deactivateEscapeInterceptor] =
@ -85,6 +72,15 @@ const Add = React.memo(() => {
setIsOpened(true); setIsOpened(true);
}, []); }, []);
const handleFieldChange = useCallback(
(_, text) => {
setData({
text,
});
},
[setData],
);
const handleFieldKeyDown = useCallback( const handleFieldKeyDown = useCallback(
(event) => { (event) => {
if (isModifierKeyPressed(event) && event.key === 'Enter') { if (isModifierKeyPressed(event) && event.key === 'Enter') {
@ -99,7 +95,7 @@ const Add = React.memo(() => {
}, []); }, []);
const handleClickAwayCancel = useCallback(() => { const handleClickAwayCancel = useCallback(() => {
textFieldRef.current?.focus(); textFieldRef.current.focus();
}, [textFieldRef]); }, [textFieldRef]);
const clickAwayProps = useClickAwayListener( const clickAwayProps = useClickAwayListener(
@ -108,16 +104,14 @@ const Add = React.memo(() => {
handleClickAwayCancel, handleClickAwayCancel,
); );
const users = useSelector(selectors.selectMembershipsForCurrentBoard); const suggestionRenderer = useCallback(
(entry, _, highlightedDisplay) => (
const handleFormFieldChange = useCallback( <div className={styles.suggestion}>
(event, newValue) => { <UserAvatar id={entry.id} size="tiny" />
handleFieldChange(null, { {highlightedDisplay}
name: 'text', </div>
value: newValue, ),
}); [],
},
[handleFieldChange],
); );
useDidUpdate(() => { useDidUpdate(() => {
@ -129,39 +123,41 @@ const Add = React.memo(() => {
}, [isOpened]); }, [isOpened]);
useDidUpdate(() => { useDidUpdate(() => {
textFieldRef.current?.focus(); textFieldRef.current.focus();
}, [selectTextFieldState]); }, [selectTextFieldState]);
return ( return (
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
<div className={styles.field}> <div className={styles.field}>
<MentionsInput <MentionsInput
// eslint-disable-next-line react/jsx-props-no-spreading {...clickAwayProps} // eslint-disable-line react/jsx-props-no-spreading
{...clickAwayProps} allowSpaceInQuery
allowSuggestionsAboveCursor
ref={mentionsInputRef} ref={mentionsInputRef}
inputRef={textFieldRef} inputRef={textFieldRef}
value={data.text} value={data.text}
placeholder={t('common.writeComment')} placeholder={t('common.writeComment')}
className="mentions-input"
style={mentionsInputStyle}
onFocus={handleFieldFocus}
onChange={handleFormFieldChange}
onKeyDown={handleFieldKeyDown}
allowSpaceInQuery
singleLine={false}
rows={isOpened ? 3 : 1}
maxLength={1048576} maxLength={1048576}
rows={isOpened ? 3 : 1}
className="mentions-input"
style={{
control: {
minHeight: isOpened ? '79px' : '37px',
},
}}
onFocus={handleFieldFocus}
onChange={handleFieldChange}
onKeyDown={handleFieldKeyDown}
> >
<Mention <Mention
trigger="@"
data={users.map((membership) => ({
id: membership.user.id,
display: membership.user.username || membership.user.name,
}))}
markup="@[__display__](__id__)"
appendSpaceOnAdd appendSpaceOnAdd
displayTransform={(id, display) => `@${display}`} data={boardMemberships.map(({ user }) => ({
renderSuggestion={renderSuggestion} id: user.id,
display: user.username || user.name,
}))}
displayTransform={(_, display) => `@${display}`}
renderSuggestion={suggestionRenderer}
className={styles.mention}
/> />
</MentionsInput> </MentionsInput>
</div> </div>

View file

@ -16,94 +16,36 @@
} }
.field { .field {
position: relative;
margin-bottom: 8px !important;
background: #fff; background: #fff;
border-radius: 4px; margin-bottom: 8px !important;
:global(.mentions-input) {
width: 100%;
textarea { textarea {
background: #fff; background: #fff;
box-shadow: none; border: 1px solid rgba(9, 30, 66, 0.13);
border: 0; border-radius: 3px;
box-sizing: border-box; box-sizing: border-box;
color: #333; color: #333;
display: block; display: block;
line-height: 1.5; line-height: 1.4;
font-size: 14px; font-size: 14px;
overflow: hidden; overflow: hidden;
padding: 8px 12px; padding: 8px 12px;
resize: none; resize: none;
width: 100%;
&:focus { &:focus {
outline: none; outline: none;
} }
} }
} }
}
}
:global { .mention {
.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 {
background-color: #f1f8ff; background-color: #f1f8ff;
} border-radius: 3px;
}
} }
&__highlighter { .suggestion {
padding: 8px 12px;
font-size: 14px;
font-family: 'Open Sans', sans-serif;
line-height: 1.4;
}
&__control {
position: relative;
}
&__input {
overflow: hidden;
}
}
}
.suggestion {
display: flex;
align-items: center; align-items: center;
display: flex;
gap: 8px; gap: 8px;
} }
.suggestionText {
flex: 1;
font-size: 14px;
line-height: 1.4;
} }

View file

@ -22,7 +22,6 @@
overflow: hidden; overflow: hidden;
padding: 8px 12px; padding: 8px 12px;
resize: none; resize: none;
width: 100%;
&:focus { &:focus {
outline: none; outline: none;

View file

@ -13,6 +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/formatters';
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';
@ -21,11 +22,6 @@ import UserAvatar from '../../users/UserAvatar';
import styles from './Item.module.scss'; 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 Item = React.memo(({ id, onClose }) => {
const selectNotificationById = useMemo(() => selectors.makeSelectNotificationById(), []); const selectNotificationById = useMemo(() => selectors.makeSelectNotificationById(), []);
const selectCreatorUserById = useMemo(() => selectors.makeSelectUserById(), []); const selectCreatorUserById = useMemo(() => selectors.makeSelectUserById(), []);
@ -88,7 +84,7 @@ const Item = React.memo(({ id, onClose }) => {
break; break;
} }
case NotificationTypes.COMMENT_CARD: { case NotificationTypes.COMMENT_CARD: {
const commentText = truncate(notification.data.text); const commentText = truncate(formatTextWithMentions(notification.data.text));
contentNode = ( contentNode = (
<Trans <Trans
@ -109,29 +105,6 @@ const Item = React.memo(({ id, onClose }) => {
break; 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: case NotificationTypes.ADD_MEMBER_TO_CARD:
contentNode = ( contentNode = (
<Trans <Trans
@ -150,6 +123,28 @@ const Item = React.memo(({ id, onClose }) => {
); );
break; 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: default:
contentNode = null; contentNode = null;
} }

View file

@ -17,7 +17,7 @@ import { StaticUserIds } from '../../../constants/StaticUsers';
import styles from './UserAvatar.module.scss'; import styles from './UserAvatar.module.scss';
export const Sizes = { const Sizes = {
TINY: 'tiny', TINY: 'tiny',
SMALL: 'small', SMALL: 'small',
MEDIUM: 'medium', MEDIUM: 'medium',

View file

@ -23,7 +23,7 @@ import { emojiDefs } from '@gravity-ui/markdown-editor/_/bundle/emoji';
/* eslint-enable import/no-unresolved */ /* eslint-enable import/no-unresolved */
import link from './link'; import link from './link';
import mentions from './mentions'; import mention from './mention';
export default [ export default [
ins, ins,
@ -42,5 +42,5 @@ export default [
meta, meta,
deflist, deflist,
link, link,
mentions, mention,
]; ];

View file

@ -3,23 +3,14 @@
* 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
*/ */
const mentionsPlugin = (md) => { const MENTION_REGEX = /@\[(.*?)\]\((.*?)\)/g;
const mentionRegex = /@\[(.*?)\]\((.*?)\)/g;
const renderMention = (tokens, idx) => { export default (md) => {
const token = tokens[idx]; md.core.ruler.push('mention', ({ tokens }) => {
const { display, userId } = token.meta; tokens.forEach((token) => {
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];
if (token.type === 'inline' && token.content) { if (token.type === 'inline' && token.content) {
const matches = [...token.content.matchAll(mentionRegex)]; const matches = [...token.content.matchAll(MENTION_REGEX)];
if (matches.length > 0) { if (matches.length > 0) {
const newChildren = []; const newChildren = [];
let lastIndex = 0; 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 // 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;

View file

@ -99,8 +99,8 @@ export const ActivityTypes = {
export const NotificationTypes = { export const NotificationTypes = {
MOVE_CARD: 'moveCard', MOVE_CARD: 'moveCard',
COMMENT_CARD: 'commentCard', COMMENT_CARD: 'commentCard',
COMMENT_MENTION: 'commentMention',
ADD_MEMBER_TO_CARD: 'addMemberToCard', ADD_MEMBER_TO_CARD: 'addMemberToCard',
MENTION_IN_COMMENT: 'mentionInComment',
}; };
export const NotificationServiceFormats = { export const NotificationServiceFormats = {

View file

@ -303,6 +303,8 @@ export default {
userMarkedTaskIncompleteOnCard: userMarkedTaskIncompleteOnCard:
'<0>{{user}}</0> marked {{task}} incomplete on <4>{{card}}</4>', '<0>{{user}}</0> marked {{task}} incomplete on <4>{{card}}</4>',
userMarkedTaskIncompleteOnThisCard: '<0>{{user}}</0> marked {{task}} incomplete on this card', 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: userMovedCardFromListToList:
'<0>{{user}}</0> moved <2>{{card}}</2> from {{fromList}} to {{toList}}', '<0>{{user}}</0> moved <2>{{card}}</2> from {{fromList}} to {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:

View file

@ -293,12 +293,13 @@ export default {
userJoinedThisCard: `<0>{{user}}</0> joined this card`, userJoinedThisCard: `<0>{{user}}</0> joined this card`,
userLeftNewCommentToCard: userLeftNewCommentToCard:
'<0>{{user}}</0> left a new comment «{{comment}}» to <2>{{card}}</2>', '<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>', userLeftCard: '<0>{{user}}</0> left <2>{{card}}</2>',
userLeftThisCard: '<0>{{user}}</0> left this card', userLeftThisCard: '<0>{{user}}</0> left this card',
userMarkedTaskIncompleteOnCard: userMarkedTaskIncompleteOnCard:
'<0>{{user}}</0> marked {{task}} incomplete on <4>{{card}}</4>', '<0>{{user}}</0> marked {{task}} incomplete on <4>{{card}}</4>',
userMarkedTaskIncompleteOnThisCard: '<0>{{user}}</0> marked {{task}} incomplete on this card', 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: userMovedCardFromListToList:
'<0>{{user}}</0> moved <2>{{card}}</2> from {{fromList}} to {{toList}}', '<0>{{user}}</0> moved <2>{{card}}</2> from {{fromList}} to {{toList}}',
userMovedThisCardFromListToList: userMovedThisCardFromListToList:

View file

@ -12,6 +12,31 @@
height: auto; 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 { .react-datepicker {
border: 0; border: 0;
color: #444444; color: #444444;
@ -192,6 +217,13 @@
font-size: .85em !important; font-size: .85em !important;
} }
.mention {
color: #0366d6;
background-color: #f1f8ff;
border-radius: 3px;
padding: 0 2px;
}
.yfm-clipboard:hover { .yfm-clipboard:hover {
.yfm-clipboard-button { .yfm-clipboard-button {
min-height: auto; min-height: auto;

View 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');

View file

@ -6,6 +6,8 @@
const escapeMarkdown = require('escape-markdown'); const escapeMarkdown = require('escape-markdown');
const escapeHtml = require('escape-html'); const escapeHtml = require('escape-html');
const { formatTextWithMentions } = require('../../../utils/formatters');
const extractMentionedUserIds = (text) => { const extractMentionedUserIds = (text) => {
const mentionRegex = /@\[.*?\]\((.*?)\)/g; const mentionRegex = /@\[.*?\]\((.*?)\)/g;
const matches = [...text.matchAll(mentionRegex)]; const matches = [...text.matchAll(mentionRegex)];
@ -15,7 +17,7 @@ const extractMentionedUserIds = (text) => {
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(comment.text); const commentText = _.truncate(formatTextWithMentions(comment.text));
await sails.helpers.utils.sendNotifications(services, t('New Comment'), { await sails.helpers.utils.sendNotifications(services, t('New Comment'), {
text: `${t( text: `${t(
@ -97,6 +99,19 @@ module.exports = {
user: values.user, 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( const cardSubscriptionUserIds = await sails.helpers.cards.getSubscriptionUserIds(
comment.cardId, comment.cardId,
comment.userId, comment.userId,
@ -107,14 +122,11 @@ module.exports = {
comment.userId, comment.userId,
); );
const mentionedUserIds = extractMentionedUserIds(values.text); const notifiableUserIds = _.union(
mentionedUserIds,
// Combine all user IDs, removing duplicates and the comment author cardSubscriptionUserIds,
const notifiableUserIds = [ boardSubscriptionUserIds,
...cardSubscriptionUserIds, );
...boardSubscriptionUserIds,
...mentionedUserIds,
].filter((id) => id !== comment.userId);
await Promise.all( await Promise.all(
notifiableUserIds.map((userId) => notifiableUserIds.map((userId) =>
@ -122,13 +134,12 @@ module.exports = {
values: { values: {
userId, userId,
comment, comment,
type: mentionedUserIds.includes(userId) type: mentionedUserIdsSet.has(userId)
? Notification.Types.COMMENT_MENTION ? Notification.Types.MENTION_IN_COMMENT
: Notification.Types.COMMENT_CARD, : Notification.Types.COMMENT_CARD,
data: { data: {
card: _.pick(values.card, ['name']), card: _.pick(values.card, ['name']),
text: comment.text, text: comment.text,
wasMentioned: mentionedUserIds.includes(userId),
}, },
creatorUser: values.user, creatorUser: values.user,
card: values.card, card: values.card,

View file

@ -6,16 +6,18 @@
const escapeMarkdown = require('escape-markdown'); const escapeMarkdown = require('escape-markdown');
const escapeHtml = require('escape-html'); const escapeHtml = require('escape-html');
const { formatTextWithMentions } = require('../../../utils/formatters');
const buildTitle = (notification, t) => { const buildTitle = (notification, t) => {
switch (notification.type) { switch (notification.type) {
case Notification.Types.MOVE_CARD: case Notification.Types.MOVE_CARD:
return t('Card Moved'); return t('Card Moved');
case Notification.Types.COMMENT_CARD: case Notification.Types.COMMENT_CARD:
return t('New Comment'); return t('New Comment');
case Notification.Types.COMMENT_MENTION:
return t('You Were Mentioned in a Comment');
case Notification.Types.ADD_MEMBER_TO_CARD: case Notification.Types.ADD_MEMBER_TO_CARD:
return t('You Were Added to Card'); return t('You Were Added to Card');
case Notification.Types.MENTION_IN_COMMENT:
return t('You Were Mentioned in Comment');
default: default:
return null; return null;
} }
@ -58,7 +60,7 @@ const buildBodyByFormat = (board, card, notification, actorUser, t) => {
}; };
} }
case Notification.Types.COMMENT_CARD: { case Notification.Types.COMMENT_CARD: {
const commentText = _.truncate(notification.data.text); const commentText = _.truncate(formatTextWithMentions(notification.data.text));
return { return {
text: `${t( text: `${t(
@ -81,30 +83,6 @@ const buildBodyByFormat = (board, card, notification, actorUser, t) => {
)}:\n\n<i>${escapeHtml(commentText)}</i>`, )}:\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: case Notification.Types.ADD_MEMBER_TO_CARD:
return { return {
text: t('%s added you to %s on %s', actorUser.name, card.name, board.name), 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), 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: default:
return null; return null;
} }
@ -164,15 +166,6 @@ const buildAndSendEmail = async (board, card, notification, actorUser, notifiabl
boardLink, boardLink,
)}</p><p>${escapeHtml(notification.data.text)}</p>`; )}</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; break;
case Notification.Types.ADD_MEMBER_TO_CARD: case Notification.Types.ADD_MEMBER_TO_CARD:
html = `<p>${t( html = `<p>${t(
@ -182,6 +175,15 @@ const buildAndSendEmail = async (board, card, notification, actorUser, notifiabl
boardLink, boardLink,
)}</p>`; )}</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; break;
default: default:
return; return;
@ -221,11 +223,11 @@ module.exports = {
values.userId = values.user.id; values.userId = values.user.id;
} }
const isCommentNotification = const isCommentRelated =
values.type === Notification.Types.COMMENT_CARD || 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; values.commentId = values.comment.id;
} else { } else {
values.actionId = values.action.id; values.actionId = values.action.id;
@ -254,7 +256,7 @@ module.exports = {
boards: [inputs.board], boards: [inputs.board],
lists: [inputs.list], lists: [inputs.list],
cards: [values.card], cards: [values.card],
...(isCommentNotification ...(isCommentRelated
? { ? {
comments: [values.comment], comments: [values.comment],
} }

View file

@ -13,8 +13,8 @@
const Types = { const Types = {
MOVE_CARD: 'moveCard', MOVE_CARD: 'moveCard',
COMMENT_CARD: 'commentCard', COMMENT_CARD: 'commentCard',
COMMENT_MENTION: 'commentMention',
ADD_MEMBER_TO_CARD: 'addMemberToCard', ADD_MEMBER_TO_CARD: 'addMemberToCard',
MENTION_IN_COMMENT: 'mentionInComment',
}; };
module.exports = { module.exports = {

View file

@ -7,8 +7,10 @@
"This is a *test* **markdown** `message`!": "This is a *test* **markdown** `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>", "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 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 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 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 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" "%s moved %s from %s to %s on %s": "%s moved %s from %s to %s on %s"
} }

View file

@ -2,15 +2,15 @@
"Card Created": "Card Created", "Card Created": "Card Created",
"Card Moved": "Card Moved", "Card Moved": "Card Moved",
"New Comment": "New Comment", "New Comment": "New Comment",
"You Were Mentioned in a Comment": "You Were Mentioned in a Comment",
"Test Title": "Test Title", "Test Title": "Test Title",
"This is a test text message!": "This is a test text message!", "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 *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>", "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 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 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 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 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"
} }

View file

@ -7,8 +7,10 @@
"This is a *test* **markdown** `message`!": "Это *тестовое* **markdown** `сообщение`!", "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>", "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 Added to Card": "Вы были добавлены к карточке",
"You Were Mentioned in Comment": "Вы были упомянуты в комментарии",
"%s added you to %s on %s": "%s добавил(а) вас к %s на %s", "%s added you to %s on %s": "%s добавил(а) вас к %s на %s",
"%s created %s in %s on %s": "%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 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" "%s moved %s from %s to %s on %s": "%s переместил(а) %s из %s в %s на %s"
} }

View file

@ -0,0 +1,7 @@
const MENTIONS_REGEX = /@\[(.*?)\]\(.*?\)/g;
const formatTextWithMentions = (text) => text.replace(MENTIONS_REGEX, '@$1');
module.exports = {
formatTextWithMentions,
};