rDrama/chat/src/features/chat/UserInput.tsx

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];
}