added publishing and deleting of images

This commit is contained in:
PhilReact 2025-05-11 15:04:11 +03:00
parent 380e7d2387
commit 4767ddd9fa
7 changed files with 212 additions and 40 deletions

View File

@ -1,3 +1,5 @@
//TODO
import { useRef, useState, useCallback, useMemo } from 'react'; import { useRef, useState, useCallback, useMemo } from 'react';
interface State { interface State {
@ -34,7 +36,7 @@ export const useModal = () => {
const onCancel = useCallback(() => { const onCancel = useCallback(() => {
const { reject } = promiseConfig.current || {}; const { reject } = promiseConfig.current || {};
hide(); hide();
reject?.(); reject?.('Declined');
}, [hide]); }, [hide]);
return useMemo( return useMemo(

View File

@ -31,18 +31,28 @@ import {
subscribeToEvent, subscribeToEvent,
unsubscribeFromEvent, unsubscribeFromEvent,
} from '../../utils/events'; } from '../../utils/events';
import { Box, ButtonBase, Divider, Typography, useTheme } from '@mui/material'; import {
Box,
ButtonBase,
Divider,
IconButton,
Tooltip,
Typography,
useTheme,
} 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 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 uid = new ShortUniqueId({ length: 5 }); const uid = new ShortUniqueId({ length: 5 });
const uidImages = new ShortUniqueId({ length: 12 }); const uidImages = new ShortUniqueId({ length: 12 });
export const ChatGroup = ({ export const ChatGroup = ({
selectedGroup, selectedGroup,
secretKey, secretKey,
@ -59,7 +69,7 @@ export const ChatGroup = ({
hideView, hideView,
isPrivate, isPrivate,
}) => { }) => {
const { isUserBlocked } = useContext(MyContext); const { isUserBlocked, show } = useContext(MyContext);
const [messages, setMessages] = useState([]); const [messages, setMessages] = useState([]);
const [chatReferences, setChatReferences] = useState({}); const [chatReferences, setChatReferences] = useState({});
const [isSending, setIsSending] = useState(false); const [isSending, setIsSending] = useState(false);
@ -72,7 +82,7 @@ export const ChatGroup = ({
const [replyMessage, setReplyMessage] = useState(null); const [replyMessage, setReplyMessage] = useState(null);
const [onEditMessage, setOnEditMessage] = useState(null); const [onEditMessage, setOnEditMessage] = useState(null);
const [isOpenQManager, setIsOpenQManager] = useState(null); const [isOpenQManager, setIsOpenQManager] = useState(null);
const [isDeleteImage, setIsDeleteImage] = useState(false);
const [messageSize, setMessageSize] = useState(0); const [messageSize, setMessageSize] = useState(0);
const [chatImagesToSave, setChatImagesToSave] = useState([]); const [chatImagesToSave, setChatImagesToSave] = useState([]);
const hasInitializedWebsocket = useRef(false); const hasInitializedWebsocket = useRef(false);
@ -780,15 +790,34 @@ export const ChatGroup = ({
isEdited: chatReference ? true : false, isEdited: chatReference ? true : false,
}; };
const imagesToPublish = []; const imagesToPublish = [];
if (!chatReference && chatImagesToSave?.length > 0) { const deleteImage =
chatImagesToSave.forEach((base64Img) => { onEditMessage && isDeleteImage && messageHasImage(onEditMessage);
const identifier = `qchat_1_group_${selectedGroup}_${uidImages.rnd()}`; if (deleteImage) {
imagesToPublish.push({ const fee = await getFee('ARBITRARY');
service: 'IMAGE',
identifier, await show({
name: myName, publishFee: fee.fee + ' QORT',
base64: base64Img, message: 'Would you like to delete your previous chat image?',
}); });
await window.sendMessage('publishOnQDN', {
data: 'RA==',
identifier: onEditMessage?.images[0]?.identifier,
service: onEditMessage?.images[0]?.service,
});
}
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( const res = await window.sendMessage(
@ -800,24 +829,29 @@ export const ChatGroup = ({
240000, 240000,
true true
); );
console.log('res', res);
if (res !== true) throw new Error('Unable to publish images'); 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
? 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: images: images,
onEditMessage?.images ||
imagesToPublish.map((item) => {
return {
name: item.name,
identifier: item.identifier,
service: item.service,
};
}),
...publicData, ...publicData,
}; };
const objectMessage = { const objectMessage = {
@ -825,6 +859,7 @@ export const ChatGroup = ({
[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 = const encryptSingle =
@ -859,6 +894,7 @@ export const ChatGroup = ({
clearEditorContent(); clearEditorContent();
setReplyMessage(null); setReplyMessage(null);
setOnEditMessage(null); setOnEditMessage(null);
setIsDeleteImage(false);
setChatImagesToSave([]); setChatImagesToSave([]);
} }
// send chat message // send chat message
@ -925,6 +961,8 @@ export const ChatGroup = ({
} }
setReplyMessage(message); setReplyMessage(message);
setOnEditMessage(null); setOnEditMessage(null);
setIsDeleteImage(false);
setChatImagesToSave([]);
editorRef?.current?.chain().focus(); editorRef?.current?.chain().focus();
}, },
[onEditMessage] [onEditMessage]
@ -1022,10 +1060,23 @@ export const ChatGroup = ({
const theme = useTheme(); const theme = useTheme();
const insertImage = useCallback((img) => { const insertImage = useCallback(
setChatImagesToSave((prev) => [...prev, img]); (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 <div
style={{ style={{
@ -1098,19 +1149,106 @@ export const ChatGroup = ({
flexWrap: 'wrap', flexWrap: 'wrap',
}} }}
> >
{chatImagesToSave?.map((imgBase64) => { {!isDeleteImage &&
return ( onEditMessage &&
<img messageHasImage(onEditMessage) &&
onEditMessage?.images?.map((_, index) => (
<div
key={index}
style={{ style={{
position: 'relative',
height: '50px', height: '50px',
width: '50px', width: '50px',
}}
>
<ImageIcon
color="primary"
sx={{
height: '100%',
width: '100%',
borderRadius: '3px',
}}
/>
<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', objectFit: 'contain',
borderRadius: '3px', borderRadius: '3px',
}} }}
src={`data:image/webp;base64,${imgBase64}`}
/> />
); <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> </Box>
{replyMessage && ( {replyMessage && (
<Box <Box
@ -1128,6 +1266,8 @@ export const ChatGroup = ({
setReplyMessage(null); setReplyMessage(null);
setOnEditMessage(null); setOnEditMessage(null);
setIsDeleteImage(false);
setChatImagesToSave([]);
}} }}
> >
<ExitIcon /> <ExitIcon />
@ -1149,7 +1289,8 @@ export const ChatGroup = ({
onClick={() => { onClick={() => {
setReplyMessage(null); setReplyMessage(null);
setOnEditMessage(null); setOnEditMessage(null);
setIsDeleteImage(false);
setChatImagesToSave([]);
clearEditorContent(); clearEditorContent();
}} }}
> >

View File

@ -292,6 +292,11 @@ export const ChatList = ({
message.editTimestamp = message.editTimestamp =
chatReferences[message.signature]?.edit?.timestamp; 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

@ -45,6 +45,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()) {
@ -366,7 +368,9 @@ export const MessageItem = React.memo(
) : ( ) : (
<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

@ -400,7 +400,6 @@ export default ({
if (compressedFile) { if (compressedFile) {
const toBase64 = await fileToBase64(compressedFile); const toBase64 = await fileToBase64(compressedFile);
insertImage(toBase64); insertImage(toBase64);
console.log('toBase64', toBase64);
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);

View File

@ -1228,7 +1228,6 @@ export const publishMultipleQDNResources = async (
sender, sender,
isFromExtension isFromExtension
) => { ) => {
console.log('data', data);
const requiredFields = ['resources']; const requiredFields = ['resources'];
const missingFields: string[] = []; const missingFields: string[] = [];
let feeAmount = null; let feeAmount = null;
@ -1695,7 +1694,7 @@ export const sendChatMessage = async (data, isFromExtension, appInfo) => {
? 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
);
};