'init'
This commit is contained in:
420
app/components/DialogInput/DialogInput.tsx
Normal file
420
app/components/DialogInput/DialogInput.tsx
Normal file
@@ -0,0 +1,420 @@
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user