This commit is contained in:
rosetta
2026-01-30 05:01:05 +02:00
commit 83f38dc63f
327 changed files with 18725 additions and 0 deletions

View 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>
)}
</>
)
}