Add input mentions (#126)
This commit is contained in:
parent
3be3a7726f
commit
fe609f0444
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from "react";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { bufToHex } from "status-communities/dist/cjs/utils";
|
||||
import styled from "styled-components";
|
||||
|
||||
|
@ -28,7 +28,8 @@ export function ChatCreation({
|
|||
|
||||
const { contacts, setActiveChannel } = useMessengerContext();
|
||||
|
||||
const addMember = (member: string) => {
|
||||
const addMember = useCallback(
|
||||
(member: string) => {
|
||||
setStyledGroup((prevMembers: string[]) => {
|
||||
if (prevMembers.find((mem) => mem === member)) {
|
||||
return prevMembers;
|
||||
|
@ -36,7 +37,9 @@ export function ChatCreation({
|
|||
return [...prevMembers, member];
|
||||
}
|
||||
});
|
||||
};
|
||||
},
|
||||
[setStyledGroup, styledGroup]
|
||||
);
|
||||
|
||||
const removeMember = (member: string) => {
|
||||
setStyledGroup((prev) => prev.filter((e) => e != member));
|
||||
|
@ -96,8 +99,8 @@ export function ChatCreation({
|
|||
{query && (
|
||||
<SearchBlock
|
||||
query={query}
|
||||
styledGroup={styledGroup}
|
||||
setStyledGroup={setStyledGroup}
|
||||
dsicludeList={styledGroup}
|
||||
onClick={addMember}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import { Picker } from "emoji-mart";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
|
||||
import { useMessengerContext } from "../../contexts/messengerProvider";
|
||||
|
@ -13,15 +19,25 @@ import { PictureIcon } from "../Icons/PictureIcon";
|
|||
import { StickerIcon } from "../Icons/StickerIcon";
|
||||
import "emoji-mart/css/emoji-mart.css";
|
||||
import { SizeLimitModal, SizeLimitModalName } from "../Modals/SizeLimitModal";
|
||||
import { SearchBlock } from "../SearchBlock";
|
||||
|
||||
export function ChatInput() {
|
||||
const { sendMessage } = useMessengerContext();
|
||||
const theme = useTheme() as Theme;
|
||||
const [content, setContent] = useState("");
|
||||
const [clearComponent, setClearComponent] = useState("");
|
||||
const [showEmoji, setShowEmoji] = useState(false);
|
||||
const [inputHeight, setInputHeight] = useState(40);
|
||||
const [imageUint, setImageUint] = useState<undefined | Uint8Array>(undefined);
|
||||
|
||||
const low = useLow();
|
||||
|
||||
const { setModal } = useModal(SizeLimitModalName);
|
||||
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const inputRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("click", () => setShowEmoji(false));
|
||||
return () => {
|
||||
|
@ -39,37 +55,139 @@ export function ChatInput() {
|
|||
const codesArray: any[] = [];
|
||||
sym.forEach((el: string) => codesArray.push("0x" + el));
|
||||
const emoji = String.fromCodePoint(...codesArray);
|
||||
if (inputRef.current) {
|
||||
inputRef.current.appendChild(document.createTextNode(emoji));
|
||||
}
|
||||
setContent((p) => p + emoji);
|
||||
}, []);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const target = e.target;
|
||||
const resizeTextArea = useCallback((target: HTMLDivElement) => {
|
||||
target.style.height = "40px";
|
||||
target.style.height = `${Math.min(target.scrollHeight, 438)}px`;
|
||||
setInputHeight(target.scrollHeight);
|
||||
setContent(target.value);
|
||||
},
|
||||
[]
|
||||
);
|
||||
}, []);
|
||||
|
||||
const onInputChange = useCallback((e: React.ChangeEvent<HTMLDivElement>) => {
|
||||
const element = document.getSelection();
|
||||
const inputElement = inputRef.current;
|
||||
if (inputElement && element && element.rangeCount > 0) {
|
||||
const selection = element?.getRangeAt(0)?.startOffset;
|
||||
const parentElement = element.anchorNode?.parentElement;
|
||||
if (parentElement && parentElement.tagName === "B") {
|
||||
parentElement.outerHTML = parentElement.innerText;
|
||||
const range = document.createRange();
|
||||
const sel = window.getSelection();
|
||||
if (element.anchorNode.firstChild) {
|
||||
const childNumber =
|
||||
element.focusOffset === 0 ? 0 : element.focusOffset - 1;
|
||||
range.setStart(element.anchorNode.childNodes[childNumber], selection);
|
||||
}
|
||||
range.collapse(true);
|
||||
|
||||
sel?.removeAllRanges();
|
||||
sel?.addRange(range);
|
||||
}
|
||||
}
|
||||
const target = e.target;
|
||||
resizeTextArea(target);
|
||||
setContent(target.textContent ?? "");
|
||||
}, []);
|
||||
|
||||
const onInputKeyPress = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key == "Enter" && !e.getModifierState("Shift")) {
|
||||
e.preventDefault();
|
||||
(e.target as HTMLTextAreaElement).style.height = "40px";
|
||||
(e.target as HTMLDivElement).style.height = "40px";
|
||||
setInputHeight(40);
|
||||
sendMessage(content, imageUint);
|
||||
setImageUint(undefined);
|
||||
setClearComponent("");
|
||||
if (inputRef.current) {
|
||||
inputRef.current.innerHTML = "";
|
||||
}
|
||||
setContent("");
|
||||
}
|
||||
},
|
||||
[content, imageUint]
|
||||
);
|
||||
|
||||
const low = useLow();
|
||||
const [selectedElement, setSelectedElement] = useState<{
|
||||
element: Selection | null;
|
||||
start: number;
|
||||
end: number;
|
||||
text: string;
|
||||
node: Node | null;
|
||||
}>({ element: null, start: 0, end: 0, text: "", node: null });
|
||||
|
||||
const { setModal } = useModal(SizeLimitModalName);
|
||||
const handleCursorChange = useCallback(() => {
|
||||
const element = document.getSelection();
|
||||
if (element && element.rangeCount > 0) {
|
||||
const selection = element?.getRangeAt(0)?.startOffset;
|
||||
const text = element?.anchorNode?.textContent;
|
||||
if (selection && text) {
|
||||
const end = text.indexOf(" ", selection);
|
||||
const start = text.lastIndexOf(" ", selection - 1);
|
||||
setSelectedElement({
|
||||
element,
|
||||
start,
|
||||
end,
|
||||
text,
|
||||
node: element.anchorNode,
|
||||
});
|
||||
const substring = text.substring(
|
||||
start > -1 ? start + 1 : 0,
|
||||
end > -1 ? end : undefined
|
||||
);
|
||||
if (substring.startsWith("@")) {
|
||||
setQuery(substring.slice(1));
|
||||
} else {
|
||||
setQuery("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(handleCursorChange, [content]);
|
||||
|
||||
const addMention = useCallback(
|
||||
(contact: string) => {
|
||||
if (inputRef?.current) {
|
||||
const { element, start, end, text, node } = selectedElement;
|
||||
if (element && text && node) {
|
||||
const firstSlice = text.slice(0, start > -1 ? start : 0);
|
||||
const secondSlice = text.slice(end > -1 ? end : content.length);
|
||||
const replaceContent = `${firstSlice} @${contact}${secondSlice}`;
|
||||
const spaceElement = document.createTextNode(" ");
|
||||
const contactElement = document.createElement("b");
|
||||
contactElement.innerText = `@${contact}`;
|
||||
|
||||
if (contactElement && element.rangeCount > 0) {
|
||||
const range = element.getRangeAt(0);
|
||||
range.setStart(node, start > -1 ? start : 0);
|
||||
if (end === -1 || end > text.length) {
|
||||
range.setEnd(node, text.length);
|
||||
} else {
|
||||
range.setEnd(node, end);
|
||||
}
|
||||
range.deleteContents();
|
||||
if (end === -1) {
|
||||
range.insertNode(spaceElement.cloneNode());
|
||||
}
|
||||
range.insertNode(contactElement);
|
||||
if (start > -1) {
|
||||
range.insertNode(spaceElement.cloneNode());
|
||||
}
|
||||
range.collapse();
|
||||
}
|
||||
inputRef.current.focus();
|
||||
setQuery("");
|
||||
setContent(replaceContent);
|
||||
resizeTextArea(inputRef.current);
|
||||
}
|
||||
}
|
||||
},
|
||||
[inputRef, inputRef?.current, content, selectedElement]
|
||||
);
|
||||
|
||||
return (
|
||||
<View>
|
||||
|
@ -126,11 +244,22 @@ export function ChatInput() {
|
|||
<ImagePreview src={image} onClick={() => setImageUint(undefined)} />
|
||||
)}
|
||||
<Input
|
||||
placeholder="Message"
|
||||
value={content}
|
||||
onChange={onInputChange}
|
||||
onKeyPress={onInputKeyPress}
|
||||
contentEditable
|
||||
onInput={onInputChange}
|
||||
onKeyDown={onInputKeyPress}
|
||||
onKeyUp={handleCursorChange}
|
||||
ref={inputRef}
|
||||
onClick={handleCursorChange}
|
||||
dangerouslySetInnerHTML={{ __html: clearComponent }}
|
||||
/>
|
||||
{query && (
|
||||
<SearchBlock
|
||||
query={query}
|
||||
dsicludeList={[]}
|
||||
onClick={addMention}
|
||||
onBotttom
|
||||
/>
|
||||
)}
|
||||
</InputWrapper>
|
||||
<InputButtons>
|
||||
<ChatButton
|
||||
|
@ -157,6 +286,7 @@ const InputWrapper = styled.div`
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const View = styled.div`
|
||||
|
@ -194,17 +324,20 @@ const ImagePreview = styled.img`
|
|||
margin-top: 9px;
|
||||
`;
|
||||
|
||||
const Input = styled.textarea`
|
||||
const Input = styled.div`
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
max-height: 438px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
padding: 8px 0 8px 12px;
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
border: 1px solid ${({ theme }) => theme.inputColor};
|
||||
color: ${({ theme }) => theme.primary};
|
||||
border-radius: 16px 16px 4px 16px;
|
||||
outline: none;
|
||||
resize: none;
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
|
|
|
@ -86,12 +86,20 @@ export function ChatMessages() {
|
|||
const [image, setImage] = useState("");
|
||||
const [link, setLink] = useState("");
|
||||
|
||||
const { setModal: setPictureModal } = useModal(PictureModalName);
|
||||
const { setModal: setLinkModal } = useModal(LinkModalName);
|
||||
const { setModal: setPictureModal, isVisible: showPictureModal } =
|
||||
useModal(PictureModalName);
|
||||
const { setModal: setLinkModal, isVisible: showLinkModal } =
|
||||
useModal(LinkModalName);
|
||||
|
||||
useEffect(() => (!image ? undefined : setPictureModal(true)), [image]);
|
||||
useEffect(() => (!link ? undefined : setLinkModal(true)), [link]);
|
||||
|
||||
useEffect(
|
||||
() => (!showPictureModal ? setImage("") : undefined),
|
||||
[showPictureModal]
|
||||
);
|
||||
useEffect(() => (!showLinkModal ? setLink("") : undefined), [showLinkModal]);
|
||||
|
||||
return (
|
||||
<MessagesWrapper ref={ref}>
|
||||
<PictureModal image={image} />
|
||||
|
|
|
@ -8,41 +8,36 @@ import { ContactsList } from "./Chat/ChatCreation";
|
|||
|
||||
interface SearchBlockProps {
|
||||
query: string;
|
||||
styledGroup: string[];
|
||||
setStyledGroup: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
dsicludeList: string[];
|
||||
onClick: (member: string) => void;
|
||||
onBotttom?: boolean;
|
||||
}
|
||||
|
||||
export const SearchBlock = ({
|
||||
query,
|
||||
styledGroup,
|
||||
setStyledGroup,
|
||||
dsicludeList,
|
||||
onClick,
|
||||
onBotttom,
|
||||
}: SearchBlockProps) => {
|
||||
const { contacts } = useMessengerContext();
|
||||
|
||||
const searchList = useMemo(() => {
|
||||
return contacts
|
||||
.filter((member) => member.id.includes(query))
|
||||
.filter((member) => !styledGroup.includes(member.id));
|
||||
}, [query, styledGroup, contacts]);
|
||||
.filter((member) => !dsicludeList.includes(member.id));
|
||||
}, [query, dsicludeList, contacts]);
|
||||
|
||||
const addMember = (member: string) => {
|
||||
setStyledGroup((prevMembers: string[]) => {
|
||||
if (prevMembers.find((mem) => mem === member)) {
|
||||
return prevMembers;
|
||||
} else {
|
||||
return [...prevMembers, member];
|
||||
}
|
||||
});
|
||||
};
|
||||
if (searchList.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<SearchContacts>
|
||||
<SearchContacts
|
||||
style={{ [onBotttom ? "bottom" : "top"]: "calc(100% + 24px)" }}
|
||||
>
|
||||
<ContactsList>
|
||||
{contacts
|
||||
.filter((member) => member.id.includes(query))
|
||||
.filter((member) => !styledGroup.includes(member.id))
|
||||
.filter((member) => !dsicludeList.includes(member.id))
|
||||
.map((member) => (
|
||||
<Channel
|
||||
key={member.id}
|
||||
|
@ -53,7 +48,7 @@ export const SearchBlock = ({
|
|||
}}
|
||||
isActive={false}
|
||||
isMuted={false}
|
||||
onClick={() => addMember(member.id)}
|
||||
onClick={() => onClick(member.id)}
|
||||
/>
|
||||
))}
|
||||
</ContactsList>
|
||||
|
@ -72,5 +67,6 @@ const SearchContacts = styled.div`
|
|||
border-radius: 8px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(100% + 24px);
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
|
Loading…
Reference in New Issue