display and insert image

This commit is contained in:
PhilReact 2025-05-12 02:10:39 +03:00
parent 6b091a9e64
commit 96e6530612
7 changed files with 324 additions and 11 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,15 +15,22 @@ 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, Typography, IconButton,
Tooltip } 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 { isExtMsg, getFee } from '../../background'
import { throttle } from 'lodash' import { throttle } from 'lodash'
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 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 });
@ -47,7 +54,9 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
const [isOpenQManager, setIsOpenQManager] = useState(null) const [isOpenQManager, setIsOpenQManager] = useState(null)
const [onEditMessage, setOnEditMessage] = useState(null) const [onEditMessage, setOnEditMessage] = useState(null)
const [messageSize, setMessageSize] = useState(0) const [messageSize, setMessageSize] = useState(0)
const {isUserBlocked} = useContext(MyContext) const {isUserBlocked, show} = useContext(MyContext)
const [chatImagesToSave, setChatImagesToSave] = useState([]);
const [isDeleteImage, setIsDeleteImage] = useState(false);
const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue(); const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue();
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
@ -610,11 +619,98 @@ const sendMessage = async ()=> {
const publicData = isPrivate ? {} : { const publicData = isPrivate ? {} : {
isEdited : chatReference ? true : false, isEdited : chatReference ? true : false,
} }
const imagesToPublish = [];
const deleteImage =
onEditMessage && isDeleteImage && messageHasImage(onEditMessage);
if (deleteImage) {
const fee = await getFee('ARBITRARY');
await show({
publishFee: fee.fee + ' QORT',
message: 'Would you like to delete your previous chat image?',
});
await new Promise((res, rej) => {
chrome?.runtime?.sendMessage(
{
action: "publishOnQDN",
payload: {
data: 'RA==',
identifier: onEditMessage?.images[0]?.identifier,
service: onEditMessage?.images[0]?.service,
},
},
(response) => {
if (!response?.error) {
res(response);
return
}
rej(response.error);
}
);
});
}
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 new Promise((res, rej) => {
chrome?.runtime?.sendMessage(
{
action: "PUBLISH_MULTIPLE_QDN_RESOURCES",
type: "qortalRequest",
payload: {
resources: imagesToPublish,
},
},
(response) => {
if (response.error) {
rej(response?.message);
return;
} else {
res(response);
}
}
);
});
if (res !== true) throw new Error('Unable to publish images');
}
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(),
images,
...publicData ...publicData
} }
const objectMessage = { const objectMessage = {
@ -650,6 +746,8 @@ const sendMessage = async ()=> {
clearEditorContent() clearEditorContent()
setReplyMessage(null) setReplyMessage(null)
setOnEditMessage(null) setOnEditMessage(null)
setIsDeleteImage(false);
setChatImagesToSave([]);
} }
// send chat message // send chat message
} catch (error) { } catch (error) {
@ -712,6 +810,8 @@ useEffect(() => {
} }
setReplyMessage(message) setReplyMessage(message)
setOnEditMessage(null) setOnEditMessage(null)
setIsDeleteImage(false);
setChatImagesToSave([]);
editorRef?.current?.chain().focus() editorRef?.current?.chain().focus()
}, []) }, [])
@ -781,6 +881,24 @@ useEffect(() => {
} }
}, [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={{
@ -823,6 +941,117 @@ useEffect(() => {
width: 'calc(100% - 100px)', width: 'calc(100% - 100px)',
justifyContent: 'flex-end' justifyContent: 'flex-end'
}}> }}>
<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',
@ -837,6 +1066,8 @@ useEffect(() => {
setReplyMessage(null) setReplyMessage(null)
setOnEditMessage(null) setOnEditMessage(null)
setIsDeleteImage(false);
setChatImagesToSave([]);
}} }}
> >
@ -857,7 +1088,8 @@ useEffect(() => {
onClick={() => { onClick={() => {
setReplyMessage(null) setReplyMessage(null)
setOnEditMessage(null) setOnEditMessage(null)
setIsDeleteImage(false);
setChatImagesToSave([]);
clearEditorContent() clearEditorContent()
}} }}
@ -868,7 +1100,7 @@ useEffect(() => {
)} )}
<Tiptap enableMentions setEditorRef={setEditorRef} onEnter={sendMessage} isChat disableEnter={isMobile ? true : false} isFocusedParent={isFocusedParent} setIsFocusedParent={setIsFocusedParent} membersWithNames={members} /> <Tiptap enableMentions setEditorRef={setEditorRef} onEnter={sendMessage} isChat disableEnter={isMobile ? true : false} isFocusedParent={isFocusedParent} setIsFocusedParent={setIsFocusedParent} membersWithNames={members} insertImage={insertImage} />
{messageSize > 750 && ( {messageSize > 750 && (
<Box sx={{ <Box sx={{
display: 'flex', display: 'flex',

View File

@ -277,7 +277,11 @@ export const ChatList = ({
message.isEdit = true message.isEdit = true
message.editTimestamp = chatReferences[message.signature]?.edit?.timestamp message.editTimestamp = chatReferences[message.signature]?.edit?.timestamp
} }
if (chatReferences[message.signature]?.edit?.images) {
message.images =
chatReferences[message.signature]?.edit?.images;
message.isEdit = true;
}
} }
// Check if message is updating // Check if message is updating

View File

@ -32,6 +32,8 @@ import level7Img from "../../assets/badges/level-7.png"
import level8Img from "../../assets/badges/level-8.png" import level8Img from "../../assets/badges/level-8.png"
import level9Img from "../../assets/badges/level-9.png" 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 { buildImageEmbedLink, messageHasImage } from "../../utils/chat";
const getBadgeImg = (level)=> { const getBadgeImg = (level)=> {
switch(level?.toString()){ switch(level?.toString()){
@ -321,6 +323,9 @@ const onSeenFunc = useCallback(()=> {
) : ( ) : (
<MessageDisplay htmlContent={message.text} /> <MessageDisplay htmlContent={message.text} />
)} )}
{message?.images && messageHasImage(message) && (
<Embed embedLink={buildImageEmbedLink(message.images[0])} />
)}
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",

View File

@ -37,6 +37,7 @@ import MentionList from './MentionList.jsx'
import { Box, Checkbox, Typography } from "@mui/material"; import { Box, Checkbox, Typography } from "@mui/material";
import { useRecoilState } from "recoil"; import { useRecoilState } from "recoil";
import { isDisabledEditorEnterAtom } from "../../atoms/global.js"; import { isDisabledEditorEnterAtom } from "../../atoms/global.js";
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, ' ', ' ');
@ -113,13 +114,13 @@ const MenuBar = ({ setEditorRef, isChat, isDisabledEditorEnter, setIsDisabledEdi
}; };
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">
@ -340,7 +341,8 @@ export default ({
overrideMobile, overrideMobile,
customEditorHeight, customEditorHeight,
membersWithNames, membersWithNames,
enableMentions enableMentions,
insertImage,
}) => { }) => {
const [isDisabledEditorEnter, setIsDisabledEditorEnter] = useRecoilState(isDisabledEditorEnterAtom) const [isDisabledEditorEnter, setIsDisabledEditorEnter] = useRecoilState(isDisabledEditorEnterAtom)
const extensionsFiltered = isChat const extensionsFiltered = isChat
@ -370,6 +372,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]);
@ -520,6 +551,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

@ -1477,7 +1477,7 @@ if (accepted || skip) {
}; };
const messageObject = fullMessageObject ? fullMessageObject : { const messageObject = fullMessageObject ? fullMessageObject : {
messageText: tiptapJson, messageText: tiptapJson,
images: [""], images: [],
repliedTo: "", repliedTo: "",
version: 3, version: 3,
}; };

22
src/utils/chat.ts Normal file
View File

@ -0,0 +1,22 @@
export function buildImageEmbedLink(image?: {
name?: string;
identifier?: string;
service?: string;
timestamp?: number;
}): string | null {
if (!image?.name || !image.identifier || !image.service) return null;
const base = `qortal://use-embed/IMAGE?name=${image.name}&identifier=${image.identifier}&service=${image.service}&mimeType=image%2Fpng&timestamp=${image?.timestamp || ''}`;
const isEncrypted = image.identifier.startsWith('grp-q-manager_0');
return isEncrypted ? `${base}&encryptionType=group` : base;
}
export const messageHasImage = (message) => {
return (
Array.isArray(message?.images) &&
message.images[0]?.identifier &&
message.images[0]?.name &&
message.images[0]?.service
);
};