Skip to content

Commit 1e667ea

Browse files
author
Sravan S
authored
feat: mention should work on paste (#438)
Mentions should be preserved from copy paste from sendbird-messages and message input * Make sure you are posting mentions of users from same channel * We dont support pasting of rich text from other applications * For copying simple text, we recommend using paste option in message context-menu Conditions tested: 1. paste simple text 2. paste text with mention 3. paste text with mention and text 4. paste text with mention and text and paste again before and after 5. copy message with mention(only one mention, no other text) and paste 6. copy message with mention from input and paste(before and after) fixes: https://sendbird.atlassian.net/browse/UIKIT-2980
1 parent 4e8263c commit 1e667ea

File tree

14 files changed

+252
-5
lines changed

14 files changed

+252
-5
lines changed

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"@sendbird/chat": "^4.2.3",
5151
"css-vars-ponyfill": "^2.3.2",
5252
"date-fns": "^2.16.1",
53+
"dompurify": "^3.0.1",
5354
"lamejs": "^1.2.1",
5455
"prop-types": "^15.7.2"
5556
},

rollup.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ module.exports = ({
4848
'react-dom',
4949
'css-vars-ponyfill',
5050
'date-fns',
51+
'dompurify',
5152
],
5253
plugins: [
5354
postcss({

scripts/package.template.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"@sendbird/chat": "^4.1.1",
3030
"css-vars-ponyfill": "^2.3.2",
3131
"date-fns": "^2.16.1",
32-
"prop-types": "^15.7.2"
32+
"prop-types": "^15.7.2",
33+
"dompurify": "^3.0.1"
3334
},
3435
"bugs": {
3536
"url": "https://community.sendbird.com"

src/smart-components/Channel/components/MessageInput/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const MessageInputWrapper = (
5454

5555
const { stringSet } = useContext(LocalizationContext);
5656
const [mentionNickname, setMentionNickname] = useState('');
57+
// todo: set type
5758
const [mentionedUsers, setMentionedUsers] = useState([]);
5859
const [mentionedUserIds, setMentionedUserIds] = useState([]);
5960
const [selectedUser, setSelectedUser] = useState(null);
@@ -173,6 +174,8 @@ const MessageInputWrapper = (
173174
onVoiceMessageIconClick={() => {
174175
setShowVoiceMessageInput(true);
175176
}}
177+
setMentionedUsers={setMentionedUsers}
178+
channel={channel}
176179
placeholder={
177180
(quoteMessage && stringSet.MESSAGE_INPUT__QUOTE_REPLY__PLACE_HOLDER)
178181
|| (utils.isDisabledBecauseFrozen(channel) && stringSet.MESSAGE_INPUT__PLACE_HOLDER__DISABLED)

src/smart-components/Thread/components/ThreadMessageInput/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ const ThreadMessageInput = (
141141
className="sendbird-thread-message-input__message-input"
142142
messageFieldId="sendbird-message-input-text-field--thread"
143143
disabled={disabled}
144+
channel={currentChannel}
145+
setMentionedUsers={setMentionedUsers}
144146
channelUrl={currentChannel?.url}
145147
mentionSelectedUser={selectedUser}
146148
isMentionEnabled={isMentionEnabled}

src/ui/MentionLabel/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ export default function MentionLabel(props: MentionLabelProps): JSX.Element {
6363
`}
6464
onClick={() => fetchUser(toggleDropdown)}
6565
ref={mentionRef}
66+
data-userid={mentionedUserId}
67+
data-nickname={mentionedUserNickname}
6668
>
6769
<Label
6870
type={LabelTypography.CAPTION_1}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const PASTE_NODE = 'sendbird-uikit__paste-node';
2+
export const TEXT_MESSAGE_CLASS = 'sendbird-word';
3+
export const MENTION_CLASS = 'sendbird-word__mention';
4+
export const MENTION_CLASS_IN_INPUT = 'sendbird-mention-user-label';
5+
export const MENTION_CLASS_COMBINED_QUERY = `.${MENTION_CLASS}, .${MENTION_CLASS_IN_INPUT}`;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { useCallback } from 'react';
2+
import DOMPurify from 'dompurify';
3+
4+
import { inserTemplateToDOM } from './insertTemplate';
5+
import { sanitizeString } from '../../utils';
6+
import { DynamicProps } from './types';
7+
import { MENTION_CLASS_COMBINED_QUERY, MENTION_CLASS_IN_INPUT, TEXT_MESSAGE_CLASS } from './consts';
8+
import {
9+
createPasteNode,
10+
hasMention,
11+
domToMessageTemplate,
12+
getUsersFromWords,
13+
extractTextFromNodes,
14+
} from './utils';
15+
16+
// conditions to test:
17+
// 1. paste simple text
18+
// 2. paste text with mention
19+
// 3. paste text with mention and text
20+
// 4. paste text with mention and text and paste again before and after
21+
// 5. copy message with mention(only one mention, no other text) and paste
22+
// 6. copy message with mention from input and paste(before and after)
23+
export default function usePaste({
24+
ref,
25+
setIsInput,
26+
setHeight,
27+
channel,
28+
setMentionedUsers,
29+
}: DynamicProps): (e: React.ClipboardEvent<HTMLDivElement>) => void {
30+
return useCallback((e) => {
31+
e.preventDefault();
32+
const html = e?.clipboardData.getData('text/html');
33+
// simple text, continue as normal
34+
if (!html) {
35+
const text = e?.clipboardData.getData('text');
36+
document.execCommand('insertHTML', false, sanitizeString(text));
37+
setIsInput(true);
38+
setHeight();
39+
return;
40+
}
41+
42+
// has html, check if there are mentions, sanitize and insert
43+
const purifier = DOMPurify(window);
44+
const clean = purifier.sanitize(html);
45+
const pasteNode = createPasteNode();
46+
pasteNode.innerHTML = clean;
47+
// does not have mention, continue as normal
48+
if (!hasMention(pasteNode)) {
49+
// to preserve space between words
50+
const text = extractTextFromNodes(Array.from(pasteNode.children) as HTMLSpanElement[]);
51+
document.execCommand('insertHTML', false, sanitizeString(text));
52+
pasteNode.remove();
53+
setIsInput(true);
54+
setHeight();
55+
return;
56+
}
57+
58+
// has mention, sanitize and insert
59+
let childNodes = pasteNode.querySelectorAll(`.${TEXT_MESSAGE_CLASS}`) as NodeListOf<HTMLSpanElement>;
60+
if (pasteNode.querySelectorAll(`.${MENTION_CLASS_IN_INPUT}`).length > 0) {
61+
// @ts-ignore
62+
childNodes = pasteNode.children;
63+
}
64+
let nodeArray = Array.from(childNodes);
65+
// handle paste when there is only one child
66+
if (pasteNode.children.length === 1 && pasteNode.querySelectorAll(MENTION_CLASS_COMBINED_QUERY).length === 1) {
67+
nodeArray = Array.from(pasteNode.children) as HTMLSpanElement[];
68+
}
69+
const words = domToMessageTemplate(nodeArray);
70+
71+
const mentionedUsers = getUsersFromWords(words, channel);
72+
setMentionedUsers(mentionedUsers);
73+
inserTemplateToDOM(words);
74+
pasteNode.remove();
75+
setIsInput(true);
76+
setHeight();
77+
return;
78+
79+
}, [ref, setIsInput, setHeight, channel, setMentionedUsers]);
80+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from 'react';
2+
import { renderToString } from 'react-dom/server';
3+
4+
import { Word } from './types';
5+
import { sanitizeString } from '../../utils';
6+
import MentionUserLabel from '../../../MentionUserLabel';
7+
8+
export function inserTemplateToDOM(templateList: Word[]): void {
9+
const nodes = templateList.map((template) => {
10+
const { text, userId } = template;
11+
if (userId) {
12+
return (
13+
renderToString(
14+
<>
15+
<MentionUserLabel userId={userId}>
16+
{text}
17+
</MentionUserLabel>
18+
</>
19+
)
20+
);
21+
}
22+
return sanitizeString(text);
23+
})
24+
.join(' ')
25+
// add a space at the end of the mention, else cursor/caret wont work
26+
.concat(' ');
27+
document.execCommand('insertHTML', false, nodes);
28+
}

0 commit comments

Comments
 (0)