import { encode } from 'html-entities'; import { motion } from 'framer-motion'; import { useNavigate, useLocation } from 'react-router-dom'; import { useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import { COMPOSER_MAX_CHARACTER_LENGTH, COMPOSER_RATE_LIMIT_BLOCK_TIME_MS } from '../../../constants'; import { CHAT_MESSAGE_EVENT_TYPES } from '../../../constants'; import { channel as $channelContent } from '../../../content'; import { CHAT_USER_ROLE, SEND_ERRORS } from './useChatConnection/utils'; import { clsm } from '../../../utils'; import { Lock } from '../../../assets/icons'; import { useChannel } from '../../../contexts/Channel'; import { useResponsiveDevice } from '../../../contexts/ResponsiveDevice'; import { useUser } from '../../../contexts/User'; import ComposerErrorMessage from './ComposerErrorMessage'; import FloatingNav from '../../../components/FloatingNav'; import Input from '../../../components/Input'; import useCurrentPage from '../../../hooks/useCurrentPage'; import { usePoll } from '../../../contexts/StreamManagerActions/Poll'; const { SEND_MESSAGE } = CHAT_MESSAGE_EVENT_TYPES; const $content = $channelContent.chat; const Composer = ({ chatUserRole, isDisabled, isFocusable, isLoading, sendAttemptError, sendMessage }) => { const navigate = useNavigate(); const location = useLocation(); const composerFieldRef = useRef(); const { channelData } = useChannel(); const { isViewerBanned: isLocked } = channelData || {}; const { isLandscape } = useResponsiveDevice(); const { isSessionValid } = useUser(); const { setComposerRefState } = usePoll(); const [message, setMessage] = useState(''); const [errorMessage, setErrorMessage] = useState(''); const [shouldShake, setShouldShake] = useState(false); // Composer has shake animated only on submit const [blockChat, setBlockChat] = useState(false); const currentPage = useCurrentPage(); const isStreamManagerPage = currentPage === 'stream_manager'; const canSendMessages = chatUserRole && [CHAT_USER_ROLE.SENDER, CHAT_USER_ROLE.MODERATOR].includes(chatUserRole); const focus = location.state?.focus; useEffect(() => { if (composerFieldRef.current) { setComposerRefState(composerFieldRef); } }, [composerFieldRef, setComposerRefState]); const setSubmitErrorStates = (_errorMessage) => { setErrorMessage(`${$content.error.message_not_sent} ${_errorMessage}`); setShouldShake(true); }; const navigateToLogin = () => navigate('/login', { state: { from: location, focus: 'COMPOSER' } }); const handleOnChange = (event) => { // If the user isn't logged in, redirect them to the login page if ((!isLoading && canSendMessages === false) || !isSessionValid) { navigateToLogin(); } const { value } = event.target; const encodedValue = encode(value); // This is done to ensure we get the correct message length as it seems the IVS Chat API trims the message before checking its length const trimmedValue = encodedValue.trim(); setMessage(value); // On change errors if (trimmedValue.length > COMPOSER_MAX_CHARACTER_LENGTH) { setErrorMessage($content.error.max_length_reached); } else if (!blockChat) { setErrorMessage(''); } }; const handleSendMessage = (event) => { event.preventDefault(); if (isDisabled) return; if (isLoading) { setSubmitErrorStates($content.error.wait_until_connected); } else { if (canSendMessages) { if (!message || blockChat) return; if (errorMessage.includes($content.error.wait_until_connected)) { setErrorMessage(''); setMessage(''); } sendMessage(message, { eventType: SEND_MESSAGE }); !errorMessage && setMessage(''); setShouldShake(false); } else { if (canSendMessages === undefined) { setSubmitErrorStates($content.error.wait_until_connected); } else { navigateToLogin(); } } } }; useEffect(() => { // If previous route has focus state, focus on composer if (focus && focus === 'COMPOSER') { composerFieldRef.current.focus(); } }, [focus]); useEffect(() => { // If user is banned, remove any message if (isLocked) setMessage(''); }, [isLocked]); useEffect(() => { // If there is a connection error, clear the current error message if (isDisabled) setErrorMessage(''); }, [isDisabled]); useEffect(() => { if (blockChat) { const blockChatTimerId = setTimeout(() => { setBlockChat(false); setErrorMessage(''); }, COMPOSER_RATE_LIMIT_BLOCK_TIME_MS); return () => clearTimeout(blockChatTimerId); } }, [blockChat]); useEffect(() => { // Send errors if (sendAttemptError) { let sendAttemptErrorMessage = ''; if (sendAttemptError.message === SEND_ERRORS.RATE_LIMIT_EXCEEDED) { setBlockChat(true); sendAttemptErrorMessage = $content.error.rate_exceeded; } else if (sendAttemptError.message === SEND_ERRORS.MAX_LENGTH_EXCEEDED) { sendAttemptErrorMessage = $content.error.max_length_reached; } // connection error or chat is loading (chat is not connected) setSubmitErrorStates(sendAttemptErrorMessage); } }, [sendAttemptError, isLoading]); return (