From fe609f04449e26d4716a9bac184ce9c61f983d6c Mon Sep 17 00:00:00 2001
From: Szymon Szlachtowicz <38212223+Szymx95@users.noreply.github.com>
Date: Mon, 15 Nov 2021 10:14:04 +0100
Subject: [PATCH] Add input mentions (#126)
---
.../src/components/Chat/ChatCreation.tsx | 27 +--
.../src/components/Chat/ChatInput.tsx | 175 +++++++++++++++---
.../src/components/Chat/ChatMessages.tsx | 12 +-
.../react-chat/src/components/SearchBlock.tsx | 34 ++--
4 files changed, 194 insertions(+), 54 deletions(-)
diff --git a/packages/react-chat/src/components/Chat/ChatCreation.tsx b/packages/react-chat/src/components/Chat/ChatCreation.tsx
index 306ab21f..4ea53ae4 100644
--- a/packages/react-chat/src/components/Chat/ChatCreation.tsx
+++ b/packages/react-chat/src/components/Chat/ChatCreation.tsx
@@ -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,15 +28,18 @@ export function ChatCreation({
const { contacts, setActiveChannel } = useMessengerContext();
- const addMember = (member: string) => {
- setStyledGroup((prevMembers: string[]) => {
- if (prevMembers.find((mem) => mem === member)) {
- return prevMembers;
- } else {
- return [...prevMembers, member];
- }
- });
- };
+ const addMember = useCallback(
+ (member: string) => {
+ setStyledGroup((prevMembers: string[]) => {
+ if (prevMembers.find((mem) => mem === member)) {
+ return prevMembers;
+ } else {
+ return [...prevMembers, member];
+ }
+ });
+ },
+ [setStyledGroup, styledGroup]
+ );
const removeMember = (member: string) => {
setStyledGroup((prev) => prev.filter((e) => e != member));
@@ -96,8 +99,8 @@ export function ChatCreation({
{query && (
)}
>
diff --git a/packages/react-chat/src/components/Chat/ChatInput.tsx b/packages/react-chat/src/components/Chat/ChatInput.tsx
index d2fb81b9..23959c0b 100644
--- a/packages/react-chat/src/components/Chat/ChatInput.tsx
+++ b/packages/react-chat/src/components/Chat/ChatInput.tsx
@@ -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);
+ const low = useLow();
+
+ const { setModal } = useModal(SizeLimitModalName);
+
+ const [query, setQuery] = useState("");
+
+ const inputRef = useRef(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) => {
- const target = e.target;
- target.style.height = "40px";
- target.style.height = `${Math.min(target.scrollHeight, 438)}px`;
- setInputHeight(target.scrollHeight);
- setContent(target.value);
- },
- []
- );
+ const resizeTextArea = useCallback((target: HTMLDivElement) => {
+ target.style.height = "40px";
+ target.style.height = `${Math.min(target.scrollHeight, 438)}px`;
+ setInputHeight(target.scrollHeight);
+ }, []);
+
+ const onInputChange = useCallback((e: React.ChangeEvent) => {
+ 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) => {
+ (e: React.KeyboardEvent) => {
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 (
@@ -126,11 +244,22 @@ export function ChatInput() {
setImageUint(undefined)} />
)}
+ {query && (
+
+ )}
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;
diff --git a/packages/react-chat/src/components/Chat/ChatMessages.tsx b/packages/react-chat/src/components/Chat/ChatMessages.tsx
index b07905bd..5f70e055 100644
--- a/packages/react-chat/src/components/Chat/ChatMessages.tsx
+++ b/packages/react-chat/src/components/Chat/ChatMessages.tsx
@@ -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 (
diff --git a/packages/react-chat/src/components/SearchBlock.tsx b/packages/react-chat/src/components/SearchBlock.tsx
index c5936a2c..288810bd 100644
--- a/packages/react-chat/src/components/SearchBlock.tsx
+++ b/packages/react-chat/src/components/SearchBlock.tsx
@@ -8,41 +8,36 @@ import { ContactsList } from "./Chat/ChatCreation";
interface SearchBlockProps {
query: string;
- styledGroup: string[];
- setStyledGroup: React.Dispatch>;
+ 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 (
-
+
{contacts
.filter((member) => member.id.includes(query))
- .filter((member) => !styledGroup.includes(member.id))
+ .filter((member) => !dsicludeList.includes(member.id))
.map((member) => (
addMember(member.id)}
+ onClick={() => onClick(member.id)}
/>
))}
@@ -72,5 +67,6 @@ const SearchContacts = styled.div`
border-radius: 8px;
position: absolute;
left: 0;
- top: calc(100% + 24px);
+ max-height: 200px;
+ overflow: auto;
`;