import { motion } from 'framer-motion'; import { useState, useRef, useEffect } from 'react'; import styled from 'styled-components'; import { Icon, Button, BoxProps, Flex, Spinner } from '../../../general'; import { InputBox, TextArea } from '../../../inputs'; import { ImageBlock } from '../ImageBlock/ImageBlock'; import { MediaBlock } from '../MediaBlock/MediaBlock'; import { isImageLink, parseMediaType } from '../../util/links'; import { FragmentType } from '../Bubble/Bubble.types'; import { FragmentImage } from '../Bubble/fragment-lib'; import { convertFragmentsToText, parseChatInput } from './fragment-parser'; import { Reply } from '../Bubble/Reply'; const CHAT_INPUT_LINE_HEIGHT = 22; const ChatBox = styled(TextArea)` resize: none; line-height: ${CHAT_INPUT_LINE_HEIGHT}px; font-size: 14px; padding-left: 4px; padding-right: 4px; `; const RemoveAttachmentButton = styled(motion.div)` position: relative; z-index: 4; transition: var(--transition); overflow: visible; ${FragmentImage} { padding: 0px; } ${Button.Base} { display: flex; justify-content: center; align-items: center; } .chat-attachment-remove-btn { position: absolute; display: flex; overflow: visible; flex-direction: column; justify-content: center; align-items: center; top: -4px; right: -4px; z-index: 4; border-radius: 12px; transition: var(--transition); } `; type ChatInputProps = { id: string; selectedChatPath: string; disabled?: boolean; isFocused?: boolean; loading?: boolean; attachments?: string[]; containerWidth?: number; replyTo?: { id: string; author: string; authorColor: string; sentAt: string; message: FragmentType[]; }; editingMessage?: FragmentType[]; error?: string; themeMode?: 'light' | 'dark'; onPaste?: (evt: React.ClipboardEvent) => void; onSend: (fragments: FragmentType[]) => void; onEditConfirm: (fragments: FragmentType[]) => void; onCancelEdit?: (evt: React.MouseEvent) => void; onCancelReply?: () => void; onAttachment?: () => void; onRemoveAttachment?: (index: number) => void; onBlur: () => void; } & BoxProps; export const parseStringToFragment = (value: string): FragmentType[] => { const fragments = value.split(' ').map((fragment) => ({ plain: fragment, })); return fragments; }; const attachmentHeight = 116; const replyHeight = 46; export const ChatInput = ({ id, selectedChatPath, replyTo, loading, tabIndex, disabled, isFocused, editingMessage, attachments, error, containerWidth, themeMode, onSend, onEditConfirm, onCancelEdit, onCancelReply, onAttachment, onRemoveAttachment, onPaste, onBlur, ...chatInputProps }: ChatInputProps) => { const [rows, setRows] = useState(1); const [value, setValue] = useState(''); const inputRef = useRef(null); useEffect(() => { if (inputRef.current && isFocused) { inputRef.current.focus(); } else { inputRef.current?.blur(); } }, [isFocused, inputRef]); useEffect(() => { if (editingMessage) { const parsedFragments = convertFragmentsToText(editingMessage); if (inputRef.current && isFocused) { inputRef.current.value = parsedFragments; changeRows(inputRef.current.value, inputRef.current.scrollHeight); inputRef.current.focus(); setValue(parsedFragments); } else { inputRef.current?.blur(); } } }, [editingMessage]); const changeRows = (newValue: string, scrollHeight: number) => { if (newValue.length < 30 && newValue.split('\n').length < 2) { setRows(1); } else if (newValue.split('\n').length < value.split('\n').length) { setRows(rows - 1); } else { setRows(scrollHeight / CHAT_INPUT_LINE_HEIGHT); } }; const onChange = (evt: React.ChangeEvent) => { changeRows(evt.target.value, evt.target.scrollHeight); setValue(evt.target.value); }; const onFocus = (evt: React.FocusEvent) => { const savedChat = localStorage.getItem(selectedChatPath); if (savedChat) { const savedChatAndType = JSON.parse(savedChat); if (savedChatAndType.isNew === !editingMessage) { evt.target.value = savedChatAndType.value; onChange(evt); } } }; const handleOnBlur = (_evt: React.FocusEvent) => { if (value) { const isNew = editingMessage ? false : true; localStorage.setItem( selectedChatPath, JSON.stringify({ isNew: isNew, value: value }) ); } else { localStorage.removeItem(selectedChatPath); } onBlur(); }; const isDisabled = disabled || loading || (value.length === 0 && !attachments) || (value.length === 0 && attachments && attachments.length === 0); const onKeyDown = (evt: React.KeyboardEvent) => { if (evt.key === 'Enter' && !evt.shiftKey) { evt.preventDefault(); if (isDisabled) return; const fragments = onParseFragments(); onSendClick(fragments); } }; const onParseFragments = () => { const parsedFragments = value ? parseChatInput(value) : []; let attachmentFragments: FragmentType[] = []; if (attachments && attachments.length > 0) { attachmentFragments = attachments.map((attachment) => { if (isImageLink(attachment)) { return { image: attachment }; } else { return { link: attachment, }; } }); } return [...attachmentFragments, ...parsedFragments]; }; const onSendClick = (parsedFragments: FragmentType[]) => { localStorage.removeItem(selectedChatPath); setValue(''); if (editingMessage) { onEditConfirm(parsedFragments); setRows(1); } else { onSend(parsedFragments); setRows(1); } }; return ( {attachments && attachments.length > 0 ? ( {attachments.map((attachment: string, index: number) => { const { linkType } = parseMediaType(attachment); let block = null; if (linkType === 'image') { block = ( ); } else { block = ( ); } return ( {block} { evt.stopPropagation(); onRemoveAttachment(index); } : undefined } > ); })} ) : null} {replyTo ? ( ) : null} {loading ? ( ) : ( )} {editingMessage && ( { setValue(''); setRows(1); if (onCancelEdit) onCancelEdit(evt); }} > )} { evt.stopPropagation(); const fragments = onParseFragments(); onSendClick(fragments); }} > ); };