1
0
mirror of https://github.com/Qortal/qortal-mobile.git synced 2025-03-26 23:44:35 +00:00

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

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

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

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

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

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

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

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

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

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