mirror of
https://github.com/hexastack/hexabot
synced 2025-01-07 19:45:30 +00:00
231 lines
6.6 KiB
TypeScript
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;
|