allow pasting of img and publish limit

This commit is contained in:
PhilReact 2025-06-05 16:12:48 +03:00
parent 4ad0fb7db3
commit a22c48667b
9 changed files with 471 additions and 115 deletions

View File

@ -48,7 +48,7 @@ export const useModal = () => {
const onCancel = () => { const onCancel = () => {
const { reject } = promiseConfig.current; const { reject } = promiseConfig.current;
hide(); hide();
reject(); reject('Declined');
setMessage({ setMessage({
publishFee: "", publishFee: "",
message: "" message: ""

View File

@ -15,12 +15,12 @@ import { CustomizedSnackbars } from '../Snackbar/Snackbar'
import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from '../../constants/codes' import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from '../../constants/codes'
import { useMessageQueue } from '../../MessageQueueContext' import { useMessageQueue } from '../../MessageQueueContext'
import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from '../../utils/events' import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from '../../utils/events'
import { Box, ButtonBase, Divider, Typography } from '@mui/material' import { Box, ButtonBase, Divider, IconButton, Tooltip, Typography } from '@mui/material'
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import { ReplyPreview } from './MessageItem' import { ReplyPreview } from './MessageItem'
import { ExitIcon } from '../../assets/Icons/ExitIcon' import { ExitIcon } from '../../assets/Icons/ExitIcon'
import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from '../../constants/resourceTypes' import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from '../../constants/resourceTypes'
import { isExtMsg } from '../../background' import { getFee, isExtMsg } from '../../background'
import MentionList from './MentionList' import MentionList from './MentionList'
import { ChatOptions } from './ChatOptions' import { ChatOptions } from './ChatOptions'
import { isFocusedParentGroupAtom } from '../../atoms/global' import { isFocusedParentGroupAtom } from '../../atoms/global'
@ -28,6 +28,10 @@ import { useRecoilState } from 'recoil'
import AppViewerContainer from '../Apps/AppViewerContainer' import AppViewerContainer from '../Apps/AppViewerContainer'
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import { throttle } from 'lodash' import { throttle } from 'lodash'
import ImageIcon from '@mui/icons-material/Image';
import { messageHasImage } from '../../utils/chat'
const uidImages = new ShortUniqueId({ length: 12 });
const uid = new ShortUniqueId({ length: 5 }); const uid = new ShortUniqueId({ length: 5 });
@ -55,8 +59,9 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
const editorRef = useRef(null); const editorRef = useRef(null);
const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue(); const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue();
const handleUpdateRef = useRef(null); const handleUpdateRef = useRef(null);
const {isUserBlocked} = useContext(MyContext) const {isUserBlocked, show} = useContext(MyContext)
const [chatImagesToSave, setChatImagesToSave] = useState([]);
const [isDeleteImage, setIsDeleteImage] = useState(false);
const lastReadTimestamp = useRef(null) const lastReadTimestamp = useRef(null)
@ -624,6 +629,8 @@ if(isFocusedParent === false){
setReplyMessage(null) setReplyMessage(null)
setOnEditMessage(null) setOnEditMessage(null)
clearEditorContent() clearEditorContent()
setIsDeleteImage(false);
setChatImagesToSave([]);
} }
}, [isFocusedParent]) }, [isFocusedParent])
const clearEditorContent = () => { const clearEditorContent = () => {
@ -646,50 +653,154 @@ const clearEditorContent = () => {
const sendMessage = async () => { const sendMessage = async () => {
try { try {
if(messageSize > 4000) return if (messageSize > 4000) return; // TODO magic number
if(isPrivate === null) throw new Error('Unable to determine if group is private') if (isPrivate === null)
if(isSending) return throw new Error(
if(+balance < 4) throw new Error('You need at least 4 QORT to send a message') "Onable to determine if group is private"
pauseAllQueues() );
if (isSending) return;
if (+balance < 4)
// TODO magic number
throw new Error(
"You need at least 4 QORT to send a message"
);
pauseAllQueues();
if (editorRef.current) { if (editorRef.current) {
const htmlContent = editorRef.current.getHTML(); let htmlContent = editorRef.current.getHTML();
const deleteImage =
onEditMessage && isDeleteImage && messageHasImage(onEditMessage);
if(!htmlContent?.trim() || htmlContent?.trim() === '<p></p>') return const hasImage =
chatImagesToSave?.length > 0 || onEditMessage?.images?.length > 0;
if (
(!htmlContent?.trim() || htmlContent?.trim() === '<p></p>') &&
!hasImage &&
!deleteImage
)
return;
if (htmlContent?.trim() === '<p></p>') {
htmlContent = null;
}
setIsSending(true);
const message =
isPrivate === false
? !htmlContent
? '<p></p>'
: editorRef.current.getJSON()
: htmlContent;
const secretKeyObject = await getSecretKey(false, true);
let repliedTo = replyMessage?.signature;
setIsSending(true)
const message = isPrivate === false ? editorRef.current.getJSON() : htmlContent
const secretKeyObject = await getSecretKey(false, true)
let repliedTo = replyMessage?.signature
if (replyMessage?.chatReference) { if (replyMessage?.chatReference) {
repliedTo = replyMessage?.chatReference repliedTo = replyMessage?.chatReference;
} }
let chatReference = onEditMessage?.signature
const publicData = isPrivate ? {} : { const chatReference = onEditMessage?.signature;
const publicData = isPrivate
? {}
: {
isEdited: chatReference ? true : false, isEdited: chatReference ? true : false,
};
interface ImageToPublish {
service: string;
identifier: string;
name: string;
base64: string;
} }
const imagesToPublish: ImageToPublish[] = [];
if (deleteImage) {
const fee = await getFee('ARBITRARY');
await show({
publishFee: fee.fee + ' QORT',
message: "Would you like to delete your previous chat image?",
});
// TODO magic string
await window.sendMessage('publishOnQDN', {
data: 'RA==',
identifier: onEditMessage?.images[0]?.identifier,
service: onEditMessage?.images[0]?.service,
uploadType: 'base64',
});
}
if (chatImagesToSave?.length > 0) {
const imageToSave = chatImagesToSave[0];
const base64ToSave = isPrivate
? await encryptChatMessage(imageToSave, secretKeyObject)
: imageToSave;
// 1 represents public group, 0 is private
const identifier = `grp-q-manager_${isPrivate ? 0 : 1}_group_${selectedGroup}_${uidImages.rnd()}`;
imagesToPublish.push({
service: 'IMAGE',
identifier,
name: myName,
base64: base64ToSave,
});
const res = await window.sendMessage(
'PUBLISH_MULTIPLE_QDN_RESOURCES',
{
resources: imagesToPublish,
},
240000,
true
);
if (res !== true)
throw new Error(
"Unable to publish image"
);
}
const images =
imagesToPublish?.length > 0
? imagesToPublish.map((item) => {
return {
name: item.name,
identifier: item.identifier,
service: item.service,
timestamp: Date.now(),
};
})
: chatReference
? isDeleteImage
? []
: onEditMessage?.images || []
: [];
const otherData = { const otherData = {
repliedTo, repliedTo,
...(onEditMessage?.decryptedData || {}), ...(onEditMessage?.decryptedData || {}),
type: chatReference ? 'edit' : '', type: chatReference ? 'edit' : '',
specialId: uid.rnd(), specialId: uid.rnd(),
...publicData images: images,
} ...publicData,
};
const objectMessage = { const objectMessage = {
...(otherData || {}), ...(otherData || {}),
[isPrivate ? 'message' : 'messageText']: message, [isPrivate ? 'message' : 'messageText']: message,
version: 3 version: 3,
} };
const message64: any = await objectToBase64(objectMessage) const message64: any = await objectToBase64(objectMessage);
const encryptSingle = isPrivate === false ? JSON.stringify(objectMessage) : await encryptChatMessage(message64, secretKeyObject) const encryptSingle =
// const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle}) isPrivate === false
? JSON.stringify(objectMessage)
: await encryptChatMessage(message64, secretKeyObject);
const sendMessageFunc = async () => { const sendMessageFunc = async () => {
return await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle, chatReference}) return await sendChatGroup({
groupId: selectedGroup,
messageText: encryptSingle,
chatReference,
});
}; };
// Add the function to the queue // Add the function to the queue
@ -699,33 +810,34 @@ const sendMessage = async ()=> {
timestamp: Date.now(), timestamp: Date.now(),
senderName: myName, senderName: myName,
sender: myAddress, sender: myAddress,
...(otherData || {}) ...(otherData || {}),
}, },
chatReference chatReference,
} };
addToQueue(sendMessageFunc, messageObj, 'chat', addToQueue(sendMessageFunc, messageObj, 'chat', selectedGroup);
selectedGroup );
setTimeout(() => { setTimeout(() => {
executeEvent("sent-new-message-group", {}) executeEvent('sent-new-message-group', {});
}, 150); }, 150);
clearEditorContent() clearEditorContent();
setReplyMessage(null) setReplyMessage(null);
setOnEditMessage(null) setOnEditMessage(null);
setIsDeleteImage(false);
setChatImagesToSave([]);
} }
// send chat message // send chat message
} catch (error) { } catch (error) {
const errorMsg = error?.message || error const errorMsg = error?.message || error;
setInfoSnack({ setInfoSnack({
type: "error", type: 'error',
message: errorMsg, message: errorMsg,
}); });
setOpenSnack(true); setOpenSnack(true);
console.error(error) console.error(error);
} finally { } finally {
setIsSending(false) setIsSending(false);
resumeAllQueues() resumeAllQueues();
}
} }
};
useEffect(() => { useEffect(() => {
if (hide) { if (hide) {
@ -742,7 +854,8 @@ const sendMessage = async ()=> {
setReplyMessage(message) setReplyMessage(message)
setOnEditMessage(null) setOnEditMessage(null)
setIsFocusedParent(true); setIsFocusedParent(true);
setIsDeleteImage(false);
setChatImagesToSave([]);
setTimeout(() => { setTimeout(() => {
editorRef?.current?.chain().focus() editorRef?.current?.chain().focus()
@ -755,7 +868,7 @@ const sendMessage = async ()=> {
setReplyMessage(null) setReplyMessage(null)
setIsFocusedParent(true); setIsFocusedParent(true);
setTimeout(() => { setTimeout(() => {
editorRef.current.chain().focus().setContent(message?.messageText || message?.text).run(); editorRef?.current?.chain().focus().setContent(message?.messageText || message?.text || '<p></p>').run();
}, 250); }, 250);
}, []) }, [])
@ -825,6 +938,24 @@ const sendMessage = async ()=> {
} }
}, [isPrivate]) }, [isPrivate])
const insertImage = useCallback(
(img) => {
if (
chatImagesToSave?.length > 0 ||
(messageHasImage(onEditMessage) && !isDeleteImage)
) {
setInfoSnack({
type: 'error',
message: 'This message already has an image',
});
setOpenSnack(true);
return;
}
setChatImagesToSave((prev) => [...prev, img]);
},
[chatImagesToSave, onEditMessage?.images, isDeleteImage]
);
return ( return (
<div style={{ <div style={{
height: isMobile ? '100%' : '100%', height: isMobile ? '100%' : '100%',
@ -864,6 +995,117 @@ const sendMessage = async ()=> {
overflow: !isMobile && "auto", overflow: !isMobile && "auto",
flexShrink: 0 flexShrink: 0
}}> }}>
<Box
sx={{
alignItems: 'flex-start',
display: 'flex',
width: '100%',
gap: '10px',
flexWrap: 'wrap',
}}
>
{!isDeleteImage &&
onEditMessage &&
messageHasImage(onEditMessage) &&
onEditMessage?.images?.map((_, index) => (
<div
key={index}
style={{
position: 'relative',
height: '50px',
width: '50px',
}}
>
<ImageIcon
sx={{
height: '100%',
width: '100%',
borderRadius: '3px',
color:'white'
}}
/>
<Tooltip title="Delete image">
<IconButton
onClick={() => setIsDeleteImage(true)}
size="small"
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: (theme) =>
theme.palette.background.paper,
color: (theme) => theme.palette.text.primary,
borderRadius: '50%',
opacity: 0,
transition: 'opacity 0.2s',
boxShadow: (theme) => theme.shadows[2],
'&:hover': {
backgroundColor: (theme) =>
theme.palette.background.default,
opacity: 1,
},
pointerEvents: 'auto',
}}
>
<CloseIcon fontSize="small" />
</IconButton>
</Tooltip>
</div>
))}
{chatImagesToSave.map((imgBase64, index) => (
<div
key={index}
style={{
position: 'relative',
height: '50px',
width: '50px',
}}
>
<img
src={`data:image/webp;base64,${imgBase64}`}
style={{
height: '100%',
width: '100%',
objectFit: 'contain',
borderRadius: '3px',
}}
/>
<Tooltip title="Remove image">
<IconButton
onClick={() =>
setChatImagesToSave((prev) =>
prev.filter((_, i) => i !== index)
)
}
size="small"
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: (theme) =>
theme.palette.background.paper,
color: (theme) => theme.palette.text.primary,
borderRadius: '50%',
opacity: 0,
transition: 'opacity 0.2s',
boxShadow: (theme) => theme.shadows[2],
'&:hover': {
backgroundColor: (theme) =>
theme.palette.background.default,
opacity: 1,
},
pointerEvents: 'auto',
}}
>
<CloseIcon fontSize="small" />
</IconButton>
</Tooltip>
</div>
))}
</Box>
{replyMessage && ( {replyMessage && (
<Box sx={{ <Box sx={{
display: 'flex', display: 'flex',
@ -895,7 +1137,7 @@ const sendMessage = async ()=> {
}}> }}>
<Tiptap isReply={onEditMessage || replyMessage} enableMentions setEditorRef={setEditorRef} onEnter={sendMessage} isChat disableEnter={isMobile ? true : false} isFocusedParent={isFocusedParent} setIsFocusedParent={setIsFocusedParent} membersWithNames={members} /> <Tiptap isReply={onEditMessage || replyMessage} enableMentions setEditorRef={setEditorRef} onEnter={sendMessage} isChat disableEnter={isMobile ? true : false} isFocusedParent={isFocusedParent} setIsFocusedParent={setIsFocusedParent} membersWithNames={members} insertImage={insertImage} />

View File

@ -261,20 +261,17 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
if (chatReferences?.[message.signature]) { if (chatReferences?.[message.signature]) {
reactions = chatReferences[message.signature]?.reactions || null; reactions = chatReferences[message.signature]?.reactions || null;
if (chatReferences[message.signature]?.edit?.message && message?.text) { if (chatReferences[message.signature]?.edit) {
message.text = chatReferences[message.signature]?.edit?.message; message.text =
message.isEdit = true chatReferences[message.signature]?.edit?.message;
message.editTimestamp = chatReferences[message.signature]?.edit?.timestamp message.messageText =
} chatReferences[message.signature]?.edit?.messageText;
if (chatReferences[message.signature]?.edit?.messageText && message?.messageText) {
message.messageText = chatReferences[message.signature]?.edit?.messageText;
message.isEdit = true
message.editTimestamp = chatReferences[message.signature]?.edit?.timestamp
}
if (chatReferences[message.signature]?.edit?.images) {
message.images = message.images =
chatReferences[message.signature]?.edit?.images; chatReferences[message.signature]?.edit?.images;
message.isEdit = true; message.isEdit = true;
message.editTimestamp =
chatReferences[message.signature]?.edit?.timestamp;
} }
} }

View File

@ -35,6 +35,7 @@ import level9Img from "../../assets/badges/level-9.png"
import level10Img from "../../assets/badges/level-10.png" import level10Img from "../../assets/badges/level-10.png"
import { Embed } from "../Embeds/Embed"; import { Embed } from "../Embeds/Embed";
import { buildImageEmbedLink, isHtmlString, messageHasImage } from "../../utils/chat"; import { buildImageEmbedLink, isHtmlString, messageHasImage } from "../../utils/chat";
import CommentsDisabledIcon from '@mui/icons-material/CommentsDisabled';
const getBadgeImg = (level)=> { const getBadgeImg = (level)=> {
switch(level?.toString()){ switch(level?.toString()){
@ -142,6 +143,13 @@ const onSeenFunc = useCallback(()=> {
onSeen(message.id); onSeen(message.id);
}, [message?.id]) }, [message?.id])
const hasNoMessage =
(!message.decryptedData?.data?.message ||
message.decryptedData?.data?.message === '<p></p>') &&
(message?.images || [])?.length === 0 &&
(!message?.messageText || message?.messageText === '<p></p>') &&
(!message?.text || message?.text === '<p></p>');
return ( return (
<MessageWragger lastMessage={lastSignature === message?.signature} isLast={isLast} onSeen={onSeenFunc}> <MessageWragger lastMessage={lastSignature === message?.signature} isLast={isLast} onSeen={onSeenFunc}>
{message?.divide && ( {message?.divide && (
@ -335,7 +343,7 @@ const onSeenFunc = useCallback(()=> {
</Box> </Box>
</> </>
)} )}
{message?.messageText && ( {htmlText && !hasNoMessage && (
<MessageDisplay <MessageDisplay
htmlContent={htmlText} htmlContent={htmlText}
setMobileViewModeKeepOpen={setMobileViewModeKeepOpen} setMobileViewModeKeepOpen={setMobileViewModeKeepOpen}
@ -343,9 +351,27 @@ const onSeenFunc = useCallback(()=> {
)} )}
{message?.decryptedData?.type === "notification" ? ( {message?.decryptedData?.type === "notification" ? (
<MessageDisplay htmlContent={message.decryptedData?.data?.message} /> <MessageDisplay htmlContent={message.decryptedData?.data?.message} />
) : ( ) : hasNoMessage ? null : (
<MessageDisplay setMobileViewModeKeepOpen={setMobileViewModeKeepOpen} htmlContent={message.text} /> <MessageDisplay setMobileViewModeKeepOpen={setMobileViewModeKeepOpen} htmlContent={message.text} />
)} )}
{hasNoMessage && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: '10px',
}}
>
<CommentsDisabledIcon sx={{
color: 'white'
}} />
<Typography sx={{
color: 'white'
}}>
No Message
</Typography>
</Box>
)}
{message?.images && messageHasImage(message) && ( {message?.images && messageHasImage(message) && (
<Embed embedLink={buildImageEmbedLink(message.images[0])} /> <Embed embedLink={buildImageEmbedLink(message.images[0])} />
)} )}
@ -523,6 +549,7 @@ const onSeenFunc = useCallback(()=> {
export const ReplyPreview = ({message, isEdit})=> { export const ReplyPreview = ({message, isEdit})=> {
const replyMessageText = useMemo(() => { const replyMessageText = useMemo(() => {
if (!message?.messageText) return null;
const isHtml = isHtmlString(message?.messageText); const isHtml = isHtmlString(message?.messageText);
if (isHtml) return message?.messageText; if (isHtml) return message?.messageText;
return generateHTML(message?.messageText, [ return generateHTML(message?.messageText, [
@ -568,7 +595,7 @@ export const ReplyPreview = ({message, isEdit})=> {
}}>Replied to {message?.senderName || message?.senderAddress}</Typography> }}>Replied to {message?.senderName || message?.senderAddress}</Typography>
)} )}
{message?.messageText && ( {replyMessageText && (
<MessageDisplay <MessageDisplay
htmlContent={replyMessageText} htmlContent={replyMessageText}
/> />

View File

@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useRef, useState } from "react"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { EditorProvider, useCurrentEditor, useEditor } from "@tiptap/react"; import { EditorProvider, useCurrentEditor, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit"; import StarterKit from "@tiptap/starter-kit";
import { Color } from "@tiptap/extension-color"; import { Color } from "@tiptap/extension-color";
@ -34,6 +34,7 @@ import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText'; import ListItemText from '@mui/material/ListItemText';
import { ReactRenderer } from '@tiptap/react' import { ReactRenderer } from '@tiptap/react'
import MentionList from './MentionList.jsx' import MentionList from './MentionList.jsx'
import { fileToBase64 } from "../../utils/fileReading/index.js";
function textMatcher(doc, from) { function textMatcher(doc, from) {
const textBeforeCursor = doc.textBetween(0, from, ' ', ' '); const textBeforeCursor = doc.textBetween(0, from, ' ', ' ');
@ -110,13 +111,13 @@ const MenuBar = ({ setEditorRef, isChat }) => {
}; };
useEffect(() => { useEffect(() => {
if (editor) { if (editor && !isChat) {
editor.view.dom.addEventListener("paste", handlePaste); editor.view.dom.addEventListener("paste", handlePaste);
return () => { return () => {
editor.view.dom.removeEventListener("paste", handlePaste); editor.view.dom.removeEventListener("paste", handlePaste);
}; };
} }
}, [editor]); }, [editor, isChat]);
return ( return (
<div className="control-group"> <div className="control-group">
@ -299,7 +300,8 @@ export default ({
customEditorHeight, customEditorHeight,
membersWithNames, membersWithNames,
enableMentions, enableMentions,
isReply isReply,
insertImage,
}) => { }) => {
const extensionsFiltered = isChat const extensionsFiltered = isChat
@ -329,7 +331,35 @@ export default ({
}, [membersWithNames]) }, [membersWithNames])
const handleImageUpload = useCallback(async (file) => {
try {
if (!file.type.includes('image')) return;
let compressedFile = file;
if (file.type !== 'image/gif') {
await new Promise<void>((resolve) => {
new Compressor(file, {
quality: 0.6,
maxWidth: 1200,
mimeType: 'image/webp',
success(result) {
compressedFile = result;
resolve();
},
error(err) {
console.error('Image compression error:', err);
},
});
});
}
if (compressedFile) {
const toBase64 = await fileToBase64(compressedFile);
insertImage(toBase64);
}
} catch (error) {
console.error(error);
}
}, [insertImage]);
const usersRef = useRef([]); const usersRef = useRef([]);
@ -470,6 +500,25 @@ export default ({
} }
return false; return false;
}, },
handlePaste(view, event) {
if(!handleImageUpload) return
if (!isChat) return;
const items = event.clipboardData?.items;
if (!items) return false;
for (const item of items) {
if (item.type.startsWith('image/')) {
const file = item.getAsFile();
if (file) {
event.preventDefault(); // Block the default paste
handleImageUpload(file); // Custom handler
return true; // Let ProseMirror know we handled it
}
}
}
return false; // fallback to default behavior otherwise
},
}} }}
/> />
</div> </div>

View File

@ -52,6 +52,8 @@ export const ImageCard = ({
backgroundColor: "#1F2023", backgroundColor: "#1F2023",
height: height, height: height,
transition: "height 0.6s ease-in-out", transition: "height 0.6s ease-in-out",
display: 'flex',
flexDirection: 'column',
}} }}
> >
<Box <Box
@ -170,8 +172,18 @@ export const ImageCard = ({
)} )}
</Box> </Box>
<Box> <Box
<CardContent> sx={{
maxHeight: '100%',
flexGrow: 1,
overflow: 'hidden',
}}
>
<CardContent
sx={{
height: '100%',
}}
>
<ImageViewer src={image} /> <ImageViewer src={image} />
</CardContent> </CardContent>
</Box> </Box>
@ -203,6 +215,7 @@ export const ImageCard = ({
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
cursor: "pointer", cursor: "pointer",
height: '100%',
}} }}
onClick={handleOpenFullscreen} onClick={handleOpenFullscreen}
> >

View File

@ -54,35 +54,19 @@ export const NewUsersCTA = ({ balance }) => {
textDecoration: "underline", textDecoration: "underline",
}} }}
onClick={() => { onClick={() => {
if (chrome && chrome.tabs) { window.open("https://link.qortal.dev/support", '_system')
chrome.tabs.create({ url: "https://link.qortal.dev/telegram-invite" }, (tab) => {
if (chrome.runtime.lastError) {
console.error("Error opening tab:", chrome.runtime.lastError);
} else {
console.log("Tab opened successfully:", tab);
}
});
}
}} }}
> >
Telegram Nextcloud
</ButtonBase> </ButtonBase>
<ButtonBase <ButtonBase
sx={{ sx={{
textDecoration: "underline", textDecoration: "underline",
}} }}
onClick={() => { onClick={() => {
if (chrome && chrome.tabs) { window.open("https://link.qortal.dev/discord-invite", '_system')
chrome.tabs.create({ url: "https://link.qortal.dev/discord-invite" }, (tab) => {
if (chrome.runtime.lastError) {
console.error("Error opening tab:", chrome.runtime.lastError);
} else {
console.log("Tab opened successfully:", tab);
}
});
}
}} }}
> >
Discord Discord
</ButtonBase> </ButtonBase>

View File

@ -173,3 +173,6 @@ const STATIC_BCRYPT_SALT = `$${BCRYPT_VERSION}$${BCRYPT_ROUNDS}$IxVE941tXVUD4cW0
const KDF_THREADS = 16 const KDF_THREADS = 16
export { TX_TYPES, ERROR_CODES, QORT_DECIMALS, PROXY_URL, STATIC_SALT, ADDRESS_VERSION, KDF_THREADS, STATIC_BCRYPT_SALT, CHAT_REFERENCE_FEATURE_TRIGGER_TIMESTAMP, DYNAMIC_FEE_TIMESTAMP } export { TX_TYPES, ERROR_CODES, QORT_DECIMALS, PROXY_URL, STATIC_SALT, ADDRESS_VERSION, KDF_THREADS, STATIC_BCRYPT_SALT, CHAT_REFERENCE_FEATURE_TRIGGER_TIMESTAMP, DYNAMIC_FEE_TIMESTAMP }
export const MAX_SIZE_PUBLIC_NODE = 500 * 1024 * 1024; // 500mb
export const MAX_SIZE_PUBLISH = 2000 * 1024 * 1024; // 2GB

View File

@ -42,7 +42,7 @@ import {
} from "../background"; } from "../background";
import { getNameInfo, uint8ArrayToObject,getAllUserNames } from "../backgroundFunctions/encryption"; import { getNameInfo, uint8ArrayToObject,getAllUserNames } from "../backgroundFunctions/encryption";
import { saveFileInChunksFromUrl, showSaveFilePicker } from "../components/Apps/useQortalMessageListener"; import { saveFileInChunksFromUrl, showSaveFilePicker } from "../components/Apps/useQortalMessageListener";
import { QORT_DECIMALS } from "../constants/constants"; import { MAX_SIZE_PUBLIC_NODE, MAX_SIZE_PUBLISH, QORT_DECIMALS } from "../constants/constants";
import Base58 from "../deps/Base58"; import Base58 from "../deps/Base58";
import { import {
base64ToUint8Array, base64ToUint8Array,
@ -955,6 +955,22 @@ export const publishQDNResource = async (
const tags = data?.tags || []; const tags = data?.tags || [];
const result = {}; const result = {};
if (file && file.size > MAX_SIZE_PUBLISH) {
throw new Error(
"Maximum file size allowed is 2 GB per file"
);
}
if (file && file.size > MAX_SIZE_PUBLIC_NODE) {
const isPublicNode = await isRunningGateway();
if (isPublicNode) {
throw new Error(
"Maximum file size allowed on the public node is 500 MB. Please use your local node for larger files."
);
}
}
// Fill tags dynamically while maintaining backward compatibility // Fill tags dynamically while maintaining backward compatibility
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
result[`tag${i + 1}`] = tags[i] || data[`tag${i + 1}`] || undefined; result[`tag${i + 1}`] = tags[i] || data[`tag${i + 1}`] || undefined;
@ -1125,6 +1141,31 @@ export const publishMultipleQDNResources = async (
throw new Error('No resources to publish'); throw new Error('No resources to publish');
} }
const isPublicNode = await isRunningGateway();
if (isPublicNode) {
const hasOversizedFilePublicNode = resources.some((resource) => {
const file = resource?.file;
return file instanceof File && file.size > MAX_SIZE_PUBLIC_NODE;
});
if (hasOversizedFilePublicNode) {
throw new Error(
"Maximum file size allowed on the public node is 500 MB. Please use your local node for larger files."
);
}
}
const hasOversizedFile = resources.some((resource) => {
const file = resource?.file;
return file instanceof File && file.size > MAX_SIZE_PUBLISH;
});
if (hasOversizedFile) {
throw new Error(
"Maximum file size allowed is 2 GB per file"
);
}
const encrypt = data?.encrypt; const encrypt = data?.encrypt;
for (const resource of resources) { for (const resource of resources) {