@sendbird/uikit-react
Version:
Sendbird UIKit for React: A feature-rich and customizable chat UI kit with messaging, channel management, and user authentication.
474 lines (470 loc) • 28.1 kB
JavaScript
import { a as __awaiter, c as __generator, e as __spreadArray } from './bundle-Bpofr334.js';
import React__default, { useRef, useState, useEffect, useCallback } from 'react';
import { u as useTypingLifecycle } from './bundle-AsQ1wnFm.js';
import { b as isDisabledBecauseFrozen, d as isDisabledBecauseMuted, e as isDisabledBecauseSuggestedReplies, c as isDisabledBecauseMessageForm } from './bundle-DiO7lolz.js';
import { u as useLocalization } from './bundle-Cdqsdoa8.js';
import { useGlobalModalContext } from '../hooks/useModal.js';
import { SuggestedMentionList } from '../GroupChannel/components/SuggestedMentionList.js';
import { useDirtyGetMentions } from '../Message/hooks/useDirtyGetMentions.js';
import QuoteMessageInput from '../ui/QuoteMessageInput.js';
import { useVoicePlayer } from '../VoicePlayer/useVoicePlayer.js';
import { useVoiceRecorder, VoiceRecorderStatus } from '../VoiceRecorder/useVoiceRecorder.js';
import { a as VoiceMessageInputStatus, V as VoiceMessageInput } from './bundle-DzB_38co.js';
import { a as Modal } from './bundle-C4anRHWY.js';
import Button, { ButtonSizes, ButtonTypes } from '../ui/Button.js';
import { V as VOICE_PLAYER_STATUS } from './bundle-DWURNKdQ.js';
import { u as uuidv4 } from './bundle-LLA95Pqf.js';
import { u as useSendbird } from './bundle-4clodtJA.js';
import { c as checkIfFileUploadEnabled, M as MessageInput } from './bundle-BCjR1Qiq.js';
import { a as usePendingFiles, u as useDragAndDrop } from './bundle-ExNQo0Ly.js';
import { i as isChannelTypeSupportsMultipleFilesMessage } from './bundle-C5-D2BAP.js';
import { u as useMediaQueryContext } from './bundle-C2ARCMSL.js';
import { M as MessageInputKeys } from './bundle-BEPoP7sp.js';
import { c as compressImages } from './bundle-BfgSx7DM.js';
var VoiceMessageInputWrapper = function (_a) {
var channel = _a.channel, onCancelClick = _a.onCancelClick, onSubmitClick = _a.onSubmitClick;
var uuid = useRef(uuidv4()).current;
var _b = useState(null), audioFile = _b[0], setAudioFile = _b[1];
var _c = useState(VoiceMessageInputStatus.READY_TO_RECORD), voiceInputState = _c[0], setVoiceInputState = _c[1];
var _d = useState(false), isSubmitted = _d[0], setSubmit = _d[1];
var _e = useState(false), isDisabled = _e[0], setDisabled = _e[1];
var _f = useState(false), showModal = _f[0], setShowModal = _f[1];
var stringSet = useLocalization().stringSet;
var state = useSendbird().state;
var config = state.config;
var _g = useVoiceRecorder({
onRecordingStarted: function () {
setVoiceInputState(VoiceMessageInputStatus.RECORDING);
},
onRecordingEnded: function (audioFile) {
setAudioFile(audioFile);
},
}), start = _g.start, stop = _g.stop, cancel = _g.cancel, recordingTime = _g.recordingTime, recordingStatus = _g.recordingStatus, recordingLimit = _g.recordingLimit;
var voicePlayer = useVoicePlayer({
channelUrl: channel === null || channel === void 0 ? void 0 : channel.url,
key: uuid,
audioFile: audioFile !== null && audioFile !== void 0 ? audioFile : undefined,
});
var play = voicePlayer.play, pause = voicePlayer.pause, playbackTime = voicePlayer.playbackTime, playingStatus = voicePlayer.playingStatus;
var stopVoicePlayer = voicePlayer.stop;
// disabled state: muted & frozen
useEffect(function () {
if (isDisabledBecauseFrozen(channel) || isDisabledBecauseMuted(channel)) {
setDisabled(true);
}
else {
setDisabled(false);
}
}, [channel === null || channel === void 0 ? void 0 : channel.myRole, channel === null || channel === void 0 ? void 0 : channel.isFrozen, channel === null || channel === void 0 ? void 0 : channel.myMutedState]);
// call onSubmitClick when submit button is clicked and recorded audio file is created
useEffect(function () {
if (isSubmitted && audioFile) {
onSubmitClick === null || onSubmitClick === void 0 ? void 0 : onSubmitClick(audioFile, recordingTime);
setSubmit(false);
setAudioFile(null);
}
}, [isSubmitted, audioFile, recordingTime]);
// operate which control button should be displayed
useEffect(function () {
if (audioFile) {
if (recordingTime < config.voiceRecord.minRecordingTime) {
setVoiceInputState(VoiceMessageInputStatus.READY_TO_RECORD);
setAudioFile(null);
}
else if (playingStatus === VOICE_PLAYER_STATUS.PLAYING) {
setVoiceInputState(VoiceMessageInputStatus.PLAYING);
}
else {
setVoiceInputState(VoiceMessageInputStatus.READY_TO_PLAY);
}
}
}, [audioFile, recordingTime, playingStatus]);
return (React__default.createElement("div", { className: "sendbird-voice-message-input-wrapper" },
React__default.createElement(VoiceMessageInput, { currentValue: recordingStatus === VoiceRecorderStatus.COMPLETED ? playbackTime : recordingTime, maximumValue: recordingStatus === VoiceRecorderStatus.COMPLETED ? recordingTime : recordingLimit, currentType: voiceInputState, onCancelClick: function () {
onCancelClick === null || onCancelClick === void 0 ? void 0 : onCancelClick();
cancel();
stopVoicePlayer();
}, onSubmitClick: function () {
if (isDisabled) {
setShowModal(true);
setVoiceInputState(VoiceMessageInputStatus.READY_TO_RECORD);
}
else {
stop();
pause();
setSubmit(true);
}
}, onControlClick: function (type) {
switch (type) {
case VoiceMessageInputStatus.READY_TO_RECORD: {
stopVoicePlayer();
start();
break;
}
case VoiceMessageInputStatus.RECORDING: {
if (recordingTime >= config.voiceRecord.minRecordingTime && !isDisabled) {
stop();
}
else if (isDisabled) {
cancel();
setShowModal(true);
setVoiceInputState(VoiceMessageInputStatus.READY_TO_RECORD);
}
else {
cancel();
setVoiceInputState(VoiceMessageInputStatus.READY_TO_RECORD);
}
break;
}
case VoiceMessageInputStatus.READY_TO_PLAY: {
play();
break;
}
case VoiceMessageInputStatus.PLAYING: {
pause();
break;
}
}
} }),
showModal && (React__default.createElement(Modal, { className: "sendbird-voice-message-input-wrapper-alert", titleText: isDisabledBecauseMuted(channel)
? stringSet.MODAL__VOICE_MESSAGE_INPUT_DISABLED__TITLE_MUTED
: stringSet.MODAL__VOICE_MESSAGE_INPUT_DISABLED__TITLE_FROZEN, hideFooter: true, isCloseOnClickOutside: true, onClose: function () {
setShowModal(false);
onCancelClick === null || onCancelClick === void 0 ? void 0 : onCancelClick();
} },
React__default.createElement("div", { className: "sendbird-voice-message-input-wrapper-alert__body" },
React__default.createElement(Button, { className: "sendbird-voice-message-input-wrapper-alert__body__ok-button", type: ButtonTypes.PRIMARY, size: ButtonSizes.BIG, onClick: function () {
setShowModal(false);
onCancelClick === null || onCancelClick === void 0 ? void 0 : onCancelClick();
} }, stringSet.BUTTON__OK))))));
};
var MessageInputWrapperView = React__default.forwardRef(function (props, ref) {
var _a;
// Props
var currentChannel = props.currentChannel, messages = props.messages, loading = props.loading, quoteMessage = props.quoteMessage, setQuoteMessage = props.setQuoteMessage, messageInputRef = props.messageInputRef, sendUserMessage = props.sendUserMessage, sendFileMessage = props.sendFileMessage, sendVoiceMessage = props.sendVoiceMessage, sendMultipleFilesMessage = props.sendMultipleFilesMessage,
// render
renderUserMentionItem = props.renderUserMentionItem, renderFileUploadIcon = props.renderFileUploadIcon, renderVoiceMessageIcon = props.renderVoiceMessageIcon, renderSendMessageIcon = props.renderSendMessageIcon, acceptableMimeTypes = props.acceptableMimeTypes, disabled = props.disabled;
var stringSet = useLocalization().stringSet;
var isMobile = useMediaQueryContext().isMobile;
var openModal = useGlobalModalContext().openModal;
var state = useSendbird().state;
var stores = state.stores, config = state.config;
var isOnline = config.isOnline, userMention = config.userMention, logger = config.logger, groupChannel = config.groupChannel, imageCompression = config.imageCompression;
var sdk = stores.sdkStore.sdk;
var maxMentionCount = userMention.maxMentionCount, maxSuggestionCount = userMention.maxSuggestionCount;
var uikitUploadSizeLimit = config.uikitUploadSizeLimit, uikitMultipleFilesMessageLimit = config.uikitMultipleFilesMessageLimit;
var isBroadcast = currentChannel === null || currentChannel === void 0 ? void 0 : currentChannel.isBroadcast;
var isOperator = (currentChannel === null || currentChannel === void 0 ? void 0 : currentChannel.myRole) === 'operator';
var isMultipleFilesMessageEnabled = (_a = props.isMultipleFilesMessageEnabled) !== null && _a !== void 0 ? _a : config.isMultipleFilesMessageEnabled;
var isMentionEnabled = groupChannel.enableMention;
var isVoiceMessageEnabled = groupChannel.enableVoiceMessage;
// States
var _b = useState(''), mentionNickname = _b[0], setMentionNickname = _b[1];
var _c = useState([]), mentionedUsers = _c[0], setMentionedUsers = _c[1];
var _d = useState([]), mentionedUserIds = _d[0], setMentionedUserIds = _d[1];
var _e = useState(null), selectedUser = _e[0], setSelectedUser = _e[1];
var _f = useState([]), mentionSuggestedUsers = _f[0], setMentionSuggestedUsers = _f[1];
var _g = useState(null), messageInputEvent = _g[0], setMessageInputEvent = _g[1];
var _h = useState(false), showVoiceMessageInput = _h[0], setShowVoiceMessageInput = _h[1];
// Conditions
var isMessageInputDisabled = loading
|| (!currentChannel || !sdk)
|| (!sdk.isCacheEnabled && !isOnline)
|| isDisabledBecauseFrozen(currentChannel)
|| isDisabledBecauseMuted(currentChannel)
|| isDisabledBecauseSuggestedReplies(currentChannel, config.groupChannel.enableSuggestedReplies)
|| isDisabledBecauseMessageForm(messages, config.groupChannel.enableFormTypeMessage)
|| disabled;
var showSuggestedMentionList = !isMessageInputDisabled
&& isMentionEnabled
&& mentionNickname.length > 0
&& !isBroadcast;
var mentionNodes = useDirtyGetMentions({ ref: (ref || messageInputRef) }, { logger: logger });
var ableMention = (mentionNodes === null || mentionNodes === void 0 ? void 0 : mentionNodes.length) < maxMentionCount;
// Composer staging — file picker, drag-drop, and clipboard paste all feed
// into pendingFiles. The submit handler drains them along with any text body.
var allowMultipleFiles = Boolean(isMultipleFilesMessageEnabled)
&& Boolean(currentChannel)
&& isChannelTypeSupportsMultipleFilesMessage(currentChannel);
var effectiveMultiLimit = allowMultipleFiles ? uikitMultipleFilesMessageLimit : 1;
var _j = usePendingFiles({
uikitUploadSizeLimit: uikitUploadSizeLimit,
uikitMultipleFilesMessageLimit: effectiveMultiLimit,
acceptableMimeTypes: acceptableMimeTypes,
openModal: openModal,
stringSet: stringSet,
logger: logger,
}), pendingFiles = _j.pendingFiles, addFiles = _j.addFiles, removeFile = _j.removeFile, clearPendingFiles = _j.clear;
// Window-level drop target — files dropped anywhere in the viewport route
// into this channel's composer EXCEPT when the drop lands inside an open
// thread panel, which has its own composer. Disabled on mobile and when
// the input itself is not accepting new files (voice recording, channel
// disabled).
var isFileUploadEnabled = checkIfFileUploadEnabled({ channel: currentChannel !== null && currentChannel !== void 0 ? currentChannel : undefined, config: config });
useDragAndDrop({
onAddFiles: addFiles,
disabled: isMobile || isMessageInputDisabled || showVoiceMessageInput || !isFileUploadEnabled,
shouldAccept: function (event) {
var target = event.target;
if (!(target instanceof Element))
return true;
return !target.closest('.sendbird-thread-ui');
},
});
var stashedMentionedUsersRef = useRef(null);
var stashedQuoteMessageRef = useRef(null);
var prevHasPendingFilesRef = useRef(false);
var hasPendingFilesInWrapper = pendingFiles.length > 0;
useEffect(function () {
if (hasPendingFilesInWrapper && !prevHasPendingFilesRef.current) {
if (mentionedUsers.length > 0) {
stashedMentionedUsersRef.current = mentionedUsers;
}
if (quoteMessage) {
stashedQuoteMessageRef.current = quoteMessage;
}
setMentionNickname('');
}
else if (!hasPendingFilesInWrapper && prevHasPendingFilesRef.current) {
if (stashedMentionedUsersRef.current) {
setMentionedUsers(stashedMentionedUsersRef.current);
stashedMentionedUsersRef.current = null;
}
if (stashedQuoteMessageRef.current) {
setQuoteMessage(stashedQuoteMessageRef.current);
stashedQuoteMessageRef.current = null;
}
}
prevHasPendingFilesRef.current = hasPendingFilesInWrapper;
}, [hasPendingFilesInWrapper]);
// Operate states
useEffect(function () {
setMentionNickname('');
setMentionedUsers([]);
setMentionedUserIds([]);
setSelectedUser(null);
setMentionSuggestedUsers([]);
setMessageInputEvent(null);
setShowVoiceMessageInput(false);
clearPendingFiles();
stashedMentionedUsersRef.current = null;
stashedQuoteMessageRef.current = null;
}, [currentChannel === null || currentChannel === void 0 ? void 0 : currentChannel.url]);
var _k = useTypingLifecycle(currentChannel), startTyping = _k.startTyping, stopTyping = _k.stopTyping;
useEffect(function () {
setMentionedUsers(mentionedUsers.filter(function (_a) {
var userId = _a.userId;
var i = mentionedUserIds.indexOf(userId);
if (i < 0) {
return false;
}
else {
mentionedUserIds.splice(i, 1);
return true;
}
}));
}, [mentionedUserIds]);
var isSubmittingFilesRef = useRef(false);
// Submit handler: drains pendingFiles XOR text body. Files and body do not
// coexist in a single send anymore — when files are present, text from the
// composer is suppressed at the UI level (textarea locked) and again here
// for defense in depth. The caption read path remains in MessageBody to
// render historical file messages that still carry a body.
var handleSubmit = useCallback(function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
var trimmed, parentMessageId, rawImageFiles, otherFiles, compressedImageFiles_1, tasks_1, useMFMBatch, file_1;
var message = _b.message, mentionTemplate = _b.mentionTemplate, files = _b.files;
return __generator(this, function (_c) {
switch (_c.label) {
case 0:
trimmed = message.trim();
parentMessageId = quoteMessage === null || quoteMessage === void 0 ? void 0 : quoteMessage.messageId;
if (files.length === 0) {
if (trimmed.length === 0)
return [2 /*return*/];
sendUserMessage({
message: message,
mentionedUsers: mentionedUsers,
mentionedMessageTemplate: mentionTemplate,
parentMessageId: parentMessageId,
});
setMentionNickname('');
setMentionedUsers([]);
setQuoteMessage(null);
stopTyping();
return [2 /*return*/];
}
if (isSubmittingFilesRef.current)
return [2 /*return*/];
isSubmittingFilesRef.current = true;
// Clear pending state and other composer state immediately so a rapid
// second send (within the compression window) finds an empty queue and
// bails out at MessageInput's sendMessage gate.
setMentionNickname('');
setMentionedUsers([]);
setQuoteMessage(null);
stashedQuoteMessageRef.current = null;
stopTyping();
clearPendingFiles();
_c.label = 1;
case 1:
_c.trys.push([1, , 3, 4]);
rawImageFiles = files.filter(function (entry) { return entry.isImage; }).map(function (entry) { return entry.file; });
otherFiles = files.filter(function (entry) { return !entry.isImage; }).map(function (entry) { return entry.file; });
return [4 /*yield*/, compressImages({
files: rawImageFiles,
imageCompression: imageCompression,
logger: logger,
})];
case 2:
compressedImageFiles_1 = (_c.sent()).compressedFiles;
tasks_1 = [];
useMFMBatch = isMultipleFilesMessageEnabled && compressedImageFiles_1.length > 1;
if (useMFMBatch) {
tasks_1.push(function () { return sendMultipleFilesMessage({
fileInfoList: compressedImageFiles_1.map(function (file) { return ({
file: file,
fileName: file.name,
fileSize: file.size,
mimeType: file.type,
}); }),
parentMessageId: parentMessageId,
}); });
}
else if (compressedImageFiles_1.length === 1) {
file_1 = compressedImageFiles_1[0];
tasks_1.push(function () { return sendFileMessage({
file: file_1,
parentMessageId: parentMessageId,
}); });
}
else if (compressedImageFiles_1.length > 1) {
compressedImageFiles_1.forEach(function (file) {
tasks_1.push(function () { return sendFileMessage({
file: file,
parentMessageId: parentMessageId,
}); });
});
}
otherFiles.forEach(function (file) {
tasks_1.push(function () { return sendFileMessage({
file: file,
parentMessageId: parentMessageId,
}); });
});
// Sequential dispatch with per-task error isolation: one failure must not
// break the rest of the batch. Fire-and-forget so UI cleanup runs now.
(function () { return __awaiter(void 0, void 0, void 0, function () {
var _i, tasks_2, task, error_1;
var _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
_i = 0, tasks_2 = tasks_1;
_b.label = 1;
case 1:
if (!(_i < tasks_2.length)) return [3 /*break*/, 6];
task = tasks_2[_i];
_b.label = 2;
case 2:
_b.trys.push([2, 4, , 5]);
return [4 /*yield*/, task()];
case 3:
_b.sent();
return [3 /*break*/, 5];
case 4:
error_1 = _b.sent();
(_a = logger.warning) === null || _a === void 0 ? void 0 : _a.call(logger, 'GroupChannel|composer: file send failed', error_1);
return [3 /*break*/, 5];
case 5:
_i++;
return [3 /*break*/, 1];
case 6: return [2 /*return*/];
}
});
}); })();
return [3 /*break*/, 4];
case 3:
isSubmittingFilesRef.current = false;
return [7 /*endfinally*/];
case 4: return [2 /*return*/];
}
});
}); }, [
sendUserMessage,
sendFileMessage,
sendMultipleFilesMessage,
mentionedUsers,
quoteMessage,
setQuoteMessage,
currentChannel,
stopTyping,
clearPendingFiles,
isMultipleFilesMessageEnabled,
imageCompression,
logger,
]);
if (isBroadcast && !isOperator) {
/* Only `Operator` can send messages in the Broadcast channel */
return null;
}
// other conditions
return (React__default.createElement("div", { className: showVoiceMessageInput ? 'sendbird-message-input-wrapper--voice-message' : 'sendbird-message-input-wrapper' },
showSuggestedMentionList && (React__default.createElement(SuggestedMentionList, { currentChannel: currentChannel, targetNickname: mentionNickname, inputEvent: messageInputEvent !== null && messageInputEvent !== void 0 ? messageInputEvent : undefined, renderUserMentionItem: renderUserMentionItem, onUserItemClick: function (user) {
if (user) {
setMentionedUsers(__spreadArray(__spreadArray([], mentionedUsers, true), [user], false));
}
setMentionNickname('');
setSelectedUser(user);
setMessageInputEvent(null);
}, onFocusItemChange: function () {
setMessageInputEvent(null);
}, onFetchUsers: function (users) {
setMentionSuggestedUsers(users);
}, ableAddMention: ableMention, maxMentionCount: maxMentionCount, maxSuggestionCount: maxSuggestionCount })),
quoteMessage && (React__default.createElement("div", { className: "sendbird-message-input-wrapper__quote-message-input" },
React__default.createElement(QuoteMessageInput, { replyingMessage: quoteMessage, onClose: function () {
setQuoteMessage(null);
stashedQuoteMessageRef.current = null;
} }))),
showVoiceMessageInput ? (React__default.createElement(VoiceMessageInputWrapper, { channel: currentChannel !== null && currentChannel !== void 0 ? currentChannel : undefined, onSubmitClick: function (recordedFile, duration) {
sendVoiceMessage({ file: recordedFile, parentMessageId: quoteMessage === null || quoteMessage === void 0 ? void 0 : quoteMessage.messageId }, duration);
setQuoteMessage(null);
setShowVoiceMessageInput(false);
}, onCancelClick: function () {
setShowVoiceMessageInput(false);
} })) : (React__default.createElement(MessageInput, { className: "sendbird-message-input-wrapper__message-input", channel: currentChannel, channelUrl: currentChannel === null || currentChannel === void 0 ? void 0 : currentChannel.url, isMobile: isMobile, acceptableMimeTypes: acceptableMimeTypes, mentionSelectedUser: selectedUser, isMentionEnabled: isMentionEnabled, isVoiceMessageEnabled: isVoiceMessageEnabled, isSelectingMultipleFilesEnabled: isMultipleFilesMessageEnabled, onVoiceMessageIconClick: function () {
setShowVoiceMessageInput(true);
}, setMentionedUsers: setMentionedUsers, placeholder: (quoteMessage && stringSet.MESSAGE_INPUT__QUOTE_REPLY__PLACE_HOLDER)
|| (isDisabledBecauseFrozen(currentChannel) && stringSet.MESSAGE_INPUT__PLACE_HOLDER__FROZEN)
|| (isDisabledBecauseMuted(currentChannel)
&& (isMobile
? stringSet.MESSAGE_INPUT__PLACE_HOLDER__MUTED_SHORT
: stringSet.MESSAGE_INPUT__PLACE_HOLDER__MUTED))
|| (isDisabledBecauseSuggestedReplies(currentChannel, config.groupChannel.enableSuggestedReplies)
&& stringSet.MESSAGE_INPUT__PLACE_HOLDER__SUGGESTED_REPLIES)
|| (isDisabledBecauseMessageForm(messages, config.groupChannel.enableFormTypeMessage)
&& stringSet.MESSAGE_INPUT__PLACE_HOLDER__MESSAGE_FORM)
|| (disabled && stringSet.MESSAGE_INPUT__PLACE_HOLDER__DISABLED)
|| undefined, ref: (ref || messageInputRef), disabled: isMessageInputDisabled, renderFileUploadIcon: renderFileUploadIcon, renderSendMessageIcon: renderSendMessageIcon, renderVoiceMessageIcon: renderVoiceMessageIcon, onStartTyping: startTyping, onStopTyping: stopTyping, pendingFiles: pendingFiles, onAddFiles: addFiles, onRemoveFile: removeFile, onSubmit: handleSubmit, onUserMentioned: function (user) {
if ((selectedUser === null || selectedUser === void 0 ? void 0 : selectedUser.userId) === (user === null || user === void 0 ? void 0 : user.userId)) {
setSelectedUser(null);
setMentionNickname('');
}
}, onMentionStringChange: function (mentionText) {
setMentionNickname(mentionText);
}, onMentionedUserIdsUpdated: function (userIds) {
setMentionedUserIds(userIds);
}, onKeyDown: function (e) {
if (showSuggestedMentionList
&& (mentionSuggestedUsers === null || mentionSuggestedUsers === void 0 ? void 0 : mentionSuggestedUsers.length) > 0
&& ((e.key === MessageInputKeys.Enter && ableMention)
|| e.key === MessageInputKeys.ArrowUp
|| e.key === MessageInputKeys.ArrowDown)) {
setMessageInputEvent(e);
return true;
}
return false;
} }))));
});
export { MessageInputWrapperView as M, VoiceMessageInputWrapper as V };
//# sourceMappingURL=bundle-CwMNZmx9.js.map