@sendbird/uikit-react
Version:
Sendbird UIKit for React: A feature-rich and customizable chat UI kit with messaging, channel management, and user authentication.
715 lines (704 loc) • 45.4 kB
JavaScript
'use strict';
var _tslib = require('./bundle-jAsAzWpU.js');
var React = require('react');
var _const$1 = require('./bundle-BSEj3ItE.js');
var _const = require('./bundle-CeCg868O.js');
var ui_IconButton = require('../ui/IconButton.js');
var ui_Button = require('../ui/Button.js');
var ui_MessageInput_hooks_usePaste = require('./bundle-DHOzCMYH.js');
var ui_Icon = require('../ui/Icon.js');
var ui_Label = require('./bundle-DxZzcGya.js');
var LocalizationContext = require('./bundle-ClT0IexP.js');
var index$1 = require('./bundle-CskFALvU.js');
var tokenize = require('./bundle-iwIElqGP.js');
var index = require('./bundle-DvHjgbFi.js');
var utils = require('./bundle-1F9guuKw.js');
var browser = require('./bundle-BVn2UMtk.js');
var useSendbird = require('./bundle-on0zTbLT.js');
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
var React__default = /*#__PURE__*/_interopDefaultCompat(React);
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 = React.useContext(LocalizationContext.LocalizationContext).stringSet;
var id = pendingFile.id, file = pendingFile.file;
var displayName = React.useMemo(function () { return fitFilenameToWidth(file.name); }, [file.name]);
var extLabel = getExtensionLabel(file.name, stringSet.MESSAGE_INPUT__PENDING_FILE__TYPE_UNKNOWN);
return (React__default.default.createElement("div", { className: "sendbird-message-input__pending-card", "data-testid": "sendbird-pending-file" },
React__default.default.createElement("div", { className: "sendbird-message-input__pending-card__body" },
React__default.default.createElement("div", { className: "sendbird-message-input__pending-card__icon" },
React__default.default.createElement(ui_Icon.default, { type: ui_Icon.IconTypes.FILE_DOCUMENT, fillColor: ui_Icon.IconColors.PRIMARY, width: "24px", height: "24px" })),
React__default.default.createElement("div", { className: "sendbird-message-input__pending-card__meta" },
React__default.default.createElement(ui_Label.Label, { className: "sendbird-message-input__pending-card__name", type: ui_Label.LabelTypography.CAPTION_1, color: ui_Label.LabelColors.ONBACKGROUND_1 }, displayName),
React__default.default.createElement(ui_Label.Label, { className: "sendbird-message-input__pending-card__type", type: ui_Label.LabelTypography.CAPTION_2, color: ui_Label.LabelColors.ONBACKGROUND_2 }, extLabel))),
React__default.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.default.createElement(ui_Icon.default, { type: ui_Icon.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 = React.useContext(LocalizationContext.LocalizationContext).stringSet;
var id = pendingFile.id, file = pendingFile.file, previewUrl = pendingFile.previewUrl, isImage = pendingFile.isImage;
var _b = React.useState(false), imageLoaded = _b[0], setImageLoaded = _b[1];
if (!isImage) {
return React__default.default.createElement(PendingFileCard, { pendingFile: pendingFile, onRemove: onRemove });
}
return (React__default.default.createElement("div", { className: "sendbird-message-input__pending-file", "data-testid": "sendbird-pending-file" },
React__default.default.createElement("div", { className: "sendbird-message-input__pending-file__thumbnail" },
previewUrl && (React__default.default.createElement("img", { className: "sendbird-message-input__pending-file__image", src: previewUrl, alt: file.name, onLoad: function () { return setImageLoaded(true); } })),
!imageLoaded && (React__default.default.createElement("div", { className: "sendbird-message-input__pending-file__image-placeholder" },
React__default.default.createElement(ui_Icon.default, { type: ui_Icon.IconTypes.PHOTO, fillColor: ui_Icon.IconColors.ON_BACKGROUND_2, width: "32px", height: "32px" }))),
React__default.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.default.createElement(ui_Icon.default, { type: ui_Icon.IconTypes.REMOVE, width: "22px", height: "22px" })))));
};
var PendingFilesPreview = function (_a) {
var pendingFiles = _a.pendingFiles, onRemove = _a.onRemove, className = _a.className;
var containerRef = React.useRef(null);
var prevCountRef = React.useRef(pendingFiles.length);
React.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); };
}, []);
React.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.default.createElement("div", { ref: containerRef, className: classNames, "data-testid": "sendbird-pending-files-preview", role: "list" }, pendingFiles.map(function (entry) { return (React__default.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 = index.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 ui_MessageInput_hooks_usePaste.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.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 : React.useRef(null);
var ghostInputRef = React.useRef(null);
var wasTypingRef = React.useRef(false);
var textFieldId = messageFieldId || TEXT_FIELD_ID;
var stringSet = LocalizationContext.useLocalization().stringSet;
var _6 = useSendbird.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 = React.useCallback(function (incoming) {
if (!fileProducerEnabled || !onAddFiles)
return;
if (incoming.length === 0)
return;
onAddFiles(incoming);
}, [fileProducerEnabled, onAddFiles]);
var fileInputRef = React.useRef();
var _7 = React.useState(false), isInput = _7[0], setIsInput = _7[1];
var _8 = React.useState([]), mentionedUserIds = _8[0], setMentionedUserIds = _8[1];
var _9 = React.useState(_tslib.__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;
React.useEffect(function () {
var textField = internalRef === null || internalRef === void 0 ? void 0 : internalRef.current;
setMentionedUserIds([]);
setIsInput(hasTextContentWithoutZeroWidthSpace(textField));
}, [initialValue]);
var stashedHtmlRef = React.useRef('');
var prevHasPendingFilesRef = React.useRef(false);
// #Mention | Clear input value when channel changes
React.useEffect(function () {
if (!isEdit) {
setIsInput(false);
resetInput(internalRef);
wasTypingRef.current = false;
stashedHtmlRef.current = '';
}
}, [channelUrl]);
React.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
React.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 = tokenize.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 === tokenize.TOKEN_TYPES.mention) {
var mentionedUser = mentionedUsers_1.find(function (user) { return user.userId === token.userId; });
var nickname = "".concat(tokenize.USER_MENTION_PREFIX).concat((mentionedUser === null || mentionedUser === void 0 ? void 0 : mentionedUser.nickname) || token.value || stringSet.MENTION_NAME__NO_NAME);
return ui_MessageInput_hooks_usePaste.renderToString({
userId: token.userId,
nickname: nickname,
});
}
return ui_MessageInput_hooks_usePaste.sanitizeString(token.value);
})
.join('');
}
}
else {
/* mention disabled */
try {
if (textField) {
textField.innerHTML = (_a = ui_MessageInput_hooks_usePaste.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 = React.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 (!index$1.arrayEqual(mentionedUserIds, newMentionedUserIds) || newMentionedUserIds.length === 0) {
onMentionedUserIdsUpdated(newMentionedUserIds);
setMentionedUserIds(newMentionedUserIds);
}
}
setIsInput(hasTextContentWithoutZeroWidthSpace(textField));
}, [targetStringInfo, isMentionEnabled]);
// #Mention | Replace selected user nickname to the MentionedUserLabel
React.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 = ui_MessageInput_hooks_usePaste.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 = ui_MessageInput_hooks_usePaste.renderToString({
userId: mentionSelectedUser === null || mentionSelectedUser === void 0 ? void 0 : mentionSelectedUser.userId,
nickname: "".concat(_const.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 = _tslib.__spreadArray(_tslib.__spreadArray(_tslib.__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(_tslib.__assign({}, initialTargetStringInfo));
useMentionedLabelDetection();
}
}
}, [mentionSelectedUser, isMentionEnabled]);
// #Mention | Detect mentioning user nickname
var useMentionInputDetection = React.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 === _const$1.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(_const.USER_MENTION_TEMP_CHAR);
for (var i = charLastIndex - 1; i > -1; i -= 1) {
if (textContent[i] === _const.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 = ui_MessageInput_hooks_usePaste.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 ? ui_MessageInput_hooks_usePaste.sanitizeString(mentionTemplate) : '',
};
if (isComposerMode && onSubmit) {
onSubmit(_tslib.__assign(_tslib.__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 (browser.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 = ui_MessageInput_hooks_usePaste.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: ui_MessageInput_hooks_usePaste.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 = ui_MessageInput_hooks_usePaste.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.default.createElement("form", { className: utils.classnames.apply(void 0, _tslib.__spreadArray(_tslib.__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.default.createElement("div", { className: utils.classnames('sendbird-message-input', disabled && 'sendbird-message-input__disabled', hasPendingFiles && 'sendbird-message-input--has-pending'), "data-testid": "sendbird-message-input" },
browser.isMobileIOS(navigator.userAgent) && (React__default.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.default.createElement("div", { id: "".concat(textFieldId).concat(isEdit ? message === null || message === void 0 ? void 0 : message.messageId : ''), className: utils.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 === _const$1.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 === _const$1.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 === _const$1.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.default.createElement(PendingFilesPreview, { pendingFiles: pendingFiles, onRemove: onRemoveFile })),
getTextContentWithoutZeroWidthSpace(internalRef === null || internalRef === void 0 ? void 0 : internalRef.current).length === 0 && (React__default.default.createElement(ui_Label.Label, { className: "sendbird-message-input--placeholder", type: ui_Label.LabelTypography.BODY_1, color: disabled ? ui_Label.LabelColors.ONBACKGROUND_4 : ui_Label.LabelColors.ONBACKGROUND_3 }, hasPendingFiles
? stringSet.MESSAGE_INPUT__PLACE_HOLDER__FILE_ATTACHED
: (placeholder || stringSet.MESSAGE_INPUT__PLACE_HOLDER))),
!isEdit && (isInput || hasPendingFiles) && (React__default.default.createElement(ui_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.default.createElement(ui_Icon.default, { type: ui_Icon.IconTypes.SEND, fillColor: disabled ? ui_Icon.IconColors.ON_BACKGROUND_4 : ui_Icon.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.default.createElement(ui_IconButton, { className: utils.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.default.createElement(ui_Icon.default, { type: ui_Icon.IconTypes.ATTACH, fillColor: disabled ? ui_Icon.IconColors.ON_BACKGROUND_4 : ui_Icon.IconColors.CONTENT_INVERSE, width: "20px", height: "20px" }),
React__default.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: index$1.getMimeTypesUIKitAccepts(acceptableMimeTypes), multiple: isSelectingMultipleFilesEnabled && ui_MessageInput_hooks_usePaste.isChannelTypeSupportsMultipleFilesMessage(channel) }))))),
isVoiceMessageEnabled && !isEdit && !isInput && !hasPendingFiles && (React__default.default.createElement(ui_IconButton, { className: "sendbird-message-input--voice-message", width: "32px", height: "32px", onClick: onVoiceMessageIconClick }, (renderVoiceMessageIcon === null || renderVoiceMessageIcon === void 0 ? void 0 : renderVoiceMessageIcon()) || (React__default.default.createElement(ui_Icon.default, { type: ui_Icon.IconTypes.AUDIO_ON_LINED, fillColor: disabled ? ui_Icon.IconColors.ON_BACKGROUND_4 : ui_Icon.IconColors.CONTENT_INVERSE, width: "20px", height: "20px" }))))),
isEdit && (React__default.default.createElement("div", { className: "sendbird-message-input--edit-action", "data-testid": "sendbird-message-input--edit-action" },
React__default.default.createElement(ui_Button.default, { className: "sendbird-message-input--edit-action__cancel", type: ui_Button.ButtonTypes.SECONDARY, size: ui_Button.ButtonSizes.SMALL, onClick: onCancelEdit }, stringSet.BUTTON__CANCEL),
React__default.default.createElement(ui_Button.default, { className: "sendbird-message-input--edit-action__save", type: ui_Button.ButtonTypes.PRIMARY, size: ui_Button.ButtonSizes.SMALL, disabled: isEditDisabled, onClick: function () { return editMessage(); } }, stringSet.BUTTON__SAVE)))));
});
exports.MessageInput = MessageInput;
exports.checkIfFileUploadEnabled = checkIfFileUploadEnabled;
//# sourceMappingURL=bundle-CzhNQgac.js.map