179 lines
4.5 KiB
TypeScript
179 lines
4.5 KiB
TypeScript
import React, {
|
|
ChangeEvent,
|
|
KeyboardEvent,
|
|
FormEvent,
|
|
useCallback,
|
|
useRef,
|
|
useMemo,
|
|
useState,
|
|
} from "react";
|
|
import { useChat, useDrawer, useEmojis } from "../../hooks";
|
|
import { EmojiDrawer, QuickEmojis } from "../emoji";
|
|
import "./UserInput.css";
|
|
|
|
export function UserInput() {
|
|
const { draft, sendMessage, updateDraft } = useChat();
|
|
const { reveal, hide, open } = useDrawer();
|
|
const builtChatInput = useRef<HTMLTextAreaElement>(null);
|
|
const { visible, addQuery } = useEmojis();
|
|
const form = useRef<HTMLFormElement>(null);
|
|
const [typingOffset, setTypingOffset] = useState(0);
|
|
const quickEmojis = useMemo(
|
|
() => visible.slice(0, process.env.QUICK_EMOJIS_MAX_COUNT),
|
|
[visible]
|
|
);
|
|
const handleChange = useCallback(
|
|
(event: ChangeEvent<HTMLTextAreaElement>) => {
|
|
const input = event.target.value;
|
|
const [openEmojiToken, closeEmojiToken] = locateEmojiTokens(input);
|
|
const emojiSegment = input.slice(openEmojiToken + 1, closeEmojiToken + 1);
|
|
|
|
updateDraft(input);
|
|
addQuery(openEmojiToken === -1 ? "" : emojiSegment);
|
|
setTypingOffset(
|
|
emojiSegment.length * process.env.APPROXIMATE_CHARACTER_WIDTH
|
|
);
|
|
},
|
|
[]
|
|
);
|
|
const handleSendMessage = useCallback(
|
|
(event?: FormEvent<HTMLFormElement>) => {
|
|
event?.preventDefault();
|
|
sendMessage();
|
|
},
|
|
[sendMessage]
|
|
);
|
|
const handleKeyUp = useCallback(
|
|
(event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
if (event.key === "Enter") {
|
|
handleSendMessage();
|
|
}
|
|
},
|
|
[handleSendMessage]
|
|
);
|
|
const handleOpenEmojiDrawer = useCallback(
|
|
() =>
|
|
reveal({
|
|
title: "Select an emoji",
|
|
content: (
|
|
<EmojiDrawer
|
|
onSelectEmoji={handleSelectEmoji}
|
|
onClose={() => builtChatInput.current?.focus()}
|
|
/>
|
|
),
|
|
}),
|
|
[]
|
|
);
|
|
const handleCloseEmojiDrawer = useCallback(() => {
|
|
builtChatInput.current?.focus();
|
|
hide();
|
|
}, [hide]);
|
|
|
|
const handleSelectEmoji = useCallback((emoji: string) => {
|
|
updateDraft((prev) => `${prev} :${emoji}: `);
|
|
}, []);
|
|
const handleInsertQuickEmoji = useCallback(
|
|
(emoji: string) => {
|
|
const [openEmojiToken, closeEmojiToken] = locateEmojiTokens(draft);
|
|
const toReplace = draft.slice(openEmojiToken, closeEmojiToken + 1);
|
|
|
|
updateDraft((prev) => prev.replace(toReplace, `:${emoji}: `));
|
|
addQuery("");
|
|
|
|
builtChatInput.current?.focus();
|
|
},
|
|
[draft]
|
|
);
|
|
|
|
return (
|
|
<form ref={form} className="UserInput" onSubmit={handleSendMessage}>
|
|
{quickEmojis.length > 0 && (
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: -254,
|
|
height: 250,
|
|
left: typingOffset,
|
|
display: "flex",
|
|
flexDirection: "column-reverse",
|
|
}}
|
|
>
|
|
<QuickEmojis
|
|
emojis={quickEmojis}
|
|
onSelectEmoji={handleInsertQuickEmoji}
|
|
/>
|
|
</div>
|
|
)}
|
|
<textarea
|
|
ref={builtChatInput}
|
|
id="builtChatInput"
|
|
className="form-control"
|
|
style={{
|
|
minHeight: 50,
|
|
height: 50,
|
|
maxHeight: 50,
|
|
}}
|
|
minLength={1}
|
|
maxLength={1000}
|
|
rows={1}
|
|
onChange={handleChange}
|
|
onKeyUp={handleKeyUp}
|
|
placeholder="Message"
|
|
autoComplete="off"
|
|
autoFocus={true}
|
|
value={draft}
|
|
/>
|
|
{open ? (
|
|
<span
|
|
role="button"
|
|
onClick={handleCloseEmojiDrawer}
|
|
className="UserInput-emoji"
|
|
style={{top: 6}}
|
|
>
|
|
X
|
|
</span>
|
|
) : (
|
|
<i
|
|
role="button"
|
|
onClick={handleOpenEmojiDrawer}
|
|
className="UserInput-emoji fas fa-smile-beam"
|
|
/>
|
|
)}
|
|
</form>
|
|
);
|
|
}
|
|
|
|
function locateEmojiTokens(text: string) {
|
|
let openEmojiInputToken = -1;
|
|
let endEmojiInputToken = -1;
|
|
|
|
if (text.length <= 1) {
|
|
return [openEmojiInputToken, endEmojiInputToken];
|
|
}
|
|
|
|
for (let i = 0; i < text.length; i++) {
|
|
const character = text[i];
|
|
|
|
if (character === process.env.EMOJI_INPUT_TOKEN) {
|
|
if (openEmojiInputToken === -1) {
|
|
openEmojiInputToken = i;
|
|
} else {
|
|
openEmojiInputToken = -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (openEmojiInputToken !== -1) {
|
|
endEmojiInputToken = openEmojiInputToken;
|
|
|
|
while (
|
|
endEmojiInputToken < text.length - 1 &&
|
|
text[endEmojiInputToken] !== " "
|
|
) {
|
|
endEmojiInputToken++;
|
|
}
|
|
}
|
|
|
|
return [openEmojiInputToken, endEmojiInputToken];
|
|
}
|