420 lines
17 KiB
TypeScript
420 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(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)
|
|
}]);
|
|
}
|
|
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>
|
|
)}
|
|
</>
|
|
)
|
|
} |