added reply

This commit is contained in:
PhilReact 2024-09-21 13:16:22 +03:00
parent c8fb4974a7
commit bcf22ca5e0
9 changed files with 320 additions and 66 deletions

View File

@ -129,7 +129,7 @@ const defaultValues: MyContextInterface = {
message: "",
},
};
export let isMobile = false
export let isMobile = true
const isMobileDevice = () => {
const userAgent = navigator.userAgent || navigator.vendor || window.opera;

View File

@ -1622,17 +1622,22 @@ async function sendChatDirect({
...(otherData || {})
};
const messageStringified = JSON.stringify(finalJson);
const tx = await createTransaction(18, keyPair, {
console.log('chatReferencefinal', chatReference)
const txBody = {
timestamp: Date.now(),
recipient: recipientAddress,
recipientPublicKey: recipientPublicKey,
hasChatReference: 0,
hasChatReference: chatReference ? 1 : 0,
message: messageStringified,
lastReference: reference,
proofOfWorkNonce: 0,
isEncrypted: 1,
isText: 1,
});
}
if(chatReference){
txBody['chatReference'] = chatReference
}
const tx = await createTransaction(18, keyPair, txBody);
// if (!hasEnoughBalance) {
// throw new Error("Must have at least 4 QORT to send a chat message");
@ -4000,7 +4005,7 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => {
address,
otherData
} = request.payload;
console.log('chatReferencebg', chatReference)
sendChatDirect({
directTo,
chatReference,

View File

@ -19,6 +19,7 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ShortUniqueId from "short-unique-id";
import { ReturnIcon } from '../../assets/Icons/ReturnIcon';
import { ExitIcon } from '../../assets/Icons/ExitIcon';
import { MessageItem, ReplyPreview } from './MessageItem';
const uid = new ShortUniqueId({ length: 5 });
@ -41,12 +42,12 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
const socketRef = useRef(null);
const timeoutIdRef = useRef(null);
const groupSocketTimeoutRef = useRef(null);
const [replyMessage, setReplyMessage] = useState(null)
const setEditorRef = (editorInstance) => {
editorRef.current = editorInstance;
};
const publicKeyOfRecipientRef = useRef(null)
console.log({messages})
const getPublicKeyFunc = async (address)=> {
try {
const publicKey = await getPublicKey(address)
@ -219,6 +220,7 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
const sendChatDirect = async ({ chatReference = undefined, messageText, otherData}: any, address, publicKeyOfRecipient, isNewChatVar)=> {
try {
console.log('chatReferencedirect', chatReference)
const directTo = isNewChatVar ? directToValue : address
if(!directTo) return
@ -282,6 +284,8 @@ const clearEditorContent = () => {
const sendMessage = async ()=> {
try {
if(+balance < 4) throw new Error('You need at least 4 QORT to send a message')
if(isSending) return
if (editorRef.current) {
@ -297,12 +301,20 @@ const clearEditorContent = () => {
await sendChatDirect({ messageText: htmlContent}, null, null, true)
return
}
let repliedTo = replyMessage?.signature
if (replyMessage?.chatReference) {
repliedTo = replyMessage?.chatReference
}
const otherData = {
specialId: uid.rnd()
specialId: uid.rnd(),
repliedTo
}
const sendMessageFunc = async () => {
await sendChatDirect({ messageText: htmlContent, otherData}, selectedDirect?.address, publicKeyOfRecipient, false)
await sendChatDirect({ chatReference: undefined, messageText: htmlContent, otherData}, selectedDirect?.address, publicKeyOfRecipient, false)
};
// Add the function to the queue
const messageObj = {
@ -321,6 +333,7 @@ const clearEditorContent = () => {
executeEvent("sent-new-message-group", {})
}, 150);
clearEditorContent()
setReplyMessage(null)
}
// send chat message
} catch (error) {
@ -337,6 +350,9 @@ const clearEditorContent = () => {
}
}
const onReply = useCallback((message)=> {
setReplyMessage(message)
}, [])
return (
@ -444,7 +460,7 @@ const clearEditorContent = () => {
</>
)}
<ChatList chatId={selectedDirect?.address} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages}/>
<ChatList onReply={onReply} chatId={selectedDirect?.address} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages}/>
<div style={{
@ -470,7 +486,24 @@ const clearEditorContent = () => {
flexGrow: isMobile && 1,
overflow: !isMobile && "auto",
}}>
{replyMessage && (
<Box sx={{
display: 'flex',
gap: '5px',
alignItems: 'flex-start',
width: '100%'
}}>
<ReplyPreview message={replyMessage} />
<ButtonBase
onClick={() => {
setReplyMessage(null)
}}
>
<ExitIcon />
</ButtonBase>
</Box>
)}
<Tiptap isFocusedParent={isFocusedParent} setEditorRef={setEditorRef} onEnter={sendMessage} isChat disableEnter={isMobile ? true : false} setIsFocusedParent={setIsFocusedParent}/>
</div>

View File

@ -15,8 +15,10 @@ import { CustomizedSnackbars } from '../Snackbar/Snackbar'
import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from '../../constants/codes'
import { useMessageQueue } from '../../MessageQueueContext'
import { executeEvent } from '../../utils/events'
import { Box } from '@mui/material'
import { Box, ButtonBase } from '@mui/material'
import ShortUniqueId from "short-unique-id";
import { ReplyPreview } from './MessageItem'
import { ExitIcon } from '../../assets/Icons/ExitIcon'
const uid = new ShortUniqueId({ length: 5 });
@ -34,6 +36,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
const [infoSnack, setInfoSnack] = React.useState(null);
const hasInitialized = useRef(false)
const [isFocusedParent, setIsFocusedParent] = useState(false);
const [replyMessage, setReplyMessage] = useState(null)
const hasInitializedWebsocket = useRef(false)
const socketRef = useRef(null); // WebSocket reference
@ -111,6 +114,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
...item,
id: item.signature,
text: item?.decryptedData?.message || "",
repliedTo: item?.decryptedData?.repliedTo,
unread: item?.sender === myAddress ? false : true
}
} )
@ -121,6 +125,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
...item,
id: item.signature,
text: item?.decryptedData?.message || "",
repliedTo: item?.decryptedData?.repliedTo,
unread: false
}
} )
@ -305,8 +310,15 @@ const clearEditorContent = () => {
setIsSending(true)
const message = htmlContent
const secretKeyObject = await getSecretKey(false, true)
let repliedTo = replyMessage?.signature
if (replyMessage?.chatReference) {
repliedTo = replyMessage?.chatReference
}
const otherData = {
specialId: uid.rnd()
specialId: uid.rnd(),
repliedTo
}
const objectMessage = {
message,
@ -338,6 +350,7 @@ const clearEditorContent = () => {
executeEvent("sent-new-message-group", {})
}, 150);
clearEditorContent()
setReplyMessage(null)
}
// send chat message
} catch (error) {
@ -362,6 +375,9 @@ const clearEditorContent = () => {
}
}, [hide]);
const onReply = useCallback((message)=> {
setReplyMessage(message)
}, [])
return (
<div style={{
@ -374,7 +390,7 @@ const clearEditorContent = () => {
left: hide && '-100000px',
}}>
<ChatList chatId={selectedGroup} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages}/>
<ChatList onReply={onReply} chatId={selectedGroup} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages}/>
<div style={{
@ -400,7 +416,25 @@ const clearEditorContent = () => {
flexGrow: isMobile && 1,
overflow: !isMobile && "auto",
}}>
{replyMessage && (
<Box sx={{
display: 'flex',
gap: '5px',
alignItems: 'flex-start',
width: '100%'
}}>
<ReplyPreview message={replyMessage} />
<ButtonBase
onClick={() => {
setReplyMessage(null)
}}
>
<ExitIcon />
</ButtonBase>
</Box>
)}
<Tiptap setEditorRef={setEditorRef} onEnter={sendMessage} isChat disableEnter={isMobile ? true : false} isFocusedParent={isFocusedParent} setIsFocusedParent={setIsFocusedParent} />
</div>

View File

@ -8,7 +8,7 @@ import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events';
// defaultHeight: 50,
// });
export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId }) => {
export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onReply }) => {
const hasLoadedInitialRef = useRef(false);
const listRef = useRef();
@ -18,7 +18,7 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId }) =
fixedWidth: true,
defaultHeight: 50,
}), [chatId]); // Recreate cache when chatId changes
console.log('messages2', messages)
useEffect(() => {
cache.clearAll();
}, []);
@ -68,6 +68,10 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId }) =
}
};
const scrollToItem = useCallback((index) => {
listRef.current.scrollToRow(index); // This scrolls to the specific index
}, []);
const recomputeListHeights = () => {
if (listRef.current) {
listRef.current.recomputeRowHeights();
@ -93,7 +97,18 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId }) =
let message = messages[index];
const isLargeMessage = message.text?.length > 200; // Adjust based on your message size threshold
// const reply = message?.repliedTo ? messages.find((msg)=> msg?.signature === message?.repliedTo) : undefined
let replyIndex = messages.findIndex((msg)=> msg?.signature === message?.repliedTo)
let reply
if(message?.repliedTo && replyIndex !== -1){
reply = messages[replyIndex]
}
if(message?.message && message?.groupDirectId){
replyIndex = messages.findIndex((msg)=> msg?.signature === message?.message?.repliedTo)
reply
if(message?.message?.repliedTo && replyIndex !== -1){
reply = messages[replyIndex]
}
message = {
...(message?.message || {}),
isTemp: true,
@ -130,6 +145,10 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId }) =
onSeen={handleMessageSeen}
isTemp={!!message?.isTemp}
myAddress={myAddress}
onReply={onReply}
reply={reply}
scrollToItem={scrollToItem}
replyIndex={replyIndex}
/>
</div>
</div>

View File

@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
import DOMPurify from 'dompurify';
import './styles.css'; // Ensure this CSS file is imported
export const MessageDisplay = ({ htmlContent }) => {
export const MessageDisplay = ({ htmlContent , isReply}) => {
const linkify = (text) => {
// Regular expression to find URLs starting with https://, http://, or www.
@ -53,7 +53,7 @@ export const MessageDisplay = ({ htmlContent }) => {
};
return (
<div
className="tiptap"
className={`tiptap ${isReply ? 'isReply' : ''}`}
dangerouslySetInnerHTML={{ __html: sanitizedContent }}
onClick={(e) => {
// Delegate click handling to the parent div

View File

@ -2,18 +2,30 @@ import { Message } from "@chatscope/chat-ui-kit-react";
import React, { useEffect } from "react";
import { useInView } from "react-intersection-observer";
import { MessageDisplay } from "./MessageDisplay";
import { Avatar, Box, Typography } from "@mui/material";
import { Avatar, Box, ButtonBase, Typography } from "@mui/material";
import { formatTimestamp } from "../../utils/time";
import { getBaseApi } from "../../background";
import { getBaseApiReact } from "../../App";
import { generateHTML } from "@tiptap/react";
import Highlight from '@tiptap/extension-highlight'
import StarterKit from '@tiptap/starter-kit'
import Underline from '@tiptap/extension-underline'
import Highlight from "@tiptap/extension-highlight";
import StarterKit from "@tiptap/starter-kit";
import Underline from "@tiptap/extension-underline";
import { executeEvent } from "../../utils/events";
import { WrapperUserAction } from "../WrapperUserAction";
export const MessageItem = ({ message, onSeen, isLast, isTemp, myAddress }) => {
import ReplyIcon from "@mui/icons-material/Reply";
export const MessageItem = ({
message,
onSeen,
isLast,
isTemp,
myAddress,
onReply,
isShowingAsReply,
reply,
replyIndex,
scrollToItem
}) => {
const { ref, inView } = useInView({
threshold: 0.7, // Fully visible
triggerOnce: true, // Only trigger once when it becomes visible
@ -34,70 +46,169 @@ export const MessageItem = ({ message, onSeen, isLast, isTemp, myAddress }) => {
borderRadius: "7px",
width: "95%",
display: "flex",
gap: '7px',
opacity: isTemp ? 0.5 : 1
gap: "7px",
opacity: isTemp ? 0.5 : 1,
}}
id={message?.signature}
>
<WrapperUserAction disabled={myAddress === message?.sender} address={message?.sender} name={message?.senderName}>
<Avatar
sx={{
backgroundColor: '#27282c',
color: 'white'
}}
alt={message?.senderName}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${message?.senderName}/qortal_avatar?async=true`}
>
{message?.senderName?.charAt(0)}
</Avatar>
</WrapperUserAction>
{isShowingAsReply ? (
<ReplyIcon
sx={{
fontSize: "30px",
}}
/>
) : (
<WrapperUserAction
disabled={myAddress === message?.sender}
address={message?.sender}
name={message?.senderName}
>
<Avatar
sx={{
backgroundColor: "#27282c",
color: "white",
}}
alt={message?.senderName}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
message?.senderName
}/qortal_avatar?async=true`}
>
{message?.senderName?.charAt(0)}
</Avatar>
</WrapperUserAction>
)}
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "7px",
width: '100%'
width: "100%",
height: isShowingAsReply && "40px",
}}
>
<WrapperUserAction disabled={myAddress === message?.sender} address={message?.sender} name={message?.senderName}>
<Typography
<Box
sx={{
fontWight: 600,
fontFamily: "Inter",
color: "cadetBlue",
display: "flex",
width: "100%",
justifyContent: "space-between",
}}
>
{message?.senderName || message?.sender}
</Typography>
</WrapperUserAction>
<WrapperUserAction
disabled={myAddress === message?.sender}
address={message?.sender}
name={message?.senderName}
>
<Typography
sx={{
fontWight: 600,
fontFamily: "Inter",
color: "cadetBlue",
}}
>
{message?.senderName || message?.sender}
</Typography>
</WrapperUserAction>
{!isShowingAsReply && (
<ButtonBase
onClick={() => {
onReply(message);
}}
>
<ReplyIcon />
</ButtonBase>
)}
</Box>
{reply && (
<Box
sx={{
marginTop: '20px',
width: "100%",
borderRadius: "5px",
backgroundColor: "var(--bg-primary)",
overflow: 'hidden',
display: 'flex',
gap: '20px',
maxHeight: '90px',
cursor: 'pointer'
}}
onClick={()=> {
scrollToItem(replyIndex)
}}
>
<Box sx={{
height: '100%',
width: '5px',
background: 'white'
}} />
<Box sx={{
padding: '5px'
}}>
<Typography sx={{
fontSize: '12px',
fontWeight: 600
}}>Replied to {reply?.senderName || reply?.senderAddress}</Typography>
{reply?.messageText && (
<MessageDisplay
htmlContent={generateHTML(reply?.messageText, [
StarterKit,
Underline,
Highlight,
])}
/>
)}
{reply?.text?.type === "notification" ? (
<MessageDisplay htmlContent={reply.text?.data?.message} />
) : (
<MessageDisplay isReply htmlContent={reply.text} />
)}
</Box>
</Box>
)}
{message?.messageText && (
<MessageDisplay htmlContent={generateHTML(message?.messageText, [StarterKit, Underline, Highlight])} />
<MessageDisplay
htmlContent={generateHTML(message?.messageText, [
StarterKit,
Underline,
Highlight,
])}
/>
)}
{message?.text?.type === "notification" ? (
<MessageDisplay htmlContent={message.text?.data?.message} />
) : (
<MessageDisplay htmlContent={message.text} />
)}
<Box sx={{
display: 'flex',
justifyContent: 'flex-end',
width: '100%',
}}>
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
width: "100%",
}}
>
{isTemp ? (
<Typography sx={{
fontSize: '14px',
color: 'gray',
fontFamily: 'Inter'
}}>Sending...</Typography>
): (
<Typography sx={{
fontSize: '14px',
color: 'gray',
fontFamily: 'Inter'
}}>{formatTimestamp(message.timestamp)}</Typography>
) }
<Typography
sx={{
fontSize: "14px",
color: "gray",
fontFamily: "Inter",
}}
>
Sending...
</Typography>
) : (
<Typography
sx={{
fontSize: "14px",
color: "gray",
fontFamily: "Inter",
}}
>
{formatTimestamp(message.timestamp)}
</Typography>
)}
</Box>
</Box>
@ -115,3 +226,51 @@ export const MessageItem = ({ message, onSeen, isLast, isTemp, myAddress }) => {
</div>
);
};
export const ReplyPreview = ({message})=> {
return (
<Box
sx={{
marginTop: '20px',
width: "100%",
borderRadius: "5px",
backgroundColor: "var(--bg-primary)",
overflow: 'hidden',
display: 'flex',
gap: '20px',
maxHeight: '90px',
cursor: 'pointer'
}}
>
<Box sx={{
height: '100%',
width: '5px',
background: 'white'
}} />
<Box sx={{
padding: '5px'
}}>
<Typography sx={{
fontSize: '12px',
fontWeight: 600
}}>Replied to {message?.senderName || message?.senderAddress}</Typography>
{message?.messageText && (
<MessageDisplay
htmlContent={generateHTML(message?.messageText, [
StarterKit,
Underline,
Highlight,
])}
/>
)}
{message?.text?.type === "notification" ? (
<MessageDisplay htmlContent={message.text?.data?.message} />
) : (
<MessageDisplay isReply htmlContent={message.text} />
)}
</Box>
</Box>
)
}

View File

@ -120,3 +120,6 @@
max-width: 100%;
}
.isReply p {
font-size: 12px !important;
}

View File

@ -31,6 +31,7 @@ const IconWrapper = ({ children, label, color }) => {
fontSize: "12px",
fontWeight: 500,
color: color,
wordBreak: 'normal'
}}
>
{label}