428 lines
17 KiB
TypeScript
428 lines
17 KiB
TypeScript
import { useDialog } from "@/app/providers/DialogProvider/useDialog";
|
||
import { useRosettaColors } from "@/app/hooks/useRosettaColors";
|
||
import { Box, Divider, Flex, Menu, Popover, Text, Transition, useComputedColorScheme } from "@mantine/core";
|
||
import { IconBarrierBlock, IconCamera, IconDoorExit, IconFile, IconMoodSmile, IconPaperclip, IconSend } from "@tabler/icons-react";
|
||
import { useEffect, useRef, useState } from "react";
|
||
import { useBlacklist } from "@/app/providers/BlacklistProvider/useBlacklist";
|
||
import { base64ImageToBlurhash, filePrapareForNetworkTransfer, generateRandomKey, imagePrepareForNetworkTransfer } from "@/app/utils/utils";
|
||
import { Attachment, AttachmentType } from "@/app/providers/ProtocolProvider/protocol/packets/packet.message";
|
||
import { DialogAttachment } from "../DialogAttachment/DialogAttachment";
|
||
import { PacketTyping } from "@/app/providers/ProtocolProvider/protocol/packets/packet.typeing";
|
||
import { usePublicKey } from "@/app/providers/AccountProvider/usePublicKey";
|
||
import { usePrivateKeyHash } from "@/app/providers/AccountProvider/usePrivateKeyHash";
|
||
import { useSender } from "@/app/providers/ProtocolProvider/useSender";
|
||
import { useReplyMessages } from "@/app/providers/DialogProvider/useReplyMessages";
|
||
import { useFileDialog, useHotkeys } from "@mantine/hooks";
|
||
import { ATTACHMENTS_NOT_ALLOWED_TO_REPLY, MAX_ATTACHMENTS_IN_MESSAGE, MAX_UPLOAD_FILESIZE_MB } from "@/app/constants";
|
||
import { useSystemAccounts } from "@/app/providers/SystemAccountsProvider/useSystemAccounts";
|
||
import { Dropzone } from '@mantine/dropzone';
|
||
import { RichTextInput } from "../RichTextInput/RichTextInput";
|
||
import EmojiPicker, { EmojiClickData, Theme } from 'emoji-picker-react';
|
||
import { useAvatars } from "@/app/providers/AvatarProvider/useAvatars";
|
||
import { useGroups } from "@/app/providers/DialogProvider/useGroups";
|
||
import { useGroupMembers } from "@/app/providers/InformationProvider/useGroupMembers";
|
||
import { AnimatedButton } from "../AnimatedButton/AnimatedButton";
|
||
import { useUserCacheFunc } from "@/app/providers/InformationProvider/useUserCacheFunc";
|
||
import { MentionList, Mention } from "../MentionList/MentionList";
|
||
import { useDrafts } from "@/app/providers/DialogProvider/useDrafts";
|
||
|
||
|
||
export function DialogInput() {
|
||
const colors = useRosettaColors();
|
||
const {sendMessage, dialog} = useDialog();
|
||
const {members, loading} = useGroupMembers(dialog);
|
||
const {hasGroup, leaveGroup} = useGroups();
|
||
const [message, setMessage] = useState<string>("");
|
||
const [blocked] = useBlacklist(dialog);
|
||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||
const publicKey = usePublicKey();
|
||
const isAdmin = hasGroup(dialog) && members[0] == publicKey;
|
||
const isBannedGroup = hasGroup(dialog) && members.length == 0 && !loading;
|
||
const privateKey = usePrivateKeyHash();
|
||
const sendPacket = useSender();
|
||
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||
const {replyMessages, deselectAllMessages} = useReplyMessages();
|
||
const computedTheme = useComputedColorScheme();
|
||
const systemAccounts = useSystemAccounts();
|
||
const [mentionList, setMentionList] = useState<Mention[]>([]);
|
||
const mentionHandling = useRef<string>("");
|
||
const {getDraft, saveDraft} = useDrafts(dialog);
|
||
|
||
|
||
const avatars = useAvatars(
|
||
hasGroup(dialog) ? dialog : publicKey
|
||
, false);
|
||
|
||
const editableDivRef = useRef<any>(null);
|
||
const getUserFromCache = useUserCacheFunc();
|
||
const regexp = new RegExp(/@([\w\d_]{3,})$/);
|
||
|
||
useHotkeys([
|
||
['Esc', () => {
|
||
setAttachments([]);
|
||
}]
|
||
], [], true);
|
||
|
||
const fileDialog = useFileDialog({
|
||
multiple: false,
|
||
//naccept: '*',
|
||
onChange: async (files) => {
|
||
if(!files){
|
||
return;
|
||
}
|
||
if(files.length == 0){
|
||
return;
|
||
}
|
||
if(attachments.length >= MAX_ATTACHMENTS_IN_MESSAGE){
|
||
return;
|
||
}
|
||
if(attachments.find(a => ATTACHMENTS_NOT_ALLOWED_TO_REPLY.includes(a.type))){
|
||
return;
|
||
}
|
||
const file = files[0];
|
||
if(((file.size / 1024) / 1024) > MAX_UPLOAD_FILESIZE_MB){
|
||
return;
|
||
}
|
||
const fileContent = await filePrapareForNetworkTransfer(file);
|
||
setAttachments([...attachments, {
|
||
blob: fileContent,
|
||
id: generateRandomKey(8),
|
||
type: AttachmentType.FILE,
|
||
preview: files[0].size + "::" + files[0].name
|
||
}]);
|
||
}
|
||
});
|
||
|
||
useEffect(() => {
|
||
const draftMessage = getDraft();
|
||
console.info("GET DRAFT", draftMessage);
|
||
if(draftMessage == "" || !editableDivRef){
|
||
return;
|
||
}
|
||
setMessage(draftMessage);
|
||
editableDivRef.current.insertHTMLInCurrentCarretPosition(draftMessage);
|
||
}, [dialog, editableDivRef]);
|
||
|
||
useEffect(() => {
|
||
if(replyMessages.inDialogInput && replyMessages.inDialogInput == dialog){
|
||
setAttachments([{
|
||
type: AttachmentType.MESSAGES,
|
||
id: generateRandomKey(8),
|
||
blob: JSON.stringify([...replyMessages.messages]),
|
||
preview: ""
|
||
}]);
|
||
editableDivRef.current.focus();
|
||
}
|
||
}, [dialog, replyMessages]);
|
||
|
||
useEffect(() => {
|
||
saveDraft(message);
|
||
if(regexp.test(message) && hasGroup(dialog)){
|
||
const username = regexp.exec(message);
|
||
if(!username){
|
||
return;
|
||
}
|
||
if(username[1].length > 2){
|
||
handleMention(username[1]);
|
||
}
|
||
}else{
|
||
setMentionList([]);
|
||
}
|
||
}, [message]);
|
||
|
||
|
||
if(systemAccounts.find((acc) => acc.publicKey == dialog)){
|
||
return <></>;
|
||
}
|
||
|
||
const handleMention = async (username: string) => {
|
||
const regexpToFindAllMentionedUsernames = new RegExp(`@([\\w\\d_]{2,})`, 'g');
|
||
const mentionedUsernamesInMessage = message.match(regexpToFindAllMentionedUsernames);
|
||
|
||
|
||
const mentionsList : Mention[] = [];
|
||
|
||
if(!isAdmin && username.startsWith('adm') && (mentionedUsernamesInMessage && !mentionedUsernamesInMessage.includes('@admin'))){
|
||
mentionsList.push({
|
||
username: 'admin',
|
||
title: 'Administrator',
|
||
publicKey: ''
|
||
});
|
||
}
|
||
|
||
for(let i = 0; i < members.length; i++){
|
||
const userInfo = await getUserFromCache(members[i]);
|
||
if(!userInfo){
|
||
continue;
|
||
}
|
||
if(!userInfo.username.startsWith(username)){
|
||
continue;
|
||
}
|
||
if(mentionedUsernamesInMessage && mentionedUsernamesInMessage.includes(`@${userInfo.username}`)){
|
||
continue;
|
||
}
|
||
mentionsList.push({
|
||
username: userInfo.username,
|
||
title: userInfo.title,
|
||
publicKey: userInfo.publicKey
|
||
});
|
||
}
|
||
setMentionList(mentionsList);
|
||
mentionHandling.current = username;
|
||
}
|
||
|
||
const send = () => {
|
||
if(blocked || (message.trim() == "" && attachments.length <= 0)) {
|
||
return;
|
||
}
|
||
sendMessage(message, attachments);
|
||
editableDivRef.current.clear();
|
||
setAttachments([]);
|
||
deselectAllMessages();
|
||
}
|
||
|
||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||
if (event.key === 'Enter' && !event.shiftKey) {
|
||
event.preventDefault();
|
||
if(mentionList.length <= 0){
|
||
send();
|
||
}
|
||
}
|
||
if (!event.shiftKey && event.key.length === 1 && !blocked) {
|
||
if (!typingTimeoutRef.current) {
|
||
sendTypeingPacket();
|
||
typingTimeoutRef.current = setTimeout(() => {
|
||
typingTimeoutRef.current = null;
|
||
}, 3000);
|
||
}
|
||
}
|
||
};
|
||
|
||
const onRemoveAttachment = (attachment: Attachment) => {
|
||
setAttachments(attachments.filter(a => a.id != attachment.id));
|
||
editableDivRef.current.focus();
|
||
}
|
||
|
||
const onClickPaperclip = () => {
|
||
fileDialog.open();
|
||
}
|
||
|
||
const onClickCamera = async () => {
|
||
if(avatars.length == 0){
|
||
return;
|
||
}
|
||
setAttachments([{
|
||
blob: avatars[0].avatar,
|
||
id: generateRandomKey(8),
|
||
type: AttachmentType.AVATAR,
|
||
preview: await base64ImageToBlurhash(avatars[0].avatar)
|
||
}]);
|
||
editableDivRef.current.focus();
|
||
}
|
||
|
||
const sendTypeingPacket = () => {
|
||
let packet = new PacketTyping();
|
||
packet.setToPublicKey(dialog);
|
||
packet.setFromPublicKey(publicKey);
|
||
packet.setPrivateKey(privateKey);
|
||
sendPacket(packet);
|
||
}
|
||
|
||
const onPaste = async (event: React.ClipboardEvent) => {
|
||
if(systemAccounts.find((acc) => acc.publicKey == dialog)){
|
||
/**
|
||
* У системных аккаунтов нельзя вызывать вложения
|
||
*/
|
||
return;
|
||
}
|
||
if(attachments.length >= MAX_ATTACHMENTS_IN_MESSAGE){
|
||
return;
|
||
}
|
||
if(attachments.find(a => ATTACHMENTS_NOT_ALLOWED_TO_REPLY.includes(a.type))){
|
||
return;
|
||
}
|
||
const items = event.clipboardData.items;
|
||
for (let i = 0; i < items.length; i++) {
|
||
const item = items[i];
|
||
if (item.type.startsWith("image/")) {
|
||
const file = item.getAsFile();
|
||
if (file) {
|
||
const base64Image = await imagePrepareForNetworkTransfer(file);
|
||
setAttachments([...attachments, {
|
||
blob: base64Image,
|
||
id: generateRandomKey(8),
|
||
type: AttachmentType.IMAGE,
|
||
preview: await base64ImageToBlurhash(base64Image)
|
||
}]);
|
||
}
|
||
if(editableDivRef.current){
|
||
editableDivRef.current.focus();
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
const onDragAndDrop = async (files : File[]) => {
|
||
if(!files){
|
||
return;
|
||
}
|
||
if(files.length == 0){
|
||
return;
|
||
}
|
||
if(attachments.length >= MAX_ATTACHMENTS_IN_MESSAGE){
|
||
return;
|
||
}
|
||
if(files.length > 1){
|
||
return;
|
||
}
|
||
const file = files[0];
|
||
if(((file.size / 1024) / 1024) > MAX_UPLOAD_FILESIZE_MB){
|
||
return;
|
||
}
|
||
let fileContent = await filePrapareForNetworkTransfer(file);
|
||
setAttachments([...attachments, {
|
||
blob: fileContent,
|
||
id: generateRandomKey(8),
|
||
type: AttachmentType.FILE,
|
||
preview: files[0].size + "::" + files[0].name
|
||
}]);
|
||
}
|
||
|
||
const onEmojiClick = (emojiData : EmojiClickData) => {
|
||
editableDivRef.current.insertHTML(
|
||
`<img alt=":emoji_${emojiData.unified}:" height="20" width="20" data=":emoji_${emojiData.unified}:" src="${emojiData.imageUrl}" />`
|
||
);
|
||
}
|
||
|
||
const onSelectMention = (mention : Mention) => {
|
||
editableDivRef.current.insertHTMLInCurrentCarretPosition(mention.username.substring(mentionHandling.current.length));
|
||
mentionHandling.current = "";
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<Transition
|
||
transition={'fade-down'}
|
||
mounted={mentionList.length > 0}>
|
||
{(styles) => (
|
||
<MentionList onSelectMention={onSelectMention} style={{...styles}} mentions={mentionList}></MentionList>
|
||
)}
|
||
</Transition>
|
||
{attachments.length > 0 &&
|
||
<Flex direction={'row'} wrap={'wrap'} p={'sm'} gap={'sm'}>
|
||
{attachments.map((m) => (
|
||
<DialogAttachment onRemove={onRemoveAttachment} attach={m} key={m.id}></DialogAttachment>
|
||
))}
|
||
</Flex>}
|
||
<Dropzone.FullScreen active={true} onDrop={onDragAndDrop}>
|
||
<Flex justify={'center'} align={'center'} mih={document.body.clientHeight - 60} style={{ pointerEvents: 'none' }}>
|
||
<Text ta={'center'} size={'md'}>Drop files here to attach without compression</Text>
|
||
</Flex>
|
||
</Dropzone.FullScreen>
|
||
{!isBannedGroup && (
|
||
<Box bg={colors.boxColor}>
|
||
<Divider color={colors.borderColor}></Divider>
|
||
{!blocked &&
|
||
<Flex h={'100%'} p={'xs'} direction={'row'} bg={colors.boxColor}>
|
||
<Flex w={25} mt={10} justify={'center'}>
|
||
<Menu width={150} withArrow>
|
||
<Menu.Target>
|
||
<IconPaperclip stroke={1.5} style={{
|
||
cursor: 'pointer'
|
||
}} size={25} color={colors.chevrons.active}></IconPaperclip>
|
||
</Menu.Target>
|
||
<Menu.Dropdown style={{
|
||
userSelect: 'none'
|
||
}}>
|
||
<Menu.Label fz={'xs'} fw={500} c={'dimmed'}>Attach</Menu.Label>
|
||
<Menu.Item fz={'xs'} fw={500} leftSection={
|
||
<IconFile size={14}></IconFile>
|
||
} onClick={onClickPaperclip}>File</Menu.Item>
|
||
{((avatars.length > 0 && !hasGroup(dialog))
|
||
|| (avatars.length > 0 && hasGroup(dialog) && isAdmin))
|
||
&& <Menu.Item fz={'xs'} fw={500} leftSection={
|
||
<IconCamera size={14}></IconCamera>
|
||
} onClick={onClickCamera}>Avatar {hasGroup(dialog) && 'group'}</Menu.Item>}
|
||
</Menu.Dropdown>
|
||
</Menu>
|
||
</Flex>
|
||
<Flex
|
||
w={'calc(100% - (25px + 50px + var(--mantine-spacing-xs)))'}
|
||
maw={'calc(100% - (25px + 50px + var(--mantine-spacing-xs)))'}
|
||
align={'center'}
|
||
>
|
||
<RichTextInput
|
||
ref={editableDivRef}
|
||
style={{
|
||
border: 0,
|
||
minHeight: 45,
|
||
fontSize: 14,
|
||
background: 'transparent',
|
||
width: '100%',
|
||
paddingLeft: 10,
|
||
paddingRight: 10,
|
||
outline: 'none',
|
||
paddingTop: 10,
|
||
paddingBottom: 8
|
||
}}
|
||
placeholder="Type message..."
|
||
autoFocus
|
||
//ref={textareaRef}
|
||
//onPaste={onPaste}
|
||
//maxLength={2500}
|
||
//w={'100%'}
|
||
//h={'100%'}
|
||
onKeyDown={handleKeyDown}
|
||
onChange={setMessage}
|
||
onPaste={onPaste}
|
||
|
||
//dangerouslySetInnerHTML={{__html: message}}
|
||
></RichTextInput>
|
||
</Flex>
|
||
<Flex mt={10} w={'calc(50px + var(--mantine-spacing-xs))'} gap={'xs'}>
|
||
<Popover withArrow>
|
||
<Popover.Target>
|
||
<IconMoodSmile color={colors.chevrons.active} stroke={1.5} size={25} style={{
|
||
cursor: 'pointer'
|
||
}}></IconMoodSmile>
|
||
</Popover.Target>
|
||
<Popover.Dropdown p={0}>
|
||
<EmojiPicker
|
||
onEmojiClick={onEmojiClick}
|
||
searchDisabled
|
||
skinTonesDisabled
|
||
theme={computedTheme == 'dark' ? Theme.DARK : Theme.LIGHT}
|
||
/>
|
||
</Popover.Dropdown>
|
||
</Popover>
|
||
<IconSend stroke={1.5} color={message.trim() == "" && attachments.length <= 0 ? colors.chevrons.active : colors.brandColor} onClick={send} style={{
|
||
cursor: 'pointer'
|
||
}} size={25}></IconSend>
|
||
</Flex>
|
||
</Flex>}
|
||
{blocked && <Box mih={62} bg={colors.boxColor}>
|
||
<Flex align={'center'} justify={'center'} h={62}>
|
||
<Flex gap={'sm'} align={'center'} justify={'center'}>
|
||
<IconBarrierBlock size={24} color={colors.error} stroke={1.2}></IconBarrierBlock>
|
||
<Text ta={'center'} c={'dimmed'} size={'xs'}>You need unblock user for send messages.</Text>
|
||
</Flex>
|
||
</Flex>
|
||
</Box>}
|
||
</Box>
|
||
)}
|
||
{isBannedGroup && (
|
||
<Flex align={'center'} bg={colors.boxColor} justify={'center'}>
|
||
<AnimatedButton onClick={() => {
|
||
leaveGroup(dialog)
|
||
}} animated={[
|
||
'#ff5656',
|
||
'#e03131'
|
||
]} color="red" animationDurationMs={1000} w={'80%'} size={'sm'} radius={'xl'} leftSection={
|
||
<IconDoorExit size={15} />
|
||
} mb={'md'}>Leave</AnimatedButton>
|
||
</Flex>
|
||
)}
|
||
</>
|
||
)
|
||
} |