UNPKG

@sendbird/uikit-react

Version:

Sendbird UIKit for React: A feature-rich and customizable chat UI kit with messaging, channel management, and user authentication.

708 lines (700 loc) 44.2 kB
import { _ as __assign, e as __spreadArray } from './bundle-Bpofr334.js'; import React__default, { useContext, useMemo, useState, useRef, useEffect, useCallback } from 'react'; import { M as MessageInputKeys, a as NodeTypes } from './bundle-BEPoP7sp.js'; import { b as USER_MENTION_TEMP_CHAR } from './bundle-CFc2hy8g.js'; import IconButton from '../ui/IconButton.js'; import Button, { ButtonSizes, ButtonTypes } from '../ui/Button.js'; import { r as renderToString, s as sanitizeString, n as nodeListToArray, u as usePaste, i as isChannelTypeSupportsMultipleFilesMessage, e as extractTextAndMentions, a as stripZeroWidthSpace } from './bundle-C5-D2BAP.js'; import Icon, { IconColors, IconTypes } from '../ui/Icon.js'; import { L as Label, a as LabelColors, c as LabelTypography } from './bundle-Cdplrrlw.js'; import { L as LocalizationContext, u as useLocalization } from './bundle-Cdqsdoa8.js'; import { a as arrayEqual, n as getMimeTypesUIKitAccepts } from './bundle-BZSLsKkw.js'; import { b as tokenizeMessage, T as TOKEN_TYPES, U as USER_MENTION_PREFIX } from './bundle-EKFQIahk.js'; import { K } from './bundle-lqEjT2ED.js'; import { c as classnames } from './bundle-DX6fRIJl.js'; import { i as isMobileIOS } from './bundle-CglqREVl.js'; import { u as useSendbird } from './bundle-4clodtJA.js'; var ELLIPSIS = '...'; /** * Truncate `filename` to fit within `maxChars` by inserting `...` near the * middle while preserving the file extension. If the filename already fits, * returns it unchanged. If the extension alone exceeds the budget, the * extension is dropped and the head is truncated. * * The utility operates on Unicode code points (after NFC normalization), so * Hangul syllables stay intact even when the input is decomposed jamo (as * macOS produces), and surrogate pairs (emoji, supplementary CJK) are never * split. The caller is responsible for picking a `maxChars` that fits the * rendered container. * * Examples: * truncateMiddleKeepExtension('File-name-is-too-long.pdf', 14) -> 'File...ong.pdf' * truncateMiddleKeepExtension('short.pdf', 14) -> 'short.pdf' * truncateMiddleKeepExtension('noextension', 14) -> 'noextension' * truncateMiddleKeepExtension('verylong.tar.gz', 10) -> 've...tar.gz' * truncateMiddleKeepExtension('long', 3) -> '...' */ function truncateMiddleKeepExtension(filename, maxChars) { if (maxChars <= 0) return ''; var normalized = filename.normalize('NFC'); var chars = Array.from(normalized); if (chars.length <= maxChars) return normalized; if (maxChars <= ELLIPSIS.length) return ELLIPSIS.slice(0, maxChars); var dotIdx = chars.lastIndexOf('.'); var hasExtension = dotIdx > 0 && dotIdx < chars.length - 1; if (!hasExtension) { return chars.slice(0, maxChars - ELLIPSIS.length).join('') + ELLIPSIS; } var extChars = chars.slice(dotIdx); var baseChars = chars.slice(0, dotIdx); var baseBudget = maxChars - ELLIPSIS.length - extChars.length; if (baseBudget <= 0) { return chars.slice(0, maxChars - ELLIPSIS.length).join('') + ELLIPSIS; } var headLen = Math.ceil(baseBudget / 2); var tailLen = baseBudget - headLen; return baseChars.slice(0, headLen).join('') + ELLIPSIS + (tailLen > 0 ? baseChars.slice(baseChars.length - tailLen).join('') : '') + extChars.join(''); } var META_WIDTH_PX = 108; var FILENAME_FONT = '700 14px Roboto, sans-serif'; function fitFilenameToWidth(filename) { if (typeof document === 'undefined') return filename; var ctx = document.createElement('canvas').getContext('2d'); if (!ctx) return filename; ctx.font = FILENAME_FONT; if (ctx.measureText(filename).width <= META_WIDTH_PX) return filename; var lo = 3; var hi = filename.length; var best = '...'; while (lo <= hi) { var mid = (lo + hi) >> 1; var candidate = truncateMiddleKeepExtension(filename, mid); if (ctx.measureText(candidate).width <= META_WIDTH_PX) { best = candidate; lo = mid + 1; } else { hi = mid - 1; } } return best; } /** Extract the uppercased extension from the filename, falling back to a * localized "FILE" label when no extension is present. */ function getExtensionLabel(filename, fallback) { var dotIdx = filename.lastIndexOf('.'); if (dotIdx <= 0 || dotIdx === filename.length - 1) return fallback; return filename.slice(dotIdx + 1).toUpperCase(); } /** * Card representation of a non-image pending file in the composer. Used in * place of the square image thumbnail when `pendingFile.isImage` is false. * The card shows a generic file icon, the (middle-truncated) filename, and * the uppercased extension label. */ var PendingFileCard = function (_a) { var pendingFile = _a.pendingFile, onRemove = _a.onRemove; var stringSet = useContext(LocalizationContext).stringSet; var id = pendingFile.id, file = pendingFile.file; var displayName = useMemo(function () { return fitFilenameToWidth(file.name); }, [file.name]); var extLabel = getExtensionLabel(file.name, stringSet.MESSAGE_INPUT__PENDING_FILE__TYPE_UNKNOWN); return (React__default.createElement("div", { className: "sendbird-message-input__pending-card", "data-testid": "sendbird-pending-file" }, React__default.createElement("div", { className: "sendbird-message-input__pending-card__body" }, React__default.createElement("div", { className: "sendbird-message-input__pending-card__icon" }, React__default.createElement(Icon, { type: IconTypes.FILE_DOCUMENT, fillColor: IconColors.PRIMARY, width: "24px", height: "24px" })), React__default.createElement("div", { className: "sendbird-message-input__pending-card__meta" }, React__default.createElement(Label, { className: "sendbird-message-input__pending-card__name", type: LabelTypography.CAPTION_1, color: LabelColors.ONBACKGROUND_1 }, displayName), React__default.createElement(Label, { className: "sendbird-message-input__pending-card__type", type: LabelTypography.CAPTION_2, color: LabelColors.ONBACKGROUND_2 }, extLabel))), React__default.createElement("button", { type: "button", className: "sendbird-message-input__pending-card__remove", "aria-label": stringSet.MESSAGE_INPUT__PENDING_FILE__REMOVE, onClick: function () { return onRemove(id); } }, React__default.createElement(Icon, { type: IconTypes.REMOVE, width: "22px", height: "22px" })))); }; /** * Renders one staged file in the composer. Images get a square thumbnail with * a corner remove button; non-images delegate to PendingFileCard, which * shows a horizontal card with icon + filename + uppercased extension. */ var PendingFileItem = function (_a) { var pendingFile = _a.pendingFile, onRemove = _a.onRemove; var stringSet = useContext(LocalizationContext).stringSet; var id = pendingFile.id, file = pendingFile.file, previewUrl = pendingFile.previewUrl, isImage = pendingFile.isImage; var _b = useState(false), imageLoaded = _b[0], setImageLoaded = _b[1]; if (!isImage) { return React__default.createElement(PendingFileCard, { pendingFile: pendingFile, onRemove: onRemove }); } return (React__default.createElement("div", { className: "sendbird-message-input__pending-file", "data-testid": "sendbird-pending-file" }, React__default.createElement("div", { className: "sendbird-message-input__pending-file__thumbnail" }, previewUrl && (React__default.createElement("img", { className: "sendbird-message-input__pending-file__image", src: previewUrl, alt: file.name, onLoad: function () { return setImageLoaded(true); } })), !imageLoaded && (React__default.createElement("div", { className: "sendbird-message-input__pending-file__image-placeholder" }, React__default.createElement(Icon, { type: IconTypes.PHOTO, fillColor: IconColors.ON_BACKGROUND_2, width: "32px", height: "32px" }))), React__default.createElement("button", { type: "button", className: "sendbird-message-input__pending-file__remove", "aria-label": stringSet.MESSAGE_INPUT__PENDING_FILE__REMOVE, onClick: function () { return onRemove(id); } }, React__default.createElement(Icon, { type: IconTypes.REMOVE, width: "22px", height: "22px" }))))); }; var PendingFilesPreview = function (_a) { var pendingFiles = _a.pendingFiles, onRemove = _a.onRemove, className = _a.className; var containerRef = useRef(null); var prevCountRef = useRef(pendingFiles.length); useEffect(function () { var el = containerRef.current; if (!el) return undefined; var onWheel = function (e) { if (el.scrollWidth <= el.clientWidth) return; if (e.deltaY === 0 || Math.abs(e.deltaX) >= Math.abs(e.deltaY)) return; e.preventDefault(); el.scrollLeft += e.deltaY; }; el.addEventListener('wheel', onWheel, { passive: false }); return function () { return el.removeEventListener('wheel', onWheel); }; }, []); useEffect(function () { var el = containerRef.current; if (!el) return; if (pendingFiles.length > prevCountRef.current) { el.scrollTo({ left: el.scrollWidth, behavior: 'smooth' }); } prevCountRef.current = pendingFiles.length; }, [pendingFiles.length]); if (pendingFiles.length === 0) return null; var classNames = ['sendbird-message-input__pending-preview', className].filter(Boolean).join(' '); return (React__default.createElement("div", { ref: containerRef, className: classNames, "data-testid": "sendbird-pending-files-preview", role: "list" }, pendingFiles.map(function (entry) { return (React__default.createElement(PendingFileItem, { key: entry.id, pendingFile: entry, onRemove: onRemove })); }))); }; /** * FIXME: * Import this ChannelType enum from @sendbird/chat * once MessageInput.spec unit tests can be run \wo jest <-> ESM issue */ var ChannelType; (function (ChannelType) { ChannelType["BASE"] = "base"; ChannelType["GROUP"] = "group"; ChannelType["OPEN"] = "open"; })(ChannelType || (ChannelType = {})); /** * FIXME: Simplify this in UIKit@v4 * If customer is using MessageInput inside our modules(ie: Channel, Thread, etc), * we should use the config from the module. * If customer is using MessageInput outside our modules(ie: custom UI), * we expect Channel to be undefined and customer gets control to show/hide file-upload. * @param {*} channel GroupChannel | OpenChannel * @param {*} config SendbirdStateConfig * @returns boolean */ var checkIfFileUploadEnabled = function (_a) { var channel = _a.channel, config = _a.config; var isEnabled = K(channel === null || channel === void 0 ? void 0 : channel.channelType) .with(ChannelType.GROUP, function () { var _a; return (_a = config === null || config === void 0 ? void 0 : config.groupChannel) === null || _a === void 0 ? void 0 : _a.enableDocument; }) .with(ChannelType.OPEN, function () { var _a; return (_a = config === null || config === void 0 ? void 0 : config.openChannel) === null || _a === void 0 ? void 0 : _a.enableDocument; }) .otherwise(function () { return true; }); return isEnabled; }; var TEXT_FIELD_ID = 'sendbird-message-input-text-field'; var noop = function () { return null; }; var resetInput = function (ref) { if (ref && ref.current) { ref.current.innerHTML = ''; } }; var getTextContentWithoutZeroWidthSpace = function (node) { var _a; return stripZeroWidthSpace((_a = node === null || node === void 0 ? void 0 : node.textContent) !== null && _a !== void 0 ? _a : ''); }; var hasTextContentWithoutZeroWidthSpace = function (node) { return getTextContentWithoutZeroWidthSpace(node).trim().length > 0; }; var initialTargetStringInfo = { targetString: '', startNodeIndex: null, startOffsetIndex: null, endNodeIndex: null, endOffsetIndex: null, }; var MessageInput = React__default.forwardRef(function (props, externalRef) { var _a; var channel = props.channel, _b = props.className, className = _b === void 0 ? '' : _b, _c = props.messageFieldId, messageFieldId = _c === void 0 ? '' : _c, _d = props.isEdit, isEdit = _d === void 0 ? false : _d, _e = props.isMobile, isMobile = _e === void 0 ? false : _e, _f = props.isMentionEnabled, isMentionEnabled = _f === void 0 ? false : _f, _g = props.isVoiceMessageEnabled, isVoiceMessageEnabled = _g === void 0 ? true : _g, _h = props.isSelectingMultipleFilesEnabled, isSelectingMultipleFilesEnabled = _h === void 0 ? false : _h, _j = props.disabled, disabled = _j === void 0 ? false : _j, _k = props.message, message = _k === void 0 ? null : _k, _l = props.placeholder, placeholder = _l === void 0 ? '' : _l, _m = props.maxLength, maxLength = _m === void 0 ? 5000 : _m, _o = props.onFileUpload, onFileUpload = _o === void 0 ? noop : _o, _p = props.onSendMessage, onSendMessage = _p === void 0 ? noop : _p, _q = props.onUpdateMessage, onUpdateMessage = _q === void 0 ? noop : _q, _r = props.onCancelEdit, onCancelEdit = _r === void 0 ? noop : _r, _s = props.onStartTyping, onStartTyping = _s === void 0 ? noop : _s, _t = props.onStopTyping, onStopTyping = _t === void 0 ? noop : _t, _u = props.channelUrl, channelUrl = _u === void 0 ? '' : _u, _v = props.mentionSelectedUser, mentionSelectedUser = _v === void 0 ? null : _v, _w = props.onUserMentioned, onUserMentioned = _w === void 0 ? noop : _w, _x = props.onMentionStringChange, onMentionStringChange = _x === void 0 ? noop : _x, _y = props.onMentionedUserIdsUpdated, onMentionedUserIdsUpdated = _y === void 0 ? noop : _y, _z = props.onVoiceMessageIconClick, onVoiceMessageIconClick = _z === void 0 ? noop : _z, _0 = props.onKeyUp, onKeyUp = _0 === void 0 ? noop : _0, _1 = props.onKeyDown, onKeyDown = _1 === void 0 ? noop : _1, _2 = props.renderFileUploadIcon, renderFileUploadIcon = _2 === void 0 ? noop : _2, _3 = props.renderVoiceMessageIcon, renderVoiceMessageIcon = _3 === void 0 ? noop : _3, _4 = props.renderSendMessageIcon, renderSendMessageIcon = _4 === void 0 ? noop : _4, _5 = props.setMentionedUsers, setMentionedUsers = _5 === void 0 ? noop : _5, acceptableMimeTypes = props.acceptableMimeTypes, pendingFiles = props.pendingFiles, onAddFiles = props.onAddFiles, onRemoveFile = props.onRemoveFile, onSubmit = props.onSubmit; var isComposerMode = typeof onAddFiles === 'function'; var hasPendingFiles = ((_a = pendingFiles === null || pendingFiles === void 0 ? void 0 : pendingFiles.length) !== null && _a !== void 0 ? _a : 0) > 0; var internalRef = (externalRef && 'current' in externalRef) ? externalRef : useRef(null); var ghostInputRef = useRef(null); var wasTypingRef = useRef(false); var textFieldId = messageFieldId || TEXT_FIELD_ID; var stringSet = useLocalization().stringSet; var _6 = useSendbird().state, config = _6.config, eventHandlers = _6.eventHandlers; var isFileUploadEnabled = checkIfFileUploadEnabled({ channel: channel, config: config, }); // Gate paste/DnD/picker on the same enableDocument flag that hides the // attach icon — otherwise feature-flag-disabled environments leak files in. // Also gate on !isEdit: today no edit-mode caller passes composer props, but // if one did, staged files would have nowhere to go (Send is replaced by // Cancel/Save). Belt-and-suspenders. var fileProducerEnabled = isComposerMode && isFileUploadEnabled && !disabled && !isEdit; var guardedAddFiles = useCallback(function (incoming) { if (!fileProducerEnabled || !onAddFiles) return; if (incoming.length === 0) return; onAddFiles(incoming); }, [fileProducerEnabled, onAddFiles]); var fileInputRef = useRef(); var _7 = useState(false), isInput = _7[0], setIsInput = _7[1]; var _8 = useState([]), mentionedUserIds = _8[0], setMentionedUserIds = _8[1]; var _9 = useState(__assign({}, initialTargetStringInfo)), targetStringInfo = _9[0], setTargetStringInfo = _9[1]; // #Edit mode // for easily initialize input value from outside, but // useEffect(_, [channelUrl]) erase it var initialValue = props === null || props === void 0 ? void 0 : props.value; useEffect(function () { var textField = internalRef === null || internalRef === void 0 ? void 0 : internalRef.current; setMentionedUserIds([]); setIsInput(hasTextContentWithoutZeroWidthSpace(textField)); }, [initialValue]); var stashedHtmlRef = useRef(''); var prevHasPendingFilesRef = useRef(false); // #Mention | Clear input value when channel changes useEffect(function () { if (!isEdit) { setIsInput(false); resetInput(internalRef); wasTypingRef.current = false; stashedHtmlRef.current = ''; } }, [channelUrl]); useEffect(function () { var textField = internalRef === null || internalRef === void 0 ? void 0 : internalRef.current; if (!textField) { prevHasPendingFilesRef.current = hasPendingFiles; return; } if (hasPendingFiles && !prevHasPendingFilesRef.current) { if (hasTextContentWithoutZeroWidthSpace(textField)) { stashedHtmlRef.current = textField.innerHTML; resetInput(internalRef); setIsInput(false); } textField.focus(); } else if (!hasPendingFiles && prevHasPendingFilesRef.current) { if (stashedHtmlRef.current) { textField.innerHTML = stashedHtmlRef.current; stashedHtmlRef.current = ''; setIsInput(true); } } prevHasPendingFilesRef.current = hasPendingFiles; }, [hasPendingFiles]); // #Mention & #Edit | Fill message input values useEffect(function () { var _a; if (isEdit && (message === null || message === void 0 ? void 0 : message.messageId)) { // const textField = document.getElementById(textFieldId); var textField = internalRef === null || internalRef === void 0 ? void 0 : internalRef.current; if (isMentionEnabled && (message === null || message === void 0 ? void 0 : message.mentionedUsers) && message.mentionedUsers.length > 0 && (message === null || message === void 0 ? void 0 : message.mentionedMessageTemplate) && message.mentionedMessageTemplate.length > 0) { /* mention enabled */ var _b = message.mentionedUsers, mentionedUsers_1 = _b === void 0 ? [] : _b; var tokens = tokenizeMessage({ messageText: message === null || message === void 0 ? void 0 : message.mentionedMessageTemplate, mentionedUsers: mentionedUsers_1, includeMarkdown: channel.isGroupChannel() && config.groupChannel.enableMarkdownForUserMessage, }); if (textField) { textField.innerHTML = tokens .map(function (token) { if (token.type === TOKEN_TYPES.mention) { var mentionedUser = mentionedUsers_1.find(function (user) { return user.userId === token.userId; }); var nickname = "".concat(USER_MENTION_PREFIX).concat((mentionedUser === null || mentionedUser === void 0 ? void 0 : mentionedUser.nickname) || token.value || stringSet.MENTION_NAME__NO_NAME); return renderToString({ userId: token.userId, nickname: nickname, }); } return sanitizeString(token.value); }) .join(''); } } else { /* mention disabled */ try { if (textField) { textField.innerHTML = (_a = sanitizeString(message === null || message === void 0 ? void 0 : message.message)) !== null && _a !== void 0 ? _a : ''; } } catch (_c) { // } setMentionedUserIds([]); } setIsInput(hasTextContentWithoutZeroWidthSpace(textField)); } }, [isEdit, message]); // #Mention | Detect MentionedLabel modified var useMentionedLabelDetection = useCallback(function () { var textField = internalRef === null || internalRef === void 0 ? void 0 : internalRef.current; if (isMentionEnabled && textField) { var newMentionedUserIds = Array.from(textField.getElementsByClassName('sendbird-mention-user-label')).map( // @ts-ignore function (node) { var _a; return (_a = node === null || node === void 0 ? void 0 : node.dataset) === null || _a === void 0 ? void 0 : _a.userid; }); if (!arrayEqual(mentionedUserIds, newMentionedUserIds) || newMentionedUserIds.length === 0) { onMentionedUserIdsUpdated(newMentionedUserIds); setMentionedUserIds(newMentionedUserIds); } } setIsInput(hasTextContentWithoutZeroWidthSpace(textField)); }, [targetStringInfo, isMentionEnabled]); // #Mention | Replace selected user nickname to the MentionedUserLabel useEffect(function () { var _a, _b, _c, _d; if (isMentionEnabled && mentionSelectedUser) { var targetString = targetStringInfo.targetString, startNodeIndex = targetStringInfo.startNodeIndex, startOffsetIndex = targetStringInfo.startOffsetIndex, endNodeIndex = targetStringInfo.endNodeIndex, endOffsetIndex = targetStringInfo.endOffsetIndex; var textField_1 = internalRef === null || internalRef === void 0 ? void 0 : internalRef.current; if (targetString && startNodeIndex !== null && startOffsetIndex !== null && endOffsetIndex !== null && endNodeIndex !== null && textField_1) { // const textField = document.getElementById(textFieldId); var childNodes = nodeListToArray(textField_1 === null || textField_1 === void 0 ? void 0 : textField_1.childNodes); var startNodeTextContent = (_b = (_a = childNodes[startNodeIndex]) === null || _a === void 0 ? void 0 : _a.textContent) !== null && _b !== void 0 ? _b : ''; var frontTextNode = document.createTextNode(startNodeTextContent.slice(0, startOffsetIndex)); var endNodeTextContent = (_d = (_c = childNodes[endNodeIndex]) === null || _c === void 0 ? void 0 : _c.textContent) !== null && _d !== void 0 ? _d : ''; var backTextNode = endOffsetIndex && document.createTextNode("\u00A0".concat(endNodeTextContent.slice(endOffsetIndex))); var mentionLabel = renderToString({ userId: mentionSelectedUser === null || mentionSelectedUser === void 0 ? void 0 : mentionSelectedUser.userId, nickname: "".concat(USER_MENTION_TEMP_CHAR).concat((mentionSelectedUser === null || mentionSelectedUser === void 0 ? void 0 : mentionSelectedUser.nickname) || stringSet.MENTION_NAME__NO_NAME), }); var div = document.createElement('div'); div.innerHTML = mentionLabel; var newNodes = __spreadArray(__spreadArray(__spreadArray([], childNodes.slice(0, startNodeIndex), true), [ frontTextNode, div.childNodes[0], backTextNode ], false), childNodes.slice(endNodeIndex + 1), true); if (textField_1) { textField_1.innerHTML = ''; newNodes.forEach(function (newNode) { if (newNode) { textField_1.appendChild(newNode); } }); } onUserMentioned(mentionSelectedUser); if (window.getSelection || document.getSelection) { // set caret postion var selection = window.getSelection() || document.getSelection(); selection === null || selection === void 0 ? void 0 : selection.removeAllRanges(); var range = new Range(); range.selectNodeContents(textField_1); range.setStart(textField_1.childNodes[startNodeIndex + 2], 1); range.setEnd(textField_1.childNodes[startNodeIndex + 2], 1); range.collapse(false); selection === null || selection === void 0 ? void 0 : selection.addRange(range); textField_1.focus(); } setTargetStringInfo(__assign({}, initialTargetStringInfo)); useMentionedLabelDetection(); } } }, [mentionSelectedUser, isMentionEnabled]); // #Mention | Detect mentioning user nickname var useMentionInputDetection = useCallback(function () { var _a, _b; var selection = ((_a = window === null || window === void 0 ? void 0 : window.getSelection) === null || _a === void 0 ? void 0 : _a.call(window)) || ((_b = document === null || document === void 0 ? void 0 : document.getSelection) === null || _b === void 0 ? void 0 : _b.call(document)); var textField = internalRef === null || internalRef === void 0 ? void 0 : internalRef.current; if ((selection === null || selection === void 0 ? void 0 : selection.anchorNode) === textField) { onMentionStringChange(''); } if (isMentionEnabled && textField && selection && selection.anchorNode === selection.focusNode && selection.anchorOffset === selection.focusOffset) { var textStack = ''; var startNodeIndex = null; var startOffsetIndex = null; var _loop_1 = function (index) { var currentNode = textField.childNodes[index]; if (currentNode.nodeType === NodeTypes.TextNode) { /* text node */ var textContent = (function () { var _a; if (currentNode === selection.anchorNode) { return (currentNode === null || currentNode === void 0 ? void 0 : currentNode.textContent) ? currentNode === null || currentNode === void 0 ? void 0 : currentNode.textContent.slice(0, selection.anchorOffset) : ''; } return (_a = currentNode === null || currentNode === void 0 ? void 0 : currentNode.textContent) !== null && _a !== void 0 ? _a : ''; })(); if (textStack.length > 0) { textStack += textContent; } else { var charLastIndex = textContent.lastIndexOf(USER_MENTION_TEMP_CHAR); for (var i = charLastIndex - 1; i > -1; i -= 1) { if (textContent[i] === USER_MENTION_TEMP_CHAR) { charLastIndex = i; } else { break; } } if (charLastIndex > -1) { textStack = textContent; startNodeIndex = index; startOffsetIndex = charLastIndex; } } } else { /* other nodes */ textStack = ''; startNodeIndex = null; startOffsetIndex = null; } if (currentNode === selection.anchorNode) { /** * targetString could be '' * startNodeIndex and startOffsetIndex could be null */ var targetString = textStack && startOffsetIndex !== null ? textStack.slice(startOffsetIndex) : ''; // include template character setTargetStringInfo({ targetString: targetString, startNodeIndex: startNodeIndex, startOffsetIndex: startOffsetIndex, endNodeIndex: index, endOffsetIndex: selection.anchorOffset, }); onMentionStringChange(targetString); return { value: void 0 }; } }; for (var index = 0; index < textField.childNodes.length; index += 1) { var state_1 = _loop_1(index); if (typeof state_1 === "object") return state_1.value; } } }, [isMentionEnabled]); var sendMessage = function () { var _a, _b; try { var textField_2 = internalRef === null || internalRef === void 0 ? void 0 : internalRef.current; if (!isEdit && textField_2) { var _c = extractTextAndMentions(textField_2.childNodes), messageText = _c.messageText, mentionTemplate = _c.mentionTemplate, isMentionedMessage = _c.isMentionedMessage; var trimmedText = messageText.trim(); // Composer mode: empty text is OK if files are staged. if (trimmedText.length === 0 && !hasPendingFiles) return; var params = { message: messageText, mentionTemplate: isMentionedMessage ? sanitizeString(mentionTemplate) : '', }; if (isComposerMode && onSubmit) { onSubmit(__assign(__assign({}, params), { files: pendingFiles !== null && pendingFiles !== void 0 ? pendingFiles : [] })); } else { onSendMessage(params); } resetInput(internalRef); wasTypingRef.current = false; /** * Note: contentEditable does not work as expected in mobile WebKit (Safari). * @see https://github.com/sendbird/sendbird-uikit-react/pull/1108 */ if (isMobileIOS(navigator.userAgent)) { if (ghostInputRef.current) ghostInputRef.current.focus(); requestAnimationFrame(function () { return textField_2.focus(); }); } else { // important: keeps the keyboard open -> must add test on refactor textField_2.focus(); } setIsInput(false); } } catch (error) { (_b = (_a = eventHandlers === null || eventHandlers === void 0 ? void 0 : eventHandlers.message) === null || _a === void 0 ? void 0 : _a.onSendMessageFailed) === null || _b === void 0 ? void 0 : _b.call(_a, message, error); } }; var isEditDisabled = !hasTextContentWithoutZeroWidthSpace(internalRef === null || internalRef === void 0 ? void 0 : internalRef.current); var editMessage = function () { var _a, _b; try { var textField = internalRef === null || internalRef === void 0 ? void 0 : internalRef.current; var messageId = message === null || message === void 0 ? void 0 : message.messageId; if (isEdit && messageId && textField) { var _c = extractTextAndMentions(textField.childNodes), messageText = _c.messageText, mentionTemplate = _c.mentionTemplate, isMentionedMessage = _c.isMentionedMessage, mentionedUserIds_1 = _c.mentionedUserIds; if (messageText.trim().length === 0) return; var params = { messageId: messageId, message: messageText, mentionTemplate: sanitizeString(isMentionedMessage ? mentionTemplate : messageText), mentionedUserIds: isMentionEnabled ? mentionedUserIds_1 : [], }; onUpdateMessage(params); resetInput(internalRef); wasTypingRef.current = false; } } catch (error) { (_b = (_a = eventHandlers === null || eventHandlers === void 0 ? void 0 : eventHandlers.message) === null || _a === void 0 ? void 0 : _a.onUpdateMessageFailed) === null || _b === void 0 ? void 0 : _b.call(_a, message, error); } }; var onPaste = usePaste({ ref: internalRef, setMentionedUsers: setMentionedUsers, channel: channel, setIsInput: setIsInput, onAddFiles: fileProducerEnabled && !isMobile ? guardedAddFiles : undefined, }); var uploadFile = function (event) { var _a, _b; var files = event.currentTarget.files; try { if (files) { var fileArray = Array.from(files); if (fileProducerEnabled) { guardedAddFiles(fileArray); } else if (!isComposerMode) { onFileUpload(fileArray); } } } catch (error) { (_b = (_a = eventHandlers === null || eventHandlers === void 0 ? void 0 : eventHandlers.message) === null || _a === void 0 ? void 0 : _a.onFileUploadFailed) === null || _b === void 0 ? void 0 : _b.call(_a, error); } finally { event.target.value = ''; } }; var adjustScrollToCaret = function () { var _a; var inputRef = internalRef; var selection = window.getSelection(); if (!selection || selection.rangeCount === 0) return; // Get the last range (caret or selected text position) from the selection var range = selection.getRangeAt(selection.rangeCount - 1); var rect = range.getBoundingClientRect(); var container = (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect(); if (!container || !inputRef.current) return; // If the caret (or selection) is below the visible container area, scroll down if (rect.bottom > container.bottom) { var scrollAmount = Math.min(rect.bottom - container.bottom, // Calculate how much we need to scroll inputRef.current.scrollHeight - inputRef.current.clientHeight); inputRef.current.scrollTop += scrollAmount; // Adjust the scroll position downward } // If the caret (or selection) is above the visible container area, scroll up else if (rect.top < container.top) { var scrollAmount = Math.min(container.top - rect.top, // Calculate how much we need to scroll inputRef.current.scrollTop); inputRef.current.scrollTop -= scrollAmount; // Adjust the scroll position upward } }; return (React__default.createElement("form", { className: classnames.apply(void 0, __spreadArray(__spreadArray([], (Array.isArray(className) ? className : [className]), false), [isEdit && 'sendbird-message-input__edit', disabled && 'sendbird-message-input-form__disabled', isComposerMode && 'sendbird-message-input--composer'], false)) }, React__default.createElement("div", { className: classnames('sendbird-message-input', disabled && 'sendbird-message-input__disabled', hasPendingFiles && 'sendbird-message-input--has-pending'), "data-testid": "sendbird-message-input" }, isMobileIOS(navigator.userAgent) && (React__default.createElement("input", { id: 'ghost-input-reset-ime-cjk', ref: ghostInputRef, style: { opacity: 0, padding: 0, margin: 0, height: 0, border: 'none', position: 'absolute', top: -9999 }, defaultValue: '_' })), React__default.createElement("div", { id: "".concat(textFieldId).concat(isEdit ? message === null || message === void 0 ? void 0 : message.messageId : ''), className: classnames('sendbird-message-input--textarea', textFieldId, hasPendingFiles && 'sendbird-message-input--textarea-locked'), contentEditable: !disabled && !hasPendingFiles, tabIndex: hasPendingFiles ? 0 : undefined, role: "textbox", "aria-label": "Text Input", "aria-disabled": disabled || hasPendingFiles, ref: internalRef, // @ts-ignore disabled: disabled, maxLength: maxLength, onKeyDown: function (e) { var _a, _b, _c; var preventEvent = onKeyDown(e); if (preventEvent) { e.preventDefault(); } else { if (!e.shiftKey && e.key === MessageInputKeys.Enter && !isMobile && (hasTextContentWithoutZeroWidthSpace(internalRef === null || internalRef === void 0 ? void 0 : internalRef.current) || hasPendingFiles) && ((_a = e === null || e === void 0 ? void 0 : e.nativeEvent) === null || _a === void 0 ? void 0 : _a.isComposing) !== true /** * NOTE: What isComposing does? * Check if the user has finished composing characters * (e.g., for languages like Korean, Japanese, where characters are composed from multiple keystrokes) * Prevents executing the code while the user is still composing characters. */ ) { e.preventDefault(); sendMessage(); } if (e.key === MessageInputKeys.Backspace && ((_c = (_b = internalRef === null || internalRef === void 0 ? void 0 : internalRef.current) === null || _b === void 0 ? void 0 : _b.childNodes) === null || _c === void 0 ? void 0 : _c.length) === 2 && !internalRef.current.childNodes[0].textContent && internalRef.current.childNodes[1].nodeType === NodeTypes.ElementNode) { internalRef.current.removeChild(internalRef.current.childNodes[1]); } } }, onKeyUp: function (e) { var preventEvent = onKeyUp(e); if (preventEvent) { e.preventDefault(); } else { useMentionInputDetection(); } }, onClick: function () { useMentionInputDetection(); }, onInput: function () { var hasContent = hasTextContentWithoutZeroWidthSpace(internalRef === null || internalRef === void 0 ? void 0 : internalRef.current); if (hasContent) { onStartTyping(); wasTypingRef.current = true; } else if (wasTypingRef.current) { onStopTyping(); wasTypingRef.current = false; } setIsInput(hasContent); useMentionedLabelDetection(); }, onPaste: function (e) { onPaste(e); setTimeout(adjustScrollToCaret); } }), !isEdit && isComposerMode && hasPendingFiles && pendingFiles && onRemoveFile && (React__default.createElement(PendingFilesPreview, { pendingFiles: pendingFiles, onRemove: onRemoveFile })), getTextContentWithoutZeroWidthSpace(internalRef === null || internalRef === void 0 ? void 0 : internalRef.current).length === 0 && (React__default.createElement(Label, { className: "sendbird-message-input--placeholder", type: LabelTypography.BODY_1, color: disabled ? LabelColors.ONBACKGROUND_4 : LabelColors.ONBACKGROUND_3 }, hasPendingFiles ? stringSet.MESSAGE_INPUT__PLACE_HOLDER__FILE_ATTACHED : (placeholder || stringSet.MESSAGE_INPUT__PLACE_HOLDER))), !isEdit && (isInput || hasPendingFiles) && (React__default.createElement(IconButton, { className: "sendbird-message-input--send", height: "32px", width: "32px", disabled: disabled, onClick: function () { return sendMessage(); }, testID: "sendbird-message-input-send-button" }, (renderSendMessageIcon === null || renderSendMessageIcon === void 0 ? void 0 : renderSendMessageIcon()) || (React__default.createElement(Icon, { type: IconTypes.SEND, fillColor: disabled ? IconColors.ON_BACKGROUND_4 : IconColors.PRIMARY, width: "20px", height: "20px" })))), !isEdit && !isInput && !hasPendingFiles && ((renderFileUploadIcon === null || renderFileUploadIcon === void 0 ? void 0 : renderFileUploadIcon()) // UIKit Dashboard configuration should have lower priority than // renderFileUploadIcon which is set in code level || (isFileUploadEnabled && (React__default.createElement(IconButton, { className: classnames('sendbird-message-input--attach', isVoiceMessageEnabled && 'is-voice-message-enabled'), height: "32px", width: "32px", onClick: function () { var _a, _b; // todo: clear previous input (_b = (_a = fileInputRef === null || fileInputRef === void 0 ? void 0 : fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click) === null || _b === void 0 ? void 0 : _b.call(_a); } }, React__default.createElement(Icon, { type: IconTypes.ATTACH, fillColor: disabled ? IconColors.ON_BACKGROUND_4 : IconColors.CONTENT_INVERSE, width: "20px", height: "20px" }), React__default.createElement("input", { className: "sendbird-message-input--attach-input", type: "file", ref: fileInputRef, // It will affect to <Channel /> and <Thread /> onChange: function (event) { return uploadFile(event); }, accept: getMimeTypesUIKitAccepts(acceptableMimeTypes), multiple: isSelectingMultipleFilesEnabled && isChannelTypeSupportsMultipleFilesMessage(channel) }))))), isVoiceMessageEnabled && !isEdit && !isInput && !hasPendingFiles && (React__default.createElement(IconButton, { className: "sendbird-message-input--voice-message", width: "32px", height: "32px", onClick: onVoiceMessageIconClick }, (renderVoiceMessageIcon === null || renderVoiceMessageIcon === void 0 ? void 0 : renderVoiceMessageIcon()) || (React__default.createElement(Icon, { type: IconTypes.AUDIO_ON_LINED, fillColor: disabled ? IconColors.ON_BACKGROUND_4 : IconColors.CONTENT_INVERSE, width: "20px", height: "20px" }))))), isEdit && (React__default.createElement("div", { className: "sendbird-message-input--edit-action", "data-testid": "sendbird-message-input--edit-action" }, React__default.createElement(Button, { className: "sendbird-message-input--edit-action__cancel", type: ButtonTypes.SECONDARY, size: ButtonSizes.SMALL, onClick: onCancelEdit }, stringSet.BUTTON__CANCEL), React__default.createElement(Button, { className: "sendbird-message-input--edit-action__save", type: ButtonTypes.PRIMARY, size: ButtonSizes.SMALL, disabled: isEditDisabled, onClick: function () { return editMessage(); } }, stringSet.BUTTON__SAVE))))); }); export { MessageInput as M, checkIfFileUploadEnabled as c }; //# sourceMappingURL=bundle-BCjR1Qiq.js.map