Skip to content

Commit 43cd83e

Browse files
authored
fix: Allow multiple line in the MessageInput of Mobile env (#985)
[CLNP-2319](https://sendbird.atlassian.net/browse/CLNP-2319) ### Issue In a desktop layout, prevent line breaks from occurring even when the 'Enter' key is pressed. It triggers sending the message. But in the Mobile layout, this behavior doesn't fit mobile UX. So need to allow line breaks when users press the 'Enter' key. ### Fix & ChangeLog * Add a prop `isMobile` to the ui/MessageInput * Fix the `editMessage` logic in the MessageInput to apply the line break
1 parent 9012e99 commit 43cd83e

File tree

5 files changed

+61
-58
lines changed

5 files changed

+61
-58
lines changed
Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
1+
// For legacy
2+
export * from '../../GroupChannel/context/const';
3+
4+
// These are not used for collections(GroupChannel)
15
export const PREV_RESULT_SIZE = 30;
26
export const NEXT_RESULT_SIZE = 15;
3-
4-
export const MAX_USER_MENTION_COUNT = 10;
5-
export const MAX_USER_SUGGESTION_COUNT = 15;
6-
export const USER_MENTION_TEMP_CHAR = '@';
7-
8-
export enum ThreadReplySelectType {
9-
PARENT = 'PARENT',
10-
THREAD = 'THREAD',
11-
}

src/modules/GroupChannel/components/MessageInputWrapper/MessageInputWrapperView.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ export const MessageInputWrapperView = React.forwardRef((
200200
className="sendbird-message-input-wrapper__message-input"
201201
channel={currentChannel}
202202
channelUrl={currentChannel?.url}
203+
isMobile={isMobile}
203204
acceptableMimeTypes={acceptableMimeTypes}
204205
mentionSelectedUser={selectedUser}
205206
isMentionEnabled={isMentionEnabled}

src/modules/Thread/components/ThreadMessageInput/index.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,19 @@ import { MutedState } from '@sendbird/chat/groupChannel';
44
import './index.scss';
55

66
import useSendbirdStateContext from '../../../../hooks/useSendbirdStateContext';
7+
import { useMediaQueryContext } from '../../../../lib/MediaQueryContext';
8+
import { useThreadContext } from '../../context/ThreadProvider';
9+
import { useLocalization } from '../../../../lib/LocalizationContext';
10+
711
import MessageInput from '../../../../ui/MessageInput';
812
import { MessageInputKeys } from '../../../../ui/MessageInput/const';
913
import { SuggestedMentionList } from '../SuggestedMentionList';
10-
import { useThreadContext } from '../../context/ThreadProvider';
11-
import { useLocalization } from '../../../../lib/LocalizationContext';
1214
import { VoiceMessageInputWrapper } from '../../../GroupChannel/components/MessageInputWrapper';
1315
import { Role } from '../../../../lib/types';
16+
1417
import { useDirtyGetMentions } from '../../../Message/hooks/useDirtyGetMentions';
15-
import { isDisabledBecauseFrozen, isDisabledBecauseMuted } from '../../../Channel/context/utils';
1618
import { useHandleUploadFiles } from '../../../Channel/context/hooks/useHandleUploadFiles';
19+
import { isDisabledBecauseFrozen, isDisabledBecauseMuted } from '../../../Channel/context/utils';
1720

1821
export interface ThreadMessageInputProps {
1922
className?: string;
@@ -37,6 +40,7 @@ const ThreadMessageInput = (
3740
} = props;
3841

3942
const { config } = useSendbirdStateContext();
43+
const { isMobile } = useMediaQueryContext();
4044
const { stringSet } = useLocalization();
4145
const {
4246
isMentionEnabled,
@@ -163,11 +167,12 @@ const ThreadMessageInput = (
163167
<MessageInput
164168
className="sendbird-thread-message-input__message-input"
165169
messageFieldId="sendbird-message-input-text-field--thread"
166-
disabled={threadInputDisabled}
167170
channel={currentChannel}
171+
channelUrl={currentChannel?.url}
172+
isMobile={isMobile}
173+
disabled={threadInputDisabled}
168174
acceptableMimeTypes={acceptableMimeTypes}
169175
setMentionedUsers={setMentionedUsers}
170-
channelUrl={currentChannel?.url}
171176
mentionSelectedUser={selectedUser}
172177
isMentionEnabled={isMentionEnabled}
173178
isVoiceMessageEnabled={isVoiceMessageEnabled}

src/ui/MessageInput/index.tsx

Lines changed: 13 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
22

33
import './index.scss';
4-
import { MessageInputKeys, NodeNames, NodeTypes } from './const';
4+
import { MessageInputKeys, NodeTypes } from './const';
55

66
import { USER_MENTION_TEMP_CHAR } from '../../modules/Channel/context/const';
77
import IconButton from '../IconButton';
@@ -12,7 +12,7 @@ import Label, { LabelColors, LabelTypography } from '../Label';
1212
import { useLocalization } from '../../lib/LocalizationContext';
1313
import useSendbirdStateContext from '../../hooks/useSendbirdStateContext';
1414

15-
import { isChannelTypeSupportsMultipleFilesMessage, nodeListToArray, sanitizeString } from './utils';
15+
import { extractTextAndMentions, isChannelTypeSupportsMultipleFilesMessage, nodeListToArray, sanitizeString } from './utils';
1616
import { arrayEqual, getClassName, getMimeTypesUIKitAccepts } from '../../utils';
1717
import usePaste from './hooks/usePaste';
1818
import { tokenizeMessage } from '../../modules/Message/utils/tokens/tokenize';
@@ -65,6 +65,7 @@ type MessageInputProps = {
6565
className?: string | string[];
6666
messageFieldId?: string;
6767
isEdit?: boolean;
68+
isMobile?: boolean;
6869
isMentionEnabled?: boolean;
6970
isVoiceMessageEnabled?: boolean;
7071
isSelectingMultipleFilesEnabled?: boolean;
@@ -96,6 +97,7 @@ const MessageInput = React.forwardRef<HTMLInputElement, MessageInputProps>((prop
9697
className = '',
9798
messageFieldId = '',
9899
isEdit = false,
100+
isMobile = false,
99101
isMentionEnabled = false,
100102
isVoiceMessageEnabled = true,
101103
isSelectingMultipleFilesEnabled = false,
@@ -363,30 +365,7 @@ const MessageInput = React.forwardRef<HTMLInputElement, MessageInputProps>((prop
363365
const sendMessage = () => {
364366
const textField = internalRef?.current;
365367
if (!isEdit && textField?.textContent) {
366-
let messageText = '';
367-
let mentionTemplate = '';
368-
textField.childNodes.forEach((node) => {
369-
if (node.nodeType === NodeTypes.ElementNode && node.nodeName === NodeNames.Span) {
370-
// @ts-ignore
371-
const { innerText, dataset = {} } = node;
372-
const { userid = '' } = dataset;
373-
messageText += innerText;
374-
mentionTemplate += `${USER_MENTION_TEMP_CHAR}{${userid}}`;
375-
} else if (node.nodeType === NodeTypes.ElementNode && node.nodeName === NodeNames.Br) {
376-
messageText += '\n';
377-
mentionTemplate += '\n';
378-
} else if (node?.nodeType === NodeTypes.ElementNode && node?.nodeName === NodeNames.Div) {
379-
// handles newline in safari
380-
const { textContent = '' } = node;
381-
messageText += `\n${textContent}`;
382-
mentionTemplate += `\n${textContent}`;
383-
} else {
384-
// other nodes including text node
385-
const { textContent = '' } = node;
386-
messageText += textContent;
387-
mentionTemplate += textContent;
388-
}
389-
});
368+
const { messageText, mentionTemplate } = extractTextAndMentions(textField.childNodes);
390369
const params = { message: messageText, mentionTemplate };
391370
onSendMessage(params);
392371
resetInput(internalRef);
@@ -401,24 +380,7 @@ const MessageInput = React.forwardRef<HTMLInputElement, MessageInputProps>((prop
401380
const textField = internalRef?.current;
402381
const messageId = message?.messageId;
403382
if (isEdit && messageId) {
404-
let messageText = '';
405-
let mentionTemplate = '';
406-
textField.childNodes.forEach((node) => {
407-
if (node.nodeType === NodeTypes.ElementNode && node.nodeName === NodeNames.Span) {
408-
// @ts-ignore
409-
const { innerText, dataset = {} } = node;
410-
const { userid = '' } = dataset;
411-
messageText += innerText;
412-
mentionTemplate += `${USER_MENTION_TEMP_CHAR}{${userid}}`;
413-
messageText += '\n';
414-
mentionTemplate += '\n';
415-
} else {
416-
// other nodes including text node
417-
const { textContent = '' } = node;
418-
messageText += textContent;
419-
mentionTemplate += textContent;
420-
}
421-
});
383+
const { messageText, mentionTemplate } = extractTextAndMentions(textField.childNodes);
422384
const params = { messageId, message: messageText, mentionTemplate };
423385
onUpdateMessage(params);
424386
resetInput(internalRef);
@@ -459,8 +421,15 @@ const MessageInput = React.forwardRef<HTMLInputElement, MessageInputProps>((prop
459421
if (
460422
!e.shiftKey
461423
&& e.key === MessageInputKeys.Enter
424+
&& !isMobile
462425
&& internalRef?.current?.textContent?.trim().length > 0
463426
&& e?.nativeEvent?.isComposing !== true
427+
/**
428+
* NOTE: What isComposing does?
429+
* Check if the user has finished composing characters
430+
* (e.g., for languages like Korean, Japanese, where characters are composed from multiple keystrokes)
431+
* Prevents executing the code while the user is still composing characters.
432+
*/
464433
) {
465434
/**
466435
* NOTE: contentEditable does not work as expected in mobile WebKit(Safari).

src/ui/MessageInput/utils.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Sanitize that special characters of HTML tags cause XSS issue
22
import { BaseChannel } from '@sendbird/chat';
3+
import { NodeNames, NodeTypes } from './const';
4+
import { USER_MENTION_TEMP_CHAR } from '../../modules/GroupChannel/context/const';
35

46
export const sanitizeString = (str?: string) => {
57
return str?.replace(/[\u00A0-\u9999<>]/gim, (i) => ''.concat('&#', String(i.charCodeAt(0)), ';'));
@@ -21,3 +23,34 @@ export const nodeListToArray = (childNodes?: Node['childNodes'] | null) => {
2123
export function isChannelTypeSupportsMultipleFilesMessage(channel: BaseChannel) {
2224
return channel && channel.isGroupChannel?.() && !channel.isBroadcast && !channel.isSuper;
2325
}
26+
27+
// Type guard: This function ensures that the node contains `innerText` and `dataset` properties
28+
function isHTMLElement(node: ChildNode): node is HTMLElement {
29+
return node.nodeType === NodeTypes.ElementNode;
30+
}
31+
32+
// eslint-disable-next-line no-undef
33+
export function extractTextAndMentions(childNodes: NodeListOf<ChildNode>) {
34+
let messageText = '';
35+
let mentionTemplate = '';
36+
childNodes.forEach((node) => {
37+
if (isHTMLElement(node) && node.nodeName === NodeNames.Span) {
38+
const { innerText, dataset = {} } = node;
39+
const { userid = '' } = dataset;
40+
messageText += innerText;
41+
mentionTemplate += `${USER_MENTION_TEMP_CHAR}{${userid}}`;
42+
} else if (isHTMLElement(node) && node.nodeName === NodeNames.Br) {
43+
messageText += '\n';
44+
mentionTemplate += '\n';
45+
} else if (isHTMLElement(node) && node.nodeName === NodeNames.Div) {
46+
const { textContent = '' } = node;
47+
messageText += `\n${textContent}`;
48+
mentionTemplate += `\n${textContent}`;
49+
} else {
50+
const { textContent = '' } = node;
51+
messageText += textContent;
52+
mentionTemplate += textContent;
53+
}
54+
});
55+
return { messageText, mentionTemplate };
56+
}

0 commit comments

Comments
 (0)