Skip to content

Commit 9a545a5

Browse files
authored
[CLNP-8323]Fixed a bug where invisible zero-width spaces inserted during paste i… (#1410)
Fixes [CLNP-8323](https://sendbird.atlassian.net/browse/CLNP-8323) ### Changelogs - Fixed a bug where invisible zero-width spaces inserted during paste in `MessageInput` could be included in sent or updated messages ### Checklist Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If unsure, ask the members. This is a reminder of what we look for before merging your code. - [x] **All tests pass locally with my changes** - [ ] **I have added tests that prove my fix is effective or that my feature works** - [ ] **Public components / utils / props are appropriately exported** - [ ] I have added necessary documentation (if appropriate) ## External Contributions This project is not yet set up to accept pull requests from external contributors. If you have a pull request that you believe should be accepted, please contact the Developer Relations team <developer-advocates@sendbird.com> with details and we'll evaluate if we can set up a [CLA](https://en.wikipedia.org/wiki/Contributor_License_Agreement) to allow for the contribution. [CLNP-8323]: https://sendbird.atlassian.net/browse/CLNP-8323?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent 116c5be commit 9a545a5

5 files changed

Lines changed: 74 additions & 53 deletions

File tree

.github/workflows/release-ticket-creation.yml

Lines changed: 0 additions & 38 deletions
This file was deleted.

src/ui/MessageInput/__tests__/MessageInput.spec.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,19 @@ describe('ui/MessageInput', () => {
204204
fireEvent.keyDown(input, { key: 'Enter' });
205205
expect(onSendMessage).not.toHaveBeenCalledWith({ mentionTemplate: '', message: mockText });
206206
});
207+
208+
it('should not call sendMessage with only zero-width spaces', () => {
209+
const onSendMessage = jest.fn();
210+
render(<MessageInput onSendMessage={onSendMessage} />);
211+
212+
const input = screen.getByRole('textbox');
213+
input.textContent = '\u200B';
214+
215+
fireEvent.input(input);
216+
fireEvent.keyDown(input, { key: 'Enter' });
217+
218+
expect(onSendMessage).not.toHaveBeenCalled();
219+
});
207220

208221
it('should render send icon if text is present', async() => {
209222
const onSendMessage = jest.fn();
@@ -367,4 +380,3 @@ describe('MessageInput error handling', () => {
367380
});
368381
});
369382

370-

src/ui/MessageInput/__tests__/utils.spec.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import jsdom from 'jsdom';
2-
import { nodeListToArray, sanitizeString } from '../utils';
2+
import { extractTextAndMentions, nodeListToArray, sanitizeString, stripZeroWidthSpace } from '../utils';
33

44
describe('MessageInputUtils/nodeListToArray', () => {
55
it('should convert node list to array', () => {
@@ -90,3 +90,30 @@ describe('Utils/sanitizeString', () => {
9090
expect(sanitizeString(input)).toBe(expectedOutput);
9191
});
9292
});
93+
94+
describe('Utils/stripZeroWidthSpace', () => {
95+
it('should remove zero-width spaces', () => {
96+
const input = 'Hello\u200BWorld\u200B';
97+
expect(stripZeroWidthSpace(input)).toBe('HelloWorld');
98+
});
99+
100+
it('should return an empty string if input is undefined', () => {
101+
expect(stripZeroWidthSpace(undefined)).toBe('');
102+
});
103+
});
104+
105+
describe('Utils/extractTextAndMentions', () => {
106+
it('should remove zero-width spaces from extracted text', () => {
107+
const dom = new jsdom.JSDOM('<div id="root">Hello\u200BWorld\u200B</div>');
108+
const root = dom.window.document.getElementById('root');
109+
if (!root) throw new Error('root element not found');
110+
111+
const result = extractTextAndMentions(root.childNodes);
112+
113+
expect(result).toEqual({
114+
isMentionedMessage: false,
115+
mentionTemplate: 'HelloWorld',
116+
messageText: 'HelloWorld',
117+
});
118+
});
119+
});

src/ui/MessageInput/index.tsx

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ import Icon, { IconColors, IconTypes } from '../Icon';
1111
import Label, { LabelColors, LabelTypography } from '../Label';
1212
import { useLocalization } from '../../lib/LocalizationContext';
1313

14-
import { extractTextAndMentions, isChannelTypeSupportsMultipleFilesMessage, nodeListToArray, sanitizeString } from './utils';
14+
import {
15+
extractTextAndMentions,
16+
isChannelTypeSupportsMultipleFilesMessage,
17+
nodeListToArray,
18+
sanitizeString,
19+
stripZeroWidthSpace,
20+
} from './utils';
1521
import { arrayEqual, getMimeTypesUIKitAccepts } from '../../utils';
1622
import { usePaste } from './hooks/usePaste';
1723
import { tokenizeMessage } from '../../modules/Message/utils/tokens/tokenize';
@@ -38,6 +44,14 @@ const resetInput = (ref: MutableRefObject<HTMLInputElement | null> | null) => {
3844
}
3945
};
4046

47+
const getTextContentWithoutZeroWidthSpace = (node?: { textContent?: string | null } | null) => {
48+
return stripZeroWidthSpace(node?.textContent ?? '');
49+
};
50+
51+
const hasTextContentWithoutZeroWidthSpace = (node?: { textContent?: string | null } | null) => {
52+
return getTextContentWithoutZeroWidthSpace(node).trim().length > 0;
53+
};
54+
4155
interface TargetStringInfo {
4256
targetString: string;
4357
startNodeIndex: number | null;
@@ -145,7 +159,7 @@ const MessageInput = React.forwardRef<HTMLInputElement, MessageInputProps>((prop
145159
useEffect(() => {
146160
const textField = internalRef?.current;
147161
setMentionedUserIds([]);
148-
setIsInput(textField?.textContent ? textField.textContent.trim().length > 0 : false);
162+
setIsInput(hasTextContentWithoutZeroWidthSpace(textField));
149163
}, [initialValue]);
150164

151165
// #Mention | Clear input value when channel changes
@@ -199,7 +213,7 @@ const MessageInput = React.forwardRef<HTMLInputElement, MessageInputProps>((prop
199213
}
200214
setMentionedUserIds([]);
201215
}
202-
setIsInput(textField?.textContent ? textField?.textContent?.trim().length > 0 : false);
216+
setIsInput(hasTextContentWithoutZeroWidthSpace(textField));
203217
}
204218
}, [isEdit, message]);
205219

@@ -216,7 +230,7 @@ const MessageInput = React.forwardRef<HTMLInputElement, MessageInputProps>((prop
216230
setMentionedUserIds(newMentionedUserIds);
217231
}
218232
}
219-
setIsInput(textField?.textContent ? textField.textContent?.trim().length > 0 : false);
233+
setIsInput(hasTextContentWithoutZeroWidthSpace(textField));
220234
}, [targetStringInfo, isMentionEnabled]);
221235

222236
// #Mention | Replace selected user nickname to the MentionedUserLabel
@@ -348,8 +362,9 @@ const MessageInput = React.forwardRef<HTMLInputElement, MessageInputProps>((prop
348362
const sendMessage = () => {
349363
try {
350364
const textField = internalRef?.current;
351-
if (!isEdit && textField?.textContent) {
365+
if (!isEdit && textField) {
352366
const { messageText, mentionTemplate, isMentionedMessage } = extractTextAndMentions(textField.childNodes);
367+
if (messageText.trim().length === 0) return;
353368
const params = { message: messageText, mentionTemplate };
354369
if (!isMentionedMessage) params.mentionTemplate = '';
355370
onSendMessage(params);
@@ -372,13 +387,14 @@ const MessageInput = React.forwardRef<HTMLInputElement, MessageInputProps>((prop
372387
eventHandlers?.message?.onSendMessageFailed?.(message, error);
373388
}
374389
};
375-
const isEditDisabled = !internalRef?.current?.textContent?.trim();
390+
const isEditDisabled = !hasTextContentWithoutZeroWidthSpace(internalRef?.current);
376391
const editMessage = () => {
377392
try {
378393
const textField = internalRef?.current;
379394
const messageId = message?.messageId;
380395
if (isEdit && messageId && textField) {
381396
const { messageText, mentionTemplate } = extractTextAndMentions(textField.childNodes);
397+
if (messageText.trim().length === 0) return;
382398
const params = { messageId, message: messageText, mentionTemplate };
383399
onUpdateMessage(params);
384400
resetInput(internalRef);
@@ -470,8 +486,7 @@ const MessageInput = React.forwardRef<HTMLInputElement, MessageInputProps>((prop
470486
!e.shiftKey
471487
&& e.key === MessageInputKeys.Enter
472488
&& !isMobile
473-
&& internalRef?.current?.textContent
474-
&& internalRef.current.textContent.trim().length > 0
489+
&& hasTextContentWithoutZeroWidthSpace(internalRef?.current)
475490
&& e?.nativeEvent?.isComposing !== true
476491
/**
477492
* NOTE: What isComposing does?
@@ -506,7 +521,7 @@ const MessageInput = React.forwardRef<HTMLInputElement, MessageInputProps>((prop
506521
}}
507522
onInput={() => {
508523
onStartTyping();
509-
setIsInput(internalRef?.current?.textContent ? internalRef.current.textContent.trim().length > 0 : false);
524+
setIsInput(hasTextContentWithoutZeroWidthSpace(internalRef?.current));
510525
useMentionedLabelDetection();
511526
}}
512527
onPaste={(e) => {
@@ -515,7 +530,7 @@ const MessageInput = React.forwardRef<HTMLInputElement, MessageInputProps>((prop
515530
}}
516531
/>
517532
{/* placeholder */}
518-
{(internalRef?.current?.textContent?.length ?? 0) === 0 && (
533+
{getTextContentWithoutZeroWidthSpace(internalRef?.current).length === 0 && (
519534
<Label
520535
className="sendbird-message-input--placeholder"
521536
type={LabelTypography.BODY_1}

src/ui/MessageInput/utils.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ export const sanitizeString = (str: string = ''): string => {
1212
return str.replace(/[<>]/g, (char) => (char === '<' ? '&#60;' : '&#62;'));
1313
};
1414

15+
export const stripZeroWidthSpace = (str: string = ''): string => {
16+
if (!str) return '';
17+
return str.replace(/\u200B/g, '');
18+
};
19+
1520
/**
1621
* NodeList cannot be used with Array methods
1722
* @param {NodeListOf<ChildNode>} childNodes
@@ -40,17 +45,17 @@ export function extractTextAndMentions(childNodes: NodeListOf<ChildNode>) {
4045
const { innerText, dataset = {} } = node;
4146
const { userid = '' } = dataset;
4247
if (userid) isMentionedMessage = true;
43-
messageText += innerText;
48+
messageText += stripZeroWidthSpace(innerText);
4449
mentionTemplate += `${USER_MENTION_TEMP_CHAR}{${userid}}`;
4550
} else if (isHTMLElement(node) && node.nodeName === NodeNames.Br) {
4651
messageText += '\n';
4752
mentionTemplate += '\n';
4853
} else if (isHTMLElement(node) && node.nodeName === NodeNames.Div) {
49-
const { textContent = '' } = node;
54+
const textContent = stripZeroWidthSpace(node.textContent ?? '');
5055
messageText += `\n${textContent}`;
5156
mentionTemplate += `\n${textContent}`;
5257
} else {
53-
const { textContent = '' } = node;
58+
const textContent = stripZeroWidthSpace(node.textContent ?? '');
5459
messageText += textContent;
5560
mentionTemplate += textContent;
5661
}

0 commit comments

Comments
 (0)