mirror of
https://github.com/Qortal/qortal-mobile.git
synced 2025-06-08 01:16:59 +00:00
allow pasting of img and publish limit
This commit is contained in:
parent
4ad0fb7db3
commit
a22c48667b
@ -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: ""
|
||||||
|
@ -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 = () => {
|
||||||
@ -644,88 +651,193 @@ 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)
|
if (replyMessage?.chatReference) {
|
||||||
const message = isPrivate === false ? editorRef.current.getJSON() : htmlContent
|
repliedTo = replyMessage?.chatReference;
|
||||||
const secretKeyObject = await getSecretKey(false, true)
|
}
|
||||||
|
|
||||||
let repliedTo = replyMessage?.signature
|
const chatReference = onEditMessage?.signature;
|
||||||
|
|
||||||
if (replyMessage?.chatReference) {
|
const publicData = isPrivate
|
||||||
repliedTo = replyMessage?.chatReference
|
? {}
|
||||||
}
|
: {
|
||||||
let chatReference = onEditMessage?.signature
|
isEdited: chatReference ? true : false,
|
||||||
|
};
|
||||||
|
|
||||||
const publicData = isPrivate ? {} : {
|
interface ImageToPublish {
|
||||||
isEdited : chatReference ? true : false,
|
service: string;
|
||||||
}
|
identifier: string;
|
||||||
const otherData = {
|
name: string;
|
||||||
repliedTo,
|
base64: string;
|
||||||
...(onEditMessage?.decryptedData || {}),
|
}
|
||||||
type: chatReference ? 'edit' : '',
|
|
||||||
specialId: uid.rnd(),
|
|
||||||
...publicData
|
|
||||||
}
|
|
||||||
const objectMessage = {
|
|
||||||
...(otherData || {}),
|
|
||||||
[isPrivate ? 'message' : 'messageText']: message,
|
|
||||||
version: 3
|
|
||||||
}
|
|
||||||
const message64: any = await objectToBase64(objectMessage)
|
|
||||||
|
|
||||||
const encryptSingle = isPrivate === false ? JSON.stringify(objectMessage) : await encryptChatMessage(message64, secretKeyObject)
|
const imagesToPublish: ImageToPublish[] = [];
|
||||||
// const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle})
|
|
||||||
|
|
||||||
const sendMessageFunc = async () => {
|
if (deleteImage) {
|
||||||
return await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle, chatReference})
|
const fee = await getFee('ARBITRARY');
|
||||||
};
|
await show({
|
||||||
|
publishFee: fee.fee + ' QORT',
|
||||||
|
message: "Would you like to delete your previous chat image?",
|
||||||
|
});
|
||||||
|
|
||||||
// Add the function to the queue
|
// TODO magic string
|
||||||
const messageObj = {
|
await window.sendMessage('publishOnQDN', {
|
||||||
message: {
|
data: 'RA==',
|
||||||
text: htmlContent,
|
identifier: onEditMessage?.images[0]?.identifier,
|
||||||
timestamp: Date.now(),
|
service: onEditMessage?.images[0]?.service,
|
||||||
senderName: myName,
|
uploadType: 'base64',
|
||||||
sender: myAddress,
|
});
|
||||||
...(otherData || {})
|
}
|
||||||
},
|
|
||||||
chatReference
|
if (chatImagesToSave?.length > 0) {
|
||||||
}
|
const imageToSave = chatImagesToSave[0];
|
||||||
addToQueue(sendMessageFunc, messageObj, 'chat',
|
|
||||||
selectedGroup );
|
const base64ToSave = isPrivate
|
||||||
setTimeout(() => {
|
? await encryptChatMessage(imageToSave, secretKeyObject)
|
||||||
executeEvent("sent-new-message-group", {})
|
: imageToSave;
|
||||||
}, 150);
|
|
||||||
clearEditorContent()
|
// 1 represents public group, 0 is private
|
||||||
setReplyMessage(null)
|
const identifier = `grp-q-manager_${isPrivate ? 0 : 1}_group_${selectedGroup}_${uidImages.rnd()}`;
|
||||||
setOnEditMessage(null)
|
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 = {
|
||||||
|
repliedTo,
|
||||||
|
...(onEditMessage?.decryptedData || {}),
|
||||||
|
type: chatReference ? 'edit' : '',
|
||||||
|
specialId: uid.rnd(),
|
||||||
|
images: images,
|
||||||
|
...publicData,
|
||||||
|
};
|
||||||
|
const objectMessage = {
|
||||||
|
...(otherData || {}),
|
||||||
|
[isPrivate ? 'message' : 'messageText']: message,
|
||||||
|
version: 3,
|
||||||
|
};
|
||||||
|
const message64: any = await objectToBase64(objectMessage);
|
||||||
|
|
||||||
|
const encryptSingle =
|
||||||
|
isPrivate === false
|
||||||
|
? JSON.stringify(objectMessage)
|
||||||
|
: await encryptChatMessage(message64, secretKeyObject);
|
||||||
|
|
||||||
|
const sendMessageFunc = async () => {
|
||||||
|
return await sendChatGroup({
|
||||||
|
groupId: selectedGroup,
|
||||||
|
messageText: encryptSingle,
|
||||||
|
chatReference,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the function to the queue
|
||||||
|
const messageObj = {
|
||||||
|
message: {
|
||||||
|
text: htmlContent,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
senderName: myName,
|
||||||
|
sender: myAddress,
|
||||||
|
...(otherData || {}),
|
||||||
|
},
|
||||||
|
chatReference,
|
||||||
|
};
|
||||||
|
addToQueue(sendMessageFunc, messageObj, 'chat', selectedGroup);
|
||||||
|
setTimeout(() => {
|
||||||
|
executeEvent('sent-new-message-group', {});
|
||||||
|
}, 150);
|
||||||
|
clearEditorContent();
|
||||||
|
setReplyMessage(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} />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
>
|
>
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
@ -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) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user