mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-07-24 10:41:24 +00:00
Merge branch 'feature/q-app-support'
This commit is contained in:
@@ -255,7 +255,7 @@ export const AnnouncementDiscussion = ({
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: isMobile ? '100%' : "100vh",
|
||||
height: isMobile ? '100%' : "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
|
@@ -11,7 +11,7 @@ import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar';
|
||||
import { getNameInfo } from '../Group/Group';
|
||||
import { Spacer } from '../../common/Spacer';
|
||||
import { CustomizedSnackbars } from '../Snackbar/Snackbar';
|
||||
import { getBaseApiReactSocket, isMobile, pauseAllQueues, resumeAllQueues } from '../../App';
|
||||
import { getBaseApiReact, getBaseApiReactSocket, isMobile, pauseAllQueues, resumeAllQueues } from '../../App';
|
||||
import { getPublicKey } from '../../background';
|
||||
import { useMessageQueue } from '../../MessageQueueContext';
|
||||
import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from '../../utils/events';
|
||||
@@ -77,9 +77,28 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
||||
}, [selectedDirect?.address])
|
||||
|
||||
|
||||
|
||||
const middletierFunc = async (data: any, selectedDirectAddress: string, myAddress: string) => {
|
||||
try {
|
||||
if (hasInitialized.current) {
|
||||
decryptMessages(data, true);
|
||||
return;
|
||||
}
|
||||
hasInitialized.current = true;
|
||||
const url = `${getBaseApiReact()}/chat/messages?involving=${selectedDirectAddress}&involving=${myAddress}&encoding=BASE64&limit=0&reverse=false`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
decryptMessages(responseData, false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const decryptMessages = (encryptedMessages: any[])=> {
|
||||
const decryptMessages = (encryptedMessages: any[], isInitiated: boolean)=> {
|
||||
try {
|
||||
return new Promise((res, rej)=> {
|
||||
chrome?.runtime?.sendMessage({ action: "decryptDirect", payload: {
|
||||
@@ -92,7 +111,7 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
||||
processWithNewMessages(response, selectedDirect?.address)
|
||||
|
||||
res(response)
|
||||
if(hasInitialized.current){
|
||||
if(isInitiated){
|
||||
|
||||
const formatted = response.map((item: any)=> {
|
||||
return {
|
||||
@@ -127,7 +146,6 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
||||
|
||||
const forceCloseWebSocket = () => {
|
||||
if (socketRef.current) {
|
||||
console.log('Force closing the WebSocket');
|
||||
clearTimeout(timeoutIdRef.current);
|
||||
clearTimeout(groupSocketTimeoutRef.current);
|
||||
socketRef.current.close(1000, 'forced');
|
||||
@@ -161,7 +179,6 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
||||
socketRef.current = new WebSocket(socketLink);
|
||||
|
||||
socketRef.current.onopen = () => {
|
||||
console.log('WebSocket connection opened');
|
||||
setTimeout(pingWebSocket, 50); // Initial ping
|
||||
};
|
||||
|
||||
@@ -171,7 +188,8 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
||||
clearTimeout(timeoutIdRef.current);
|
||||
groupSocketTimeoutRef.current = setTimeout(pingWebSocket, 45000); // Ping every 45 seconds
|
||||
} else {
|
||||
decryptMessages(JSON.parse(e.data));
|
||||
middletierFunc(JSON.parse(e.data), selectedDirect?.address, myAddress)
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
|
@@ -97,27 +97,28 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
||||
}
|
||||
|
||||
const middletierFunc = async (data: any, groupId: string) => {
|
||||
try {
|
||||
if (hasInitialized.current) {
|
||||
decryptMessages(data, true);
|
||||
return;
|
||||
}
|
||||
hasInitialized.current = true;
|
||||
const url = `${getBaseApiReact()}/chat/messages?txGroupId=${groupId}&encoding=BASE64&limit=0&reverse=false`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
decryptMessages(responseData, false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
try {
|
||||
if (hasInitialized.current) {
|
||||
decryptMessages(data, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
hasInitialized.current = true;
|
||||
const url = `${getBaseApiReact()}/chat/messages?txGroupId=${groupId}&encoding=BASE64&limit=0&reverse=false`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
decryptMessages(responseData, false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const decryptMessages = ( encryptedMessages: any[], isInitiated: boolean )=> {
|
||||
|
||||
const decryptMessages = (encryptedMessages: any[], isInitiated: boolean )=> {
|
||||
try {
|
||||
if(!secretKeyRef.current){
|
||||
checkForFirstSecretKeyNotification(encryptedMessages)
|
||||
@@ -231,6 +232,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
||||
}
|
||||
} )
|
||||
setMessages(formatted)
|
||||
|
||||
setChatReferences((prev) => {
|
||||
let organizedChatReferences = { ...prev };
|
||||
|
||||
|
@@ -1,21 +1,33 @@
|
||||
import React, { useCallback, useState, useEffect, useRef } from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import React, { useCallback, useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { MessageItem } from './MessageItem';
|
||||
import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events';
|
||||
import { useInView } from 'react-intersection-observer'
|
||||
|
||||
export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onReply, handleReaction, chatReferences, tempChatReferences }) => {
|
||||
|
||||
const virtuosoRef = useRef();
|
||||
const parentRef = useRef();
|
||||
const [messages, setMessages] = useState(initialMessages);
|
||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||
const hasLoadedInitialRef = useRef(false);
|
||||
const isAtBottomRef = useRef(true); //
|
||||
const isAtBottomRef = useRef(true);
|
||||
// const [ref, inView] = useInView({
|
||||
// threshold: 0.7
|
||||
// })
|
||||
|
||||
// useEffect(() => {
|
||||
// if (inView) {
|
||||
|
||||
// }
|
||||
// }, [inView])
|
||||
// Update message list with unique signatures and tempMessages
|
||||
useEffect(() => {
|
||||
let uniqueInitialMessagesMap = new Map();
|
||||
|
||||
// Only add a message if it doesn't already exist in the Map
|
||||
initialMessages.forEach((message) => {
|
||||
uniqueInitialMessagesMap.set(message.signature, message);
|
||||
if (!uniqueInitialMessagesMap.has(message.signature)) {
|
||||
uniqueInitialMessagesMap.set(message.signature, message);
|
||||
}
|
||||
});
|
||||
|
||||
const uniqueInitialMessages = Array.from(uniqueInitialMessagesMap.values()).sort(
|
||||
@@ -29,22 +41,14 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
||||
|
||||
setTimeout(() => {
|
||||
const hasUnreadMessages = totalMessages.some((msg) => msg.unread && !msg?.chatReference);
|
||||
|
||||
if (virtuosoRef.current) {
|
||||
|
||||
|
||||
if (virtuosoRef.current && !isAtBottomRef.current && hasUnreadMessages) {
|
||||
|
||||
|
||||
|
||||
|
||||
setShowScrollButton(hasUnreadMessages);
|
||||
if (parentRef.current) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = parentRef.current;
|
||||
const atBottom = scrollTop + clientHeight >= scrollHeight - 10; // Adjust threshold as needed
|
||||
if (!atBottom && hasUnreadMessages) {
|
||||
setShowScrollButton(hasUnreadMessages);
|
||||
} else {
|
||||
handleMessageSeen();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
if (!hasLoadedInitialRef.current) {
|
||||
scrollToBottom(totalMessages);
|
||||
@@ -53,7 +57,14 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
||||
}, 500);
|
||||
}, [initialMessages, tempMessages]);
|
||||
|
||||
|
||||
const scrollToBottom = (initialMsgs) => {
|
||||
const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1;
|
||||
if (rowVirtualizer) {
|
||||
rowVirtualizer.scrollToIndex(index, { align: 'end' });
|
||||
}
|
||||
handleMessageSeen()
|
||||
};
|
||||
|
||||
|
||||
const handleMessageSeen = useCallback(() => {
|
||||
setMessages((prevMessages) =>
|
||||
@@ -62,34 +73,16 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
||||
unread: false,
|
||||
}))
|
||||
);
|
||||
setShowScrollButton(false)
|
||||
}, []);
|
||||
|
||||
const scrollToItem = useCallback((index) => {
|
||||
if (virtuosoRef.current) {
|
||||
virtuosoRef.current.scrollToIndex({ index, behavior: 'smooth' });
|
||||
}
|
||||
}, []);
|
||||
// const scrollToBottom = (initialMsgs) => {
|
||||
// const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1;
|
||||
// if (parentRef.current) {
|
||||
// parentRef.current.scrollToIndex(index);
|
||||
// }
|
||||
// };
|
||||
|
||||
const scrollToBottom = (initialMsgs) => {
|
||||
|
||||
const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1
|
||||
if (virtuosoRef.current) {
|
||||
virtuosoRef.current.scrollToIndex({ index});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleScroll = (scrollState) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollState;
|
||||
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 50;
|
||||
const hasUnreadMessages = messages.some((msg) => msg.unread);
|
||||
|
||||
if (isAtBottom) {
|
||||
handleMessageSeen();
|
||||
}
|
||||
|
||||
setShowScrollButton(!isAtBottom && hasUnreadMessages);
|
||||
};
|
||||
|
||||
const sentNewMessageGroupFunc = useCallback(() => {
|
||||
scrollToBottom();
|
||||
@@ -102,96 +95,134 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
||||
};
|
||||
}, [sentNewMessageGroupFunc]);
|
||||
|
||||
const rowRenderer = (index) => {
|
||||
let message = messages[index];
|
||||
let replyIndex = messages.findIndex((msg)=> msg?.signature === message?.repliedTo)
|
||||
let reply
|
||||
let reactions = null
|
||||
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,
|
||||
unread: false
|
||||
const lastSignature = useMemo(()=> {
|
||||
if(!messages || messages?.length === 0) return null
|
||||
const lastIndex = messages.length - 1
|
||||
return messages[lastIndex]?.signature
|
||||
}, [messages])
|
||||
|
||||
|
||||
// Initialize the virtualizer
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: messages.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 80, // Provide an estimated height of items, adjust this as needed
|
||||
overscan: 10, // Number of items to render outside the visible area to improve smoothness
|
||||
measureElement:
|
||||
typeof window !== 'undefined' &&
|
||||
navigator.userAgent.indexOf('Firefox') === -1
|
||||
? element => {
|
||||
return element?.getBoundingClientRect().height
|
||||
}
|
||||
}
|
||||
|
||||
if(chatReferences && chatReferences[message?.signature]){
|
||||
if(chatReferences[message.signature]?.reactions){
|
||||
reactions = chatReferences[message.signature]?.reactions
|
||||
}
|
||||
}
|
||||
let isUpdating = false
|
||||
if(tempChatReferences && tempChatReferences?.find((item)=> item?.chatReference === message?.signature)){
|
||||
isUpdating = true
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '10px 0', display: 'flex', justifyContent: 'center', width: '100%', minHeight: '50px' , overscrollBehavior: "none"}}>
|
||||
<MessageItem
|
||||
isLast={index === messages.length - 1}
|
||||
message={message}
|
||||
onSeen={handleMessageSeen}
|
||||
isTemp={!!message?.isTemp}
|
||||
myAddress={myAddress}
|
||||
onReply={onReply}
|
||||
reply={reply}
|
||||
replyIndex={replyIndex}
|
||||
scrollToItem={scrollToItem}
|
||||
handleReaction={handleReaction}
|
||||
reactions={reactions}
|
||||
isUpdating={isUpdating}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const handleAtBottomStateChange = (atBottom) => {
|
||||
isAtBottomRef.current = atBottom;
|
||||
if(atBottom){
|
||||
handleMessageSeen();
|
||||
setShowScrollButton(false)
|
||||
}
|
||||
};
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={messages}
|
||||
itemContent={rowRenderer}
|
||||
atBottomThreshold={50}
|
||||
followOutput="smooth"
|
||||
atBottomStateChange={handleAtBottomStateChange} // Detect bottom status
|
||||
increaseViewportBy={3000}
|
||||
|
||||
/>
|
||||
<>
|
||||
<div ref={parentRef} style={{ height: '100%', overflow: 'auto', position: 'relative', display: 'flex' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center', // Center items horizontally
|
||||
gap: '10px', // Add gap between items
|
||||
flexGrow: 1
|
||||
}}
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const index = virtualRow.index;
|
||||
let message = messages[index];
|
||||
let replyIndex = messages.findIndex((msg) => msg?.signature === message?.repliedTo);
|
||||
let reply;
|
||||
let reactions = null;
|
||||
|
||||
{showScrollButton && (
|
||||
<button
|
||||
onClick={()=> scrollToBottom()}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
backgroundColor: '#ff5a5f',
|
||||
color: 'white',
|
||||
padding: '10px 20px',
|
||||
borderRadius: '20px',
|
||||
cursor: 'pointer',
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
Scroll to Unread Messages
|
||||
</button>
|
||||
)}
|
||||
if (message?.repliedTo && replyIndex !== -1) {
|
||||
reply = messages[replyIndex];
|
||||
}
|
||||
|
||||
if (message?.message && message?.groupDirectId) {
|
||||
replyIndex = messages.findIndex((msg) => msg?.signature === message?.message?.repliedTo);
|
||||
if (message?.message?.repliedTo && replyIndex !== -1) {
|
||||
reply = messages[replyIndex];
|
||||
}
|
||||
message = {
|
||||
...(message?.message || {}),
|
||||
isTemp: true,
|
||||
unread: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (chatReferences && chatReferences[message?.signature]) {
|
||||
if (chatReferences[message.signature]?.reactions) {
|
||||
reactions = chatReferences[message.signature]?.reactions;
|
||||
}
|
||||
}
|
||||
|
||||
let isUpdating = false;
|
||||
if (tempChatReferences && tempChatReferences?.find((item) => item?.chatReference === message?.signature)) {
|
||||
isUpdating = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-index={virtualRow.index} //needed for dynamic row height measurement
|
||||
ref={node => rowVirtualizer.measureElement(node)} //measure dynamic row height
|
||||
key={message.signature}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: '50%', // Move to the center horizontally
|
||||
transform: `translateY(${virtualRow.start}px) translateX(-50%)`, // Adjust for centering
|
||||
width: '100%', // Control width (90% of the parent)
|
||||
padding: '10px 0',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
overscrollBehavior: 'none',
|
||||
}}
|
||||
>
|
||||
<MessageItem
|
||||
isLast={index === messages.length - 1}
|
||||
lastSignature={lastSignature}
|
||||
message={message}
|
||||
onSeen={handleMessageSeen}
|
||||
isTemp={!!message?.isTemp}
|
||||
myAddress={myAddress}
|
||||
onReply={onReply}
|
||||
reply={reply}
|
||||
replyIndex={replyIndex}
|
||||
scrollToItem={(idx) => rowVirtualizer.scrollToIndex(idx)}
|
||||
handleReaction={handleReaction}
|
||||
reactions={reactions}
|
||||
isUpdating={isUpdating}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
{showScrollButton && (
|
||||
<button
|
||||
onClick={() => scrollToBottom()}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
backgroundColor: '#ff5a5f',
|
||||
color: 'white',
|
||||
padding: '10px 20px',
|
||||
borderRadius: '20px',
|
||||
cursor: 'pointer',
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
Scroll to Unread Messages
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
@@ -29,7 +29,8 @@ export const MessageItem = ({
|
||||
scrollToItem,
|
||||
handleReaction,
|
||||
reactions,
|
||||
isUpdating
|
||||
isUpdating,
|
||||
lastSignature
|
||||
}) => {
|
||||
const { ref, inView } = useInView({
|
||||
threshold: 0.7, // Fully visible
|
||||
@@ -42,9 +43,10 @@ export const MessageItem = ({
|
||||
}
|
||||
}, [inView, message.id, message.unread, onSeen]);
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={isLast ? ref : null}
|
||||
ref={lastSignature === message?.signature ? ref : null}
|
||||
style={{
|
||||
padding: "10px",
|
||||
backgroundColor: "#232428",
|
||||
|
Reference in New Issue
Block a user