hexabot/widget/src/components/UserInput.tsx
2024-09-29 13:02:28 +01:00

231 lines
6.6 KiB
TypeScript

/*
* Copyright © 2024 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import React, { useEffect, useRef, useState } from 'react';
import EmojiButton from './buttons/EmojiButton';
import FileButton from './buttons/FileButton';
import LocationButton from './buttons/LocationButton';
import MenuButton from './buttons/MenuButton';
import SendButton from './buttons/SendButton';
import CloseIcon from './icons/CloseIcon';
import FileInputIcon from './icons/FileInputIcon';
import Suggestions from './Suggestions';
import { useTranslation } from '../hooks/useTranslation';
import { useChat } from '../providers/ChatProvider';
import { useColors } from '../providers/ColorProvider';
import { useSettings } from '../providers/SettingsProvider';
import { TOutgoingMessageType } from '../types/message.types';
import { OutgoingMessageState } from '../types/state.types';
import './UserInput.scss';
const UserInput: React.FC = () => {
const { t } = useTranslation();
const { colors } = useColors();
const {
suggestions,
message,
setMessage,
file,
setFile,
send,
outgoingMessageState,
} = useChat();
const {
menu,
focusOnOpen,
autoFlush,
allowedUploadTypes,
allowedUploadSize,
showEmoji,
showLocation,
showFile,
placeholder,
} = useSettings();
const userInputRef = useRef<HTMLDivElement>(null);
const [fileError, setFileError] = useState<string | null>(null);
const [inputActive, setInputActive] = useState(false);
useEffect(() => {
// if (userInputRef.current) {
// userInputRef.current.innerHTML = message.current || '';
// }
if (focusOnOpen) {
focusUserInput();
}
}, [message, focusOnOpen]);
useEffect(() => {
if (message === '') {
userInputRef.current!.innerHTML = '';
}
}, [message]);
useEffect(() => {
setFileError(null);
}, [file]);
const cancelFile = () => {
setFile(null);
setFileError(null);
};
const handleInput = () => {
setMessage(
userInputRef.current?.innerText ||
userInputRef.current?.textContent ||
'',
);
};
const sendMessage = (
event: React.MouseEvent | React.KeyboardEvent,
source: string = 'send-button',
) => {
if (message) {
send({
event,
source,
data: {
type: TOutgoingMessageType.text,
data: { text: message },
},
});
if (autoFlush) {
setMessage('');
}
}
if (file) {
setFileError(null);
const typeCheck = allowedUploadTypes.includes(file.type) || false;
if (!typeCheck) {
setFileError(t('messages.file_message.unsupported_file_type'));
} else if (file.size > (allowedUploadSize || 0)) {
setFileError(t('messages.file_message.unsupported_file_size'));
} else {
send({
event,
source,
data: {
type: TOutgoingMessageType.file,
data: {
name: file.name,
size: file.size,
type: file.type,
file,
},
},
});
autoFlush && setFile(null);
}
}
};
const handleKey = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
sendMessage(event, 'enter-key');
event.preventDefault();
}
};
const focusUserInput = () => {
userInputRef.current?.focus();
};
const uploading = outgoingMessageState === OutgoingMessageState.uploading;
return (
<div
className="sc-user-input-wrapper"
style={{ fill: colors.userInput.text }}
>
{suggestions.length > 0 && <Suggestions />}
{(file || uploading) && (
<div
className="sc-file-container"
style={{
backgroundColor: colors.userInput.text,
color: colors.userInput.bg,
}}
>
<FileInputIcon
width="16px"
height="16px"
className="icon-file-message"
/>
{fileError && <span>{fileError}</span>}
{uploading && <span>Loading...</span>}
{file && file.name && !fileError && (
<span>
{file.name.length > 23
? `${file.name.substring(0, 23)}...`
: file.name}
</span>
)}
<span className="delete-file-message" onClick={cancelFile}>
<CloseIcon height="10" />
</span>
</div>
)}
<form
className={`sc-user-input ${inputActive ? 'active' : ''}`}
style={{ background: colors.userInput.bg }}
>
{menu.length > 0 && <MenuButton />}
<div
role="textbox"
tabIndex={0}
onFocus={() => setInputActive(true)}
onBlur={() => setInputActive(false)}
onKeyDown={handleKey}
onInput={handleInput}
onPaste={(e) => {
e.preventDefault();
(e.target as HTMLInputElement).innerText =
e.clipboardData.getData('text/plain');
handleInput();
}}
contentEditable
suppressContentEditableWarning={true}
spellCheck
aria-autocomplete="list"
// @ts-expect-error to check
placeholder={placeholder} // Adjust for localization
className="sc-user-input--text"
ref={userInputRef}
style={{ color: colors.userInput.text }}
/>
<div className="sc-user-input--buttons">
{showEmoji && (
<div className="sc-user-input--button">
<EmojiButton inputRef={userInputRef} onInput={handleInput} />
</div>
)}
{showLocation && (
<div className="sc-user-input--button">
<LocationButton />
</div>
)}
{showFile && (
<div className="sc-user-input--button">
<FileButton />
</div>
)}
<div className="sc-user-input--button">
<SendButton
disabled={!message}
onClick={(event) => sendMessage(event)}
/>
</div>
</div>
</form>
</div>
);
};
export default UserInput;