stream-chat-react
Version:
React components to create chat conversations or livestream style chat
114 lines (113 loc) • 8.97 kB
JavaScript
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import clsx from 'clsx';
import { useDropzone } from 'react-dropzone';
import { AttachmentSelector as DefaultAttachmentSelector, SimpleAttachmentSelector, } from './AttachmentSelector';
import { AttachmentPreviewList as DefaultAttachmentPreviewList } from './AttachmentPreviewList';
import { CooldownTimer as DefaultCooldownTimer } from './CooldownTimer';
import { SendButton as DefaultSendButton } from './SendButton';
import { StopAIGenerationButton as DefaultStopAIGenerationButton } from './StopAIGenerationButton';
import { AudioRecorder as DefaultAudioRecorder, RecordingPermissionDeniedNotification as DefaultRecordingPermissionDeniedNotification, StartRecordingAudioButton as DefaultStartRecordingAudioButton, RecordingPermission, } from '../MediaRecorder';
import { QuotedMessagePreview as DefaultQuotedMessagePreview, QuotedMessagePreviewHeader, } from './QuotedMessagePreview';
import { LinkPreviewList as DefaultLinkPreviewList } from './LinkPreviewList';
import { ChatAutoComplete } from '../ChatAutoComplete/ChatAutoComplete';
import { RecordingAttachmentType } from '../MediaRecorder/classes';
import { useChatContext } from '../../context/ChatContext';
import { useChannelActionContext } from '../../context/ChannelActionContext';
import { useChannelStateContext } from '../../context/ChannelStateContext';
import { useTranslationContext } from '../../context/TranslationContext';
import { useMessageInputContext } from '../../context/MessageInputContext';
import { useComponentContext } from '../../context/ComponentContext';
import { AIStates, useAIState } from '../AIStateIndicator';
export const MessageInputFlat = () => {
const { t } = useTranslationContext('MessageInputFlat');
const { asyncMessagesMultiSendEnabled, attachments, cooldownRemaining, findAndEnqueueURLsToEnrich, handleSubmit, hideSendButton, isUploadEnabled, linkPreviews, maxFilesLeft, message, numberOfUploads, parent, recordingController, setCooldownRemaining, text, uploadNewFiles, } = useMessageInputContext('MessageInputFlat');
const { AttachmentPreviewList = DefaultAttachmentPreviewList, AttachmentSelector = message ? SimpleAttachmentSelector : DefaultAttachmentSelector, AudioRecorder = DefaultAudioRecorder, CooldownTimer = DefaultCooldownTimer, EmojiPicker, LinkPreviewList = DefaultLinkPreviewList, QuotedMessagePreview = DefaultQuotedMessagePreview, RecordingPermissionDeniedNotification = DefaultRecordingPermissionDeniedNotification, SendButton = DefaultSendButton, StartRecordingAudioButton = DefaultStartRecordingAudioButton, StopAIGenerationButton: StopAIGenerationButtonOverride, } = useComponentContext('MessageInputFlat');
const { acceptedFiles = [], multipleUploads, quotedMessage, } = useChannelStateContext('MessageInputFlat');
const { setQuotedMessage } = useChannelActionContext('MessageInputFlat');
const { channel } = useChatContext('MessageInputFlat');
const { aiState } = useAIState(channel);
const stopGenerating = useCallback(() => channel?.stopAIResponse(), [channel]);
const [showRecordingPermissionDeniedNotification, setShowRecordingPermissionDeniedNotification,] = useState(false);
const closePermissionDeniedNotification = useCallback(() => {
setShowRecordingPermissionDeniedNotification(false);
}, []);
const failedUploadsCount = useMemo(() => attachments.filter((a) => a.localMetadata?.uploadState === 'failed').length, [attachments]);
const accept = useMemo(() => acceptedFiles.reduce((mediaTypeMap, mediaType) => {
mediaTypeMap[mediaType] ?? (mediaTypeMap[mediaType] = []);
return mediaTypeMap;
}, {}), [acceptedFiles]);
const { getRootProps, isDragActive, isDragReject } = useDropzone({
accept,
disabled: !isUploadEnabled || maxFilesLeft === 0,
multiple: multipleUploads,
noClick: true,
onDrop: uploadNewFiles,
});
useEffect(() => {
const handleQuotedMessageUpdate = (e) => {
if (e.message?.id !== quotedMessage?.id)
return;
if (e.type === 'message.deleted') {
setQuotedMessage(undefined);
return;
}
setQuotedMessage(e.message);
};
channel?.on('message.deleted', handleQuotedMessageUpdate);
channel?.on('message.updated', handleQuotedMessageUpdate);
return () => {
channel?.off('message.deleted', handleQuotedMessageUpdate);
channel?.off('message.updated', handleQuotedMessageUpdate);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [channel, quotedMessage]);
if (recordingController.recordingState)
return React.createElement(AudioRecorder, null);
// TODO: "!message" condition is a temporary fix for shared
// state when editing a message (fix shared state issue)
const displayQuotedMessage = !message && quotedMessage && quotedMessage.parent_id === parent?.id;
const recordingEnabled = !!(recordingController.recorder && navigator.mediaDevices); // account for requirement on iOS as per this bug report: https://bugs.webkit.org/show_bug.cgi?id=252303
const isRecording = !!recordingController.recordingState;
/**
* This bit here is needed to make sure that we can get rid of the default behaviour
* if need be. Essentially this allows us to pass StopAIGenerationButton={null} and
* completely circumvent the default logic if it's not what we want. We need it as a
* prop because there is no other trivial way to override the SendMessage button otherwise.
*/
const StopAIGenerationButton = StopAIGenerationButtonOverride === undefined
? DefaultStopAIGenerationButton
: StopAIGenerationButtonOverride;
const shouldDisplayStopAIGeneration = [AIStates.Thinking, AIStates.Generating].includes(aiState) &&
!!StopAIGenerationButton;
return (React.createElement(React.Fragment, null,
React.createElement("div", { ...getRootProps({ className: 'str-chat__message-input' }) },
recordingEnabled &&
recordingController.permissionState === 'denied' &&
showRecordingPermissionDeniedNotification && (React.createElement(RecordingPermissionDeniedNotification, { onClose: closePermissionDeniedNotification, permissionName: RecordingPermission.MIC })),
findAndEnqueueURLsToEnrich && (React.createElement(LinkPreviewList, { linkPreviews: Array.from(linkPreviews.values()) })),
isDragActive && (React.createElement("div", { className: clsx('str-chat__dropzone-container', {
'str-chat__dropzone-container--not-accepted': isDragReject,
}) },
!isDragReject && React.createElement("p", null, t('Drag your files here')),
isDragReject && React.createElement("p", null, t('Some of the files will not be accepted')))),
displayQuotedMessage && React.createElement(QuotedMessagePreviewHeader, null),
React.createElement("div", { className: 'str-chat__message-input-inner' },
React.createElement(AttachmentSelector, null),
React.createElement("div", { className: 'str-chat__message-textarea-container' },
displayQuotedMessage && (React.createElement(QuotedMessagePreview, { quotedMessage: quotedMessage })),
isUploadEnabled &&
!!(numberOfUploads + failedUploadsCount || attachments.length > 0) && (React.createElement(AttachmentPreviewList, null)),
React.createElement("div", { className: 'str-chat__message-textarea-with-emoji-picker' },
React.createElement(ChatAutoComplete, null),
EmojiPicker && React.createElement(EmojiPicker, null))),
shouldDisplayStopAIGeneration ? (React.createElement(StopAIGenerationButton, { onClick: stopGenerating })) : (!hideSendButton && (React.createElement(React.Fragment, null, cooldownRemaining ? (React.createElement(CooldownTimer, { cooldownInterval: cooldownRemaining, setCooldownRemaining: setCooldownRemaining })) : (React.createElement(React.Fragment, null,
React.createElement(SendButton, { disabled: !numberOfUploads &&
!text.length &&
attachments.length - failedUploadsCount === 0, sendMessage: handleSubmit }),
recordingEnabled && (React.createElement(StartRecordingAudioButton, { disabled: isRecording ||
(!asyncMessagesMultiSendEnabled &&
attachments.some((a) => a.type === RecordingAttachmentType.VOICE_RECORDING)), onClick: () => {
recordingController.recorder?.start();
setShowRecordingPermissionDeniedNotification(true);
} })))))))))));
};