/* eslint-disable react/prop-types */ import React, { useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import './ChannelViewInput.scss'; import TextareaAutosize from 'react-autosize-textarea'; import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; import settings from '../../../client/state/settings'; import { openEmojiBoard } from '../../../client/action/navigation'; import { bytesToSize } from '../../../util/common'; import { getUsername } from '../../../util/matrixUtil'; import colorMXID from '../../../util/colorMXID'; import Text from '../../atoms/text/Text'; import RawIcon from '../../atoms/system-icons/RawIcon'; import IconButton from '../../atoms/button/IconButton'; import ScrollView from '../../atoms/scroll/ScrollView'; import { MessageReply } from '../../molecules/message/Message'; import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg'; import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg'; import SendIC from '../../../../public/res/ic/outlined/send.svg'; import ShieldIC from '../../../../public/res/ic/outlined/shield.svg'; import VLCIC from '../../../../public/res/ic/outlined/vlc.svg'; import VolumeFullIC from '../../../../public/res/ic/outlined/volume-full.svg'; import MarkdownIC from '../../../../public/res/ic/outlined/markdown.svg'; import FileIC from '../../../../public/res/ic/outlined/file.svg'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; const CMD_REGEX = /(\/|>[#*@]|:|@)(\S*)$/; let isTyping = false; let isCmdActivated = false; let cmdCursorPos = null; function ChannelViewInput({ roomId, roomTimeline, timelineScroll, viewEvent, }) { const [attachment, setAttachment] = useState(null); const [isMarkdown, setIsMarkdown] = useState(settings.isMarkdown); const [replyTo, setReplyTo] = useState(null); const textAreaRef = useRef(null); const inputBaseRef = useRef(null); const uploadInputRef = useRef(null); const uploadProgressRef = useRef(null); const rightOptionsRef = useRef(null); const TYPING_TIMEOUT = 5000; const mx = initMatrix.matrixClient; const { roomsInput } = initMatrix; useEffect(() => { settings.on(cons.events.settings.MARKDOWN_TOGGLED, setIsMarkdown); return () => { settings.removeListener(cons.events.settings.MARKDOWN_TOGGLED, setIsMarkdown); }; }, []); const sendIsTyping = (isT) => { mx.sendTyping(roomId, isT, isT ? TYPING_TIMEOUT : undefined); isTyping = isT; if (isT === true) { setTimeout(() => { if (isTyping) sendIsTyping(false); }, TYPING_TIMEOUT); } }; function uploadingProgress(myRoomId, { loaded, total }) { if (myRoomId !== roomId) return; const progressPer = Math.round((loaded * 100) / total); uploadProgressRef.current.textContent = `Uploading: ${bytesToSize(loaded)}/${bytesToSize(total)} (${progressPer}%)`; inputBaseRef.current.style.backgroundImage = `linear-gradient(90deg, var(--bg-surface-hover) ${progressPer}%, var(--bg-surface-low) ${progressPer}%)`; } function clearAttachment(myRoomId) { if (roomId !== myRoomId) return; setAttachment(null); inputBaseRef.current.style.backgroundImage = 'unset'; uploadInputRef.current.value = null; } function rightOptionsA11Y(A11Y) { const rightOptions = rightOptionsRef.current.children; for (let index = 0; index < rightOptions.length; index += 1) { rightOptions[index].disabled = !A11Y; } } function activateCmd(prefix) { isCmdActivated = true; requestAnimationFrame(() => { inputBaseRef.current.style.boxShadow = '0 0 0 1px var(--bg-positive)'; }); rightOptionsA11Y(false); viewEvent.emit('cmd_activate', prefix); } function deactivateCmd() { if (inputBaseRef.current !== null) { requestAnimationFrame(() => { inputBaseRef.current.style.boxShadow = 'var(--bs-surface-border)'; }); rightOptionsA11Y(true); } isCmdActivated = false; cmdCursorPos = null; } function errorCmd() { requestAnimationFrame(() => { inputBaseRef.current.style.boxShadow = '0 0 0 1px var(--bg-danger)'; }); } function setCursorPosition(pos) { setTimeout(() => { textAreaRef.current.focus(); textAreaRef.current.setSelectionRange(pos, pos); }, 0); } function replaceCmdWith(msg, cursor, replacement) { if (msg === null) return null; const targetInput = msg.slice(0, cursor); const cmdParts = targetInput.match(CMD_REGEX); const leadingInput = msg.slice(0, cmdParts.index); if (replacement.length > 0) setCursorPosition(leadingInput.length + replacement.length); return leadingInput + replacement + msg.slice(cursor); } function firedCmd(cmdData) { const msg = textAreaRef.current.value; textAreaRef.current.value = replaceCmdWith( msg, cmdCursorPos, typeof cmdData?.replace !== 'undefined' ? cmdData.replace : '', ); deactivateCmd(); } function setUpReply(userId, eventId, content) { setReplyTo({ userId, eventId, content }); roomsInput.setReplyTo(roomId, { userId, eventId, content }); } useEffect(() => { roomsInput.on(cons.events.roomsInput.UPLOAD_PROGRESS_CHANGES, uploadingProgress); roomsInput.on(cons.events.roomsInput.ATTACHMENT_CANCELED, clearAttachment); roomsInput.on(cons.events.roomsInput.FILE_UPLOADED, clearAttachment); viewEvent.on('cmd_error', errorCmd); viewEvent.on('cmd_fired', firedCmd); viewEvent.on('reply_to', setUpReply); if (textAreaRef?.current !== null) { isTyping = false; textAreaRef.current.focus(); textAreaRef.current.value = roomsInput.getMessage(roomId); setAttachment(roomsInput.getAttachment(roomId)); setReplyTo(roomsInput.getReplyTo(roomId)); } return () => { roomsInput.removeListener(cons.events.roomsInput.UPLOAD_PROGRESS_CHANGES, uploadingProgress); roomsInput.removeListener(cons.events.roomsInput.ATTACHMENT_CANCELED, clearAttachment); roomsInput.removeListener(cons.events.roomsInput.FILE_UPLOADED, clearAttachment); viewEvent.removeListener('cmd_error', errorCmd); viewEvent.removeListener('cmd_fired', firedCmd); viewEvent.removeListener('reply_to', setUpReply); if (isCmdActivated) deactivateCmd(); if (textAreaRef?.current === null) return; const msg = textAreaRef.current.value; inputBaseRef.current.style.backgroundImage = 'unset'; if (msg.trim() === '') { roomsInput.setMessage(roomId, ''); return; } roomsInput.setMessage(roomId, msg); }; }, [roomId]); async function sendMessage() { if (isCmdActivated) { viewEvent.emit('cmd_exe'); return; } const msgBody = textAreaRef.current.value; if (roomsInput.isSending(roomId)) return; if (msgBody.trim() === '' && attachment === null) return; sendIsTyping(false); roomsInput.setMessage(roomId, msgBody); if (attachment !== null) { roomsInput.setAttachment(roomId, attachment); } textAreaRef.current.disabled = true; textAreaRef.current.style.cursor = 'not-allowed'; await roomsInput.sendInput(roomId); textAreaRef.current.disabled = false; textAreaRef.current.style.cursor = 'unset'; textAreaRef.current.focus(); textAreaRef.current.value = roomsInput.getMessage(roomId); timelineScroll.reachBottom(); viewEvent.emit('message_sent'); textAreaRef.current.style.height = 'unset'; if (replyTo !== null) setReplyTo(null); } function processTyping(msg) { const isEmptyMsg = msg === ''; if (isEmptyMsg && isTyping) { sendIsTyping(false); return; } if (!isEmptyMsg && !isTyping) { sendIsTyping(true); } } function getCursorPosition() { return textAreaRef.current.selectionStart; } function recognizeCmd(rawInput) { const cursor = getCursorPosition(); const targetInput = rawInput.slice(0, cursor); const cmdParts = targetInput.match(CMD_REGEX); if (cmdParts === null) { if (isCmdActivated) { deactivateCmd(); viewEvent.emit('cmd_deactivate'); } return; } const cmdPrefix = cmdParts[1]; const cmdSlug = cmdParts[2]; if (cmdPrefix === ':') { // skip emoji autofill command if link is suspected. const checkForLink = targetInput.slice(0, cmdParts.index); if (checkForLink.match(/(http|https|mailto|matrix|ircs|irc)$/)) { deactivateCmd(); viewEvent.emit('cmd_deactivate'); return; } } cmdCursorPos = cursor; if (cmdSlug === '') { activateCmd(cmdPrefix); return; } if (!isCmdActivated) activateCmd(cmdPrefix); requestAnimationFrame(() => { inputBaseRef.current.style.boxShadow = '0 0 0 1px var(--bg-caution)'; }); viewEvent.emit('cmd_process', cmdPrefix, cmdSlug); } function handleMsgTyping(e) { const msg = e.target.value; recognizeCmd(e.target.value); if (!isCmdActivated) processTyping(msg); } function handleKeyDown(e) { if (e.keyCode === 13 && e.shiftKey === false) { e.preventDefault(); sendMessage(); } } function addEmoji(emoji) { textAreaRef.current.value += emoji.unicode; } function handleUploadClick() { if (attachment === null) uploadInputRef.current.click(); else { roomsInput.cancelAttachment(roomId); } } function uploadFileChange(e) { const file = e.target.files.item(0); setAttachment(file); if (file !== null) roomsInput.setAttachment(roomId, file); } function renderInputs() { return ( <>
{roomTimeline.isEncryptedRoom() && } timelineScroll.autoReachBottom()} onKeyDown={handleKeyDown} placeholder="Send a message..." /> {isMarkdown && }
{ const boxInfo = e.target.getBoundingClientRect(); openEmojiBoard({ x: boxInfo.x + (document.dir === 'rtl' ? -80 : 80), y: boxInfo.y - 250, detail: e.detail, }, addEmoji); }} tooltip="Emoji" src={EmojiIC} />
); } function attachFile() { const fileType = attachment.type.slice(0, attachment.type.indexOf('/')); return (
{fileType === 'image' && {attachment.name}} {fileType === 'video' && } {fileType === 'audio' && } {fileType !== 'image' && fileType !== 'video' && fileType !== 'audio' && }
{attachment.name} {`size: ${bytesToSize(attachment.size)}`}
); } function attachReply() { return (
{ roomsInput.cancelReplyTo(roomId); setReplyTo(null); }} src={CrossIC} tooltip="Cancel reply" size="extra-small" />
); } return ( <> { replyTo !== null && attachReply()} { attachment !== null && attachFile() }
{ e.preventDefault(); }}> { roomTimeline.room.isSpaceRoom() ? Spaces are yet to be implemented : renderInputs() }
); } ChannelViewInput.propTypes = { roomId: PropTypes.string.isRequired, roomTimeline: PropTypes.shape({}).isRequired, timelineScroll: PropTypes.shape({ reachBottom: PropTypes.func, autoReachBottom: PropTypes.func, tryRestoringScroll: PropTypes.func, enableSmoothScroll: PropTypes.func, disableSmoothScroll: PropTypes.func, }).isRequired, viewEvent: PropTypes.shape({}).isRequired, }; export default ChannelViewInput;