diff --git a/package-lock.json b/package-lock.json index 9d8df9c..0fd15ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,9 +18,11 @@ "@testing-library/jest-dom": "^6.4.6", "@testing-library/user-event": "^14.5.2", "@tiptap/extension-color": "^2.5.9", + "@tiptap/extension-highlight": "^2.6.6", "@tiptap/extension-image": "^2.6.6", "@tiptap/extension-placeholder": "^2.6.2", "@tiptap/extension-text-style": "^2.5.9", + "@tiptap/extension-underline": "^2.6.6", "@tiptap/pm": "^2.5.9", "@tiptap/react": "^2.5.9", "@tiptap/starter-kit": "^2.5.9", @@ -2300,6 +2302,18 @@ "@tiptap/core": "^2.5.9" } }, + "node_modules/@tiptap/extension-highlight": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-2.6.6.tgz", + "integrity": "sha512-Z02AYWm1AJAfhmfT4fGCI3YitijF4uNu+eiuq7OxhCiVf9IYaq8xlH2YMxa09QvMUo70ovklxk97+vQUUHeqfQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.6.6" + } + }, "node_modules/@tiptap/extension-history": { "version": "2.5.9", "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.5.9.tgz", @@ -2435,6 +2449,18 @@ "@tiptap/core": "^2.5.9" } }, + "node_modules/@tiptap/extension-underline": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-2.6.6.tgz", + "integrity": "sha512-3A4HqsDM/AFb2VaeWACpGexjgI257kz0yU4jNV8uyydDR2KhqeinuEnoSoOmx9T3pL006TWfPg4vaQYPO3qvrQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.6.6" + } + }, "node_modules/@tiptap/pm": { "version": "2.6.6", "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.6.6.tgz", diff --git a/package.json b/package.json index 9ca8095..2f65a5b 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,11 @@ "@testing-library/jest-dom": "^6.4.6", "@testing-library/user-event": "^14.5.2", "@tiptap/extension-color": "^2.5.9", + "@tiptap/extension-highlight": "^2.6.6", "@tiptap/extension-image": "^2.6.6", "@tiptap/extension-placeholder": "^2.6.2", "@tiptap/extension-text-style": "^2.5.9", + "@tiptap/extension-underline": "^2.6.6", "@tiptap/pm": "^2.5.9", "@tiptap/react": "^2.5.9", "@tiptap/starter-kit": "^2.5.9", diff --git a/public/content-script.js b/public/content-script.js index fd2b902..e84cd7d 100644 --- a/public/content-script.js +++ b/public/content-script.js @@ -23,7 +23,7 @@ document.addEventListener('qortalExtensionRequests', async (event) => { })); return } - chrome.runtime.sendMessage({ action: "userInfo" }, (response) => { + chrome?.runtime?.sendMessage({ action: "userInfo" }, (response) => { if (response.error) { document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { detail: { type: "USER_INFO", data: { @@ -38,7 +38,7 @@ document.addEventListener('qortalExtensionRequests', async (event) => { } }); } else if (type === 'REQUEST_IS_INSTALLED') { - chrome.runtime.sendMessage({ action: "version" }, (response) => { + chrome?.runtime?.sendMessage({ action: "version" }, (response) => { if (response.error) { console.error("Error:", response.error); } else { @@ -49,9 +49,8 @@ document.addEventListener('qortalExtensionRequests', async (event) => { } }); } else if (type === 'REQUEST_CONNECTION') { - console.log('REQUEST_CONNECTION') const hostname = window.location.hostname - chrome.runtime.sendMessage({ action: "connection", payload: { + chrome?.runtime?.sendMessage({ action: "connection", payload: { hostname }, timeout }, (response) => { if (response.error) { @@ -75,7 +74,7 @@ document.addEventListener('qortalExtensionRequests', async (event) => { return } - chrome.runtime.sendMessage({ action: "oauth", payload: { + chrome?.runtime?.sendMessage({ action: "oauth", payload: { nodeBaseUrl: payload.nodeBaseUrl, senderAddress: payload.senderAddress, senderPublicKey: payload.senderPublicKey, timestamp: payload.timestamp @@ -105,9 +104,10 @@ document.addEventListener('qortalExtensionRequests', async (event) => { return } - chrome.runtime.sendMessage({ action: "buyOrder", payload: { - qortalAtAddress: payload.qortalAtAddress, - hostname + chrome?.runtime?.sendMessage({ action: "buyOrder", payload: { + qortalAtAddresses: payload.qortalAtAddresses, + hostname, + useLocal: payload?.useLocal }, timeout}, (response) => { if (response.error) { @@ -136,7 +136,7 @@ document.addEventListener('qortalExtensionRequests', async (event) => { })); return } - chrome.runtime.sendMessage({ action: "ltcBalance", payload: { + chrome?.runtime?.sendMessage({ action: "ltcBalance", payload: { hostname }, timeout }, (response) => { @@ -153,6 +153,36 @@ document.addEventListener('qortalExtensionRequests', async (event) => { })); } }); + } else if(type === 'CHECK_IF_LOCAL'){ + + + const hostname = window.location.hostname + const res = await connection(hostname) + if(!res){ + document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { + detail: { type: "USER_INFO", data: { + error: "Not authorized" + }, requestId } + })); + return + } + chrome?.runtime?.sendMessage({ action: "checkLocal", payload: { + hostname + }, timeout }, (response) => { + + if (response.error) { + document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { + detail: { type: "CHECK_IF_LOCAL", data: { + error: response.error + }, requestId } + })); + } else { + // Include the requestId in the detail when dispatching the response + document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { + detail: { type: "CHECK_IF_LOCAL", data: response, requestId } + })); + } + }); } else if (type === 'REQUEST_AUTHENTICATION') { const hostname = window.location.hostname const res = await connection(hostname) @@ -164,7 +194,7 @@ document.addEventListener('qortalExtensionRequests', async (event) => { })); return } - chrome.runtime.sendMessage({ action: "authentication", payload: { + chrome?.runtime?.sendMessage({ action: "authentication", payload: { hostname }, timeout }, (response) => { if (response.error) { @@ -191,7 +221,7 @@ document.addEventListener('qortalExtensionRequests', async (event) => { })); return } - chrome.runtime.sendMessage({ action: "sendQort", payload: { + chrome?.runtime?.sendMessage({ action: "sendQort", payload: { hostname, amount: payload.amount, description: payload.description, @@ -221,7 +251,7 @@ document.addEventListener('qortalExtensionRequests', async (event) => { })); return } - chrome.runtime.sendMessage({ action: "closePopup" }, (response) => { + chrome?.runtime?.sendMessage({ action: "closePopup" }, (response) => { if (response.error) { document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { @@ -241,7 +271,7 @@ document.addEventListener('qortalExtensionRequests', async (event) => { }); -chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { +chrome.runtime?.onMessage.addListener(function(message, sender, sendResponse) { if (message.type === "LOGOUT") { // Notify the web page window.postMessage({ diff --git a/src/App-styles.ts b/src/App-styles.ts index b33bb0a..c2a036f 100644 --- a/src/App-styles.ts +++ b/src/App-styles.ts @@ -77,7 +77,7 @@ display: flex; border: 1px solid var(--50-white, rgba(255, 255, 255, 0.5)); justify-content: space-between; align-items: center; -width: 132px; +width: 140px; height: 25px; padding: 5px 15px 5px 15px; gap: 5px; diff --git a/src/App.tsx b/src/App.tsx index 940fa7c..7208d66 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -41,6 +41,8 @@ import Logout from "./assets/svgs/Logout.svg"; import Return from "./assets/svgs/Return.svg"; import Success from "./assets/svgs/Success.svg"; import Info from "./assets/svgs/Info.svg"; +import CloseIcon from '@mui/icons-material/Close'; + import { createAccount, generateRandomSentence, @@ -67,7 +69,7 @@ import { Spacer } from "./common/Spacer"; import { Loader } from "./components/Loader"; import { PasswordField, ErrorText } from "./components"; import { ChatGroup } from "./components/Chat/ChatGroup"; -import { Group, requestQueueMemberNames } from "./components/Group/Group"; +import { Group, requestQueueMemberNames } from "./components/Group/Group"; import { TaskManger } from "./components/TaskManager/TaskManger"; import { useModal } from "./common/useModal"; import { LoadingButton } from "@mui/lab"; @@ -80,9 +82,10 @@ import { groupApiSocket, groupApiSocketLocal, } from "./background"; -import { executeEvent } from "./utils/events"; +import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "./utils/events"; import { requestQueueCommentCount, requestQueuePublishedAccouncements } from "./components/Chat/GroupAnnouncements"; import { requestQueueGroupJoinRequests } from "./components/Group/GroupJoinRequests"; +import { DrawerComponent } from "./components/Drawer/Drawer"; type extStates = | "not-authenticated" @@ -126,6 +129,28 @@ const defaultValues: MyContextInterface = { message: "", }, }; +export let isMobile = false + +const isMobileDevice = () => { + const userAgent = navigator.userAgent || navigator.vendor || window.opera; + + if (/android/i.test(userAgent)) { + return true; // Android device + } + + if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) { + return true; // iOS device + } + + return false; +}; + +if (isMobileDevice()) { + isMobile = true + console.log("Running on a mobile device"); +} else { + console.log("Running on a desktop"); +} export const allQueues = { requestQueueCommentCount: requestQueueCommentCount, @@ -160,7 +185,7 @@ export const clearAllQueues = () => { export const pauseAllQueues = () => { controlAllQueues('pause'); - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "pauseAllQueues", payload: { @@ -171,7 +196,7 @@ export const pauseAllQueues = () => { } export const resumeAllQueues = () => { controlAllQueues('resume'); - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "resumeAllQueues", payload: { @@ -198,6 +223,24 @@ export const getBaseApiReact = (customApi?: string) => { return groupApi; } }; +// export const getArbitraryEndpointReact = () => { + + +// if (globalApiKey) { +// return `/arbitrary/resources/search`; +// } else { +// return `/arbitrary/resources/searchsimple`; +// } +// }; +export const getArbitraryEndpointReact = () => { + + + if (globalApiKey) { + return `/arbitrary/resources/search`; + } else { + return `/arbitrary/resources/searchsimple`; + } +}; export const getBaseApiReactSocket = (customApi?: string) => { if (customApi) { @@ -266,11 +309,36 @@ function App() { const [openAdvancedSettings, setOpenAdvancedSettings] = useState(false); const [useLocalNode, setUseLocalNode] = useState(false); const [confirmUseOfLocal, setConfirmUseOfLocal] = useState(false); - + const [isOpenDrawerProfile, setIsOpenDrawerProfile] = useState(false); const [apiKey, setApiKey] = useState(""); - + const [isOpenSendQort, setIsOpenSendQort] = useState(false) + const [isOpenSendQortSuccess, setIsOpenSendQortSuccess] = useState(false) + const [rootHeight, setRootHeight] = useState('100%') useEffect(() => { - chrome.runtime.sendMessage({ action: "getApiKey" }, (response) => { + if(!isMobile) return + // Function to set the height of the app to the viewport height + const resetHeight = () => { + const height = window.visualViewport ? window.visualViewport.height : window.innerHeight; + // Set the height to the root element (usually #root) + document.getElementById('root').style.height = height + "px"; + setRootHeight(height + "px") + }; + + // Set the initial height + resetHeight(); + + // Add event listeners for resize and visualViewport changes + window.addEventListener('resize', resetHeight); + window.visualViewport?.addEventListener('resize', resetHeight); + + // Clean up the event listeners when the component unmounts + return () => { + window.removeEventListener('resize', resetHeight); + window.visualViewport?.removeEventListener('resize', resetHeight); + }; + }, []); + useEffect(() => { + chrome?.runtime?.sendMessage({ action: "getApiKey" }, (response) => { if (response) { globalApiKey = response; @@ -304,25 +372,25 @@ function App() { } }; - const checkIfUserHasLocalNode = useCallback(async () => { - try { - const url = `http://127.0.0.1:12391/admin/status`; - const response = await fetch(url, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - const data = await response.json(); - if (data?.isSynchronizing === false && data?.syncPercent === 100) { - setHasLocalNode(true); - } - } catch (error) {} - }, []); + // const checkIfUserHasLocalNode = useCallback(async () => { + // try { + // const url = `http://127.0.0.1:12391/admin/status`; + // const response = await fetch(url, { + // method: "GET", + // headers: { + // "Content-Type": "application/json", + // }, + // }); + // const data = await response.json(); + // if (data?.isSynchronizing === false && data?.syncPercent === 100) { + // setHasLocalNode(true); + // } + // } catch (error) {} + // }, []); - useEffect(() => { - checkIfUserHasLocalNode(); - }, [extState]); + // useEffect(() => { + // checkIfUserHasLocalNode(); + // }, [extState]); const address = useMemo(() => { if (!rawWallet?.address0) return ""; @@ -405,7 +473,7 @@ function App() { }; const storeWalletInfo = (wallet: any) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "storeWalletInfo", wallet }, (response) => { if (response) { @@ -427,7 +495,7 @@ function App() { const getBalanceFunc = () => { setQortBalanceLoading(true); - chrome.runtime.sendMessage({ action: "balance" }, (response) => { + chrome?.runtime?.sendMessage({ action: "balance" }, (response) => { if (!response?.error && !isNaN(+response)) { setBalance(response); } @@ -436,7 +504,7 @@ function App() { }; const getLtcBalanceFunc = () => { setLtcBalanceLoading(true); - chrome.runtime.sendMessage({ action: "ltcBalance" }, (response) => { + chrome?.runtime?.sendMessage({ action: "ltcBalance" }, (response) => { if (!response?.error && !isNaN(+response)) { setLtcBalance(response); } @@ -459,7 +527,7 @@ function App() { return; } setIsLoading(true); - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "sendCoin", payload: { @@ -472,7 +540,9 @@ function App() { if (response?.error) { setSendPaymentError(response.error); } else { - setExtstate("transfer-success-regular"); + setIsOpenSendQort(false) + setIsOpenSendQortSuccess(true) + // setExtstate("transfer-success-regular"); // setSendPaymentSuccess("Payment successfully sent"); } setIsLoading(false); @@ -487,7 +557,7 @@ function App() { useEffect(() => { // Listen for messages from the background script - chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + chrome.runtime?.onMessage.addListener((message, sender, sendResponse) => { // Check if the message is to update the state if ( message.action === "UPDATE_STATE_CONFIRM_SEND_QORT" && @@ -563,7 +633,7 @@ function App() { //param = isDecline const confirmPayment = (isDecline: boolean) => { if (isDecline) { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "sendQortConfirmation", payload: { @@ -586,7 +656,7 @@ function App() { } setIsLoading(true); - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "sendQortConfirmation", payload: { @@ -613,13 +683,14 @@ function App() { const confirmBuyOrder = (isDecline: boolean) => { if (isDecline) { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "buyOrderConfirmation", payload: { crosschainAtInfo: requestBuyOrder?.crosschainAtInfo, interactionId: requestBuyOrder?.interactionId, isDecline: true, + useLocal: requestBuyOrder?.useLocal }, }, (response) => { @@ -630,13 +701,14 @@ function App() { } setIsLoading(true); - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "buyOrderConfirmation", payload: { crosschainAtInfo: requestBuyOrder?.crosschainAtInfo, interactionId: requestBuyOrder?.interactionId, isDecline: false, + useLocal: requestBuyOrder?.useLocal }, }, (response) => { @@ -657,7 +729,7 @@ function App() { hostname: string, interactionId: string ) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "responseToConnectionRequest", payload: { isOkay, hostname, interactionId }, @@ -679,7 +751,7 @@ function App() { useEffect(() => { try { setIsLoading(true); - chrome.runtime.sendMessage({ action: "getWalletInfo" }, (response) => { + chrome?.runtime?.sendMessage({ action: "getWalletInfo" }, (response) => { if (response && response?.walletInfo) { setRawWallet(response?.walletInfo); if ( @@ -708,7 +780,7 @@ function App() { }, 10000); }); } - chrome.runtime.sendMessage({ action: "userInfo" }, (response) => { + chrome?.runtime?.sendMessage({ action: "userInfo" }, (response) => { if (response && !response.error) { setUserInfo(response); } @@ -799,7 +871,7 @@ function App() { crypto.kdfThreads, () => {} ); - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "decryptWallet", payload: { @@ -814,7 +886,7 @@ function App() { wallet, qortAddress: wallet.address0, }); - chrome.runtime.sendMessage({ action: "userInfo" }, (response2) => { + chrome?.runtime?.sendMessage({ action: "userInfo" }, (response2) => { setIsLoading(false); if (response2 && !response2.error) { setUserInfo(response); @@ -835,7 +907,7 @@ function App() { const logoutFunc = () => { try { - chrome.runtime.sendMessage({ action: "logout" }, (response) => { + chrome?.runtime?.sendMessage({ action: "logout" }, (response) => { if (response) { resetAllStates(); executeEvent("logout-event", {}); @@ -854,6 +926,8 @@ function App() { setWalletToBeDownloaded(null); setWalletToBeDownloadedPassword(""); setExtstate("authenticated"); + setIsOpenSendQort(false) + setIsOpenSendQortSuccess(false) }; const resetAllStates = () => { @@ -889,6 +963,11 @@ function App() { setMemberGroups([]) }; + function roundUpToDecimals(number, decimals = 8) { + const factor = Math.pow(10, decimals); // Create a factor based on the number of decimals + return Math.ceil(+number * factor) / factor; + } + const authenticateWallet = async () => { try { setIsLoading(true); @@ -898,7 +977,7 @@ function App() { res(); }, 250); }); - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "decryptWallet", payload: { @@ -911,14 +990,14 @@ function App() { setAuthenticatePassword(""); setExtstate("authenticated"); setWalletToBeDecryptedError(""); - chrome.runtime.sendMessage({ action: "userInfo" }, (response) => { + chrome?.runtime?.sendMessage({ action: "userInfo" }, (response) => { setIsLoading(false); if (response && !response.error) { setUserInfo(response); } }); getBalanceFunc(); - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "getWalletInfo" }, (response) => { if (response && response?.walletInfo) { @@ -979,6 +1058,17 @@ function App() { // Handler for when the window gains focus const handleFocus = () => { setIsFocused(true); + if(isMobile){ + chrome?.runtime?.sendMessage( + { + action: "clearAllNotifications", + payload: { + + }, + } + ); + } + console.log("Webview is focused"); }; @@ -996,6 +1086,16 @@ function App() { const handleVisibilityChange = () => { if (document.visibilityState === "visible") { setIsFocused(true); + if(isMobile){ + chrome?.runtime?.sendMessage( + { + action: "clearAllNotifications", + payload: { + + }, + } + ); + } console.log("Webview is visible"); } else { setIsFocused(false); @@ -1013,6 +1113,22 @@ function App() { }; }, []); + + const openPaymentInternal = (e) => { + const directAddress = e.detail?.address; + const name = e.detail?.name + setIsOpenSendQort(true) + setPaymentTo(name || directAddress) + }; + + useEffect(() => { + subscribeToEvent("openPaymentInternal", openPaymentInternal); + + return () => { + unsubscribeFromEvent("openPaymentInternal", openPaymentInternal); + }; + }, []); + const registerName = async () => { try { if (!userInfo?.address) throw new Error("Your address was not found"); @@ -1023,7 +1139,7 @@ function App() { }); setIsLoadingRegisterName(true); new Promise((res, rej) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "registerName", payload: { @@ -1076,8 +1192,235 @@ function App() { } }; + const renderProfile = ()=> { + return ( + + {isMobile && ( + { + setIsOpenDrawerProfile(false) + }} sx={{ + cursor: 'pointer', + color: 'white' + }} /> + )} + + + + + {authenticatedMode === "ltc" ? ( + <> + + + + + {rawWallet?.ltcAddress?.slice(0, 6)}... + {rawWallet?.ltcAddress?.slice(-4)} + + + + {ltcBalanceLoading && ( + + )} + {!isNaN(+ltcBalance) && !ltcBalanceLoading && ( + + + {ltcBalance} LTC + + + + )} + + ) : ( + <> + + + + {userInfo?.name} + + + + + {rawWallet?.address0?.slice(0, 6)}... + {rawWallet?.address0?.slice(-4)} + + + + {qortBalanceLoading && ( + + )} + {!qortBalanceLoading && balance >= 0 && ( + + + {balance?.toFixed(2)} QORT + + + + )} + + + {userInfo && !userInfo?.name && ( + { + setOpenRegisterName(true); + }} + > + REGISTER NAME + + )} + + { + setIsOpenSendQort(true) + // setExtstate("send-qort"); + setIsOpenDrawerProfile(false) + }} + > + Transfer QORT + + + )} + { + chrome.tabs.create({ url: "https://www.qort.trade" }); + }} + > + Get QORT at qort.trade + + + + + { + setExtstate("download-wallet"); + setIsOpenDrawerProfile(false) + }} + src={Download} + style={{ + cursor: "pointer", + }} + /> + {!isMobile && ( + <> + + { + logoutFunc() + setIsOpenDrawerProfile(false) + }} + style={{ + cursor: "pointer", + }} + /> + + )} + + + {authenticatedMode === "qort" && ( + { + setAuthenticatedMode("ltc"); + }} + src={ltcLogo} + style={{ + cursor: "pointer", + width: "20px", + height: "auto", + }} + /> + )} + {authenticatedMode === "ltc" && ( + { + setAuthenticatedMode("qort"); + }} + src={qortLogo} + style={{ + cursor: "pointer", + width: "20px", + height: "auto", + }} + /> + )} + + + ) + } + return ( - + {/* {extState === 'group' && ( )} */} @@ -1150,7 +1493,7 @@ function App() { }} /> - {hasLocalNode && ( + <> { const valueToSet = !confirmUseOfLocal const payload = valueToSet ? apiKey : null - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "setApiKey", payload }, (response) => { if (response) { @@ -1260,13 +1603,13 @@ function App() { )} - )} + )} {/* {extState !== "not-authenticated" && ( )} */} - {extState === "authenticated" && isMainWindow && ( + {extState === "authenticated" && isMainWindow && ( - - - - - {authenticatedMode === "ltc" ? ( - <> - - - - - {rawWallet?.ltcAddress?.slice(0, 6)}... - {rawWallet?.ltcAddress?.slice(-4)} - - - - {ltcBalanceLoading && ( - - )} - {!isNaN(+ltcBalance) && !ltcBalanceLoading && ( - - - {ltcBalance} LTC - - - - )} - - ) : ( - <> - - - - {userInfo?.name} - - - - - {rawWallet?.address0?.slice(0, 6)}... - {rawWallet?.address0?.slice(-4)} - - - - {qortBalanceLoading && ( - - )} - {!qortBalanceLoading && balance >= 0 && ( - - - {balance?.toFixed(2)} QORT - - - - )} - - - {userInfo && !userInfo?.name && ( - { - setOpenRegisterName(true); - }} - > - REGISTER NAME - - )} - - { - setExtstate("send-qort"); - }} - > - Transfer QORT - - - )} - { - chrome.tabs.create({ url: "https://www.qort.trade" }); - }} - > - Get QORT at qort.trade - - - - - { - setExtstate("download-wallet"); - }} - src={Download} - style={{ - cursor: "pointer", - }} - /> - - - - {authenticatedMode === "qort" && ( - { - setAuthenticatedMode("ltc"); - }} - src={ltcLogo} - style={{ - cursor: "pointer", - width: "20px", - height: "auto", - }} - /> - )} - {authenticatedMode === "ltc" && ( - { - setAuthenticatedMode("qort"); - }} - src={qortLogo} - style={{ - cursor: "pointer", - width: "20px", - height: "auto", - }} - /> - )} - - - - - + {!isMobile && renderProfile()} + + {!isMobile && ( + + + + )} + )} - {extState === "send-qort" && isMainWindow && ( - <> + {isOpenSendQort && isMainWindow && ( + Send - + )} {extState === "web-app-request-buy-order" && !isMainWindow && ( <> @@ -1625,7 +1792,7 @@ function App() { > The Application

{" "} {requestBuyOrder?.hostname}

- is requesting a buy order + is requesting {requestBuyOrder?.crosschainAtInfo?.length} {`buy order${requestBuyOrder?.crosschainAtInfo.length === 1 ? '' : 's'}`} - {+requestBuyOrder?.crosschainAtInfo?.qortAmount} QORT + {requestBuyOrder?.crosschainAtInfo?.reduce((latest, cur)=> { + return latest + +cur?.qortAmount + }, 0)} QORT - {requestBuyOrder?.crosschainAtInfo?.expectedForeignAmount}{" "} - {requestBuyOrder?.crosschainAtInfo?.foreignBlockchain} + {roundUpToDecimals(requestBuyOrder?.crosschainAtInfo?.reduce((latest, cur)=> { + return latest + +cur?.expectedForeignAmount + }, 0))} + {` ${requestBuyOrder?.crosschainAtInfo?.[0]?.foreignBlockchain}`} {/* @@ -2145,8 +2316,17 @@ function App() { )} )} - {extState === "transfer-success-regular" && ( - <> + {isOpenSendQortSuccess && ( + @@ -2166,7 +2346,7 @@ function App() { > Continue - + )} {extState === "transfer-success-request" && ( <> @@ -2316,6 +2496,7 @@ function App() { info={infoSnack} setInfo={setInfoSnack} /> + {renderProfile()}
); } diff --git a/src/MessageQueueContext.tsx b/src/MessageQueueContext.tsx index 2284da0..166a4c3 100644 --- a/src/MessageQueueContext.tsx +++ b/src/MessageQueueContext.tsx @@ -12,12 +12,11 @@ export const MessageQueueProvider = ({ children }) => { const [queueChats, setQueueChats] = useState({}); // Stores chats and status for display const isProcessingRef = useRef(false); // To track if the queue is being processed const maxRetries = 4; - const clearStatesMessageQueueProvider = useCallback(() => { - setQueueChats({}) - messageQueue = [] - isProcessingRef.current = false - }, []) + setQueueChats({}); + messageQueue = []; + isProcessingRef.current = false; + }, []); // Function to add a message to the queue const addToQueue = useCallback((sendMessageFunc, messageObj, type, groupDirectId) => { @@ -40,16 +39,43 @@ export const MessageQueueProvider = ({ children }) => { // Add the message to the global messageQueue messageQueue = [ ...messageQueue, - { func: sendMessageFunc, identifier: tempId, groupDirectId } + { func: sendMessageFunc, identifier: tempId, groupDirectId, specialId: messageObj?.message?.specialId } ]; // Start processing the queue if not already processing processQueue(); }, []); - // Function to process the messageQueue - // Function to process the messageQueue -const processQueue = useCallback(async () => { + // Method to process with new messages and groupDirectId + const processWithNewMessages = (newMessages, groupDirectId) => { + processQueue(newMessages, groupDirectId); + }; + + // Function to process the messageQueue and handle new messages + const processQueue = useCallback(async (newMessages = [], groupDirectId) => { + // Filter out any message in the queue that matches the specialId from newMessages + messageQueue = messageQueue.filter((msg) => { + return !newMessages.some(newMsg => newMsg?.specialId === msg?.specialId); + }); + + // Remove any corresponding entries in queueChats for the provided groupDirectId + setQueueChats((prev) => { + const updatedChats = { ...prev }; + if (updatedChats[groupDirectId]) { + // Remove any message in queueChats that has a matching specialId + updatedChats[groupDirectId] = updatedChats[groupDirectId].filter((chat) => { + + return !newMessages.some(newMsg => newMsg?.specialId === chat?.message?.specialId); + }); + + // If no more chats for this group, delete the groupDirectId entry + if (updatedChats[groupDirectId].length === 0) { + delete updatedChats[groupDirectId]; + } + } + return updatedChats; + }); + // If currently processing or the queue is empty, return if (isProcessingRef.current || messageQueue.length === 0) return; @@ -77,18 +103,17 @@ const processQueue = useCallback(async () => { // Execute the function stored in the messageQueue await currentMessage.func(); - // Remove the message from the messageQueue after successful sending - messageQueue = messageQueue.slice(1); + messageQueue = messageQueue.slice(1); // Slice here remains for successful messages // Remove the message from queueChats after success - setQueueChats((prev) => { - const updatedChats = { ...prev }; - updatedChats[groupDirectId] = updatedChats[groupDirectId].filter( - (item) => item.identifier !== identifier - ); - return updatedChats; - }); + // setQueueChats((prev) => { + // const updatedChats = { ...prev }; + // updatedChats[groupDirectId] = updatedChats[groupDirectId].filter( + // (item) => item.identifier !== identifier + // ); + // return updatedChats; + // }); } catch (error) { console.error('Message sending failed', error); @@ -109,7 +134,7 @@ const processQueue = useCallback(async () => { updatedChats[groupDirectId][chatIndex].status = 'failed-permanent'; // Remove the message from the messageQueue after max retries - messageQueue = messageQueue.slice(1); + messageQueue = messageQueue.slice(1); // Slice for failed messages after max retries // Remove the message from queueChats after failure updatedChats[groupDirectId] = updatedChats[groupDirectId].filter( @@ -129,9 +154,8 @@ const processQueue = useCallback(async () => { isProcessingRef.current = false; }, []); - return ( - + {children} ); diff --git a/src/assets/Icons/ArrowDownIcon.tsx b/src/assets/Icons/ArrowDownIcon.tsx new file mode 100644 index 0000000..7e20634 --- /dev/null +++ b/src/assets/Icons/ArrowDownIcon.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +export const ArrowDownIcon= ({ color = 'white', height, width }) => { + return ( + + + + + + + + ); + }; + \ No newline at end of file diff --git a/src/assets/Icons/ChatIcon.tsx b/src/assets/Icons/ChatIcon.tsx new file mode 100644 index 0000000..f68ed35 --- /dev/null +++ b/src/assets/Icons/ChatIcon.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +export const ChatIcon= ({ color = 'white', height = 15, width = 15 }) => { + return ( + + + + + + ); + }; + \ No newline at end of file diff --git a/src/assets/Icons/ExitIcon.tsx b/src/assets/Icons/ExitIcon.tsx new file mode 100644 index 0000000..cfbeefa --- /dev/null +++ b/src/assets/Icons/ExitIcon.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +export const ExitIcon= ({ color = 'white', height, width }) => { + return ( + + + + + + + ); + }; + \ No newline at end of file diff --git a/src/assets/Icons/HomeIcon.tsx b/src/assets/Icons/HomeIcon.tsx new file mode 100644 index 0000000..38f880d --- /dev/null +++ b/src/assets/Icons/HomeIcon.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +export const HomeIcon= ({ color, height = 20, width = 23 }) => { + return ( + + + + + + ); + }; + \ No newline at end of file diff --git a/src/assets/Icons/HubsIcon.tsx b/src/assets/Icons/HubsIcon.tsx new file mode 100644 index 0000000..b96ba8a --- /dev/null +++ b/src/assets/Icons/HubsIcon.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +export const HubsIcon= ({ color, height = 31, width = 32 }) => { + return ( + + + + + + ); + }; + \ No newline at end of file diff --git a/src/assets/Icons/LogoutIcon.tsx b/src/assets/Icons/LogoutIcon.tsx new file mode 100644 index 0000000..9288de1 --- /dev/null +++ b/src/assets/Icons/LogoutIcon.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +export const LogoutIcon= ({ color, height = 20, width = 18}) => { + return ( + + + + + + ); + }; + \ No newline at end of file diff --git a/src/assets/Icons/MembersIcon.tsx b/src/assets/Icons/MembersIcon.tsx new file mode 100644 index 0000000..8bbc0e1 --- /dev/null +++ b/src/assets/Icons/MembersIcon.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +export const MembersIcon= ({ color = 'white', height = 9, width = 15 }) => { + return ( + + + + + + + + + + + + + ); + }; + \ No newline at end of file diff --git a/src/assets/Icons/MessagingIcon.tsx b/src/assets/Icons/MessagingIcon.tsx new file mode 100644 index 0000000..0de459b --- /dev/null +++ b/src/assets/Icons/MessagingIcon.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +export const MessagingIcon= ({ color, height = 31, width = 31 }) => { + return ( + + + + + + + ); + }; + \ No newline at end of file diff --git a/src/assets/Icons/MessagingIcon2.tsx b/src/assets/Icons/MessagingIcon2.tsx new file mode 100644 index 0000000..5cbdb98 --- /dev/null +++ b/src/assets/Icons/MessagingIcon2.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +export const MessagingIcon2= ({ color = '#8F8F91', height = 24, width =24 }) => { + return ( + + + + + + + + ); + }; + \ No newline at end of file diff --git a/src/assets/Icons/NotificationIcon.tsx b/src/assets/Icons/NotificationIcon.tsx new file mode 100644 index 0000000..cc9092c --- /dev/null +++ b/src/assets/Icons/NotificationIcon.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +export const NotificationIcon= ({ color = 'white', height = 16, width = 14 }) => { + return ( + + + + + + + + ); + }; + \ No newline at end of file diff --git a/src/assets/Icons/NotificationIcon2.tsx b/src/assets/Icons/NotificationIcon2.tsx new file mode 100644 index 0000000..10d7a03 --- /dev/null +++ b/src/assets/Icons/NotificationIcon2.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +export const NotificationIcon2= ({ color = 'white', height = 15, width = 15 }) => { + return ( + + + + + ); + }; + \ No newline at end of file diff --git a/src/assets/Icons/ReturnIcon.tsx b/src/assets/Icons/ReturnIcon.tsx new file mode 100644 index 0000000..633eafc --- /dev/null +++ b/src/assets/Icons/ReturnIcon.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +export const ReturnIcon= ({ color = 'white', height, width }) => { + return ( + + + + + + + + ); + }; + \ No newline at end of file diff --git a/src/assets/Icons/ThreadsIcon.tsx b/src/assets/Icons/ThreadsIcon.tsx new file mode 100644 index 0000000..86dfeaa --- /dev/null +++ b/src/assets/Icons/ThreadsIcon.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +export const ThreadsIcon= ({ color = 'white', height = 11, width = 15 }) => { + return ( + + + + + + + ); + }; + \ No newline at end of file diff --git a/src/assets/Icons/TradingIcon.tsx b/src/assets/Icons/TradingIcon.tsx new file mode 100644 index 0000000..2c72300 --- /dev/null +++ b/src/assets/Icons/TradingIcon.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +export const TradingIcon= ({ color, height, width }) => { + return ( + + + + + ); + }; + \ No newline at end of file diff --git a/src/assets/Icons/WalletIcon.tsx b/src/assets/Icons/WalletIcon.tsx new file mode 100644 index 0000000..ff993d4 --- /dev/null +++ b/src/assets/Icons/WalletIcon.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +export const WalletIcon= ({ color, height, width }) => { + return ( + + + + + + ); + }; + \ No newline at end of file diff --git a/src/assets/svgs/BottomLogo.svg b/src/assets/svgs/BottomLogo.svg new file mode 100644 index 0000000..74848da --- /dev/null +++ b/src/assets/svgs/BottomLogo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/svgs/BottomLogo2.svg b/src/assets/svgs/BottomLogo2.svg new file mode 100644 index 0000000..3a7fcea --- /dev/null +++ b/src/assets/svgs/BottomLogo2.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/svgs/BottomLogo3.svg b/src/assets/svgs/BottomLogo3.svg new file mode 100644 index 0000000..67a0480 --- /dev/null +++ b/src/assets/svgs/BottomLogo3.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/svgs/BottomLogo4.svg b/src/assets/svgs/BottomLogo4.svg new file mode 100644 index 0000000..562ff64 --- /dev/null +++ b/src/assets/svgs/BottomLogo4.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/svgs/BottomLogo5.svg b/src/assets/svgs/BottomLogo5.svg new file mode 100644 index 0000000..89256fe --- /dev/null +++ b/src/assets/svgs/BottomLogo5.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/svgs/Forum.svg b/src/assets/svgs/Forum.svg new file mode 100644 index 0000000..7957b06 --- /dev/null +++ b/src/assets/svgs/Forum.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/background.ts b/src/background.ts index 9539e5b..053bb0e 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,5 +1,6 @@ // @ts-nocheck // import { encryptAndPublishSymmetricKeyGroupChat } from "./backgroundFunctions/encryption"; +import { constant, isArray } from "lodash"; import { decryptGroupEncryption, encryptAndPublishSymmetricKeyGroupChat, @@ -24,24 +25,46 @@ import PhraseWallet from "./utils/generateWallet/phrase-wallet"; import { RequestQueueWithPromise } from "./utils/queue/queue"; import { validateAddress } from "./utils/validateAddress"; import { Sha256 } from "asmcrypto.js"; +import { TradeBotRespondMultipleRequest } from "./transactions/TradeBotRespondMultipleRequest"; -let lastGroupNotification -export const groupApi = "https://ext-node.qortal.link" -export const groupApiSocket = "wss://ext-node.qortal.link" -export const groupApiLocal = "http://127.0.0.1:12391" -export const groupApiSocketLocal = "ws://127.0.0.1:12391" -const timeDifferenceForNotificationChatsBackground = 600000 -const requestQueueAnnouncements = new RequestQueueWithPromise(1) +let lastGroupNotification; +export const groupApi = "https://ext-node.qortal.link"; +export const groupApiSocket = "wss://ext-node.qortal.link"; +export const groupApiLocal = "http://127.0.0.1:12391"; +export const groupApiSocketLocal = "ws://127.0.0.1:12391"; +const timeDifferenceForNotificationChatsBackground = 600000; +const requestQueueAnnouncements = new RequestQueueWithPromise(1); +let isMobile = false; +const isMobileDevice = () => { + const userAgent = navigator.userAgent || navigator.vendor || window.opera; + + if (/android/i.test(userAgent)) { + return true; // Android device + } + + if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) { + return true; // iOS device + } + + return false; +}; + +if (isMobileDevice()) { + isMobile = true; + console.log("Running on a mobile device"); +} else { + console.log("Running on a desktop"); +} const allQueues = { requestQueueAnnouncements: requestQueueAnnouncements, -} +}; const controlAllQueues = (action) => { Object.keys(allQueues).forEach((key) => { const val = allQueues[key]; try { - if (typeof val[action] === 'function') { + if (typeof val[action] === "function") { val[action](); } } catch (error) { @@ -59,16 +82,18 @@ export const clearAllQueues = () => { console.error(error); } }); -} +}; - const pauseAllQueues = () => controlAllQueues('pause'); - const resumeAllQueues = () => controlAllQueues('resume'); -const checkDifference = (createdTimestamp)=> { - return (Date.now() - createdTimestamp) < timeDifferenceForNotificationChatsBackground -} +const pauseAllQueues = () => controlAllQueues("pause"); +const resumeAllQueues = () => controlAllQueues("resume"); +const checkDifference = (createdTimestamp) => { + return ( + Date.now() - createdTimestamp < timeDifferenceForNotificationChatsBackground + ); +}; const getApiKeyFromStorage = async () => { return new Promise((resolve, reject) => { - chrome.storage.local.get('apiKey', (result) => { + chrome.storage.local.get("apiKey", (result) => { if (chrome.runtime.lastError) { return reject(chrome.runtime.lastError); } @@ -77,9 +102,24 @@ const getApiKeyFromStorage = async () => { }); }; +// const getArbitraryEndpoint = ()=> { +// const apiKey = await getApiKeyFromStorage(); // Retrieve apiKey asynchronously +// if (apiKey) { +// return `/arbitrary/resources/search`; +// } else { +// return `/arbitrary/resources/searchsimple`; +// } +// } +const getArbitraryEndpoint = async () => { + const apiKey = await getApiKeyFromStorage(); // Retrieve apiKey asynchronously + if (apiKey) { + return `/arbitrary/resources/search`; + } else { + return `/arbitrary/resources/searchsimple`; + } +}; + export const getBaseApi = async (customApi?: string) => { - - if (customApi) { return customApi; } @@ -94,7 +134,7 @@ export const getBaseApi = async (customApi?: string) => { export const createEndpointSocket = async (endpoint) => { const apiKey = await getApiKeyFromStorage(); // Retrieve apiKey asynchronously - + if (apiKey) { return `${groupApiSocketLocal}${endpoint}`; } else { @@ -111,14 +151,13 @@ export const createEndpoint = async (endpoint, customApi) => { if (apiKey) { // Check if the endpoint already contains a query string - const separator = endpoint.includes('?') ? '&' : '?'; + const separator = endpoint.includes("?") ? "&" : "?"; return `${groupApiLocal}${endpoint}${separator}apiKey=${apiKey}`; } else { return `${groupApi}${endpoint}`; } }; - export const walletVersion = 2; // List of your API endpoints const apiEndpoints = [ @@ -136,14 +175,14 @@ const buyTradeNodeBaseUrl = "https://appnode.qortal.org"; const proxyAccountAddress = "QXPejUe5Za1KD3zCMViWCX35AreMQ9H7ku"; const proxyAccountPublicKey = "5hP6stDWybojoDw5t8z9D51nV945oMPX7qBd29rhX1G7"; const pendingResponses = new Map(); -let groups = null +let groups = null; -let socket +let socket; let timeoutId; let groupSocketTimeout; let socketTimeout: any; -let interval -let intervalThreads +let interval; +let intervalThreads; // Function to check each API endpoint export async function findUsableApi() { for (const endpoint of apiEndpoints) { @@ -166,6 +205,24 @@ export async function findUsableApi() { throw new Error("No usable API found"); } +export function isExtMsg(data) { + let isMsgFromExtensionGroup = true; + try { + const decode1 = atob(data); + const decode2 = atob(decode1); + const keyStr = decode2.slice(0, 10); + + // Convert the key string back to a number + const highestKey = parseInt(keyStr, 10); + if (isNaN(highestKey)) { + isMsgFromExtensionGroup = false; + } + } catch (error) { + isMsgFromExtensionGroup = false; + } + + return isMsgFromExtensionGroup; +} async function checkWebviewFocus() { return new Promise((resolve) => { @@ -175,7 +232,7 @@ async function checkWebviewFocus() { }, 1000); // Send message to the content script to check focus - chrome.runtime.sendMessage({ action: 'CHECK_FOCUS' }, (response) => { + chrome.runtime.sendMessage({ action: "CHECK_FOCUS" }, (response) => { clearTimeout(timeout); // Clear the timeout if we get a response if (chrome.runtime.lastError) { @@ -188,180 +245,212 @@ async function checkWebviewFocus() { } function playNotificationSound() { - chrome.runtime.sendMessage({ action: 'PLAY_NOTIFICATION_SOUND' }); + // chrome.runtime.sendMessage({ action: "PLAY_NOTIFICATION_SOUND" }); } -const handleNotificationDirect = async (directs)=> { - let isFocused +const handleNotificationDirect = async (directs) => { + let isFocused; const wallet = await getSaveWallet(); const address = wallet.address0; - const dataDirects = directs.filter((direct)=> direct?.sender !== address) + const dataDirects = directs.filter((direct) => direct?.sender !== address); try { + if (!dataDirects || dataDirects?.length === 0) return; + isFocused = await checkWebviewFocus(); - isFocused = await checkWebviewFocus() - - if(isFocused){ - throw new Error('isFocused') + if (isFocused) { + throw new Error("isFocused"); } - const newActiveChats= dataDirects - const oldActiveChats = await getChatHeadsDirect() + const newActiveChats = dataDirects; + const oldActiveChats = await getChatHeadsDirect(); -if(newActiveChats?.length === 0) return + if (newActiveChats?.length === 0) return; -let newestLatestTimestamp -let oldestLatestTimestamp -// Find the latest timestamp from newActiveChats -newActiveChats?.forEach(newChat => { -if (!newestLatestTimestamp || newChat?.timestamp > newestLatestTimestamp?.timestamp) { - newestLatestTimestamp = newChat; -} -}); + let newestLatestTimestamp; + let oldestLatestTimestamp; + // Find the latest timestamp from newActiveChats + newActiveChats?.forEach((newChat) => { + if ( + !newestLatestTimestamp || + newChat?.timestamp > newestLatestTimestamp?.timestamp + ) { + newestLatestTimestamp = newChat; + } + }); -// Find the latest timestamp from oldActiveChats -oldActiveChats?.forEach(oldChat => { -if (!oldestLatestTimestamp || oldChat?.timestamp > oldestLatestTimestamp?.timestamp) { - oldestLatestTimestamp = oldChat; -} -}); + // Find the latest timestamp from oldActiveChats + oldActiveChats?.forEach((oldChat) => { + if ( + !oldestLatestTimestamp || + oldChat?.timestamp > oldestLatestTimestamp?.timestamp + ) { + oldestLatestTimestamp = oldChat; + } + }); - - if(checkDifference(newestLatestTimestamp.timestamp) && !oldestLatestTimestamp || (newestLatestTimestamp && newestLatestTimestamp?.timestamp > oldestLatestTimestamp?.timestamp)){ - const notificationId = 'chat_notification_' + Date.now() + '_type=direct' + `_from=${newestLatestTimestamp.address}`; - chrome.notifications.create(notificationId, { - type: 'basic', - iconUrl: 'qort.png', // Add an appropriate icon for chat notifications - title: `New Direct message! ${newestLatestTimestamp?.name && `from ${newestLatestTimestamp.name}`}`, - message: 'You have received a new direct message', - priority: 2, // Use the maximum priority to ensure it's noticeable - // buttons: [ - // { title: 'Go to group' } - // ] - }); - setTimeout(() => { - chrome.notifications.clear(notificationId); - }, 7000); - // chrome.runtime.sendMessage( - // { - // action: "notification", - // payload: { - // }, - // } - // ) - // audio.play(); - playNotificationSound() - - - - } - - } catch (error) { - - if(!isFocused){ - chrome.runtime.sendMessage( - { - action: "notification", - payload: { - }, - }, - (response) => { - - if (!response?.error) { - - } - - } - ); - const notificationId = 'chat_notification_' + Date.now() + if ( + (checkDifference(newestLatestTimestamp.timestamp) && + !oldestLatestTimestamp) || + (newestLatestTimestamp && + newestLatestTimestamp?.timestamp > oldestLatestTimestamp?.timestamp) + ) { + const notificationId = + "chat_notification_" + + Date.now() + + "_type=direct" + + `_from=${newestLatestTimestamp.address}`; chrome.notifications.create(notificationId, { - type: 'basic', - iconUrl: 'qort.png', // Add an appropriate icon for chat notifications - title: `New Direct message!`, - message: 'You have received a new direct message', + type: "basic", + iconUrl: "qort.png", // Add an appropriate icon for chat notifications + title: `New Direct message! ${ + newestLatestTimestamp?.name && `from ${newestLatestTimestamp.name}` + }`, + message: "You have received a new direct message", priority: 2, // Use the maximum priority to ensure it's noticeable // buttons: [ // { title: 'Go to group' } // ] }); - setTimeout(() => { - chrome.notifications.clear(notificationId); - }, 7000); - playNotificationSound() - // audio.play(); - // } - } - - } finally { - setChatHeadsDirect(dataDirects) + if (!isMobile) { + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 7000); + } + // chrome.runtime.sendMessage( // { - // action: "setChatHeads", + // action: "notification", // payload: { - // data, // }, // } - // ); - + // ) + // audio.play(); + playNotificationSound(); + } + } catch (error) { + if (!isFocused) { + chrome.runtime.sendMessage( + { + action: "notification", + payload: {}, + }, + (response) => { + if (!response?.error) { + } + } + ); + const notificationId = "chat_notification_" + Date.now(); + chrome.notifications.create(notificationId, { + type: "basic", + iconUrl: "qort.png", // Add an appropriate icon for chat notifications + title: `New Direct message!`, + message: "You have received a new direct message", + priority: 2, // Use the maximum priority to ensure it's noticeable + // buttons: [ + // { title: 'Go to group' } + // ] + }); + if (!isMobile) { + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 7000); + } + playNotificationSound(); + // audio.play(); + // } + } + } finally { + setChatHeadsDirect(dataDirects); + // chrome.runtime.sendMessage( + // { + // action: "setChatHeads", + // payload: { + // data, + // }, + // } + // ); } -} -async function getThreadActivity(){ +}; +async function getThreadActivity() { const wallet = await getSaveWallet(); const address = wallet.address0; - const key = `threadactivity-${address}` + const key = `threadactivity-${address}`; const res = await chrome.storage.local.get([key]); -if (res?.[key]) { - const parsedData = JSON.parse(res[key]) - return parsedData; -} else { - return null -} + if (res?.[key]) { + const parsedData = JSON.parse(res[key]); + return parsedData; + } else { + return null; + } } -async function updateThreadActivity({threadId, qortalName, groupId, thread}) { +async function updateThreadActivity({ threadId, qortalName, groupId, thread }) { const wallet = await getSaveWallet(); const address = wallet.address0; const ONE_WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000; // One week in milliseconds - let lastResetTime = 0 + let lastResetTime = 0; // Retrieve the last reset timestamp from storage - const key = `threadactivity-${address}` + const key = `threadactivity-${address}`; chrome.storage.local.get([key], (data) => { - let threads - + let threads; + if (!data[key] || Object.keys(data?.[key]?.length === 0)) { - threads = { createdThreads: [], mostVisitedThreads: [], recentThreads: [] }; + threads = { + createdThreads: [], + mostVisitedThreads: [], + recentThreads: [], + }; } else { - threads = JSON.parse(data[key]) + threads = JSON.parse(data[key]); } - if(threads?.lastResetTime){ - lastResetTime = threads.lastResetTime + if (threads?.lastResetTime) { + lastResetTime = threads.lastResetTime; } - + const currentTime = Date.now(); - + // Check if a week has passed since the last reset if (!lastResetTime || currentTime - lastResetTime > ONE_WEEK_IN_MS) { // Reset the visit counts for all most visited threads - threads.mostVisitedThreads.forEach(thread => thread.visitCount = 0); + threads.mostVisitedThreads.forEach((thread) => (thread.visitCount = 0)); lastResetTime = currentTime; // Update the last reset time - threads.lastResetTime = lastResetTime + threads.lastResetTime = lastResetTime; } // Update the recent threads list - threads.recentThreads = threads.recentThreads.filter(t => t.threadId !== threadId); - threads.recentThreads.unshift({ threadId, qortalName, groupId, thread, visitCount: 1, lastVisited: Date.now() }); - + threads.recentThreads = threads.recentThreads.filter( + (t) => t.threadId !== threadId + ); + threads.recentThreads.unshift({ + threadId, + qortalName, + groupId, + thread, + visitCount: 1, + lastVisited: Date.now(), + }); + // Sort the recent threads by lastVisited time (descending) threads.recentThreads.sort((a, b) => b.lastVisited - a.lastVisited); // Limit the recent threads list to 2 items threads.recentThreads = threads.recentThreads.slice(0, 2); // Update the most visited threads list - const existingThread = threads.mostVisitedThreads.find(t => t.threadId === threadId); + const existingThread = threads.mostVisitedThreads.find( + (t) => t.threadId === threadId + ); if (existingThread) { existingThread.visitCount += 1; existingThread.lastVisited = Date.now(); // Update the last visited time as well } else { - threads.mostVisitedThreads.push({ threadId, qortalName, groupId, thread, visitCount: 1, lastVisited: Date.now() }); + threads.mostVisitedThreads.push({ + threadId, + qortalName, + groupId, + thread, + visitCount: 1, + lastVisited: Date.now(), + }); } // Sort the most visited threads by visitCount (descending) @@ -371,65 +460,90 @@ async function updateThreadActivity({threadId, qortalName, groupId, thread}) { // Store the updated thread information and last reset time // chrome.storage.local.set({ threads, lastResetTime }); - + const dataString = JSON.stringify(threads); - chrome.storage.local.set({ [`threadactivity-${address}`]: dataString }) + chrome.storage.local.set({ [`threadactivity-${address}`]: dataString }); }); } - -const handleNotification = async (groups)=> { +const handleNotification = async (groups) => { const wallet = await getSaveWallet(); const address = wallet.address0; - let isFocused - const data = groups.filter((group)=> group?.sender !== address) + let mutedGroups = await getUserSettings({key: 'mutedGroups'}) || [] + if(!isArray(mutedGroups)) mutedGroups = [] + + let isFocused; + const data = groups.filter((group) => group?.sender !== address && !mutedGroups.includes(group.groupId)); try { - if(!data || data?.length === 0) return - isFocused = await checkWebviewFocus() - - if(isFocused){ - throw new Error('isFocused') + if (!data || data?.length === 0) return; + isFocused = await checkWebviewFocus(); + + if (isFocused) { + throw new Error("isFocused"); } - const newActiveChats= data - const oldActiveChats = await getChatHeads() + const newActiveChats = data; + const oldActiveChats = await getChatHeads(); + let results = []; + let newestLatestTimestamp; + let oldestLatestTimestamp; + // Find the latest timestamp from newActiveChats + newActiveChats?.forEach((newChat) => { + if ( + !newestLatestTimestamp || + newChat?.timestamp > newestLatestTimestamp?.timestamp + ) { + newestLatestTimestamp = newChat; + } + }); + + // Find the latest timestamp from oldActiveChats + oldActiveChats?.forEach((oldChat) => { + if ( + !oldestLatestTimestamp || + oldChat?.timestamp > oldestLatestTimestamp?.timestamp + ) { + oldestLatestTimestamp = oldChat; + } + }); + + if ( + (checkDifference(newestLatestTimestamp.timestamp) && + !oldestLatestTimestamp) || + (newestLatestTimestamp && + newestLatestTimestamp?.timestamp > oldestLatestTimestamp?.timestamp) + ) { + if ( + !lastGroupNotification || + Date.now() - lastGroupNotification >= 120000 + ) { + if ( + !newestLatestTimestamp?.data || + !isExtMsg(newestLatestTimestamp?.data) + ) + return; + + const notificationId = + "chat_notification_" + + Date.now() + + "_type=group" + + `_from=${newestLatestTimestamp.groupId}`; - let results = [] - let newestLatestTimestamp - let oldestLatestTimestamp - // Find the latest timestamp from newActiveChats - newActiveChats?.forEach(newChat => { - if (!newestLatestTimestamp || newChat?.timestamp > newestLatestTimestamp?.timestamp) { - newestLatestTimestamp = newChat; - } - }); - - // Find the latest timestamp from oldActiveChats - oldActiveChats?.forEach(oldChat => { - if (!oldestLatestTimestamp || oldChat?.timestamp > oldestLatestTimestamp?.timestamp) { - oldestLatestTimestamp = oldChat; - } - }); - - - if(checkDifference(newestLatestTimestamp.timestamp) && !oldestLatestTimestamp || (newestLatestTimestamp && newestLatestTimestamp?.timestamp > oldestLatestTimestamp?.timestamp)){ - if (!lastGroupNotification || ((Date.now() - lastGroupNotification) >= 120000)) { - - const notificationId = 'chat_notification_' + Date.now() + '_type=group' + `_from=${newestLatestTimestamp.groupId}`; - chrome.notifications.create(notificationId, { - type: 'basic', - iconUrl: 'qort.png', // Add an appropriate icon for chat notifications - title: 'New Group Message!', + type: "basic", + iconUrl: "qort.png", // Add an appropriate icon for chat notifications + title: "New Group Message!", message: `You have received a new message from ${newestLatestTimestamp?.groupName}`, priority: 2, // Use the maximum priority to ensure it's noticeable // buttons: [ // { title: 'Go to group' } // ] }); - setTimeout(() => { - chrome.notifications.clear(notificationId); - }, 7000); + if (!isMobile) { + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 7000); + } // chrome.runtime.sendMessage( // { // action: "notification", @@ -438,175 +552,173 @@ const handleNotification = async (groups)=> { // } // ) // audio.play(); - playNotificationSound() - lastGroupNotification = Date.now() - + playNotificationSound(); + lastGroupNotification = Date.now(); } } - } catch (error) { - - if(!isFocused){ + if (!isFocused) { chrome.runtime.sendMessage( { action: "notification", - payload: { - }, + payload: {}, }, (response) => { - if (!response?.error) { - } - } ); - const notificationId = 'chat_notification_' + Date.now(); + const notificationId = "chat_notification_" + Date.now(); chrome.notifications.create(notificationId, { - type: 'basic', - iconUrl: 'qort.png', // Add an appropriate icon for chat notifications - title: 'New Group Message!', - message: 'You have received a new message from one of your groups', + type: "basic", + iconUrl: "qort.png", // Add an appropriate icon for chat notifications + title: "New Group Message!", + message: "You have received a new message from one of your groups", priority: 2, // Use the maximum priority to ensure it's noticeable // buttons: [ // { title: 'Go to group' } // ] }); - setTimeout(() => { - chrome.notifications.clear(notificationId); - }, 7000); - playNotificationSound() + if (!isMobile) { + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 7000); + } + playNotificationSound(); // audio.play(); - lastGroupNotification = Date.now() - // } + lastGroupNotification = Date.now(); + // } } - } finally { - if(!data || data?.length === 0) return - setChatHeads(data) - // chrome.runtime.sendMessage( - // { - // action: "setChatHeads", - // payload: { - // data, - // }, - // } - // ); - + if (!data || data?.length === 0) return; + setChatHeads(data); + // chrome.runtime.sendMessage( + // { + // action: "setChatHeads", + // payload: { + // data, + // }, + // } + // ); } -} +}; const checkThreads = async (bringBack) => { try { - - let myName = "" - const userData = await getUserInfo() - if(userData?.name){ - myName = userData.name - } - let newAnnouncements = [] - let dataToBringBack = [] - const threadActivity = await getThreadActivity() - if(!threadActivity) return null - + let myName = ""; + const userData = await getUserInfo(); + if (userData?.name) { + myName = userData.name; + } + let newAnnouncements = []; + let dataToBringBack = []; + const threadActivity = await getThreadActivity(); + if (!threadActivity) return null; + const selectedThreads = [ ...threadActivity.createdThreads.slice(0, 2), ...threadActivity.mostVisitedThreads.slice(0, 2), ...threadActivity.recentThreads.slice(0, 2), - ] + ]; - if(selectedThreads?.length === 0) return null - const tempData = { - - } - for (const thread of selectedThreads){ + if (selectedThreads?.length === 0) return null; + const tempData = {}; + for (const thread of selectedThreads) { try { - const identifier = `thmsg-${thread?.threadId}` - const name = thread?.qortalName - const url = await createEndpoint(`/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=1&includemetadata=false&offset=${0}&reverse=true&prefix=true`); + const identifier = `thmsg-${thread?.threadId}`; + const name = thread?.qortalName; + const endpoint = await getArbitraryEndpoint(); + const url = await createEndpoint( + `${endpoint}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=1&includemetadata=false&offset=${0}&reverse=true&prefix=true` + ); const response = await fetch(url, { - method: 'GET', + method: "GET", headers: { - 'Content-Type': 'application/json' - } - }) - const responseData = await response.json() - - const latestMessage = responseData.filter((pub)=> pub?.name !== myName)[0] + "Content-Type": "application/json", + }, + }); + const responseData = await response.json(); + + const latestMessage = responseData.filter( + (pub) => pub?.name !== myName + )[0]; // const latestMessage = responseData[0] if (!latestMessage) { - continue + continue; } - - if(checkDifference(latestMessage.created) && latestMessage.created > thread?.lastVisited && (!thread?.lastNotified || thread?.lastNotified < thread?.created)){ - tempData[thread.threadId] = latestMessage.created - newAnnouncements.push(thread) - + + if ( + checkDifference(latestMessage.created) && + latestMessage.created > thread?.lastVisited && + (!thread?.lastNotified || thread?.lastNotified < thread?.created) + ) { + tempData[thread.threadId] = latestMessage.created; + newAnnouncements.push(thread); } - if(latestMessage.created > thread?.lastVisited){ - dataToBringBack.push(thread) - + if (latestMessage.created > thread?.lastVisited) { + dataToBringBack.push(thread); } } catch (error) { - conosle.log({error}) + conosle.log({ error }); } } - - if(bringBack){ - - return dataToBringBack + if (bringBack) { + return dataToBringBack; } const updateThreadWithLastNotified = { ...threadActivity, - createdThreads: (threadActivity?.createdThreads || [])?.map((item)=> { - if(tempData[item.threadId]){ + createdThreads: (threadActivity?.createdThreads || [])?.map((item) => { + if (tempData[item.threadId]) { return { ...item, - lastNotified: tempData[item.threadId] - } + lastNotified: tempData[item.threadId], + }; } else { - return item + return item; } - }), - mostVisitedThreads: (threadActivity?.mostVisitedThreads || [])?.map((item)=> { - if(tempData[item.threadId]){ + mostVisitedThreads: (threadActivity?.mostVisitedThreads || [])?.map( + (item) => { + if (tempData[item.threadId]) { + return { + ...item, + lastNotified: tempData[item.threadId], + }; + } else { + return item; + } + } + ), + recentThreads: (threadActivity?.recentThreads || [])?.map((item) => { + if (tempData[item.threadId]) { return { ...item, - lastNotified: tempData[item.threadId] - } + lastNotified: tempData[item.threadId], + }; } else { - return item + return item; } }), - recentThreads: (threadActivity?.recentThreads || [])?.map((item)=> { - if(tempData[item.threadId]){ - return { - ...item, - lastNotified: tempData[item.threadId] - } - } else { - return item - } - }), - } - - const wallet = await getSaveWallet(); - const address = wallet.address0; - const dataString = JSON.stringify(updateThreadWithLastNotified); - chrome.storage.local.set({ [`threadactivity-${address}`]: dataString }) - + }; - - if(newAnnouncements.length > 0){ - const notificationId = 'chat_notification_' + Date.now() + '_type=thread-post' + `_data=${JSON.stringify(newAnnouncements[0])}`; + const wallet = await getSaveWallet(); + const address = wallet.address0; + const dataString = JSON.stringify(updateThreadWithLastNotified); + chrome.storage.local.set({ [`threadactivity-${address}`]: dataString }); + + if (newAnnouncements.length > 0) { + const notificationId = + "chat_notification_" + + Date.now() + + "_type=thread-post" + + `_data=${JSON.stringify(newAnnouncements[0])}`; chrome.notifications.create(notificationId, { - type: 'basic', - iconUrl: 'qort.png', // Add an appropriate icon for chat notifications + type: "basic", + iconUrl: "qort.png", // Add an appropriate icon for chat notifications title: `New thread post!`, message: `New post in ${newAnnouncements[0]?.thread?.threadData?.title}`, priority: 2, // Use the maximum priority to ensure it's noticeable @@ -614,156 +726,171 @@ const checkThreads = async (bringBack) => { // { title: 'Go to group' } // ] }); - setTimeout(() => { - chrome.notifications.clear(notificationId); - }, 7000); - playNotificationSound() - } - const savedtimestampAfter = await getTimestampGroupAnnouncement() - chrome.runtime.sendMessage({ - action: "SET_GROUP_ANNOUNCEMENTS", - payload: savedtimestampAfter, - }); + if (!isMobile) { + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 7000); + } + playNotificationSound(); + } + const savedtimestampAfter = await getTimestampGroupAnnouncement(); + chrome.runtime.sendMessage({ + action: "SET_GROUP_ANNOUNCEMENTS", + payload: savedtimestampAfter, + }); } catch (error) { - } finally { } -} -const checkNewMessages = - async () => { - try { - let myName = "" - const userData = await getUserInfo() - if(userData?.name){ - myName = userData.name - } - - let newAnnouncements = [] - const activeData = await getStoredData('active-groups-directs') || { groups: [], directs: [] }; - const groups = activeData?.groups - if(!groups || groups?.length === 0) return - const savedtimestamp = await getTimestampGroupAnnouncement() +}; +const checkNewMessages = async () => { + try { + let mutedGroups = await getUserSettings({key: 'mutedGroups'}) || [] + if(!isArray(mutedGroups)) mutedGroups = [] + let myName = ""; + const userData = await getUserInfo(); + if (userData?.name) { + myName = userData.name; + } - await Promise.all(groups.map(async (group) => { + let newAnnouncements = []; + const activeData = (await getStoredData("active-groups-directs")) || { + groups: [], + directs: [], + }; + const groups = activeData?.groups; + if (!groups || groups?.length === 0) return; + const savedtimestamp = await getTimestampGroupAnnouncement(); + + await Promise.all( + groups.map(async (group) => { try { const identifier = `grp-${group.groupId}-anc-`; - const url = await createEndpoint(`/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=1&includemetadata=false&offset=0&reverse=true&prefix=true`); - const response = await requestQueueAnnouncements.enqueue(()=> { - return fetch(url, { - method: 'GET', + const endpoint = await getArbitraryEndpoint(); + const url = await createEndpoint( + `${endpoint}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=1&includemetadata=false&offset=0&reverse=true&prefix=true` + ); + const response = await requestQueueAnnouncements.enqueue(() => { + return fetch(url, { + method: "GET", headers: { - 'Content-Type': 'application/json' - } + "Content-Type": "application/json", + }, }); - }) + }); const responseData = await response.json(); - - const latestMessage = responseData.filter((pub) => pub?.name !== myName)[0]; + + const latestMessage = responseData.filter( + (pub) => pub?.name !== myName + )[0]; if (!latestMessage) { return; // continue to the next group } - - if (checkDifference(latestMessage.created) && (!savedtimestamp[group.groupId] || latestMessage.created > savedtimestamp?.[group.groupId]?.notification)) { + + if ( + checkDifference(latestMessage.created) && + (!savedtimestamp[group.groupId] || + latestMessage.created > + savedtimestamp?.[group.groupId]?.notification) + ) { newAnnouncements.push(group); - await addTimestampGroupAnnouncement({ groupId: group.groupId, timestamp: Date.now() }); + await addTimestampGroupAnnouncement({ + groupId: group.groupId, + timestamp: Date.now(), + }); // save new timestamp } } catch (error) { console.error(error); // Handle error if needed } - })); - if(newAnnouncements.length > 0){ - const notificationId = 'chat_notification_' + Date.now() + '_type=group-announcement' + `_from=${newAnnouncements[0]?.groupId}`; + }) + ); + if (newAnnouncements.length > 0 && !mutedGroups.includes(newAnnouncements[0]?.groupId)) { + const notificationId = + "chat_notification_" + + Date.now() + + "_type=group-announcement" + + `_from=${newAnnouncements[0]?.groupId}`; - chrome.notifications.create(notificationId, { - type: 'basic', - iconUrl: 'qort.png', // Add an appropriate icon for chat notifications - title: `New group announcement!`, - message: `You have received a new announcement from ${newAnnouncements[0]?.groupName}`, - priority: 2, // Use the maximum priority to ensure it's noticeable - // buttons: [ - // { title: 'Go to group' } - // ] - }); + chrome.notifications.create(notificationId, { + type: "basic", + iconUrl: "qort.png", // Add an appropriate icon for chat notifications + title: `New group announcement!`, + message: `You have received a new announcement from ${newAnnouncements[0]?.groupName}`, + priority: 2, // Use the maximum priority to ensure it's noticeable + // buttons: [ + // { title: 'Go to group' } + // ] + }); + if (!isMobile) { setTimeout(() => { chrome.notifications.clear(notificationId); }, 7000); - playNotificationSound() - } - const savedtimestampAfter = await getTimestampGroupAnnouncement() - chrome.runtime.sendMessage({ + } + playNotificationSound(); + } + const savedtimestampAfter = await getTimestampGroupAnnouncement(); + chrome.runtime.sendMessage({ action: "SET_GROUP_ANNOUNCEMENTS", payload: savedtimestampAfter, }); - } catch (error) { - - } finally { - } - } - -const listenForNewGroupAnnouncements = async ()=> { - try { - setTimeout(() => { - checkNewMessages() - }, 500); - if(interval){ - clearInterval(interval) - } - - let isCalling = false - interval = setInterval(async () => { - if (isCalling) return - isCalling = true - const res = await checkNewMessages() - isCalling = false - }, 180000) } catch (error) { - - } -} -const listenForThreadUpdates = async ()=> { - try { - setTimeout(() => { - checkThreads() - }, 500); - if(intervalThreads){ - clearInterval(intervalThreads) - } - - let isCalling = false - intervalThreads = setInterval(async () => { - if (isCalling) return - isCalling = true - const res = await checkThreads() - isCalling = false - }, 60000) - } catch (error) { - - } -} - - - - -const forceCloseWebSocket = () => { - if (socket) { - - - clearTimeout(timeoutId); - clearTimeout(groupSocketTimeout); - clearTimeout(socketTimeout); - timeoutId = null - groupSocketTimeout = null - socket.close(1000, 'forced') - socket = null + } finally { } }; +const listenForNewGroupAnnouncements = async () => { + try { + setTimeout(() => { + checkNewMessages(); + }, 500); + if (interval) { + clearInterval(interval); + } + + let isCalling = false; + interval = setInterval(async () => { + if (isCalling) return; + isCalling = true; + const res = await checkNewMessages(); + isCalling = false; + }, 180000); + } catch (error) {} +}; +const listenForThreadUpdates = async () => { + try { + setTimeout(() => { + checkThreads(); + }, 500); + if (intervalThreads) { + clearInterval(intervalThreads); + } + + let isCalling = false; + intervalThreads = setInterval(async () => { + if (isCalling) return; + isCalling = true; + const res = await checkThreads(); + isCalling = false; + }, 60000); + } catch (error) {} +}; + +const forceCloseWebSocket = () => { + if (socket) { + clearTimeout(timeoutId); + clearTimeout(groupSocketTimeout); + clearTimeout(socketTimeout); + timeoutId = null; + groupSocketTimeout = null; + socket.close(1000, "forced"); + socket = null; + } +}; async function getNameInfo() { const wallet = await getSaveWallet(); const address = wallet.address0; - const validApi = await getBaseApi() + const validApi = await getBaseApi(); const response = await fetch(validApi + "/names/address/" + address); const nameData = await response.json(); if (nameData?.length > 0) { @@ -773,7 +900,7 @@ async function getNameInfo() { } } async function getAddressInfo(address) { - const validApi = await getBaseApi() + const validApi = await getBaseApi(); const response = await fetch(validApi + "/addresses/" + address); const data = await response.json(); @@ -805,6 +932,13 @@ async function getSaveWallet() { } } +async function clearAllNotifications() { + const notifications = await chrome.notifications.getAll(); + for (const notificationId of Object.keys(notifications)) { + await chrome.notifications.clear(notificationId); + } +} + async function getUserInfo() { const wallet = await getSaveWallet(); const address = wallet.address0; @@ -830,11 +964,18 @@ async function getTradeInfo(qortalAtAddress) { const data = await response.json(); return data; } +async function getTradesInfo(qortalAtAddresses) { + // Use Promise.all to fetch data for all addresses concurrently + const trades = await Promise.all( + qortalAtAddresses.map((address) => getTradeInfo(address)) + ); + return trades; // Return the array of trade info objects +} async function getBalanceInfo() { const wallet = await getSaveWallet(); const address = wallet.address0; - const validApi = await getBaseApi() + const validApi = await getBaseApi(); const response = await fetch(validApi + "/addresses/balance/" + address); if (!response?.ok) throw new Error("Cannot fetch balance"); @@ -863,7 +1004,10 @@ async function getLTCBalance() { const processTransactionVersion2Chat = async (body: any, customApi) => { // const validApi = await findUsableApi(); - const url = await createEndpoint("/transactions/process?apiVersion=2", customApi); + const url = await createEndpoint( + "/transactions/process?apiVersion=2", + customApi + ); return fetch(url, { method: "POST", headers: {}, @@ -901,7 +1045,7 @@ const processTransactionVersion2 = async (body: any) => { } catch (jsonError) { try { const text = await response.text(); - return text + return text; } catch (textError) { throw new Error(`Failed to parse response as both JSON and text.`); } @@ -912,7 +1056,6 @@ const processTransactionVersion2 = async (body: any) => { } }; - const transaction = async ( { type, params, apiVersion, keyPair }: any, validApi @@ -924,9 +1067,9 @@ const transaction = async ( const signedBytes = Base58.encode(tx.signedBytes); res = await processTransactionVersion2(signedBytes, validApi); } - let success = true - if(res?.error){ - success = false + let success = true; + if (res?.error) { + success = false; } return { @@ -964,7 +1107,7 @@ const makeTransactionRequest = async ( const getLastRef = async () => { const wallet = await getSaveWallet(); const address = wallet.address0; - const validApi = await getBaseApi() + const validApi = await getBaseApi(); const response = await fetch( validApi + "/addresses/lastreference/" + address ); @@ -973,7 +1116,7 @@ const getLastRef = async () => { return data; }; const sendQortFee = async () => { - const validApi = await getBaseApi() + const validApi = await getBaseApi(); const response = await fetch( validApi + "/transactions/unitfee?txType=PAYMENT" ); @@ -993,7 +1136,7 @@ async function getNameOrAddress(receiver) { if (isAddress) { return receiver; } - const validApi = await getBaseApi() + const validApi = await getBaseApi(); const response = await fetch(validApi + "/names/" + receiver); const data = await response.json(); @@ -1008,24 +1151,156 @@ async function getNameOrAddress(receiver) { } } -async function getPublicKey(receiver) { +export async function getPublicKey(receiver) { try { - const validApi = await getBaseApi() + const validApi = await getBaseApi(); const response = await fetch(validApi + "/addresses/publickey/" + receiver); if (!response?.ok) throw new Error("Cannot fetch recipient's public key"); const data = await response.text(); - if (!data?.error && data !== 'false') return data; + if (!data?.error && data !== "false") return data; if (data?.error) { throw new Error("Cannot fetch recipient's public key"); } - throw new Error("Cannot fetch recipient's public key"); + throw new Error("Cannot fetch recipient's public key"); } catch (error) { throw new Error(error?.message || "cannot validate address or name"); } } +const MAX_STORAGE_SIZE = 3 * 1024 * 1024; // 3MB in bytes + +async function getDataPublishes(groupId, type) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + + return new Promise((resolve) => { + chrome.storage.local.get([`${address}-publishData`], (result) => { + if (chrome.runtime.lastError) { + console.error("Error retrieving data:", chrome.runtime.lastError); + resolve(null); // Return null in case of an error + return; + } + + let storedData = result[`${address}-publishData`] || {}; // Get the stored data or initialize an empty object + let groupData = storedData[groupId] || {}; // Get data by groupId + let typeData = groupData[type] || {}; // Get data by type + + resolve(typeData); // Resolve with the data inside the specific type + }); + }); +} + +async function addDataPublishes(newData, groupId, type) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const nameIdentifier = `${newData.name}-${newData.identifier}`; + + // Prevent adding data larger than 50KB + if (newData?.size > 50000) return false; + + return new Promise((res) => { + chrome.storage.local.get([`${address}-publishData`], (result) => { + let storedData = result[`${address}-publishData`] || {}; // Get existing data or initialize + let groupData = storedData[groupId] || {}; // Get or initialize group by groupId + let typeData = groupData[type] || {}; // Get or initialize the type within the group + + let totalSize = 0; + + // Calculate the current size of all stored data + Object.values(storedData).forEach((group) => { + Object.values(group).forEach((type) => { + Object.values(type).forEach((data) => { + totalSize += data.size; // Accumulate the sizes of actual data + }); + }); + }); + + // Check if adding the new data exceeds 3MB + if (totalSize + newData.size > MAX_STORAGE_SIZE) { + // Sort and remove older data within the group and type + let dataEntries = Object.entries(typeData); + dataEntries.sort((a, b) => a[1].timestampSaved - b[1].timestampSaved); + + // Remove old data until there's enough space + while ( + totalSize + newData.size > MAX_STORAGE_SIZE && + dataEntries.length > 0 + ) { + const removedEntry = dataEntries.shift(); + totalSize -= removedEntry[1].size; + delete typeData[removedEntry[0]]; // Remove from the typeData + } + } + + // Add or update the new data within the group and type + if (totalSize + newData.size <= MAX_STORAGE_SIZE) { + typeData[`${nameIdentifier}`] = newData; // Add new data under name-identifier + groupData[type] = typeData; // Update type data within the group + storedData[groupId] = groupData; // Update group data within the stored data + + // Save the updated structure back to chrome.storage.local + chrome.storage.local.set( + { [`${address}-publishData`]: storedData }, + () => { + res(true); // Data successfully added + } + ); + } else { + console.error("Failed to add data, still exceeds storage limit."); + res(false); // Failure due to storage limit + } + }); + }); +} + +// Fetch user settings based on the key +async function getUserSettings({ key }) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + + return new Promise((resolve) => { + chrome.storage.local.get([`${address}-userSettings`], (result) => { + if (chrome.runtime.lastError) { + console.error("Error retrieving data:", chrome.runtime.lastError); + resolve(null); // Return null in case of an error + return; + } + + const storedData = result[`${address}-userSettings`] || {}; // Get the stored data or initialize an empty object + const value = storedData[key] || null; // Get data by key + + resolve(value); // Resolve with the data for the specific key + }); + }); +} + +// Add or update user settings +async function addUserSettings({ keyValue }) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const { key, value } = keyValue; + + // No need to check size here, unless value is a large object. For simple settings, size checks aren't necessary. + + return new Promise((res) => { + chrome.storage.local.get([`${address}-userSettings`], (result) => { + let storedData = result[`${address}-userSettings`] || {}; // Get existing data or initialize + + storedData[key] = value; // Update the key-value pair within the stored data + + // Save the updated structure back to chrome.storage.local + chrome.storage.local.set( + { [`${address}-userSettings`]: storedData }, + () => { + res(true); // Data successfully added + } + ); + }); + }); +} + async function decryptWallet({ password, wallet, walletVersion }) { try { const response = await decryptStoredWallet(password, wallet); @@ -1068,17 +1343,19 @@ async function decryptWallet({ password, wallet, walletVersion }) { return true; } catch (error) { - throw new Error(error.message); } } -async function signChatFunc(chatBytesArray, chatNonce, customApi, keyPair) { +async function signChatFunc(chatBytesArray, chatNonce, customApi, keyPair) { let response; try { const signedChatBytes = signChat(chatBytesArray, chatNonce, keyPair); - - const res = await processTransactionVersion2Chat(signedChatBytes, customApi); + + const res = await processTransactionVersion2Chat( + signedChatBytes, + customApi + ); response = res; } catch (e) { console.error(e); @@ -1153,9 +1430,8 @@ const getStoredData = async (key) => { }); }; -async function handleActiveGroupDataFromSocket({groups, directs}){ +async function handleActiveGroupDataFromSocket({ groups, directs }) { try { - chrome.runtime.sendMessage({ action: "SET_GROUPS", payload: groups, @@ -1164,25 +1440,20 @@ async function handleActiveGroupDataFromSocket({groups, directs}){ action: "SET_DIRECTS", payload: directs, }); - groups = groups - directs = directs + groups = groups; + directs = directs; const activeData = { - groups: groups || [], // Your groups data here - directs: directs || [] // Your directs data here + groups: groups || [], // Your groups data here + directs: directs || [], // Your directs data here }; - - // Save the active data to localStorage - chrome.storage.local.set({ 'active-groups-directs': activeData }); - try { - handleNotification(groups) - handleNotificationDirect(directs) - } catch (error) { - - } - } catch (error) { - - } + // Save the active data to localStorage + chrome.storage.local.set({ "active-groups-directs": activeData }); + try { + handleNotification(groups); + handleNotificationDirect(directs); + } catch (error) {} + } catch (error) {} } async function sendChat({ qortAddress, recipientPublicKey, message }) { @@ -1204,7 +1475,7 @@ async function sendChat({ qortAddress, recipientPublicKey, message }) { const hasEnoughBalance = +balance < 4 ? false : true; const difficulty = 8; const jsonData = { - atAddress: message.atAddress, + addresses: message.addresses, foreignKey: message.foreignKey, receivingAddress: message.receivingAddress, }; @@ -1253,7 +1524,6 @@ async function sendChat({ qortAddress, recipientPublicKey, message }) { return _response; } - async function sendChatGroup({ groupId, typeMessage, @@ -1272,10 +1542,10 @@ async function sendChatGroup({ privateKey: uint8PrivateKey, publicKey: uint8PublicKey, }; - const balance = await getBalanceInfo(); - const hasEnoughBalance = +balance < 4 ? false : true; + // const balance = await getBalanceInfo(); + // const hasEnoughBalance = +balance < 4 ? false : true; const difficulty = 8; - + const tx = await createTransaction(181, keyPair, { timestamp: Date.now(), groupID: Number(groupId), @@ -1288,10 +1558,10 @@ async function sendChatGroup({ isEncrypted: 0, // Set default to not encrypted for groups isText: 1, }); - - if (!hasEnoughBalance) { - throw new Error("Must have at least 4 QORT to send a chat message"); - } + + // if (!hasEnoughBalance) { + // throw new Error("Must have at least 4 QORT to send a chat message"); + // } const path = chrome.runtime.getURL("memory-pow.wasm.full"); const { nonce, chatBytesArray } = await computePow({ @@ -1299,12 +1569,7 @@ async function sendChatGroup({ path, difficulty, }); - let _response = await signChatFunc( - chatBytesArray, - nonce, - null, - keyPair - ); + let _response = await signChatFunc(chatBytesArray, nonce, null, keyPair); if (_response?.error) { throw new Error(_response?.message); } @@ -1312,18 +1577,28 @@ async function sendChatGroup({ } async function sendChatDirect({ + address, directTo, typeMessage, chatReference, messageText, + publicKeyOfRecipient, + otherData }) { - - - const recipientAddress = await getNameOrAddress(directTo) - const recipientPublicKey = await getPublicKey(recipientAddress) - - if(!recipientPublicKey) throw new Error('Cannot retrieve publickey') - + let recipientPublicKey; + let recipientAddress = address; + if (publicKeyOfRecipient) { + recipientPublicKey = publicKeyOfRecipient; + } else { + recipientAddress = await getNameOrAddress(directTo); + recipientPublicKey = await getPublicKey(recipientAddress); + } + if (!recipientAddress) { + recipientAddress = await getNameOrAddress(directTo); + } + + if (!recipientPublicKey) throw new Error("Cannot retrieve publickey"); + let _reference = new Uint8Array(64); self.crypto.getRandomValues(_reference); @@ -1336,73 +1611,57 @@ async function sendChatDirect({ privateKey: uint8PrivateKey, publicKey: uint8PublicKey, }; - const balance = await getBalanceInfo(); - const hasEnoughBalance = +balance < 4 ? false : true; - + // const balance = await getBalanceInfo(); + // const hasEnoughBalance = +balance < 4 ? false : true; + const difficulty = 8; - + const finalJson = { message: messageText, version: 2, + ...(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, + isEncrypted: 1, isText: 1, - }); - - if (!hasEnoughBalance) { - throw new Error("Must have at least 4 QORT to send a chat message"); } + 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"); + // } const path = chrome.runtime.getURL("memory-pow.wasm.full"); - - + const { nonce, chatBytesArray } = await computePow({ chatBytes: tx.chatBytes, path, difficulty, }); - - let _response = await signChatFunc( - chatBytesArray, - nonce, - null, - keyPair - ); + + let _response = await signChatFunc(chatBytesArray, nonce, null, keyPair); if (_response?.error) { throw new Error(_response?.message); } return _response; } -async function decryptSingleFunc({ messages, secretKeyObject, skipDecodeBase64 }) { - let holdMessages = []; - - for (const message of messages) { - try { - const res = await decryptSingle({ - data64: message.data, - secretKeyObject, - skipDecodeBase64 - }); - - const decryptToUnit8Array = base64ToUint8Array(res); - const responseData = uint8ArrayToObject(decryptToUnit8Array); - holdMessages.push({ ...message, text: responseData }); - } catch (error) { - - } - } - return holdMessages; -} -async function decryptSingleForPublishes({ messages, secretKeyObject, skipDecodeBase64 }) { +async function decryptSingleFunc({ + messages, + secretKeyObject, + skipDecodeBase64, +}) { let holdMessages = []; for (const message of messages) { @@ -1410,21 +1669,41 @@ async function decryptSingleForPublishes({ messages, secretKeyObject, skipDecode const res = await decryptSingle({ data64: message.data, secretKeyObject, - skipDecodeBase64 + skipDecodeBase64, }); - + const decryptToUnit8Array = base64ToUint8Array(res); const responseData = uint8ArrayToObject(decryptToUnit8Array); holdMessages.push({ ...message, decryptedData: responseData }); - } catch (error) { + } catch (error) {} + } + return holdMessages; +} +async function decryptSingleForPublishes({ + messages, + secretKeyObject, + skipDecodeBase64, +}) { + let holdMessages = []; - } + for (const message of messages) { + try { + const res = await decryptSingle({ + data64: message.data, + secretKeyObject, + skipDecodeBase64, + }); + + const decryptToUnit8Array = base64ToUint8Array(res); + const responseData = uint8ArrayToObject(decryptToUnit8Array); + holdMessages.push({ ...message, decryptedData: responseData }); + } catch (error) {} } return holdMessages; } async function decryptDirectFunc({ messages, involvingAddress }) { - const senderPublicKey = await getPublicKey(involvingAddress) + const senderPublicKey = await getPublicKey(involvingAddress); let holdMessages = []; const resKeyPair = await getKeyPair(); @@ -1445,22 +1724,85 @@ async function decryptDirectFunc({ messages, involvingAddress }) { ); const parsedMessage = JSON.parse(decodedMessage); holdMessages.push({ ...message, ...parsedMessage }); - } catch (error) { - - } + } catch (error) {} } return holdMessages; } -async function createBuyOrderTx({ crosschainAtInfo }) { +async function createBuyOrderTx({ crosschainAtInfo, useLocal }) { try { + if(useLocal){ + const wallet = await getSaveWallet(); + + const address = wallet.address0; + + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const message = { + addresses: crosschainAtInfo.map((order)=> order.qortalAtAddress), + foreignKey: parsedData.ltcPrivateKey, + receivingAddress: address, + }; + let responseVar + const txn = new TradeBotRespondMultipleRequest().createTransaction(message) + const apiKey = await getApiKeyFromStorage(); + const responseFetch = await fetch(`http://127.0.0.1:12391/crosschain/tradebot/respondmultiple?apiKey=${apiKey}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(txn), + }); + + const res = await responseFetch.json(); + + if(res === false){ + responseVar = { response: "Unable to execute buy order", success: false }; + } else { + responseVar = { response: res, success: true }; + } + const { response, success } = responseVar + let responseMessage; + if (success) { + responseMessage = { + callResponse: response, + extra: { + message: 'Transaction processed successfully!', + atAddresses: crosschainAtInfo.map((order)=> order.qortalAtAddress), + + } + }; + } else { + responseMessage = { + callResponse: 'ERROR', + extra: { + message: response, + atAddresses: crosschainAtInfo.map((order)=> order.qortalAtAddress), + + } + }; + } + + setTimeout(() => { + chrome.tabs.query({}, function (tabs) { + tabs.forEach((tab) => { + chrome.tabs.sendMessage(tab.id, { + type: "RESPONSE_FOR_TRADES", + message: responseMessage, + }); + }); + }); + }, 5000); + + return + } const wallet = await getSaveWallet(); const address = wallet.address0; const resKeyPair = await getKeyPair(); const parsedData = JSON.parse(resKeyPair); const message = { - atAddress: crosschainAtInfo.qortalAtAddress, + addresses: crosschainAtInfo.map((order)=> order.qortalAtAddress), foreignKey: parsedData.ltcPrivateKey, receivingAddress: address, }; @@ -1478,7 +1820,7 @@ async function createBuyOrderTx({ crosschainAtInfo }) { }); if (res?.encryptedMessageToBase58) { return { - atAddress: crosschainAtInfo.qortalAtAddress, + atAddresses: crosschainAtInfo.map((order)=> order.qortalAtAddress), encryptedMessageToBase58: res?.encryptedMessageToBase58, node: buyTradeNodeBaseUrl, qortAddress: address, @@ -1489,7 +1831,7 @@ async function createBuyOrderTx({ crosschainAtInfo }) { }; } return { - atAddress: crosschainAtInfo.qortalAtAddress, + atAddresses: crosschainAtInfo.map((order)=> order.qortalAtAddress), chatSignature: res?.signature, node: buyTradeNodeBaseUrl, qortAddress: address, @@ -1502,25 +1844,30 @@ async function createBuyOrderTx({ crosschainAtInfo }) { } } -async function sendChatNotification(res, groupId, secretKeyObject, numberOfMembers){ +async function sendChatNotification( + res, + groupId, + secretKeyObject, + numberOfMembers +) { try { -const data = await objectToBase64({ - type: "notification", - subType: "new-group-encryption", - data: { - timestamp: res.timestamp, - name: res.name, - message: `${res.name} has updated the encryption key`, - numberOfMembers - }, -}) - + const data = await objectToBase64({ + type: "notification", + subType: "new-group-encryption", + data: { + timestamp: res.timestamp, + name: res.name, + message: `${res.name} has updated the encryption key`, + numberOfMembers, + }, + }); + encryptSingle({ data64: data, secretKeyObject: secretKeyObject, }) .then((res2) => { - pauseAllQueues() + pauseAllQueues(); sendChatGroup({ groupId, typeMessage: undefined, @@ -1529,37 +1876,35 @@ const data = await objectToBase64({ }) .then(() => {}) .catch((error) => { - console.error('1',error.message); - }).finally(()=> { - resumeAllQueues() + console.error("1", error.message); }) + .finally(() => { + resumeAllQueues(); + }); }) .catch((error) => { - console.error('2',error.message); - + console.error("2", error.message); }); - } catch (error) { - - } + } catch (error) {} } -export const getFee = async(txType)=> { +export const getFee = async (txType) => { + const timestamp = Date.now(); + const data = await reusableGet( + `/transactions/unitfee?txType=${txType}×tamp=${timestamp}` + ); + const arbitraryFee = (Number(data) / 1e8).toFixed(8); - const timestamp = Date.now() - const data = await reusableGet(`/transactions/unitfee?txType=${txType}×tamp=${timestamp}`) - const arbitraryFee = (Number(data) / 1e8).toFixed(8) + return { + timestamp, + fee: arbitraryFee, + }; +}; - return { - timestamp, - fee: arbitraryFee - } - -} - -async function leaveGroup({groupId}){ +async function leaveGroup({ groupId }) { const wallet = await getSaveWallet(); const address = wallet.address0; - const lastReference = await getLastRef() + const lastReference = await getLastRef(); const resKeyPair = await getKeyPair(); const parsedData = JSON.parse(resKeyPair); const uint8PrivateKey = Base58.decode(parsedData.privateKey); @@ -1568,29 +1913,27 @@ async function leaveGroup({groupId}){ privateKey: uint8PrivateKey, publicKey: uint8PublicKey, }; - const feeres = await getFee('LEAVE_GROUP') + const feeres = await getFee("LEAVE_GROUP"); const tx = await createTransaction(32, keyPair, { fee: feeres.fee, registrantAddress: address, - rGroupId: groupId, - lastReference: lastReference, - - - + rGroupId: groupId, + lastReference: lastReference, }); - + const signedBytes = Base58.encode(tx.signedBytes); - const res = await processTransactionVersion2(signedBytes) - if(!res?.signature) throw new Error('Transaction was not able to be processed') - return res + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res; } -async function joinGroup({groupId}){ +async function joinGroup({ groupId }) { const wallet = await getSaveWallet(); const address = wallet.address0; - const lastReference = await getLastRef() + const lastReference = await getLastRef(); const resKeyPair = await getKeyPair(); const parsedData = JSON.parse(resKeyPair); const uint8PrivateKey = Base58.decode(parsedData.privateKey); @@ -1599,27 +1942,25 @@ async function joinGroup({groupId}){ privateKey: uint8PrivateKey, publicKey: uint8PublicKey, }; - const feeres = await getFee('JOIN_GROUP') + const feeres = await getFee("JOIN_GROUP"); const tx = await createTransaction(31, keyPair, { fee: feeres.fee, registrantAddress: address, - rGroupId: groupId, - lastReference: lastReference, - - - + rGroupId: groupId, + lastReference: lastReference, }); const signedBytes = Base58.encode(tx.signedBytes); - const res = await processTransactionVersion2(signedBytes) - if(!res?.signature) throw new Error(res?.message || 'Transaction was not able to be processed') - return res + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error(res?.message || "Transaction was not able to be processed"); + return res; } -async function cancelInvitationToGroup({groupId, qortalAddress}){ - const lastReference = await getLastRef() +async function cancelInvitationToGroup({ groupId, qortalAddress }) { + const lastReference = await getLastRef(); const resKeyPair = await getKeyPair(); const parsedData = JSON.parse(resKeyPair); const uint8PrivateKey = Base58.decode(parsedData.privateKey); @@ -1628,25 +1969,25 @@ async function cancelInvitationToGroup({groupId, qortalAddress}){ privateKey: uint8PrivateKey, publicKey: uint8PublicKey, }; - const feeres = await getFee('CANCEL_GROUP_INVITE') + const feeres = await getFee("CANCEL_GROUP_INVITE"); const tx = await createTransaction(30, keyPair, { fee: feeres.fee, - recipient: qortalAddress, - rGroupId: groupId, - lastReference: lastReference, - + recipient: qortalAddress, + rGroupId: groupId, + lastReference: lastReference, }); - + const signedBytes = Base58.encode(tx.signedBytes); - const res = await processTransactionVersion2(signedBytes) - if(!res?.signature) throw new Error('Transaction was not able to be processed') - return res + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res; } -async function cancelBan({groupId, qortalAddress}){ - const lastReference = await getLastRef() +async function cancelBan({ groupId, qortalAddress }) { + const lastReference = await getLastRef(); const resKeyPair = await getKeyPair(); const parsedData = JSON.parse(resKeyPair); const uint8PrivateKey = Base58.decode(parsedData.privateKey); @@ -1655,24 +1996,24 @@ async function cancelBan({groupId, qortalAddress}){ privateKey: uint8PrivateKey, publicKey: uint8PublicKey, }; - const feeres = await getFee('CANCEL_GROUP_BAN') + const feeres = await getFee("CANCEL_GROUP_BAN"); const tx = await createTransaction(27, keyPair, { fee: feeres.fee, - recipient: qortalAddress, - rGroupId: groupId, - lastReference: lastReference, - + recipient: qortalAddress, + rGroupId: groupId, + lastReference: lastReference, }); - + const signedBytes = Base58.encode(tx.signedBytes); - const res = await processTransactionVersion2(signedBytes) - if(!res?.signature) throw new Error('Transaction was not able to be processed') - return res + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res; } -async function registerName({name}){ - const lastReference = await getLastRef() +async function registerName({ name }) { + const lastReference = await getLastRef(); const resKeyPair = await getKeyPair(); const parsedData = JSON.parse(resKeyPair); const uint8PrivateKey = Base58.decode(parsedData.privateKey); @@ -1681,24 +2022,24 @@ async function registerName({name}){ privateKey: uint8PrivateKey, publicKey: uint8PublicKey, }; - const feeres = await getFee('REGISTER_NAME') + const feeres = await getFee("REGISTER_NAME"); const tx = await createTransaction(3, keyPair, { fee: feeres.fee, name, value: "", - lastReference: lastReference, - + lastReference: lastReference, }); - + const signedBytes = Base58.encode(tx.signedBytes); - const res = await processTransactionVersion2(signedBytes) - if(!res?.signature) throw new Error('Transaction was not able to be processed') - return res + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res; } -async function makeAdmin({groupId, qortalAddress}){ - const lastReference = await getLastRef() +async function makeAdmin({ groupId, qortalAddress }) { + const lastReference = await getLastRef(); const resKeyPair = await getKeyPair(); const parsedData = JSON.parse(resKeyPair); const uint8PrivateKey = Base58.decode(parsedData.privateKey); @@ -1707,25 +2048,25 @@ async function makeAdmin({groupId, qortalAddress}){ privateKey: uint8PrivateKey, publicKey: uint8PublicKey, }; - const feeres = await getFee('ADD_GROUP_ADMIN') + const feeres = await getFee("ADD_GROUP_ADMIN"); const tx = await createTransaction(24, keyPair, { fee: feeres.fee, - recipient: qortalAddress, - rGroupId: groupId, - lastReference: lastReference, - + recipient: qortalAddress, + rGroupId: groupId, + lastReference: lastReference, }); - + const signedBytes = Base58.encode(tx.signedBytes); - const res = await processTransactionVersion2(signedBytes) - if(!res?.signature) throw new Error('Transaction was not able to be processed') - return res + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res; } -async function removeAdmin({groupId, qortalAddress}){ - const lastReference = await getLastRef() +async function removeAdmin({ groupId, qortalAddress }) { + const lastReference = await getLastRef(); const resKeyPair = await getKeyPair(); const parsedData = JSON.parse(resKeyPair); const uint8PrivateKey = Base58.decode(parsedData.privateKey); @@ -1734,25 +2075,30 @@ async function removeAdmin({groupId, qortalAddress}){ privateKey: uint8PrivateKey, publicKey: uint8PublicKey, }; - const feeres = await getFee('REMOVE_GROUP_ADMIN') + const feeres = await getFee("REMOVE_GROUP_ADMIN"); const tx = await createTransaction(25, keyPair, { fee: feeres.fee, - recipient: qortalAddress, - rGroupId: groupId, - lastReference: lastReference, - + recipient: qortalAddress, + rGroupId: groupId, + lastReference: lastReference, }); - + const signedBytes = Base58.encode(tx.signedBytes); - const res = await processTransactionVersion2(signedBytes) - if(!res?.signature) throw new Error('Transaction was not able to be processed') - return res + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res; } -async function banFromGroup({groupId, qortalAddress, rBanReason = "", rBanTime}){ - const lastReference = await getLastRef() +async function banFromGroup({ + groupId, + qortalAddress, + rBanReason = "", + rBanTime, +}) { + const lastReference = await getLastRef(); const resKeyPair = await getKeyPair(); const parsedData = JSON.parse(resKeyPair); const uint8PrivateKey = Base58.decode(parsedData.privateKey); @@ -1761,29 +2107,27 @@ async function banFromGroup({groupId, qortalAddress, rBanReason = "", rBanTime}) privateKey: uint8PrivateKey, publicKey: uint8PublicKey, }; - const feeres = await getFee('GROUP_BAN') + const feeres = await getFee("GROUP_BAN"); const tx = await createTransaction(26, keyPair, { fee: feeres.fee, - recipient: qortalAddress, - rGroupId: groupId, - rBanReason: rBanReason, - rBanTime, - lastReference: lastReference, - + recipient: qortalAddress, + rGroupId: groupId, + rBanReason: rBanReason, + rBanTime, + lastReference: lastReference, }); const signedBytes = Base58.encode(tx.signedBytes); - const res = await processTransactionVersion2(signedBytes) - if(!res?.signature) throw new Error('Transaction was not able to be processed') - return res + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res; } - - -async function kickFromGroup({groupId, qortalAddress, rBanReason = ""}){ - const lastReference = await getLastRef() +async function kickFromGroup({ groupId, qortalAddress, rBanReason = "" }) { + const lastReference = await getLastRef(); const resKeyPair = await getKeyPair(); const parsedData = JSON.parse(resKeyPair); const uint8PrivateKey = Base58.decode(parsedData.privateKey); @@ -1792,32 +2136,37 @@ async function kickFromGroup({groupId, qortalAddress, rBanReason = ""}){ privateKey: uint8PrivateKey, publicKey: uint8PublicKey, }; - const feeres = await getFee('GROUP_KICK') + const feeres = await getFee("GROUP_KICK"); const tx = await createTransaction(28, keyPair, { fee: feeres.fee, - recipient: qortalAddress, - rGroupId: groupId, - rBanReason: rBanReason, - lastReference: lastReference, - + recipient: qortalAddress, + rGroupId: groupId, + rBanReason: rBanReason, + lastReference: lastReference, }); - + const signedBytes = Base58.encode(tx.signedBytes); - const res = await processTransactionVersion2(signedBytes) - if(!res?.signature) throw new Error('Transaction was not able to be processed') - return res + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res; } - - -async function createGroup({ groupName, groupDescription, groupType, groupApprovalThreshold, minBlock, maxBlock}){ +async function createGroup({ + groupName, + groupDescription, + groupType, + groupApprovalThreshold, + minBlock, + maxBlock, +}) { const wallet = await getSaveWallet(); const address = wallet.address0; - if(!address) throw new Error('Cannot find user') - const lastReference = await getLastRef() - const feeres = await getFee('CREATE_GROUP') + if (!address) throw new Error("Cannot find user"); + const lastReference = await getLastRef(); + const feeres = await getFee("CREATE_GROUP"); const resKeyPair = await getKeyPair(); const parsedData = JSON.parse(resKeyPair); const uint8PrivateKey = Base58.decode(parsedData.privateKey); @@ -1828,29 +2177,29 @@ async function createGroup({ groupName, groupDescription, groupType, groupApprov }; const tx = await createTransaction(22, keyPair, { - fee: feeres.fee, - registrantAddress: address, - rGroupName: groupName, - rGroupDesc: groupDescription, - rGroupType: groupType, - rGroupApprovalThreshold: groupApprovalThreshold, - rGroupMinimumBlockDelay: minBlock, - rGroupMaximumBlockDelay: maxBlock, - lastReference: lastReference, - + fee: feeres.fee, + registrantAddress: address, + rGroupName: groupName, + rGroupDesc: groupDescription, + rGroupType: groupType, + rGroupApprovalThreshold: groupApprovalThreshold, + rGroupMinimumBlockDelay: minBlock, + rGroupMaximumBlockDelay: maxBlock, + lastReference: lastReference, }); - + const signedBytes = Base58.encode(tx.signedBytes); - const res = await processTransactionVersion2(signedBytes) - if(!res?.signature) throw new Error('Transaction was not able to be processed') - return res + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res; } -async function inviteToGroup({groupId, qortalAddress, inviteTime}){ - const address = await getNameOrAddress(qortalAddress) - if(!address) throw new Error('Cannot find user') - const lastReference = await getLastRef() - const feeres = await getFee('GROUP_INVITE') +async function inviteToGroup({ groupId, qortalAddress, inviteTime }) { + const address = await getNameOrAddress(qortalAddress); + if (!address) throw new Error("Cannot find user"); + const lastReference = await getLastRef(); + const feeres = await getFee("GROUP_INVITE"); const resKeyPair = await getKeyPair(); const parsedData = JSON.parse(resKeyPair); const uint8PrivateKey = Base58.decode(parsedData.privateKey); @@ -1860,21 +2209,20 @@ async function inviteToGroup({groupId, qortalAddress, inviteTime}){ publicKey: uint8PublicKey, }; - const tx = await createTransaction(29, keyPair, { fee: feeres.fee, - recipient: address, - rGroupId: groupId, - rInviteTime: inviteTime, - lastReference: lastReference, - + recipient: address, + rGroupId: groupId, + rInviteTime: inviteTime, + lastReference: lastReference, }); - + const signedBytes = Base58.encode(tx.signedBytes); - const res = await processTransactionVersion2(signedBytes) - if(!res?.signature) throw new Error('Transaction was not able to be processed') - return res + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res; } async function sendCoin({ password, amount, receiver }, skipConfirmPassword) { @@ -1902,7 +2250,7 @@ async function sendCoin({ password, amount, receiver }, skipConfirmPassword) { const lastRef = await getLastRef(); const fee = await sendQortFee(); - const validApi = await findUsableApi() + const validApi = await findUsableApi(); const res = await makeTransactionRequest( confirmReceiver, @@ -1912,7 +2260,7 @@ async function sendCoin({ password, amount, receiver }, skipConfirmPassword) { keyPair, validApi ); - + return { res, validApi }; } catch (error) { throw new Error(error.message); @@ -1969,6 +2317,7 @@ async function fetchMessagesForBuyOrders(apiCall, signature, senderPublicKey) { try { const response = await fetch(apiCall); let data = await response.json(); + data = data.filter( (item) => !triedChatMessage.includes(item.signature) ); @@ -1982,7 +2331,7 @@ async function fetchMessagesForBuyOrders(apiCall, signature, senderPublicKey) { privateKey: uint8PrivateKey, publicKey: uint8PublicKey, }; - + const decodedMessage = decryptChatMessage( encodedMessageObj.data, keyPair.privateKey, @@ -2108,7 +2457,7 @@ async function listenForChatMessageForBuyOrder({ } } -function removeDuplicateWindow(popupUrl){ +function removeDuplicateWindow(popupUrl) { chrome.windows.getAll( { populate: true, windowTypes: ["popup"] }, (windows) => { @@ -2123,31 +2472,32 @@ function removeDuplicateWindow(popupUrl){ const existingPopups = windows.filter( (w) => w.tabs && - w.tabs.some( - (tab) => tab.url && tab.url.startsWith(popupUrl) - ) + w.tabs.some((tab) => tab.url && tab.url.startsWith(popupUrl)) ); - - if(existingPopupsPending.length > 1){ - chrome.windows.remove(existingPopupsPending?.[0]?.tabs?.[0]?.windowId, () => { - - }); - } else if(existingPopupsPending.length > 0 && existingPopups.length > 0){ - chrome.windows.remove(existingPopupsPending?.[0]?.tabs?.[0]?.windowId, () => { - - }); + + if (existingPopupsPending.length > 1) { + chrome.windows.remove( + existingPopupsPending?.[0]?.tabs?.[0]?.windowId, + () => {} + ); + } else if ( + existingPopupsPending.length > 0 && + existingPopups.length > 0 + ) { + chrome.windows.remove( + existingPopupsPending?.[0]?.tabs?.[0]?.windowId, + () => {} + ); } } ); } - - -async function setChatHeads(data){ +async function setChatHeads(data) { const wallet = await getSaveWallet(); const address = wallet.address0; const dataString = JSON.stringify(data); - return await new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { chrome.storage.local.set({ [`chatheads-${address}`]: dataString }, () => { if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); @@ -2158,10 +2508,16 @@ async function setChatHeads(data){ }); } -async function getTempPublish(){ +async function checkLocalFunc(){ + const apiKey = await getApiKeyFromStorage() + return !!apiKey + +} + +async function getTempPublish() { const wallet = await getSaveWallet(); const address = wallet.address0; - const key = `tempPublish-${address}` + const key = `tempPublish-${address}`; const res = await chrome.storage.local.get([key]); const SIX_MINUTES = 6 * 60 * 1000; // 6 minutes in milliseconds @@ -2182,7 +2538,6 @@ async function getTempPublish(){ }) ); - if (JSON.stringify(filteredData) !== JSON.stringify(parsedData)) { const dataString = JSON.stringify(filteredData); await chrome.storage.local.set({ [key]: dataString }); @@ -2193,21 +2548,21 @@ async function getTempPublish(){ } } -async function saveTempPublish({data, key}){ - const existingTemp = await getTempPublish() +async function saveTempPublish({ data, key }) { + const existingTemp = await getTempPublish(); const wallet = await getSaveWallet(); const address = wallet.address0; const newTemp = { ...existingTemp, [key]: { ...(existingTemp[key] || {}), - [data.identifier] : { + [data.identifier]: { data, - timestampSaved: Date.now() - } - } - } - + timestampSaved: Date.now(), + }, + }, + }; + const dataString = JSON.stringify(newTemp); return await new Promise((resolve, reject) => { @@ -2215,66 +2570,125 @@ async function saveTempPublish({data, key}){ if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); } else { - resolve(newTemp[key]); } }); }); } -async function setChatHeadsDirect(data){ +async function setChatHeadsDirect(data) { const wallet = await getSaveWallet(); const address = wallet.address0; const dataString = JSON.stringify(data); - return await new Promise((resolve, reject) => { - chrome.storage.local.set({ [`chatheads-direct-${address}`]: dataString }, () => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)); - } else { - resolve(true); + return await new Promise((resolve, reject) => { + chrome.storage.local.set( + { [`chatheads-direct-${address}`]: dataString }, + () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(true); + } } - }); + ); }); } - - -async function getTimestampEnterChat(){ +async function getTimestampEnterChat() { const wallet = await getSaveWallet(); const address = wallet.address0; - const key = `enter-chat-timestamp-${address}` + const key = `enter-chat-timestamp-${address}`; const res = await chrome.storage.local.get([key]); -if (res?.[key]) { - const parsedData = JSON.parse(res[key]) - return parsedData; -} else { - return {} + if (res?.[key]) { + const parsedData = JSON.parse(res[key]); + return parsedData; + } else { + return {}; + } } -} -async function getTimestampGroupAnnouncement(){ +async function getTimestampGroupAnnouncement() { const wallet = await getSaveWallet(); const address = wallet.address0; - const key = `group-announcement-${address}` + const key = `group-announcement-${address}`; const res = await chrome.storage.local.get([key]); -if (res?.[key]) { - const parsedData = JSON.parse(res[key]) - return parsedData; -} else { - return {} -} + if (res?.[key]) { + const parsedData = JSON.parse(res[key]); + return parsedData; + } else { + return {}; + } } -async function addTimestampGroupAnnouncement({groupId, timestamp, seenTimestamp}){ +async function addTimestampGroupAnnouncement({ + groupId, + timestamp, + seenTimestamp, +}) { const wallet = await getSaveWallet(); const address = wallet.address0; - const data = await getTimestampGroupAnnouncement() || {} + const data = (await getTimestampGroupAnnouncement()) || {}; data[groupId] = { notification: timestamp, - seentimestamp: seenTimestamp ? true : false + seentimestamp: seenTimestamp ? true : false, + }; + const dataString = JSON.stringify(data); + return await new Promise((resolve, reject) => { + chrome.storage.local.set( + { [`group-announcement-${address}`]: dataString }, + () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(true); + } + } + ); + }); +} + +async function getGroupData() { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key = `group-data-${address}`; + const res = await chrome.storage.local.get([key]); + if (res?.[key]) { + const parsedData = JSON.parse(res[key]); + return parsedData; + } else { + return {}; } +} +async function getGroupDataSingle(groupId) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key = `group-data-${address}`; + const res = await chrome.storage.local.get([key]); + if (res?.[key]) { + const parsedData = JSON.parse(res[key]); + return parsedData[groupId] || null; + } else { + return null; + } +} + +async function setGroupData({ + groupId, + secretKeyData, + secretKeyResource, + admins, +}) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const data = (await getGroupData()) || {}; + data[groupId] = { + timestampLastSet: Date.now(), + admins, + secretKeyData, + secretKeyResource, + }; const dataString = JSON.stringify(data); - return await new Promise((resolve, reject) => { - chrome.storage.local.set({ [`group-announcement-${address}`]: dataString }, () => { + return await new Promise((resolve, reject) => { + chrome.storage.local.set({ [`group-data-${address}`]: dataString }, () => { if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); } else { @@ -2284,64 +2698,66 @@ async function addTimestampGroupAnnouncement({groupId, timestamp, seenTimestamp} }); } - -async function addTimestampEnterChat({groupId, timestamp}){ +async function addTimestampEnterChat({ groupId, timestamp }) { const wallet = await getSaveWallet(); const address = wallet.address0; - const data = await getTimestampEnterChat() - data[groupId] = timestamp + const data = await getTimestampEnterChat(); + data[groupId] = timestamp; const dataString = JSON.stringify(data); - return await new Promise((resolve, reject) => { - chrome.storage.local.set({ [`enter-chat-timestamp-${address}`]: dataString }, () => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)); - } else { - resolve(true); + return await new Promise((resolve, reject) => { + chrome.storage.local.set( + { [`enter-chat-timestamp-${address}`]: dataString }, + () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(true); + } } - }); + ); }); } -async function notifyAdminRegenerateSecretKey({groupName, adminAddress}){ +async function notifyAdminRegenerateSecretKey({ groupName, adminAddress }) { const wallet = await getSaveWallet(); const address = wallet.address0; - const name = await getNameInfo(address) - const nameOrAddress = name || address + const name = await getNameInfo(address); + const nameOrAddress = name || address; await sendChatDirect({ directTo: adminAddress, typeMessage: undefined, chatReference: undefined, - messageText: `

Member ${nameOrAddress} has requested that you regenerate the group's secret key. Group: ${groupName}

` - }) - return true + messageText: `

Member ${nameOrAddress} has requested that you regenerate the group's secret key. Group: ${groupName}

`, + }); + return true; } -async function getChatHeads(){ +async function getChatHeads() { const wallet = await getSaveWallet(); const address = wallet.address0; - const key = `chatheads-${address}` + const key = `chatheads-${address}`; const res = await chrome.storage.local.get([key]); -if (res?.[key]) { - const parsedData = JSON.parse(res[key]) - return parsedData; -} else { - throw new Error("No Chatheads saved"); -} + if (res?.[key]) { + const parsedData = JSON.parse(res[key]); + return parsedData; + } else { + throw new Error("No Chatheads saved"); + } } -async function getChatHeadsDirect(){ +async function getChatHeadsDirect() { const wallet = await getSaveWallet(); const address = wallet.address0; - const key = `chatheads-direct-${address}` + const key = `chatheads-direct-${address}`; const res = await chrome.storage.local.get([key]); -if (res?.[key]) { - const parsedData = JSON.parse(res[key]) - return parsedData; -} else { - throw new Error("No Chatheads saved"); + if (res?.[key]) { + const parsedData = JSON.parse(res[key]); + return parsedData; + } else { + throw new Error("No Chatheads saved"); + } } -} -chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { +chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { if (request) { switch (request.action) { case "version": @@ -2395,7 +2811,6 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { case "userInfo": getUserInfo() .then((name) => { - sendResponse(name); }) .catch((error) => { @@ -2446,10 +2861,10 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { { const { receiver, password, amount } = request.payload; sendCoin({ receiver, password, amount }) - .then(({res}) => { - if(!res?.success){ + .then(({ res }) => { + if (!res?.success) { sendResponse({ error: res?.data?.message }); - return + return; } sendResponse(true); }) @@ -2460,7 +2875,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { } break; - case "inviteToGroup": + case "inviteToGroup": { const { groupId, qortalAddress, inviteTime } = request.payload; inviteToGroup({ groupId, qortalAddress, inviteTime }) @@ -2474,7 +2889,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { } break; - case "saveTempPublish": + case "saveTempPublish": { const { data, key } = request.payload; saveTempPublish({ data, key }) @@ -2485,12 +2900,11 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { sendResponse({ error: error.message }); console.error(error.message); }); - return true + return true; } break; - case "getTempPublish": + case "getTempPublish": { - getTempPublish() .then((res) => { sendResponse(res); @@ -2500,152 +2914,218 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { console.error(error.message); }); } - - break; - - case "createGroup": - { - const { groupName, groupDescription, groupType, groupApprovalThreshold, minBlock, maxBlock } = request.payload; - createGroup({ groupName, groupDescription, groupType, groupApprovalThreshold, minBlock, maxBlock }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } break; - case "cancelInvitationToGroup": - { - const { groupId, qortalAddress } = request.payload; - cancelInvitationToGroup({ groupId, qortalAddress }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - case "leaveGroup": - { - const { groupId } = request.payload; - leaveGroup({ groupId }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - case "joinGroup": - { - const { groupId } = request.payload; - joinGroup({ groupId }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - - case "kickFromGroup": - { - const { groupId, qortalAddress, rBanReason } = request.payload; - kickFromGroup({ groupId, qortalAddress, rBanReason }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - case "banFromGroup": - { - const { groupId, qortalAddress, rBanReason, rBanTime } = request.payload; - banFromGroup({ groupId, qortalAddress, rBanReason, rBanTime }) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - case "cancelBan": - { - const { groupId, qortalAddress } = request.payload; - cancelBan({ groupId, qortalAddress}) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - case "registerName": - { - const { name } = request.payload; - registerName({ name}) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - case "makeAdmin": - { - const { groupId, qortalAddress } = request.payload; - makeAdmin({ groupId, qortalAddress}) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - case "removeAdmin": - { - const { groupId, qortalAddress } = request.payload; - removeAdmin({ groupId, qortalAddress}) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - } - - break; - + + case "createGroup": + { + const { + groupName, + groupDescription, + groupType, + groupApprovalThreshold, + minBlock, + maxBlock, + } = request.payload; + createGroup({ + groupName, + groupDescription, + groupType, + groupApprovalThreshold, + minBlock, + maxBlock, + }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "cancelInvitationToGroup": + { + const { groupId, qortalAddress } = request.payload; + cancelInvitationToGroup({ groupId, qortalAddress }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "leaveGroup": + { + const { groupId } = request.payload; + leaveGroup({ groupId }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "joinGroup": + { + const { groupId } = request.payload; + joinGroup({ groupId }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + + case "kickFromGroup": + { + const { groupId, qortalAddress, rBanReason } = request.payload; + kickFromGroup({ groupId, qortalAddress, rBanReason }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "banFromGroup": + { + const { groupId, qortalAddress, rBanReason, rBanTime } = + request.payload; + banFromGroup({ groupId, qortalAddress, rBanReason, rBanTime }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "addDataPublishes": + { + const { data, groupId, type } = request.payload; + addDataPublishes(data, groupId, type) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + break; + case "getDataPublishes": + { + const { groupId, type } = request.payload; + getDataPublishes(groupId, type) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + break; + case "addUserSettings": + { + const { keyValue } = request.payload; + addUserSettings({keyValue}) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + break; + case "getUserSettings": + { + const { key } = request.payload; + getUserSettings({key}) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + break; + case "cancelBan": + { + const { groupId, qortalAddress } = request.payload; + cancelBan({ groupId, qortalAddress }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "registerName": + { + const { name } = request.payload; + registerName({ name }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "makeAdmin": + { + const { groupId, qortalAddress } = request.payload; + makeAdmin({ groupId, qortalAddress }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "removeAdmin": + { + const { groupId, qortalAddress } = request.payload; + removeAdmin({ groupId, qortalAddress }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "oauth": { const { nodeBaseUrl, senderAddress, senderPublicKey, timestamp } = request.payload; @@ -2667,11 +3147,10 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { break; } case "setChatHeads": { - const { data} = - request.payload; + const { data } = request.payload; - setChatHeads({ - data + setChatHeads({ + data, }) .then((res) => { sendResponse(res); @@ -2684,10 +3163,8 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { break; } case "getChatHeads": { - - getChatHeads() + getChatHeads() .then((res) => { - sendResponse(res); }) .catch((error) => { @@ -2698,32 +3175,29 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { break; } case "notification": { - const notificationId = 'chat_notification_' + Date.now(); // Create a unique ID + const notificationId = "chat_notification_" + Date.now(); // Create a unique ID - const { } = - request.payload; - chrome.notifications.create(notificationId, { - type: 'basic', - iconUrl: 'qort.png', // Add an appropriate icon for chat notifications - title: 'New Group Message!', - message: 'You have received a new message from one of your groups', - priority: 2, // Use the maximum priority to ensure it's noticeable - // buttons: [ - // { title: 'Go to group' } - // ] - }); - // Set a timeout to clear the notification after 'timeout' milliseconds - setTimeout(() => { - chrome.notifications.clear(notificationId); - }, 3000); - sendResponse(true) + const {} = request.payload; + chrome.notifications.create(notificationId, { + type: "basic", + iconUrl: "qort.png", // Add an appropriate icon for chat notifications + title: "New Group Message!", + message: "You have received a new message from one of your groups", + priority: 2, // Use the maximum priority to ensure it's noticeable + // buttons: [ + // { title: 'Go to group' } + // ] + }); + // Set a timeout to clear the notification after 'timeout' milliseconds + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 3000); + sendResponse(true); break; } case "addTimestampEnterChat": { - - const { groupId, timestamp } = - request.payload; - addTimestampEnterChat({groupId, timestamp}) + const { groupId, timestamp } = request.payload; + addTimestampEnterChat({ groupId, timestamp }) .then((res) => { sendResponse(res); }) @@ -2736,48 +3210,29 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { case "setApiKey": { const { payload } = request; - - + // Save the apiKey in chrome.storage.local for persistence chrome.storage.local.set({ apiKey: payload }, () => { - - sendResponse(true) + sendResponse(true); }); - return true + return true; break; } case "getApiKey": { getApiKeyFromStorage() - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - return true + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + return true; break; } case "notifyAdminRegenerateSecretKey": { - const { groupName, adminAddress } = - request.payload; - notifyAdminRegenerateSecretKey({groupName, adminAddress}) - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - break; - } - - - case "addGroupNotificationTimestamp": { - - const { groupId, timestamp } = - request.payload; - addTimestampGroupAnnouncement({groupId, timestamp, seenTimestamp: true}) + const { groupName, adminAddress } = request.payload; + notifyAdminRegenerateSecretKey({ groupName, adminAddress }) .then((res) => { sendResponse(res); }) @@ -2787,8 +3242,62 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { }); break; } + + case "addGroupNotificationTimestamp": { + const { groupId, timestamp } = request.payload; + addTimestampGroupAnnouncement({ + groupId, + timestamp, + seenTimestamp: true, + }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + break; + } + case "clearAllNotifications": { + clearAllNotifications() + .then((res) => {}) + .catch((error) => {}); + break; + } + case "setGroupData": { + const { groupId, secretKeyData, secretKeyResource, admins } = + request.payload; + setGroupData({ + groupId, + secretKeyData, + secretKeyResource, + admins, + }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + break; + } + case "getGroupDataSingle": { + const { groupId } = request.payload; + getGroupDataSingle(groupId) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + return true; + break; + } case "getTimestampEnterChat": { - getTimestampEnterChat() + getTimestampEnterChat() .then((res) => { sendResponse(res); }) @@ -2800,15 +3309,15 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { } case "getGroupNotificationTimestamp": { getTimestampGroupAnnouncement() - .then((res) => { - sendResponse(res); - }) - .catch((error) => { - sendResponse({ error: error.message }); - console.error(error.message); - }); - break; - } + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + break; + } case "authentication": { getSaveWallet() @@ -2816,7 +3325,9 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { sendResponse(true); }) .catch((error) => { - const popupUrl = chrome.runtime.getURL("index.html?secondary=true"); + const popupUrl = chrome.runtime.getURL( + "index.html?secondary=true" + ); chrome.windows.getAll( { populate: true, windowTypes: ["popup"] }, @@ -2851,16 +3362,21 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { const topPosition = (primaryDisplay.bounds.height - windowHeight) / 2; - chrome.windows.create({ - url: chrome.runtime.getURL("index.html?secondary=true"), - type: "popup", - width: windowWidth, - height: windowHeight, - left: leftPosition, - top: 0, - } , () => { - removeDuplicateWindow(popupUrl) - }); + chrome.windows.create( + { + url: chrome.runtime.getURL( + "index.html?secondary=true" + ), + type: "popup", + width: windowWidth, + height: windowHeight, + left: leftPosition, + top: 0, + }, + () => { + removeDuplicateWindow(popupUrl); + } + ); }); } @@ -2907,7 +3423,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { error: "User has not authenticated, try again.", }); clearInterval(intervalId); // Stop checking due to timeout - + // Handle timeout situation if needed } }; @@ -2920,12 +3436,12 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { break; case "buyOrder": { - - const { qortalAtAddress, hostname } = request.payload; - getTradeInfo(qortalAtAddress) + const { qortalAtAddresses, hostname, useLocal } = request.payload; + getTradesInfo(qortalAtAddresses) .then((crosschainAtInfo) => { - - const popupUrl = chrome.runtime.getURL("index.html?secondary=true") + const popupUrl = chrome.runtime.getURL( + "index.html?secondary=true" + ); chrome.windows.getAll( { populate: true, windowTypes: ["popup"] }, @@ -2960,16 +3476,21 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { const topPosition = (primaryDisplay.bounds.height - windowHeight) / 2; - chrome.windows.create({ - url: chrome.runtime.getURL("index.html?secondary=true"), - type: "popup", - width: windowWidth, - height: windowHeight, - left: leftPosition, - top: 0, - }, () => { - removeDuplicateWindow(popupUrl) - }); + chrome.windows.create( + { + url: chrome.runtime.getURL( + "index.html?secondary=true" + ), + type: "popup", + width: windowWidth, + height: windowHeight, + left: leftPosition, + top: 0, + }, + () => { + removeDuplicateWindow(popupUrl); + } + ); }); } @@ -2986,6 +3507,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { hostname, crosschainAtInfo, interactionId, + useLocal }, }); }, 500); @@ -3003,7 +3525,6 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { break; case "connection": { - const { hostname } = request.payload; connection(hostname) @@ -3014,7 +3535,9 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { ) { sendResponse(true); } else { - const popupUrl = chrome.runtime.getURL("index.html?secondary=true"); + const popupUrl = chrome.runtime.getURL( + "index.html?secondary=true" + ); chrome.windows.getAll( { populate: true, windowTypes: ["popup"] }, (windows) => { @@ -3026,19 +3549,16 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { (tab) => tab.url && tab.url.startsWith(popupUrl) ) ); - + if (existingPopup) { // If the popup exists but is minimized or not focused, focus it chrome.windows.update(existingPopup.id, { focused: true, state: "normal", }); - } else { - - + } else { // No existing popup found, create a new one chrome.system.display.getInfo((displays) => { - // Assuming the primary display is the first one (adjust logic as needed) const primaryDisplay = displays[0]; const screenWidth = primaryDisplay.bounds.width; @@ -3052,16 +3572,19 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { const topPosition = (primaryDisplay.bounds.height - windowHeight) / 2; - chrome.windows.create({ + chrome.windows.create( + { url: popupUrl, type: "popup", width: windowWidth, height: windowHeight, left: leftPosition, top: 0, - }, () => { - removeDuplicateWindow(popupUrl) - }); + }, + () => { + removeDuplicateWindow(popupUrl); + } + ); }); } @@ -3129,16 +3652,19 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { const topPosition = (primaryDisplay.bounds.height - windowHeight) / 2; - chrome.windows.create({ - url: chrome.runtime.getURL("index.html?secondary=true"), - type: "popup", - width: windowWidth, - height: windowHeight, - left: leftPosition, - top: 0, - }, () => { - removeDuplicateWindow(popupUrl) - }); + chrome.windows.create( + { + url: chrome.runtime.getURL("index.html?secondary=true"), + type: "popup", + width: windowWidth, + height: windowHeight, + left: leftPosition, + top: 0, + }, + () => { + removeDuplicateWindow(popupUrl); + } + ); }); } @@ -3227,7 +3753,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { break; case "buyOrderConfirmation": { - const { crosschainAtInfo, isDecline } = request.payload; + const { crosschainAtInfo, isDecline, useLocal } = request.payload; const interactionId2 = request.payload.interactionId; // Retrieve the stored sendResponse callback const originalSendResponse = pendingResponses.get(interactionId2); @@ -3239,7 +3765,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { pendingResponses.delete(interactionId2); return; } - createBuyOrderTx({ crosschainAtInfo }) + createBuyOrderTx({ crosschainAtInfo, useLocal }) .then((res) => { sendResponse(true); originalSendResponse(res); @@ -3262,10 +3788,10 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { previousData, previousNumber, }) - .then(({data, numberOfMembers}) => { + .then(({ data, numberOfMembers }) => { sendResponse(data); - - if(!previousData){ + + if (!previousData) { // first secret key of the group sendChatGroup({ groupId, @@ -3275,11 +3801,11 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { }) .then(() => {}) .catch((error) => { - console.error('1',error.message); + console.error("1", error.message); }); - return + return; } - sendChatNotification(data, groupId, previousData, numberOfMembers) + sendChatNotification(data, groupId, previousData, numberOfMembers); }) .catch((error) => { console.error(error.message); @@ -3292,27 +3818,27 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { const { encryptedData, identifier } = request.payload; publishGroupEncryptedResource({ - encryptedData, identifier + encryptedData, + identifier, }) .then((data) => { sendResponse(data); - }) .catch((error) => { console.error(error.message); sendResponse({ error: error.message }); }); - return true + return true; break; } case "handleActiveGroupDataFromSocket": { const { groups, directs } = request.payload; handleActiveGroupDataFromSocket({ - groups, directs + groups, + directs, }) .then((data) => { sendResponse(true); - }) .catch((error) => { console.error(error.message); @@ -3324,9 +3850,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { case "getThreadActivity": { checkThreads(true) .then((data) => { - sendResponse(data); - }) .catch((error) => { console.error(error.message); @@ -3336,14 +3860,12 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { break; } - case "updateThreadActivity": { const { threadId, qortalName, groupId, thread } = request.payload; - updateThreadActivity({threadId, qortalName, groupId, thread}) + updateThreadActivity({ threadId, qortalName, groupId, thread }) .then(() => { sendResponse(true); - }) .catch((error) => { console.error(error.message); @@ -3371,7 +3893,6 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { encryptSingle({ data64: data, secretKeyObject: secretKeyObject }) .then((res) => { - sendResponse(res); }) .catch((error) => { @@ -3386,7 +3907,6 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { decryptSingleFunc({ messages: data, secretKeyObject, skipDecodeBase64 }) .then((res) => { - sendResponse(res); }) .catch((error) => { @@ -3397,24 +3917,40 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { break; } case "pauseAllQueues": { - pauseAllQueues() - sendResponse(res); + pauseAllQueues(); + sendResponse(true); break; - break; + } case "resumeAllQueues": { - resumeAllQueues() - sendResponse(res); + resumeAllQueues(); + sendResponse(true); + + break; + } + case "checkLocal": { + checkLocalFunc() + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + break; } case "decryptSingleForPublishes": { const { data, secretKeyObject, skipDecodeBase64 } = request.payload; - decryptSingleForPublishes({ messages: data, secretKeyObject, skipDecodeBase64 }) + decryptSingleForPublishes({ + messages: data, + secretKeyObject, + skipDecodeBase64, + }) .then((res) => { - sendResponse(res); }) .catch((error) => { @@ -3424,13 +3960,12 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { break; } - + case "decryptDirect": { const { data, involvingAddress } = request.payload; decryptDirectFunc({ messages: data, involvingAddress }) .then((res) => { - sendResponse(res); }) .catch((error) => { @@ -3440,7 +3975,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { break; } - + case "sendChatGroup": { const { groupId, @@ -3451,7 +3986,6 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { sendChatGroup({ groupId, typeMessage, chatReference, messageText }) .then((res) => { - sendResponse(res); }) .catch((error) => { @@ -3467,11 +4001,21 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { typeMessage = undefined, chatReference = undefined, messageText, + publicKeyOfRecipient, + address, + otherData } = request.payload; - - sendChatDirect({ directTo, chatReference, messageText, typeMessage }) + console.log('chatReferencebg', chatReference) + sendChatDirect({ + directTo, + chatReference, + messageText, + typeMessage, + publicKeyOfRecipient, + address, + otherData + }) .then((res) => { - sendResponse(res); }) .catch((error) => { @@ -3482,11 +4026,9 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { break; } case "setupGroupWebsocket": { - - checkNewMessages() - checkThreads() - - + checkNewMessages(); + checkThreads(); + // if(socket){ // if(groups){ // console.log('hasgroups1') @@ -3510,7 +4052,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { // listenForNewGroupAnnouncements() // listenForThreadUpdates() // }, 200); - sendResponse(true) + sendResponse(true); break; } @@ -3518,38 +4060,47 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { case "logout": { try { - const logoutFunc = async()=> { - forceCloseWebSocket() - clearAllQueues() - if(interval){ - // for announcement notification - clearInterval(interval) - } + const logoutFunc = async () => { + forceCloseWebSocket(); + clearAllQueues(); + if (interval) { + // for announcement notification + clearInterval(interval); + } - const wallet = await getSaveWallet(); - const address = wallet.address0; - const key1 = `tempPublish-${address}` - - chrome.storage.local.remove(["keyPair", "walletInfo", "apiKey", "active-groups-directs", key1], () => { - if (chrome.runtime.lastError) { - // Handle error - console.error(chrome.runtime.lastError.message); - } else { - chrome.tabs.query({}, function (tabs) { - tabs.forEach((tab) => { - chrome.tabs.sendMessage(tab.id, { type: "LOGOUT" }); - }); - }); - // Data removed successfully - sendResponse(true); - } - }); - } - logoutFunc() - } catch (error) { - - } - + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key1 = `tempPublish-${address}`; + const key2 = `group-data-${address}`; + const key3 = `${address}-publishData`; + chrome.storage.local.remove( + [ + "keyPair", + "walletInfo", + "apiKey", + "active-groups-directs", + key1, + key2, + key3, + ], + () => { + if (chrome.runtime.lastError) { + // Handle error + console.error(chrome.runtime.lastError.message); + } else { + chrome.tabs.query({}, function (tabs) { + tabs.forEach((tab) => { + chrome.tabs.sendMessage(tab.id, { type: "LOGOUT" }); + }); + }); + // Data removed successfully + sendResponse(true); + } + } + ); + }; + logoutFunc(); + } catch (error) {} } break; @@ -3562,17 +4113,20 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { const saveWindowBounds = (windowId) => { chrome.windows.get(windowId, (window) => { const { top, left, width, height } = window; - chrome.storage.local.set({ - windowBounds: { top, left, width, height } - }, () => { - console.log('Window bounds saved:', { top, left, width, height }); - }); + chrome.storage.local.set( + { + windowBounds: { top, left, width, height }, + }, + () => { + console.log("Window bounds saved:", { top, left, width, height }); + } + ); }); }; // Function to restore window position and size const restoreWindowBounds = (callback) => { - chrome.storage.local.get('windowBounds', (data) => { + chrome.storage.local.get("windowBounds", (data) => { if (data.windowBounds) { callback(data.windowBounds); } else { @@ -3581,26 +4135,38 @@ const restoreWindowBounds = (callback) => { }); }; -chrome.action.onClicked.addListener((tab) => { +chrome.action?.onClicked?.addListener((tab) => { const popupUrl = chrome.runtime.getURL("index.html?main=true"); chrome.windows.getAll( { populate: true, windowTypes: ["popup"] }, (windows) => { // Attempt to find an existing popup window that has a tab with the correct URL - const existingPopup = windows.find( - (w) => - { - - return w.tabs && - w.tabs.some((tab) => tab.url && tab.url.startsWith(popupUrl)); - } - ); + const existingPopup = windows.find((w) => { + return ( + w.tabs && + w.tabs.some((tab) => tab.url && tab.url.startsWith(popupUrl)) + ); + }); if (existingPopup) { // If the popup exists but is minimized or not focused, focus it - chrome.windows.update(existingPopup.id, { - focused: true, - state: "normal", - }); + + if (isMobile) { + const correctTab = existingPopup.tabs.find( + (tab) => tab.url && tab.url.startsWith(popupUrl) + ); + if (correctTab) { + chrome.tabs.update(correctTab.id, { active: true }); + chrome.windows.update(existingPopup.id, { + focused: true, + state: "normal", + }); + } + } else { + chrome.windows.update(existingPopup.id, { + focused: true, + state: "normal", + }); + } } else { // No existing popup found, restore the saved bounds or create a new one restoreWindowBounds((savedBounds) => { @@ -3611,31 +4177,31 @@ chrome.action.onClicked.addListener((tab) => { const screenHeight = primaryDisplay.bounds.height; // Create a new window that uses the saved bounds if available - chrome.windows.create({ - url: chrome.runtime.getURL("index.html?main=true"), - type: "popup", - width: savedBounds ? savedBounds.width : screenWidth, - height: savedBounds ? savedBounds.height : screenHeight, - left: savedBounds ? savedBounds.left : 0, - top: savedBounds ? savedBounds.top : 0, - }, (newWindow) => { - + chrome.windows.create( + { + url: chrome.runtime.getURL("index.html?main=true"), + type: "popup", + width: savedBounds ? savedBounds.width : screenWidth, + height: savedBounds ? savedBounds.height : screenHeight, + left: savedBounds ? savedBounds.left : 0, + top: savedBounds ? savedBounds.top : 0, + }, + (newWindow) => { + // Listen for changes in the window's size or position and save them + chrome.windows.onBoundsChanged.addListener((window) => { + if (window.id === newWindow.id) { + saveWindowBounds(newWindow.id); + } + }); - // Listen for changes in the window's size or position and save them - chrome.windows.onBoundsChanged.addListener((window) => { - if (window.id === newWindow.id) { - saveWindowBounds(newWindow.id); - } - }); - - // Save the final window bounds when the window is closed - chrome.windows.onRemoved.addListener((windowId) => { - if (windowId === newWindow.id) { - - saveWindowBounds(windowId); // Save the position/size before it’s closed - } - }); - }); + // Save the final window bounds when the window is closed + chrome.windows.onRemoved.addListener((windowId) => { + if (windowId === newWindow.id) { + saveWindowBounds(windowId); // Save the position/size before it’s closed + } + }); + } + ); }); }); } @@ -3655,9 +4221,8 @@ chrome.action.onClicked.addListener((tab) => { ); }); -const checkGroupList = async() => { +const checkGroupList = async () => { try { - const wallet = await getSaveWallet(); const address = wallet.address0; const url = await createEndpoint(`/chat/active/${address}`); @@ -3668,73 +4233,73 @@ const checkGroupList = async() => { }, }); const data = await response.json(); - - const filteredGroups = data.groups?.filter(item => item?.groupId !== 0) || []; - const sortedGroups = filteredGroups.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); - const sortedDirects = (data?.direct || []).filter(item => - item?.name !== 'extension-proxy' && item?.address !== 'QSMMGSgysEuqDCuLw3S4cHrQkBrh3vP3VH' - ).sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); + + const filteredGroups = + data.groups?.filter((item) => item?.groupId !== 0) || []; + const sortedGroups = filteredGroups.sort( + (a, b) => (b.timestamp || 0) - (a.timestamp || 0) + ); + const sortedDirects = (data?.direct || []) + .filter( + (item) => + item?.name !== "extension-proxy" && + item?.address !== "QSMMGSgysEuqDCuLw3S4cHrQkBrh3vP3VH" + ) + .sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); handleActiveGroupDataFromSocket({ groups: sortedGroups, - directs: sortedDirects - }) + directs: sortedDirects, + }); } catch (error) { - console.error(error) + console.error(error); } finally { } -} +}; -const checkActiveChatsForNotifications = async ()=> { +const checkActiveChatsForNotifications = async () => { try { - - const popupUrl = chrome.runtime.getURL("index.html?main=true"); chrome.windows.getAll( { populate: true, windowTypes: ["popup"] }, (windows) => { // Attempt to find an existing popup window that has a tab with the correct URL - const existingPopup = windows.find( - (w) => { - - return w.tabs && w.tabs.some((tab) => tab.url && tab.url.startsWith(popupUrl)); - } - ); - + const existingPopup = windows.find((w) => { + return ( + w.tabs && + w.tabs.some((tab) => tab.url && tab.url.startsWith(popupUrl)) + ); + }); + if (existingPopup) { - } else { - checkGroupList() - + checkGroupList(); } - - } ); - } catch (error) { - - } -} -chrome.notifications.onClicked.addListener( (notificationId) => { - + } catch (error) {} +}; +chrome.notifications?.onClicked?.addListener((notificationId) => { const popupUrl = chrome.runtime.getURL("index.html?main=true"); - const isDirect = notificationId.includes('_type=direct_'); - const isGroup = notificationId.includes('_type=group_'); - const isGroupAnnouncement = notificationId.includes('_type=group-announcement_'); - const isNewThreadPost = notificationId.includes('_type=thread-post_'); + const isDirect = notificationId.includes("_type=direct_"); + const isGroup = notificationId.includes("_type=group_"); + const isGroupAnnouncement = notificationId.includes( + "_type=group-announcement_" + ); + const isNewThreadPost = notificationId.includes("_type=thread-post_"); - let isExisting = false + let isExisting = false; chrome.windows.getAll( { populate: true, windowTypes: ["popup"] }, async (windows) => { // Attempt to find an existing popup window that has a tab with the correct URL - const existingPopup = windows.find( - (w) => { - - return w.tabs && w.tabs.some((tab) => tab.url && tab.url.startsWith(popupUrl)); - } - ); + const existingPopup = windows.find((w) => { + return ( + w.tabs && + w.tabs.some((tab) => tab.url && tab.url.startsWith(popupUrl)) + ); + }); if (existingPopup) { // If the popup exists but is minimized or not focused, focus it @@ -3742,7 +4307,7 @@ chrome.notifications.onClicked.addListener( (notificationId) => { focused: true, state: "normal", }); - isExisting = true + isExisting = true; } else { // No existing popup found, restore saved bounds or create a new one restoreWindowBounds((savedBounds) => { @@ -3753,84 +4318,91 @@ chrome.notifications.onClicked.addListener( (notificationId) => { const screenHeight = primaryDisplay.bounds.height; // Create a new window that takes up the full screen or uses saved bounds - chrome.windows.create({ - url: chrome.runtime.getURL("index.html?main=true"), - type: "popup", - width: savedBounds ? savedBounds.width : screenWidth, - height: savedBounds ? savedBounds.height : screenHeight, - left: savedBounds ? savedBounds.left : 0, - top: savedBounds ? savedBounds.top : 0, - }, (newWindow) => { - + chrome.windows.create( + { + url: chrome.runtime.getURL("index.html?main=true"), + type: "popup", + width: savedBounds ? savedBounds.width : screenWidth, + height: savedBounds ? savedBounds.height : screenHeight, + left: savedBounds ? savedBounds.left : 0, + top: savedBounds ? savedBounds.top : 0, + }, + (newWindow) => { + // Listen for changes in the window's size or position and save them + chrome.windows.onBoundsChanged.addListener((window) => { + if (window.id === newWindow.id) { + saveWindowBounds(newWindow.id); + } + }); - // Listen for changes in the window's size or position and save them - chrome.windows.onBoundsChanged.addListener((window) => { - if (window.id === newWindow.id) { - saveWindowBounds(newWindow.id); - } - }); - - // Save the final window bounds when the window is closed - chrome.windows.onRemoved.addListener((windowId) => { - if (windowId === newWindow.id) { - - saveWindowBounds(windowId); // Save the position/size before it’s closed - } - }); - }); + // Save the final window bounds when the window is closed + chrome.windows.onRemoved.addListener((windowId) => { + if (windowId === newWindow.id) { + saveWindowBounds(windowId); // Save the position/size before it’s closed + } + }); + } + ); }); }); } - const activeData = await getStoredData('active-groups-directs') || { groups: [], directs: [] }; - setTimeout(() => { - - - chrome.runtime.sendMessage({ - action: "SET_GROUPS", - payload: activeData?.groups || [], - }); - chrome.runtime.sendMessage({ - action: "SET_DIRECTS", - payload: activeData?.directs || [], - }); - }, isExisting ? 100 : 1000); + const activeData = (await getStoredData("active-groups-directs")) || { + groups: [], + directs: [], + }; + setTimeout( + () => { + chrome.runtime.sendMessage({ + action: "SET_GROUPS", + payload: activeData?.groups || [], + }); + chrome.runtime.sendMessage({ + action: "SET_DIRECTS", + payload: activeData?.directs || [], + }); + }, + isExisting ? 100 : 1000 + ); const interactionId = Date.now().toString(); // Simple example; consider a better unique ID - setTimeout(() => { - chrome.runtime.sendMessage({ - action: "INITIATE_MAIN", - payload: {}, - }); + setTimeout( + () => { + chrome.runtime.sendMessage({ + action: "INITIATE_MAIN", + payload: {}, + }); - // Handle different types of notifications - if (isDirect) { - const fromValue = notificationId.split('_from=')[1]; - chrome.runtime.sendMessage({ - action: "NOTIFICATION_OPEN_DIRECT", - payload: { from: fromValue }, - }); - } else if (isGroup) { - const fromValue = notificationId.split('_from=')[1]; - chrome.runtime.sendMessage({ - action: "NOTIFICATION_OPEN_GROUP", - payload: { from: fromValue }, - }); - } else if (isGroupAnnouncement) { - const fromValue = notificationId.split('_from=')[1]; - chrome.runtime.sendMessage({ - action: "NOTIFICATION_OPEN_ANNOUNCEMENT_GROUP", - payload: { from: fromValue }, - }); - } else if (isNewThreadPost) { - const dataValue = notificationId.split('_data=')[1]; - const dataParsed = JSON.parse(dataValue); - - chrome.runtime.sendMessage({ - action: "NOTIFICATION_OPEN_THREAD_NEW_POST", - payload: { data: dataParsed }, - }); - } - }, isExisting ? 400 : 3000); + // Handle different types of notifications + if (isDirect) { + const fromValue = notificationId.split("_from=")[1]; + chrome.runtime.sendMessage({ + action: "NOTIFICATION_OPEN_DIRECT", + payload: { from: fromValue }, + }); + } else if (isGroup) { + const fromValue = notificationId.split("_from=")[1]; + chrome.runtime.sendMessage({ + action: "NOTIFICATION_OPEN_GROUP", + payload: { from: fromValue }, + }); + } else if (isGroupAnnouncement) { + const fromValue = notificationId.split("_from=")[1]; + chrome.runtime.sendMessage({ + action: "NOTIFICATION_OPEN_ANNOUNCEMENT_GROUP", + payload: { from: fromValue }, + }); + } else if (isNewThreadPost) { + const dataValue = notificationId.split("_data=")[1]; + const dataParsed = JSON.parse(dataValue); + + chrome.runtime.sendMessage({ + action: "NOTIFICATION_OPEN_THREAD_NEW_POST", + payload: { data: dataParsed }, + }); + } + }, + isExisting ? 400 : 3000 + ); // Store sendResponse callback with the interaction ID pendingResponses.set(interactionId, sendResponse); @@ -3839,23 +4411,25 @@ chrome.notifications.onClicked.addListener( (notificationId) => { }); // Reconnect when service worker wakes up -chrome.runtime.onStartup.addListener(() => { +chrome.runtime?.onStartup.addListener(() => { console.log("Service worker started up, reconnecting WebSocket..."); // initWebsocketMessageGroup(); // listenForNewGroupAnnouncements() // listenForThreadUpdates() }); -chrome.runtime.onInstalled.addListener((details) => { +chrome.runtime?.onInstalled.addListener((details) => { if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) { - console.log('Extension Installed'); + console.log("Extension Installed"); // Perform tasks that should only happen on extension installation // Example: Initialize WebSocket, set default settings, etc. } else if (details.reason === chrome.runtime.OnInstalledReason.UPDATE) { - console.log('Extension Updated'); + console.log("Extension Updated"); // Handle the update logic here (e.g., migrate settings) - } else if (details.reason === chrome.runtime.OnInstalledReason.CHROME_UPDATE) { - console.log('Chrome updated'); + } else if ( + details.reason === chrome.runtime.OnInstalledReason.CHROME_UPDATE + ) { + console.log("Chrome updated"); // Optional: Handle Chrome-specific updates if necessary } @@ -3866,28 +4440,23 @@ chrome.runtime.onInstalled.addListener((details) => { }); // Check if the alarm already exists before creating it -chrome.alarms.get("checkForNotifications", (existingAlarm) => { +chrome.alarms?.get("checkForNotifications", (existingAlarm) => { if (!existingAlarm) { // If the alarm does not exist, create it - chrome.alarms.create("checkForNotifications", { periodInMinutes: 4 }); + chrome.alarms.create("checkForNotifications", { periodInMinutes: 10 }); } }); -chrome.alarms.onAlarm.addListener(async (alarm) => { - try { - - if (alarm.name === "checkForNotifications") { +chrome.alarms?.onAlarm.addListener(async (alarm) => { + try { + if (alarm.name === "checkForNotifications") { // initWebsocketMessageGroup(address); const wallet = await getSaveWallet(); const address = wallet.address0; - if(!address) return - checkActiveChatsForNotifications() - checkNewMessages() - checkThreads() - - } - } catch (error) { - - } + if (!address) return; + checkActiveChatsForNotifications(); + checkNewMessages(); + checkThreads(); + } + } catch (error) {} }); - diff --git a/src/common/CustomSvg.tsx b/src/common/CustomSvg.tsx new file mode 100644 index 0000000..c1d8f09 --- /dev/null +++ b/src/common/CustomSvg.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +export const CustomSvg = ({ src, color = 'black', size = 24 }) => { + return ( + + {src} + + ); + }; + \ No newline at end of file diff --git a/src/components/Chat/AnnouncementDiscussion.tsx b/src/components/Chat/AnnouncementDiscussion.tsx index 7a4bb9c..a162c8b 100644 --- a/src/components/Chat/AnnouncementDiscussion.tsx +++ b/src/components/Chat/AnnouncementDiscussion.tsx @@ -10,7 +10,7 @@ import { decryptPublishes, getTempPublish, saveTempPublish } from "./GroupAnnoun import { AnnouncementList } from "./AnnouncementList"; import { Spacer } from "../../common/Spacer"; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import { getBaseApiReact, pauseAllQueues, resumeAllQueues } from "../../App"; +import { getArbitraryEndpointReact, getBaseApiReact, isMobile, pauseAllQueues, resumeAllQueues } from "../../App"; const tempKey = 'accouncement-comment' @@ -26,6 +26,8 @@ export const AnnouncementDiscussion = ({ }) => { const [isSending, setIsSending] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [isFocusedParent, setIsFocusedParent] = useState(false); + const [comments, setComments] = useState([]) const [tempPublishedList, setTempPublishedList] = useState([]) const firstMountRef = useRef(false) @@ -38,6 +40,12 @@ export const AnnouncementDiscussion = ({ const clearEditorContent = () => { if (editorRef.current) { editorRef.current.chain().focus().clearContent().run(); + if(isMobile){ + setTimeout(() => { + editorRef.current?.chain().blur().run(); + setIsFocusedParent(false) + }, 200); + } } }; @@ -47,6 +55,7 @@ export const AnnouncementDiscussion = ({ const res = await fetch( `${getBaseApiReact()}/arbitrary/DOCUMENT/${name}/${identifier}?encoding=base64` ); + if(!res?.ok) return const data = await res.text(); const response = await decryptPublishes([{ data }], secretKey); @@ -66,7 +75,7 @@ export const AnnouncementDiscussion = ({ if (!selectedAnnouncement) return; return new Promise((res, rej) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "publishGroupEncryptedResource", payload: { @@ -78,6 +87,7 @@ export const AnnouncementDiscussion = ({ if (!response?.error) { res(response); + return } rej(response.error); } @@ -170,7 +180,7 @@ export const AnnouncementDiscussion = ({ // dispatch(setIsLoadingGlobal(true)) const identifier = `cm-${selectedAnnouncement.identifier}`; - const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true`; + const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; const response = await fetch(url, { method: "GET", headers: { @@ -200,7 +210,7 @@ export const AnnouncementDiscussion = ({ const offset = comments.length const identifier = `cm-${selectedAnnouncement.identifier}`; - const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; + const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; const response = await fetch(url, { method: "GET", headers: { @@ -245,7 +255,7 @@ export const AnnouncementDiscussion = ({ return (
- + setSelectedAnnouncement(null)} sx={{ cursor: 'pointer' }} /> @@ -274,6 +286,7 @@ export const AnnouncementDiscussion = ({ disableComment showLoadMore={comments.length > 0 && comments.length % 20 === 0} loadMore={loadMore} + myName={myName} />
@@ -297,6 +314,7 @@ export const AnnouncementDiscussion = ({ display: "flex", flexDirection: "column", // height: '100%', + flexGrow: isMobile && 1, overflow: "auto", }} > @@ -304,8 +322,42 @@ export const AnnouncementDiscussion = ({ setEditorRef={setEditorRef} onEnter={publishComment} disableEnter + maxHeightOffset="60px" + isFocusedParent={isFocusedParent} setIsFocusedParent={setIsFocusedParent} + />
+ + {isFocusedParent && ( + { + if(isSending) return + setIsFocusedParent(false) + clearEditorContent() + // Unfocus the editor + }} + style={{ + marginTop: 'auto', + alignSelf: 'center', + cursor: isSending ? 'default' : 'pointer', + flexShrink: 0, + padding: isMobile && '5px', + fontSize: isMobile && '14px', + background: 'red', + }} + > + + {` Close`} + + + )} { if (isSending) return; @@ -317,6 +369,8 @@ export const AnnouncementDiscussion = ({ cursor: isSending ? "default" : "pointer", background: isSending && "rgba(0, 0, 0, 0.8)", flexShrink: 0, + padding: isMobile && '5px', + fontSize: isMobile && '14px' }} > {isSending && ( @@ -334,6 +388,8 @@ export const AnnouncementDiscussion = ({ )} {` Publish Comment`} + +
{ +import { getArbitraryEndpointReact, getBaseApiReact } from "../../App"; +import { WrapperUserAction } from "../WrapperUserAction"; +export const AnnouncementItem = ({ message, messageData, setSelectedAnnouncement, disableComment, myName }) => { const [commentLength, setCommentLength] = useState(0) const getNumberOfComments = React.useCallback( @@ -20,7 +21,7 @@ export const AnnouncementItem = ({ message, messageData, setSelectedAnnouncement // dispatch(setIsLoadingGlobal(true)) const identifier = `cm-${message.identifier}`; - const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=0&includemetadata=false&offset=${offset}&reverse=true`; + const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=0&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; const response = await requestQueueCommentCount.enqueue(() => { return fetch(url, { @@ -60,8 +61,10 @@ export const AnnouncementItem = ({ message, messageData, setSelectedAnnouncement + {message?.name?.charAt(0)} + + {message?.name} + {!messageData?.decryptedData && ( { const listRef = useRef(); @@ -55,6 +56,7 @@ export const AnnouncementList = ({ return (
- +
); diff --git a/src/components/Chat/ChatDirect.tsx b/src/components/Chat/ChatDirect.tsx index beaca3c..280a42d 100644 --- a/src/components/Chat/ChatDirect.tsx +++ b/src/components/Chat/ChatDirect.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react' import { objectToBase64 } from '../../qdn/encryption/group-encryption' import { ChatList } from './ChatList' @@ -6,18 +6,29 @@ import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; import Tiptap from './TipTap' import { CustomButton } from '../../App-styles' import CircularProgress from '@mui/material/CircularProgress'; -import { Input } from '@mui/material'; +import { Box, ButtonBase, Input, Typography } from '@mui/material'; import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar'; import { getNameInfo } from '../Group/Group'; import { Spacer } from '../../common/Spacer'; import { CustomizedSnackbars } from '../Snackbar/Snackbar'; -import { getBaseApiReactSocket } from '../../App'; +import { getBaseApiReactSocket, isMobile, pauseAllQueues, resumeAllQueues } from '../../App'; +import { getPublicKey } from '../../background'; +import { useMessageQueue } from '../../MessageQueueContext'; +import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from '../../utils/events'; +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 }); +export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDirect, setNewChat, getTimestampEnterChat, myName, balance, close, setMobileViewModeKeepOpen}) => { + const { queueChats, addToQueue, processWithNewMessages} = useMessageQueue(); + const [isFocusedParent, setIsFocusedParent] = useState(false); -export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDirect, setNewChat, getTimestampEnterChat, myName}) => { const [messages, setMessages] = useState([]) const [isSending, setIsSending] = useState(false) const [directToValue, setDirectToValue] = useState('') @@ -25,14 +36,45 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi const [isLoading, setIsLoading] = useState(false) const [openSnack, setOpenSnack] = React.useState(false); const [infoSnack, setInfoSnack] = React.useState(null); + const [publicKeyOfRecipient, setPublicKeyOfRecipient] = React.useState("") const hasInitializedWebsocket = useRef(false) const editorRef = useRef(null); + const socketRef = useRef(null); + const timeoutIdRef = useRef(null); + const groupSocketTimeoutRef = useRef(null); + const [replyMessage, setReplyMessage] = useState(null) const setEditorRef = (editorInstance) => { editorRef.current = editorInstance; }; + const [, forceUpdate] = useReducer((x) => x + 1, 0); - + const triggerRerender = () => { + forceUpdate(); // Trigger re-render by updating the state + }; + const publicKeyOfRecipientRef = useRef(null) + const getPublicKeyFunc = async (address)=> { + try { + const publicKey = await getPublicKey(address) + if(publicKeyOfRecipientRef.current !== selectedDirect?.address) return + setPublicKeyOfRecipient(publicKey) + } catch (error) { + + } + } + const tempMessages = useMemo(()=> { + if(!selectedDirect?.address) return [] + if(queueChats[selectedDirect?.address]){ + return queueChats[selectedDirect?.address] + } + return [] + }, [selectedDirect?.address, queueChats]) + useEffect(()=> { + if(selectedDirect?.address){ + publicKeyOfRecipientRef.current = selectedDirect?.address + getPublicKeyFunc(publicKeyOfRecipientRef.current) + } + }, [selectedDirect?.address]) @@ -40,12 +82,15 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi const decryptMessages = (encryptedMessages: any[])=> { try { return new Promise((res, rej)=> { - chrome.runtime.sendMessage({ action: "decryptDirect", payload: { + chrome?.runtime?.sendMessage({ action: "decryptDirect", payload: { data: encryptedMessages, involvingAddress: selectedDirect?.address }}, (response) => { if (!response?.error) { + + processWithNewMessages(response, selectedDirect?.address) + res(response) if(hasInitialized.current){ @@ -54,7 +99,7 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi ...item, id: item.signature, text: item.message, - unread: true + unread: item?.sender === myAddress ? false : true } } ) setMessages((prev)=> [...prev, ...formatted]) @@ -80,78 +125,115 @@ 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'); + socketRef.current = null; + } + }; + + const pingWebSocket = () => { + try { + if (socketRef.current?.readyState === WebSocket.OPEN) { + socketRef.current.send('ping'); + timeoutIdRef.current = setTimeout(() => { + if (socketRef.current) { + socketRef.current.close(); + clearTimeout(groupSocketTimeoutRef.current); + } + }, 5000); // Close if no pong in 5 seconds + } + } catch (error) { + console.error('Error during ping:', error); + } + }; + + const initWebsocketMessageGroup = () => { - let timeoutId - let groupSocketTimeout - - let socketTimeout: any - let socketLink = `${getBaseApiReactSocket()}/websockets/chat/messages?involving=${selectedDirect?.address}&involving=${myAddress}&encoding=BASE64&limit=100` - const socket = new WebSocket(socketLink) - - const pingGroupSocket = () => { - socket.send('ping') - timeoutId = setTimeout(() => { - socket.close() - clearTimeout(groupSocketTimeout) - }, 5000) // Close the WebSocket connection if no pong message is received within 5 seconds. - } - socket.onopen = () => { - - setTimeout(pingGroupSocket, 50) - } - socket.onmessage = (e) => { + forceCloseWebSocket(); // Close any existing connection + + if (!selectedDirect?.address || !myAddress) return; + + const socketLink = `${getBaseApiReactSocket()}/websockets/chat/messages?involving=${selectedDirect?.address}&involving=${myAddress}&encoding=BASE64&limit=100`; + socketRef.current = new WebSocket(socketLink); + + socketRef.current.onopen = () => { + console.log('WebSocket connection opened'); + setTimeout(pingWebSocket, 50); // Initial ping + }; + + socketRef.current.onmessage = (e) => { try { if (e.data === 'pong') { - - clearTimeout(timeoutId) - groupSocketTimeout = setTimeout(pingGroupSocket, 45000) - return + clearTimeout(timeoutIdRef.current); + groupSocketTimeoutRef.current = setTimeout(pingWebSocket, 45000); // Ping every 45 seconds } else { - decryptMessages(JSON.parse(e.data)) - setIsLoading(false) + decryptMessages(JSON.parse(e.data)); + setIsLoading(false); } - } catch (error) { - + console.error('Error handling WebSocket message:', error); } - - } - socket.onclose = () => { - console.log('closed') - clearTimeout(socketTimeout) - setTimeout(() => initWebsocketMessageGroup(), 50) - - } - socket.onerror = (e) => { - clearTimeout(groupSocketTimeout) - socket.close() - } + }; + + socketRef.current.onclose = (event) => { + clearTimeout(groupSocketTimeoutRef.current); + clearTimeout(timeoutIdRef.current); + console.warn(`WebSocket closed: ${event.reason || 'unknown reason'}`); + if (event.reason !== 'forced' && event.code !== 1000) { + setTimeout(() => initWebsocketMessageGroup(), 10000); // Retry after 10 seconds + } + }; + + socketRef.current.onerror = (error) => { + console.error('WebSocket error:', error); + clearTimeout(groupSocketTimeoutRef.current); + clearTimeout(timeoutIdRef.current); + if (socketRef.current) { + socketRef.current.close(); + } + }; + }; + + const setDirectChatValueFunc = async (e)=> { + setDirectToValue(e.detail.directToValue) } - - + useEffect(() => { + subscribeToEvent("setDirectToValueNewChat", setDirectChatValueFunc); - useEffect(()=> { - if(hasInitializedWebsocket.current) return - setIsLoading(true) - initWebsocketMessageGroup() - hasInitializedWebsocket.current = true - }, []) + return () => { + unsubscribeFromEvent("setDirectToValueNewChat", setDirectChatValueFunc); + }; + }, []); + useEffect(() => { + if (hasInitializedWebsocket.current || isNewChat) return; + setIsLoading(true); + initWebsocketMessageGroup(); + hasInitializedWebsocket.current = true; + + return () => { + forceCloseWebSocket(); // Clean up WebSocket on component unmount + }; + }, [selectedDirect?.address, myAddress, isNewChat]); -const sendChatDirect = async ({ chatReference = undefined, messageText}: any)=> { +const sendChatDirect = async ({ chatReference = undefined, messageText, otherData}: any, address, publicKeyOfRecipient, isNewChatVar)=> { try { - const directTo = isNewChat ? directToValue : selectedDirect.address + const directTo = isNewChatVar ? directToValue : address if(!directTo) return return new Promise((res, rej)=> { - chrome.runtime.sendMessage({ action: "sendChatDirect", payload: { - directTo, chatReference, messageText + chrome?.runtime?.sendMessage({ action: "sendChatDirect", payload: { + directTo, chatReference, messageText, otherData, publicKeyOfRecipient, address: directTo }}, async (response) => { if (!response?.error) { - if(isNewChat){ + if(isNewChatVar){ let getRecipientName = null try { @@ -167,7 +249,7 @@ const sendChatDirect = async ({ chatReference = undefined, messageText}: any)=> "senderName": myName }) setNewChat(null) - chrome.runtime.sendMessage({ + chrome?.runtime?.sendMessage({ action: "addTimestampEnterChat", payload: { timestamp: Date.now(), @@ -186,87 +268,281 @@ const sendChatDirect = async ({ chatReference = undefined, messageText}: any)=> }) } catch (error) { throw new Error(error) + } finally { } } const clearEditorContent = () => { if (editorRef.current) { editorRef.current.chain().focus().clearContent().run(); + if(isMobile){ + setTimeout(() => { + editorRef.current?.chain().blur().run(); + setIsFocusedParent(false) + executeEvent("sent-new-message-group", {}) + setTimeout(() => { + triggerRerender(); + }, 300); + }, 200); + } } }; 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) { const htmlContent = editorRef.current.getHTML(); if(!htmlContent?.trim() || htmlContent?.trim() === '

') return setIsSending(true) + pauseAllQueues() const message = JSON.stringify(htmlContent) - const res = await sendChatDirect({ messageText: htmlContent}) + + if(isNewChat){ + await sendChatDirect({ messageText: htmlContent}, null, null, true) + return + } + let repliedTo = replyMessage?.signature + + if (replyMessage?.chatReference) { + repliedTo = replyMessage?.chatReference + } + const otherData = { + specialId: uid.rnd(), + repliedTo + } + const sendMessageFunc = async () => { + await sendChatDirect({ chatReference: undefined, messageText: htmlContent, otherData}, selectedDirect?.address, publicKeyOfRecipient, false) + }; + + + + // Add the function to the queue + const messageObj = { + message: { + text: htmlContent, + timestamp: Date.now(), + senderName: myName, + sender: myAddress, + ...(otherData || {}) + }, + + } + addToQueue(sendMessageFunc, messageObj, 'chat-direct', + selectedDirect?.address ); + setTimeout(() => { + executeEvent("sent-new-message-group", {}) + }, 150); clearEditorContent() + setReplyMessage(null) } // send chat message } catch (error) { + const errorMsg = error?.message || error setInfoSnack({ type: "error", - message: error, + message: errorMsg === 'invalid signature' ? 'You need at least 4 QORT to send a message' : errorMsg, }); setOpenSnack(true); console.error(error) } finally { setIsSending(false) + resumeAllQueues() } } + const onReply = useCallback((message)=> { + setReplyMessage(message) + }, []) - return (
+ {!isMobile && ( + + + Close Direct Chat + + )} + {isMobile && ( + + + + { + close() + }} + > + + + + + {isNewChat ? '' : selectedDirect?.name || (selectedDirect?.address?.slice(0,10) + '...')} + + + { + setSelectedDirect(null) + setMobileViewModeKeepOpen('') + setNewChat(false) + }} + > + + + + + + )} {isNewChat && ( <> setDirectToValue(e.target.value)} /> )} - +
+ {replyMessage && ( + + + { + setReplyMessage(null) + }} + > + + + + )} - +
+ + {isFocusedParent && ( + { + if(isSending) return + setIsFocusedParent(false) + clearEditorContent() + // Unfocus the editor + }} + style={{ + marginTop: 'auto', + alignSelf: 'center', + cursor: isSending ? 'default' : 'pointer', + background: 'red', + flexShrink: 0, + padding: isMobile && '5px' + }} + > + + {` Close`} + + + )} { if(isSending) return @@ -277,7 +553,8 @@ const clearEditorContent = () => { alignSelf: 'center', cursor: isSending ? 'default' : 'pointer', background: isSending && 'rgba(0, 0, 0, 0.8)', - flexShrink: 0 + flexShrink: 0, + padding: isMobile && '5px' }} > {isSending && ( @@ -295,6 +572,8 @@ const clearEditorContent = () => { )} {` Send`} + +
{ +export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, myAddress, handleNewEncryptionNotification, hide, handleSecretKeyCreationInProgress, triedToFetchSecretKey, myName, balance}) => { const [messages, setMessages] = useState([]) const [isSending, setIsSending] = useState(false) const [isLoading, setIsLoading] = useState(false) @@ -28,13 +35,20 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, const [openSnack, setOpenSnack] = React.useState(false); 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 const timeoutIdRef = useRef(null); // Timeout ID reference const groupSocketTimeoutRef = useRef(null); // Group Socket Timeout reference const editorRef = useRef(null); - const { queueChats, addToQueue, } = useMessageQueue(); - + const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue(); + const [, forceUpdate] = useReducer((x) => x + 1, 0); + + const triggerRerender = () => { + forceUpdate(); // Trigger re-render by updating the state + }; const setEditorRef = (editorInstance) => { editorRef.current = editorInstance; }; @@ -85,12 +99,17 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, return } return new Promise((res, rej)=> { - chrome.runtime.sendMessage({ action: "decryptSingle", payload: { + chrome?.runtime?.sendMessage({ action: "decryptSingle", payload: { data: encryptedMessages, secretKeyObject: secretKey }}, (response) => { - if (!response?.error) { + processWithNewMessages(response?.map((item)=> { + return { + ...item, + ...(item?.decryptedData || {}) + } + }), selectedGroup) res(response) if(hasInitialized.current){ @@ -98,7 +117,8 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, return { ...item, id: item.signature, - text: item.text, + text: item?.decryptedData?.message || "", + repliedTo: item?.decryptedData?.repliedTo, unread: item?.sender === myAddress ? false : true } } ) @@ -108,7 +128,8 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, return { ...item, id: item.signature, - text: item.text, + text: item?.decryptedData?.message || "", + repliedTo: item?.decryptedData?.repliedTo, unread: false } } ) @@ -233,7 +254,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, const encryptChatMessage = async (data: string, secretKeyObject: any)=> { try { return new Promise((res, rej)=> { - chrome.runtime.sendMessage({ action: "encryptSingle", payload: { + chrome?.runtime?.sendMessage({ action: "encryptSingle", payload: { data, secretKeyObject }}, (response) => { @@ -252,7 +273,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, const sendChatGroup = async ({groupId, typeMessage = undefined, chatReference = undefined, messageText}: any)=> { try { return new Promise((res, rej)=> { - chrome.runtime.sendMessage({ action: "sendChatGroup", payload: { + chrome?.runtime?.sendMessage({ action: "sendChatGroup", payload: { groupId, typeMessage, chatReference, messageText }}, (response) => { @@ -270,6 +291,16 @@ const sendChatGroup = async ({groupId, typeMessage = undefined, chatReference = const clearEditorContent = () => { if (editorRef.current) { editorRef.current.chain().focus().clearContent().run(); + if(isMobile){ + setTimeout(() => { + editorRef.current?.chain().blur().run(); + setIsFocusedParent(false) + executeEvent("sent-new-message-group", {}) + setTimeout(() => { + triggerRerender(); + }, 300); + }, 200); + } } }; @@ -277,6 +308,7 @@ const clearEditorContent = () => { const sendMessage = async ()=> { try { if(isSending) return + if(+balance < 4) throw new Error('You need at least 4 QORT to send a message') pauseAllQueues() if (editorRef.current) { const htmlContent = editorRef.current.getHTML(); @@ -285,11 +317,25 @@ const clearEditorContent = () => { setIsSending(true) const message = htmlContent const secretKeyObject = await getSecretKey(false, true) - const message64: any = await objectToBase64(message) + + let repliedTo = replyMessage?.signature + + if (replyMessage?.chatReference) { + repliedTo = replyMessage?.chatReference + } + const otherData = { + specialId: uid.rnd(), + repliedTo + } + const objectMessage = { + message, + ...(otherData || {}) + } + const message64: any = await objectToBase64(objectMessage) const encryptSingle = await encryptChatMessage(message64, secretKeyObject) // const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle}) - + const sendMessageFunc = async () => { await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle}) }; @@ -300,7 +346,8 @@ const clearEditorContent = () => { text: message, timestamp: Date.now(), senderName: myName, - sender: myAddress + sender: myAddress, + ...(otherData || {}) }, } @@ -310,12 +357,14 @@ const clearEditorContent = () => { executeEvent("sent-new-message-group", {}) }, 150); clearEditorContent() + setReplyMessage(null) } // send chat message } catch (error) { + const errorMsg = error?.message || error setInfoSnack({ type: "error", - message: error, + message: errorMsg, }); setOpenSnack(true); console.error(error) @@ -333,10 +382,13 @@ const clearEditorContent = () => { } }, [hide]); + const onReply = useCallback((message)=> { + setReplyMessage(message) + }, []) return (
{ left: hide && '-100000px', }}> - +
+ {replyMessage && ( + + + { + setReplyMessage(null) + }} + > + + + + )} - + +
+ + {isFocusedParent && ( + { + if(isSending) return + setIsFocusedParent(false) + clearEditorContent() + // Unfocus the editor + }} + style={{ + marginTop: 'auto', + alignSelf: 'center', + cursor: isSending ? 'default' : 'pointer', + background: 'red', + flexShrink: 0, + padding: isMobile && '5px' + }} + > + + {` Close`} + + + )} { if(isSending) return @@ -381,7 +485,9 @@ const clearEditorContent = () => { alignSelf: 'center', cursor: isSending ? 'default' : 'pointer', background: isSending && 'rgba(0, 0, 0, 0.8)', - flexShrink: 0 + flexShrink: 0, + padding: isMobile && '5px', + }} > {isSending && ( @@ -399,6 +505,8 @@ const clearEditorContent = () => { )} {` Send`} + + {/* */}
{/* */} diff --git a/src/components/Chat/ChatList.tsx b/src/components/Chat/ChatList.tsx index b4cae82..3ebb362 100644 --- a/src/components/Chat/ChatList.tsx +++ b/src/components/Chat/ChatList.tsx @@ -3,19 +3,21 @@ import { List, AutoSizer, CellMeasurerCache, CellMeasurer } from 'react-virtuali import { MessageItem } from './MessageItem'; import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events'; -const cache = new CellMeasurerCache({ - fixedWidth: true, - defaultHeight: 50, -}); +// const cache = new CellMeasurerCache({ +// fixedWidth: true, +// defaultHeight: 50, +// }); -export const ChatList = ({ initialMessages, myAddress, tempMessages }) => { +export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onReply }) => { const hasLoadedInitialRef = useRef(false); const listRef = useRef(); const [messages, setMessages] = useState(initialMessages); const [showScrollButton, setShowScrollButton] = useState(false); - - + const cache = useMemo(() => new CellMeasurerCache({ + fixedWidth: true, + defaultHeight: 50, + }), [chatId]); // Recreate cache when chatId changes useEffect(() => { cache.clearAll(); }, []); @@ -65,6 +67,10 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages }) => { } }; + const scrollToItem = useCallback((index) => { + listRef.current.scrollToRow(index); // This scrolls to the specific index + }, []); + const recomputeListHeights = () => { if (listRef.current) { listRef.current.recomputeRowHeights(); @@ -90,7 +96,18 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages }) => { 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, @@ -126,6 +143,11 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages }) => { message={message} onSeen={handleMessageSeen} isTemp={!!message?.isTemp} + myAddress={myAddress} + onReply={onReply} + reply={reply} + scrollToItem={scrollToItem} + replyIndex={replyIndex} />
@@ -174,7 +196,7 @@ let uniqueInitialMessages = Array.from(uniqueInitialMessagesMap.values()).sort(( // }, [messages, myAddress]); return ( -
+
{({ height, width }) => ( { +export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, secretKeyDetails, userInfo, noSecretKey, setHideCommonKeyPopup}) => { const { show, setTxList } = useContext(MyContext); const [openSnack, setOpenSnack] = React.useState(false); @@ -18,9 +18,9 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec const getPublishesFromAdmins = async (admins: string[]) => { // const validApi = await findUsableApi(); const queryString = admins.map((name) => `name=${name}`).join("&"); - const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=DOCUMENT_PRIVATE&identifier=symmetric-qchat-group-${ + const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=symmetric-qchat-group-${ groupId - }&exactmatchnames=true&limit=0&reverse=true&${queryString}`; + }&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`; const response = await fetch(url); if(!response.ok){ throw new Error('network error') @@ -51,11 +51,11 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec - const groupAdmins = await getGroupAdimns(groupId); - if(!groupAdmins.length){ + const {names} = await getGroupAdimns(groupId); + if(!names.length){ throw new Error('Network error') } - const publish = await getPublishesFromAdmins(groupAdmins); + const publish = await getPublishesFromAdmins(names); if (publish === false) { @@ -107,7 +107,7 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec const secretKeyToSend = !secretKey2 ? null : secretKey2 - chrome.runtime.sendMessage({ action: "encryptAndPublishSymmetricKeyGroupChat", payload: { + chrome?.runtime?.sendMessage({ action: "encryptAndPublishSymmetricKeyGroupChat", payload: { groupId: groupId, previousData: secretKeyToSend } }, (response) => { @@ -142,7 +142,7 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec flexDirection: 'column', gap: '25px', maxWidth: '350px', - background: '#4444' + background: '#444444' }}> Re-encyrpt key {noSecretKey ? ( @@ -158,6 +158,15 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec The group member list has changed. Please re-encrypt the secret key. )} + + + diff --git a/src/components/Chat/GroupAnnouncements.tsx b/src/components/Chat/GroupAnnouncements.tsx index 9f8b91f..dee3494 100644 --- a/src/components/Chat/GroupAnnouncements.tsx +++ b/src/components/Chat/GroupAnnouncements.tsx @@ -25,21 +25,30 @@ import { Spacer } from "../../common/Spacer"; import ShortUniqueId from "short-unique-id"; import { AnnouncementList } from "./AnnouncementList"; const uid = new ShortUniqueId({ length: 8 }); -import CampaignIcon from '@mui/icons-material/Campaign'; -import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import CampaignIcon from "@mui/icons-material/Campaign"; +import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import { AnnouncementDiscussion } from "./AnnouncementDiscussion"; -import { MyContext, getBaseApiReact, pauseAllQueues, resumeAllQueues } from "../../App"; +import { + MyContext, + getArbitraryEndpointReact, + getBaseApiReact, + isMobile, + pauseAllQueues, + resumeAllQueues, +} from "../../App"; import { RequestQueueWithPromise } from "../../utils/queue/queue"; import { CustomizedSnackbars } from "../Snackbar/Snackbar"; +import { addDataPublishesFunc, getDataPublishesFunc } from "../Group/Group"; +import { getRootHeight } from "../../utils/mobile/mobileUtils"; -export const requestQueueCommentCount = new RequestQueueWithPromise(3) -export const requestQueuePublishedAccouncements = new RequestQueueWithPromise(3) +export const requestQueueCommentCount = new RequestQueueWithPromise(3); +export const requestQueuePublishedAccouncements = new RequestQueueWithPromise( + 3 +); export const saveTempPublish = async ({ data, key }: any) => { - - return new Promise((res, rej) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "saveTempPublish", payload: { @@ -48,9 +57,9 @@ export const saveTempPublish = async ({ data, key }: any) => { }, }, (response) => { - if (!response?.error) { res(response); + return; } rej(response.error); } @@ -59,18 +68,16 @@ export const saveTempPublish = async ({ data, key }: any) => { }; export const getTempPublish = async () => { - - return new Promise((res, rej) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "getTempPublish", - payload: { - }, + payload: {}, }, (response) => { if (!response?.error) { res(response); + return; } rej(response.error); } @@ -81,7 +88,7 @@ export const getTempPublish = async () => { export const decryptPublishes = async (encryptedMessages: any[], secretKey) => { try { return await new Promise((res, rej) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "decryptSingleForPublishes", payload: { @@ -91,7 +98,6 @@ export const decryptPublishes = async (encryptedMessages: any[], secretKey) => { }, }, (response) => { - if (!response?.error) { res(response); // if(hasInitialized.current){ @@ -126,37 +132,62 @@ export const GroupAnnouncements = ({ handleNewEncryptionNotification, isAdmin, hide, - myName + myName, }) => { const [messages, setMessages] = useState([]); const [isSending, setIsSending] = useState(false); const [isLoading, setIsLoading] = useState(true); const [announcements, setAnnouncements] = useState([]); - const [tempPublishedList, setTempPublishedList] = useState([]) + const [tempPublishedList, setTempPublishedList] = useState([]); const [announcementData, setAnnouncementData] = useState({}); const [selectedAnnouncement, setSelectedAnnouncement] = useState(null); - const { show } = React.useContext(MyContext); + const [isFocusedParent, setIsFocusedParent] = useState(false); + + const { show, rootHeight } = React.useContext(MyContext); const [openSnack, setOpenSnack] = React.useState(false); const [infoSnack, setInfoSnack] = React.useState(null); const hasInitialized = useRef(false); const hasInitializedWebsocket = useRef(false); const editorRef = useRef(null); - + const dataPublishes = useRef({}); const setEditorRef = (editorInstance) => { editorRef.current = editorInstance; }; + const [, forceUpdate] = React.useReducer((x) => x + 1, 0); - const getAnnouncementData = async ({ identifier, name }) => { + const triggerRerender = () => { + forceUpdate(); // Trigger re-render by updating the state + }; + useEffect(() => { + if (!selectedGroup) return; + (async () => { + const res = await getDataPublishesFunc(selectedGroup, "anc"); + dataPublishes.current = res || {}; + })(); + }, [selectedGroup]); + + const getAnnouncementData = async ({ identifier, name, resource }) => { try { - - const res = await requestQueuePublishedAccouncements.enqueue(()=> { - return fetch( - `${getBaseApiReact()}/arbitrary/DOCUMENT/${name}/${identifier}?encoding=base64` - ); - }) - const data = await res.text(); + let data = dataPublishes.current[`${name}-${identifier}`]; + if ( + !data || + data?.update || + data?.created !== (resource?.updated || resource?.created) + ) { + const res = await requestQueuePublishedAccouncements.enqueue(() => { + return fetch( + `${getBaseApiReact()}/arbitrary/DOCUMENT/${name}/${identifier}?encoding=base64` + ); + }); + if (!res?.ok) return; + data = await res.text(); + await addDataPublishesFunc({ ...resource, data }, selectedGroup, "anc"); + } else { + data = data.data; + } + const response = await decryptPublishes([{ data }], secretKey); - + const messageData = response[0]; setAnnouncementData((prev) => { return { @@ -164,14 +195,11 @@ export const GroupAnnouncements = ({ [`${identifier}-${name}`]: messageData, }; }); - - } catch (error) {} + } catch (error) { + console.log("error", error); + } }; - - - - useEffect(() => { if (!secretKey || hasInitializedWebsocket.current) return; setIsLoading(true); @@ -182,7 +210,7 @@ export const GroupAnnouncements = ({ const encryptChatMessage = async (data: string, secretKeyObject: any) => { try { return new Promise((res, rej) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "encryptSingle", payload: { @@ -203,58 +231,61 @@ export const GroupAnnouncements = ({ }; const publishAnc = async ({ encryptedData, identifier }: any) => { - - - return new Promise((res, rej) => { - chrome.runtime.sendMessage( - { - action: "publishGroupEncryptedResource", - payload: { - encryptedData, - identifier, - }, + return new Promise((res, rej) => { + chrome?.runtime?.sendMessage( + { + action: "publishGroupEncryptedResource", + payload: { + encryptedData, + identifier, }, - (response) => { - if (!response?.error) { - res(response); - } - rej(response.error); + }, + (response) => { + if (!response?.error) { + res(response); } - ); - }); + rej(response.error); + } + ); + }); }; const clearEditorContent = () => { if (editorRef.current) { editorRef.current.chain().focus().clearContent().run(); + if (isMobile) { + setTimeout(() => { + editorRef.current?.chain().blur().run(); + setIsFocusedParent(false); + setTimeout(() => { + triggerRerender(); + }, 300); + }, 200); + } } }; - const setTempData = async ()=> { + const setTempData = async () => { try { - const getTempAnnouncements = await getTempPublish() - if(getTempAnnouncements?.announcement){ - let tempData = [] - Object.keys(getTempAnnouncements?.announcement || {}).map((key)=> { - const value = getTempAnnouncements?.announcement[key] - tempData.push(value.data) - }) - setTempPublishedList(tempData) - } - } catch (error) { - - } - - } + const getTempAnnouncements = await getTempPublish(); + if (getTempAnnouncements?.announcement) { + let tempData = []; + Object.keys(getTempAnnouncements?.announcement || {}).map((key) => { + const value = getTempAnnouncements?.announcement[key]; + tempData.push(value.data); + }); + setTempPublishedList(tempData); + } + } catch (error) {} + }; const publishAnnouncement = async () => { try { - - pauseAllQueues() - const fee = await getFee('ARBITRARY') + pauseAllQueues(); + const fee = await getFee("ARBITRARY"); await show({ - message: "Would you like to perform a ARBITRARY transaction?" , - publishFee: fee.fee + ' QORT' - }) + message: "Would you like to perform a ARBITRARY transaction?", + publishFee: fee.fee + " QORT", + }); if (isSending) return; if (editorRef.current) { const htmlContent = editorRef.current.getHTML(); @@ -263,8 +294,8 @@ export const GroupAnnouncements = ({ const message = { version: 1, extra: {}, - message: htmlContent - } + message: htmlContent, + }; const secretKeyObject = await getSecretKey(false, true); const message64: any = await objectToBase64(message); const encryptSingle = await encryptChatMessage( @@ -272,38 +303,40 @@ export const GroupAnnouncements = ({ secretKeyObject ); const randomUid = uid.rnd(); - const identifier = `grp-${selectedGroup}-anc-${randomUid}`; + const identifier = `grp-${selectedGroup}-anc-${randomUid}`; const res = await publishAnc({ encryptedData: encryptSingle, - identifier + identifier, }); const dataToSaveToStorage = { name: myName, identifier, - service: 'DOCUMENT', + service: "DOCUMENT", tempData: message, - created: Date.now() - } - await saveTempPublish({data: dataToSaveToStorage, key: 'announcement'}) - setTempData() + created: Date.now(), + }; + await saveTempPublish({ + data: dataToSaveToStorage, + key: "announcement", + }); + setTempData(); clearEditorContent(); } // send chat message } catch (error) { + if (!error) return; setInfoSnack({ type: "error", message: error, }); - setOpenSnack(true) + setOpenSnack(true); } finally { - resumeAllQueues() + resumeAllQueues(); setIsSending(false); } }; - - const getAnnouncements = React.useCallback( async (selectedGroup) => { try { @@ -311,7 +344,7 @@ export const GroupAnnouncements = ({ // dispatch(setIsLoadingGlobal(true)) const identifier = `grp-${selectedGroup}-anc-`; - const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; + const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; const response = await fetch(url, { method: "GET", headers: { @@ -319,12 +352,16 @@ export const GroupAnnouncements = ({ }, }); const responseData = await response.json(); - - setTempData() + + setTempData(); setAnnouncements(responseData); setIsLoading(false); for (const data of responseData) { - getAnnouncementData({ name: data.name, identifier: data.identifier }); + getAnnouncementData({ + name: data.name, + identifier: data.identifier, + resource: data, + }); } } catch (error) { } finally { @@ -333,196 +370,206 @@ export const GroupAnnouncements = ({ }, [secretKey] ); - + React.useEffect(() => { - if (selectedGroup && secretKey && !hasInitialized.current) { + if (selectedGroup && secretKey && !hasInitialized.current && !hide) { getAnnouncements(selectedGroup); - hasInitialized.current = true + hasInitialized.current = true; } - }, [selectedGroup, secretKey]); + }, [selectedGroup, secretKey, hide]); - - const loadMore = async()=> { + const loadMore = async () => { try { setIsLoading(true); - const offset = announcements.length - const identifier = `grp-${selectedGroup}-anc-`; - const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; - const response = await fetch(url, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - const responseData = await response.json(); + const offset = announcements.length; + const identifier = `grp-${selectedGroup}-anc-`; + const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const responseData = await response.json(); - setAnnouncements((prev)=> [...prev, ...responseData]); - setIsLoading(false); + setAnnouncements((prev) => [...prev, ...responseData]); + setIsLoading(false); + for (const data of responseData) { + getAnnouncementData({ name: data.name, identifier: data.identifier }); + } + } catch (error) {} + }; + + const interval = useRef(null); + + const checkNewMessages = React.useCallback(async () => { + try { + const identifier = `grp-${selectedGroup}-anc-`; + const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${0}&reverse=true&prefix=true`; + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const responseData = await response.json(); + const latestMessage = announcements[0]; + if (!latestMessage) { for (const data of responseData) { - getAnnouncementData({ name: data.name, identifier: data.identifier }); - } - } catch (error) { - - } - - } - - const interval = useRef(null) - - const checkNewMessages = React.useCallback( - async () => { - try { - - const identifier = `grp-${selectedGroup}-anc-`; - const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${0}&reverse=true&prefix=true`; - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }) - const responseData = await response.json() - const latestMessage = announcements[0] - if (!latestMessage) { - for (const data of responseData) { - try { - - getAnnouncementData({ name: data.name, identifier: data.identifier }); - - } catch (error) {} - } - setAnnouncements(responseData) - return - } - const findMessage = responseData?.findIndex( - (item: any) => item?.identifier === latestMessage?.identifier - ) - - if(findMessage === -1) return - const newArray = responseData.slice(0, findMessage) - - for (const data of newArray) { try { - - getAnnouncementData({ name: data.name, identifier: data.identifier }); - + getAnnouncementData({ + name: data.name, + identifier: data.identifier, + }); } catch (error) {} } - setAnnouncements((prev)=> [...newArray, ...prev]) - } catch (error) { - } finally { + setAnnouncements(responseData); + return; } - }, - [announcements, secretKey, selectedGroup] - ) + const findMessage = responseData?.findIndex( + (item: any) => item?.identifier === latestMessage?.identifier + ); + + if (findMessage === -1) return; + const newArray = responseData.slice(0, findMessage); + + for (const data of newArray) { + try { + getAnnouncementData({ name: data.name, identifier: data.identifier }); + } catch (error) {} + } + setAnnouncements((prev) => [...newArray, ...prev]); + } catch (error) { + } finally { + } + }, [announcements, secretKey, selectedGroup]); const checkNewMessagesFunc = useCallback(() => { - let isCalling = false + let isCalling = false; interval.current = setInterval(async () => { - if (isCalling) return - isCalling = true - const res = await checkNewMessages() - isCalling = false - }, 20000) - }, [checkNewMessages]) + if (isCalling) return; + isCalling = true; + const res = await checkNewMessages(); + isCalling = false; + }, 20000); + }, [checkNewMessages]); useEffect(() => { - if(!secretKey) return - checkNewMessagesFunc() + if (!secretKey || hide) return; + checkNewMessagesFunc(); return () => { if (interval?.current) { - clearInterval(interval.current) + clearInterval(interval.current); } - } - }, [checkNewMessagesFunc]) - - + }; + }, [checkNewMessagesFunc, hide]); const combinedListTempAndReal = useMemo(() => { // Combine the two lists const combined = [...tempPublishedList, ...announcements]; - + // Remove duplicates based on the "identifier" const uniqueItems = new Map(); - combined.forEach(item => { - uniqueItems.set(item.identifier, item); // This will overwrite duplicates, keeping the last occurrence + combined.forEach((item) => { + uniqueItems.set(item.identifier, item); // This will overwrite duplicates, keeping the last occurrence }); - + // Convert the map back to an array and sort by "created" timestamp in descending order - const sortedList = Array.from(uniqueItems.values()).sort((a, b) => b.created - a.created); - + const sortedList = Array.from(uniqueItems.values()).sort( + (a, b) => b.created - a.created + ); + return sortedList; }, [tempPublishedList, announcements]); - - if(selectedAnnouncement){ + if (selectedAnnouncement) { return (
- + style={{ + // reference to change height + height: isMobile ? `calc(${rootHeight} - 127px` : "calc(100vh - 70px)", + display: "flex", + flexDirection: "column", + width: "100%", + visibility: hide && "hidden", + position: hide && "fixed", + left: hide && "-1000px", + }} + > +
- ) + ); } - - return (
-
- - - Group Announcements - - + {!isMobile && ( + + + Group Announcements + + )} +
{!isLoading && combinedListTempAndReal?.length === 0 && ( - - No announcements + + + No announcements + )} 0 && announcements.length % 20 === 0} + showLoadMore={ + announcements.length > 0 && announcements.length % 20 === 0 + } loadMore={loadMore} + myName={myName} /> - - -{isAdmin && ( -
-
- -
- { - if (isSending) return; - publishAnnouncement(); - }} - style={{ - marginTop: "auto", - alignSelf: "center", - cursor: isSending ? "default" : "pointer", - background: isSending && "rgba(0, 0, 0, 0.8)", - flexShrink: 0, - }} - > - {isSending && ( - - )} - {` Publish Announcement`} - -
-)} - + {isAdmin && ( +
+
+ +
+ + {isFocusedParent && ( + { + if (isSending) return; + setIsFocusedParent(false); + clearEditorContent(); + setTimeout(() => { + triggerRerender(); + }, 300); + // Unfocus the editor + }} + style={{ + marginTop: "auto", + alignSelf: "center", + cursor: isSending ? "default" : "pointer", + background: "red", + flexShrink: 0, + padding: isMobile && "5px", + fontSize: isMobile && "14px", + }} + > + {` Close`} + + )} + { + if (isSending) return; + publishAnnouncement(); + }} + style={{ + marginTop: "auto", + alignSelf: "center", + cursor: isSending ? "default" : "pointer", + background: isSending && "rgba(0, 0, 0, 0.8)", + flexShrink: 0, + padding: isMobile && "5px", + fontSize: isMobile && "14px", + }} + > + {isSending && ( + + )} + {` Publish Announcement`} + + +
+ )} + + { + const { rootHeight } = useContext(MyContext); const [isMoved, setIsMoved] = useState(false); useEffect(() => { if (hide) { @@ -35,7 +39,8 @@ export const GroupForum = ({ return (
- +
); diff --git a/src/components/Chat/MessageDisplay.tsx b/src/components/Chat/MessageDisplay.tsx index 885f986..f4cdce7 100644 --- a/src/components/Chat/MessageDisplay.tsx +++ b/src/components/Chat/MessageDisplay.tsx @@ -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 (
{ // Delegate click handling to the parent div diff --git a/src/components/Chat/MessageItem.tsx b/src/components/Chat/MessageItem.tsx index b53ce75..7df771c 100644 --- a/src/components/Chat/MessageItem.tsx +++ b/src/components/Chat/MessageItem.tsx @@ -2,13 +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 { executeEvent } from "../../utils/events"; +import { WrapperUserAction } from "../WrapperUserAction"; +import ReplyIcon from "@mui/icons-material/Reply"; -export const MessageItem = ({ message, onSeen, isLast, isTemp }) => { - +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 @@ -29,62 +46,169 @@ export const MessageItem = ({ message, onSeen, isLast, isTemp }) => { borderRadius: "7px", width: "95%", display: "flex", - gap: '7px', - opacity: isTemp ? 0.5 : 1 + gap: "7px", + opacity: isTemp ? 0.5 : 1, }} + id={message?.signature} > - - {message?.senderName?.charAt(0)} - + {isShowingAsReply ? ( + + ) : ( + + + {message?.senderName?.charAt(0)} + + + )} + - - {message?.senderName || message?.sender} - + + + {message?.senderName || message?.sender} + + + {!isShowingAsReply && ( + { + onReply(message); + }} + > + + + )} + + {reply && ( + { + scrollToItem(replyIndex) + + + }} + > + + + Replied to {reply?.senderName || reply?.senderAddress} + {reply?.messageText && ( + + )} + {reply?.text?.type === "notification" ? ( + + ) : ( + + )} + + + )} + {message?.messageText && ( + + )} {message?.text?.type === "notification" ? ( ) : ( )} - + {isTemp ? ( - Sending... - ): ( - {formatTimestamp(message.timestamp)} - ) } - + + Sending... + + ) : ( + + {formatTimestamp(message.timestamp)} + + )} @@ -102,3 +226,51 @@ export const MessageItem = ({ message, onSeen, isLast, isTemp }) => {
); }; + + +export const ReplyPreview = ({message})=> { + + return ( + + + + Replied to {message?.senderName || message?.senderAddress} + {message?.messageText && ( + + )} + {message?.text?.type === "notification" ? ( + + ) : ( + + )} + + + ) +} \ No newline at end of file diff --git a/src/components/Chat/TipTap.tsx b/src/components/Chat/TipTap.tsx index 4bdf3c6..be6abff 100644 --- a/src/components/Chat/TipTap.tsx +++ b/src/components/Chat/TipTap.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { EditorProvider, useCurrentEditor } from '@tiptap/react'; import StarterKit from '@tiptap/starter-kit'; import { Color } from '@tiptap/extension-color'; @@ -25,6 +25,7 @@ import CustomImage from './CustomImage'; import Compressor from 'compressorjs' import ImageResize from 'tiptap-extension-resize-image'; // Import the ResizeImage extension +import { isMobile } from '../../App'; const MenuBar = ({ setEditorRef, isChat }) => { const { editor } = useCurrentEditor(); const fileInputRef = useRef(null); @@ -88,10 +89,11 @@ const MenuBar = ({ setEditorRef, isChat }) => { } // color={editor.isActive('bold') ? 'white' : 'gray'} sx={{ - color: editor.isActive('bold') ? 'white' : 'gray' + color: editor.isActive('bold') ? 'white' : 'gray', + padding: isMobile ? '5px' : 'revert' }} > - + editor.chain().focus().toggleItalic().run()} @@ -104,7 +106,8 @@ const MenuBar = ({ setEditorRef, isChat }) => { } // color={editor.isActive('italic') ? 'white' : 'gray'} sx={{ - color: editor.isActive('italic') ? 'white' : 'gray' + color: editor.isActive('italic') ? 'white' : 'gray', + padding: isMobile ? '5px' : 'revert' }} > @@ -120,7 +123,8 @@ const MenuBar = ({ setEditorRef, isChat }) => { } // color={editor.isActive('strike') ? 'white' : 'gray'} sx={{ - color: editor.isActive('strike') ? 'white' : 'gray' + color: editor.isActive('strike') ? 'white' : 'gray', + padding: isMobile ? '5px' : 'revert' }} > @@ -136,19 +140,23 @@ const MenuBar = ({ setEditorRef, isChat }) => { } // color={editor.isActive('code') ? 'white' : 'gray'} sx={{ - color: editor.isActive('code') ? 'white' : 'gray' + color: editor.isActive('code') ? 'white' : 'gray', + padding: isMobile ? '5px' : 'revert' }} > - editor.chain().focus().unsetAllMarks().run()}> + editor.chain().focus().unsetAllMarks().run()}> editor.chain().focus().toggleBulletList().run()} // color={editor.isActive('bulletList') ? 'white' : 'gray'} sx={{ - color: editor.isActive('bulletList') ? 'white' : 'gray' + color: editor.isActive('bulletList') ? 'white' : 'gray', + padding: isMobile ? '5px' : 'revert' }} > @@ -157,7 +165,8 @@ const MenuBar = ({ setEditorRef, isChat }) => { onClick={() => editor.chain().focus().toggleOrderedList().run()} // color={editor.isActive('orderedList') ? 'white' : 'gray'} sx={{ - color: editor.isActive('orderedList') ? 'white' : 'gray' + color: editor.isActive('orderedList') ? 'white' : 'gray', + padding: isMobile ? '5px' : 'revert' }} > @@ -166,7 +175,8 @@ const MenuBar = ({ setEditorRef, isChat }) => { onClick={() => editor.chain().focus().toggleCodeBlock().run()} // color={editor.isActive('codeBlock') ? 'white' : 'gray'} sx={{ - color: editor.isActive('codeBlock') ? 'white' : 'gray' + color: editor.isActive('codeBlock') ? 'white' : 'gray', + padding: isMobile ? '5px' : 'revert' }} > @@ -175,7 +185,8 @@ const MenuBar = ({ setEditorRef, isChat }) => { onClick={() => editor.chain().focus().toggleBlockquote().run()} // color={editor.isActive('blockquote') ? 'white' : 'gray'} sx={{ - color: editor.isActive('blockquote') ? 'white' : 'gray' + color: editor.isActive('blockquote') ? 'white' : 'gray', + padding: isMobile ? '5px' : 'revert' }} > @@ -187,7 +198,8 @@ const MenuBar = ({ setEditorRef, isChat }) => { onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} // color={editor.isActive('heading', { level: 1 }) ? 'white' : 'gray'} sx={{ - color: editor.isActive('heading', { level: 1 }) ? 'white' : 'gray' + color: editor.isActive('heading', { level: 1 }) ? 'white' : 'gray', + padding: isMobile ? '5px' : 'revert' }} > @@ -202,7 +214,8 @@ const MenuBar = ({ setEditorRef, isChat }) => { .run() } sx={{ - color: 'gray' + color: 'gray', + padding: isMobile ? '5px' : 'revert' }} > @@ -227,7 +240,8 @@ const MenuBar = ({ setEditorRef, isChat }) => { @@ -268,32 +282,66 @@ const extensions = [ const content = ``; -export default ({ setEditorRef, onEnter, disableEnter, isChat }) => { - +export default ({ setEditorRef, onEnter, disableEnter, isChat, maxHeightOffset, setIsFocusedParent, isFocusedParent, overrideMobile, customEditorHeight }) => { + const [isFocused, setIsFocused] = useState(false); const extensionsFiltered = isChat ? extensions.filter((item)=> item?.name !== 'image') : extensions + const editorRef = useRef(null); + const setEditorRefFunc = (editorInstance) => { + editorRef.current = editorInstance; + setEditorRef(editorInstance) + }; + const handleFocus = () => { + if(!isMobile) return + // setIsFocused(true); + setIsFocusedParent(true) + }; + + const handleBlur = () => { + const htmlContent = editorRef.current.getHTML(); + + if (!htmlContent?.trim() || htmlContent?.trim() === "

"){ + // setIsFocused(false); + // setIsFocusedParent(false) + }; + + }; + // useEffect(()=> { + // setIsFocused(isFocusedParent) + // },[isFocusedParent]) + return ( } - extensions={extensionsFiltered} - content={content} - editorProps={{ - handleKeyDown(view, event) { - if (!disableEnter && event.key === 'Enter') { - if (event.shiftKey) { - // Shift+Enter: Insert a hard break - view.dispatch(view.state.tr.replaceSelectionWith(view.state.schema.nodes.hardBreak.create())); - return true; - } else { - // Enter: Call the callback function - if (typeof onEnter === 'function') { - onEnter(); - } - return true; // Prevent the default action of adding a new line + slotBefore={(isFocusedParent || !isMobile || overrideMobile) && } + extensions={extensionsFiltered} + content={content} + onCreate={({ editor }) => { + editor.on('focus', handleFocus); // Listen for focus event + editor.on('blur', handleBlur); // Listen for blur event + }} + onUpdate={({ editor }) => { + editor.on('focus', handleFocus); // Ensure focus is updated + editor.on('blur', handleBlur); // Ensure blur is updated + }} + editorProps={{ + attributes: { + class: 'tiptap-prosemirror', + style: isMobile && `overflow: auto; min-height: ${customEditorHeight ? '200px' : '0px'}; 200px; max-height:calc(100svh - ${ customEditorHeight ? customEditorHeight : '140px'})`, + }, + handleKeyDown(view, event) { + if (!disableEnter && event.key === 'Enter') { + if (event.shiftKey) { + view.dispatch(view.state.tr.replaceSelectionWith(view.state.schema.nodes.hardBreak.create())); + return true; + } else { + if (typeof onEnter === 'function') { + onEnter(); } + return true; } - return false; // Allow default handling for other keys - }, - }} - /> - ); + } + return false; + }, + }} + /> + ) }; diff --git a/src/components/Chat/styles.css b/src/components/Chat/styles.css index f7b28ea..7c65eb0 100644 --- a/src/components/Chat/styles.css +++ b/src/components/Chat/styles.css @@ -118,4 +118,8 @@ .tiptap img { display: block; max-width: 100%; -} \ No newline at end of file +} + +.isReply p { + font-size: 12px !important; +} diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx new file mode 100644 index 0000000..6779bb0 --- /dev/null +++ b/src/components/ContextMenu.tsx @@ -0,0 +1,165 @@ +import React, { useState, useRef, useMemo, useEffect } from 'react'; +import { ListItemIcon, Menu, MenuItem, Typography, styled } from '@mui/material'; +import MailOutlineIcon from '@mui/icons-material/MailOutline'; +import NotificationsOffIcon from '@mui/icons-material/NotificationsOff'; +import { executeEvent } from '../utils/events'; + +const CustomStyledMenu = styled(Menu)(({ theme }) => ({ + '& .MuiPaper-root': { + backgroundColor: '#f9f9f9', + borderRadius: '12px', + padding: theme.spacing(1), + boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)', + }, + '& .MuiMenuItem-root': { + fontSize: '14px', // Smaller font size for the menu item text + color: '#444', + transition: '0.3s background-color', + '&:hover': { + backgroundColor: '#f0f0f0', // Explicit hover state + }, + + }, + })); + +export const ContextMenu = ({ children, groupId, getUserSettings, mutedGroups }) => { + const [menuPosition, setMenuPosition] = useState(null); + const longPressTimeout = useRef(null); + const preventClick = useRef(false); // Flag to prevent click after long-press or right-click + + const isMuted = useMemo(()=> { + return mutedGroups.includes(groupId) + }, [mutedGroups, groupId]) + + // Handle right-click (context menu) for desktop + const handleContextMenu = (event) => { + event.preventDefault(); + event.stopPropagation(); // Prevent parent click + + // Set flag to prevent any click event after right-click + preventClick.current = true; + + setMenuPosition({ + mouseX: event.clientX, + mouseY: event.clientY, + }); + }; + + // Handle long-press for mobile + const handleTouchStart = (event) => { + longPressTimeout.current = setTimeout(() => { + preventClick.current = true; // Prevent the next click after long-press + event.stopPropagation(); // Prevent parent click + setMenuPosition({ + mouseX: event.touches[0].clientX, + mouseY: event.touches[0].clientY, + }); + }, 500); // Long press duration + }; + + const handleTouchEnd = (event) => { + clearTimeout(longPressTimeout.current); + + if (preventClick.current) { + event.preventDefault(); + event.stopPropagation(); // Prevent synthetic click after long-press + preventClick.current = false; // Reset the flag + } + }; + + + + const handleSetGroupMute = ()=> { + try { + let value = [...mutedGroups] + if(isMuted){ + value = value.filter((group)=> group !== groupId) + } else { + value.push(groupId) + } + chrome?.runtime?.sendMessage( + { + action: "addUserSettings", + payload: { + keyValue: { + key: 'mutedGroups', + value + }, + }, + } + ); + setTimeout(() => { + getUserSettings() + }, 400); + + } catch (error) { + + } + } + + + + const handleClose = (e) => { + e.preventDefault(); + e.stopPropagation(); + setMenuPosition(null); + }; + + return ( +
+ {children} + + { + e.stopPropagation(); + }} + > + { + handleClose(e) + executeEvent("markAsRead", { + groupId + }); + }}> + + + + + Mark As Read + + + { + + handleClose(e) + handleSetGroupMute() + + }}> + + + + + {isMuted ? 'Unmute ' : 'Mute '}Push Notifications + + + +
+ ); +}; + + diff --git a/src/components/Desktop/DesktopFooter.tsx b/src/components/Desktop/DesktopFooter.tsx new file mode 100644 index 0000000..4eafe8b --- /dev/null +++ b/src/components/Desktop/DesktopFooter.tsx @@ -0,0 +1,118 @@ +import * as React from "react"; +import { + BottomNavigation, + BottomNavigationAction, + ButtonBase, + Typography, +} from "@mui/material"; +import { Home, Groups, Message, ShowChart } from "@mui/icons-material"; +import Box from "@mui/material/Box"; +import BottomLogo from "../../assets/svgs/BottomLogo5.svg"; +import { CustomSvg } from "../../common/CustomSvg"; +import { WalletIcon } from "../../assets/Icons/WalletIcon"; +import { HubsIcon } from "../../assets/Icons/HubsIcon"; +import { TradingIcon } from "../../assets/Icons/TradingIcon"; +import { MessagingIcon } from "../../assets/Icons/MessagingIcon"; +import { HomeIcon } from "../../assets/Icons/HomeIcon"; + +const IconWrapper = ({ children, label, color, selected }) => { + return ( + + {children} + + {label} + + + ); +}; + +export const DesktopFooter = ({ + selectedGroup, + groupSection, + isUnread, + goToAnnouncements, + isUnreadChat, + goToChat, + goToThreads, + setOpenManageMembers, + groupChatHasUnread, + groupsAnnHasUnread, + directChatHasUnread, + chatMode, + openDrawerGroups, + goToHome, + setIsOpenDrawerProfile, + mobileViewMode, + setMobileViewMode, + setMobileViewModeKeepOpen, + hasUnreadGroups, + hasUnreadDirects, + isHome, + isGroups, + isDirects, + setDesktopSideView +}) => { + const [value, setValue] = React.useState(0); + return ( + + + { + goToHome() + }}> + + + + + { + setDesktopSideView('groups') + }}> + + + + + { + setDesktopSideView('directs') + }}> + + + + + + + + + ); +}; diff --git a/src/components/Desktop/DesktopHeader.tsx b/src/components/Desktop/DesktopHeader.tsx new file mode 100644 index 0000000..84dab09 --- /dev/null +++ b/src/components/Desktop/DesktopHeader.tsx @@ -0,0 +1,280 @@ +import * as React from "react"; +import { + BottomNavigation, + BottomNavigationAction, + ButtonBase, + Typography, +} from "@mui/material"; +import { Home, Groups, Message, ShowChart } from "@mui/icons-material"; +import Box from "@mui/material/Box"; +import BottomLogo from "../../assets/svgs/BottomLogo5.svg"; +import { CustomSvg } from "../../common/CustomSvg"; +import { WalletIcon } from "../../assets/Icons/WalletIcon"; +import { HubsIcon } from "../../assets/Icons/HubsIcon"; +import { TradingIcon } from "../../assets/Icons/TradingIcon"; +import { MessagingIcon } from "../../assets/Icons/MessagingIcon"; +import { HomeIcon } from "../../assets/Icons/HomeIcon"; +import { NotificationIcon2 } from "../../assets/Icons/NotificationIcon2"; +import { ChatIcon } from "../../assets/Icons/ChatIcon"; +import { ThreadsIcon } from "../../assets/Icons/ThreadsIcon"; +import { MembersIcon } from "../../assets/Icons/MembersIcon"; + +const IconWrapper = ({ children, label, color, selected, selectColor }) => { + return ( + + {children} + + {label} + + + ); +}; + +export const DesktopHeader = ({ + selectedGroup, + groupSection, + isUnread, + goToAnnouncements, + isUnreadChat, + goToChat, + goToThreads, + setOpenManageMembers, + groupChatHasUnread, + groupsAnnHasUnread, + directChatHasUnread, + chatMode, + openDrawerGroups, + goToHome, + setIsOpenDrawerProfile, + mobileViewMode, + setMobileViewMode, + setMobileViewModeKeepOpen, + hasUnreadGroups, + hasUnreadDirects, + isHome, + isGroups, + isDirects, + setDesktopSideView, + hasUnreadAnnouncements, + isAnnouncement, + hasUnreadChat, + isChat, + isForum, + setGroupSection +}) => { + const [value, setValue] = React.useState(0); + return ( + + + + {selectedGroup?.groupName} + + + + { + goToHome(); + }} + > + + + + + { + setDesktopSideView("groups"); + }} + > + + + + + { + setDesktopSideView("directs"); + }} + > + + + + + + { + goToAnnouncements() + }} + > + + + + + + { + goToChat() + }} + > + + + + + + { + setGroupSection("forum"); + + }} + > + + + + + { + setOpenManageMembers(true) + + }} + > + + + + + + + ); +}; diff --git a/src/components/Drawer/Drawer.tsx b/src/components/Drawer/Drawer.tsx new file mode 100644 index 0000000..2e545cd --- /dev/null +++ b/src/components/Drawer/Drawer.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Drawer from '@mui/material/Drawer'; +import Button from '@mui/material/Button'; +import List from '@mui/material/List'; +import Divider from '@mui/material/Divider'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import InboxIcon from '@mui/icons-material/MoveToInbox'; +import MailIcon from '@mui/icons-material/Mail'; +import CloseIcon from '@mui/icons-material/Close'; +export const DrawerComponent = ({open, setOpen, children}) => { + + const toggleDrawer = (newOpen: boolean) => () => { + setOpen(newOpen); + }; + + + return ( +
+ + + + {children} + + +
+ ); +} diff --git a/src/components/Group/AddGroup.tsx b/src/components/Group/AddGroup.tsx index 47bbf0e..288e6f3 100644 --- a/src/components/Group/AddGroup.tsx +++ b/src/components/Group/AddGroup.tsx @@ -29,7 +29,7 @@ import { AddGroupList } from "./AddGroupList"; import { UserListOfInvites } from "./UserListOfInvites"; import { CustomizedSnackbars } from "../Snackbar/Snackbar"; import { getFee } from "../../background"; -import { MyContext } from "../../App"; +import { MyContext, isMobile } from "../../App"; import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; export const Label = styled("label")( @@ -103,7 +103,7 @@ export const AddGroup = ({ address, open, setOpen }) => { }) await new Promise((res, rej) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "createGroup", payload: { @@ -220,44 +220,50 @@ export const AddGroup = ({ address, open, setOpen }) => { }} > - - - - - + + + + + {value === 0 && ( diff --git a/src/components/Group/AddGroupList.tsx b/src/components/Group/AddGroupList.tsx index d0b8772..4f459e0 100644 --- a/src/components/Group/AddGroupList.tsx +++ b/src/components/Group/AddGroupList.tsx @@ -108,7 +108,7 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => { }) setIsLoading(true); await new Promise((res, rej) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "joinGroup", payload: { @@ -247,7 +247,7 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => { style={{ position: "relative", height: "500px", - width: "600px", + width: "100%", display: "flex", flexDirection: "column", flexShrink: 1, diff --git a/src/components/Group/Forum/GroupMail.tsx b/src/components/Group/Forum/GroupMail.tsx index d962347..6d26848 100644 --- a/src/components/Group/Forum/GroupMail.tsx +++ b/src/components/Group/Forum/GroupMail.tsx @@ -53,7 +53,9 @@ import ArrowDownSVG from "../../../assets/svgs/ArrowDown.svg"; import { LoadingSnackbar } from "../../Snackbar/LoadingSnackbar"; import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../../utils/events"; import RefreshIcon from '@mui/icons-material/Refresh'; -import { getBaseApiReact } from "../../../App"; +import { getArbitraryEndpointReact, getBaseApiReact } from "../../../App"; +import { WrapperUserAction } from "../../WrapperUserAction"; +import { addDataPublishesFunc, getDataPublishesFunc } from "../Group"; const filterOptions = ["Recently active", "Newest", "Oldest"]; export const threadIdentifier = "DOCUMENT"; @@ -63,7 +65,8 @@ export const GroupMail = ({ getSecretKey, secretKey, defaultThread, - setDefaultThread + setDefaultThread, + hide }) => { const [viewedThreads, setViewedThreads] = React.useState({}); const [filterMode, setFilterMode] = useState("Recently active"); @@ -74,6 +77,7 @@ export const GroupMail = ({ const [isOpenFilterList, setIsOpenFilterList] = useState(false); const anchorElInstanceFilter = useRef(null); const [tempPublishedList, setTempPublishedList] = useState([]) + const dataPublishes = useRef({}) const [isLoading, setIsLoading] = useState(false) const groupIdRef = useRef(null); @@ -81,6 +85,14 @@ export const GroupMail = ({ return selectedGroup?.groupId; }, [selectedGroup]); + useEffect(()=> { + if(!groupId) return + (async ()=> { + const res = await getDataPublishesFunc(groupId, 'thread') + dataPublishes.current = res || {} + })() + }, [groupId]) + useEffect(() => { if (groupId !== groupIdRef?.current) { setCurrentThread(null); @@ -108,12 +120,19 @@ export const GroupMail = ({ } - const getEncryptedResource = async ({ name, identifier }) => { - + const getEncryptedResource = async ({ name, identifier, resource }) => { + let data = dataPublishes.current[`${name}-${identifier}`] + if(!data || (data?.update || data?.created !== (resource?.updated || resource?.created))){ const res = await fetch( `${getBaseApiReact()}/arbitrary/DOCUMENT/${name}/${identifier}?encoding=base64` ); - const data = await res.text(); + if(!res?.ok) return + data = await res.text(); + await addDataPublishesFunc({...resource, data}, groupId, 'thread') + + } else { + data = data.data + } const response = await decryptPublishes([{ data }], secretKey); const messageData = response[0]; @@ -123,7 +142,7 @@ export const GroupMail = ({ const updateThreadActivity = async ({threadId, qortalName, groupId, thread}) => { try { await new Promise((res, rej) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "updateThreadActivity", payload: { @@ -158,7 +177,7 @@ export const GroupMail = ({ } const identifier = `grp-${groupId}-thread-`; - const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=${20}&includemetadata=false&offset=${offset}&reverse=${isReverse}&prefix=true`; + const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=${20}&includemetadata=false&offset=${offset}&reverse=${isReverse}&prefix=true`; const response = await fetch(url, { method: "GET", headers: { @@ -188,6 +207,7 @@ export const GroupMail = ({ getEncryptedResource({ name: message.name, identifier: message.identifier, + resource: message }), delay(5000), ]); @@ -244,7 +264,7 @@ export const GroupMail = ({ // dispatch(setIsLoadingCustom("Loading recent threads")); const identifier = `thmsg-grp-${groupId}-thread-`; - const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=100&includemetadata=false&offset=${0}&reverse=true&prefix=true`; + const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=100&includemetadata=false&offset=${0}&reverse=true&prefix=true`; const response = await fetch(url, { method: "GET", headers: { @@ -280,7 +300,7 @@ export const GroupMail = ({ const getMessageForThreads = newArray.map(async (message: any) => { try { const identifierQuery = message.threadId; - const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifierQuery}&limit=1&includemetadata=false&offset=${0}&reverse=true&prefix=true`; + const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=${threadIdentifier}&identifier=${identifierQuery}&limit=1&includemetadata=false&offset=${0}&reverse=true&prefix=true`; const response = await fetch(url, { method: "GET", headers: { @@ -307,6 +327,7 @@ export const GroupMail = ({ getEncryptedResource({ name: thread.name, identifier: message.threadId, + resource: thread }), delay(10000), ]); @@ -353,6 +374,7 @@ export const GroupMail = ({ const filterModeRef = useRef(""); useEffect(() => { + if(hide) return if (filterModeRef.current !== filterMode) { firstMount.current = false; } @@ -368,7 +390,7 @@ export const GroupMail = ({ setTempData() firstMount.current = true; } - }, [groupId, members, filterMode]); + }, [groupId, members, filterMode, hide]); const closeThread = useCallback(() => { setCurrentThread(null); @@ -656,6 +678,11 @@ export const GroupMail = ({ thread?.threadData?.createdAt < hasViewedRecent?.timestamp; return ( { setCurrentThread(thread); if(thread?.threadId && thread?.threadData?.name){ @@ -665,6 +692,7 @@ export const GroupMail = ({ } }} > + {thread?.threadData?.name?.charAt(0)} + + by {thread?.threadData?.name} + {formatTimestamp(thread?.threadData?.createdAt)} diff --git a/src/components/Group/Forum/NewThread.tsx b/src/components/Group/Forum/NewThread.tsx index f4a088b..231c860 100644 --- a/src/components/Group/Forum/NewThread.tsx +++ b/src/components/Group/Forum/NewThread.tsx @@ -30,7 +30,7 @@ import { formatBytes } from "../../../utils/Size"; import { CreateThreadIcon } from "../../../assets/svgs/CreateThreadIcon"; import { SendNewMessage } from "../../../assets/svgs/SendNewMessage"; import { TextEditor } from "./TextEditor"; -import { MyContext, pauseAllQueues, resumeAllQueues } from "../../../App"; +import { MyContext, isMobile, pauseAllQueues, resumeAllQueues } from "../../../App"; import { getFee } from "../../../background"; import TipTap from "../../Chat/TipTap"; import { MessageDisplay } from "../../Chat/MessageDisplay"; @@ -94,7 +94,7 @@ export const publishGroupEncryptedResource = async ({ identifier, }) => { return new Promise((res, rej) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "publishGroupEncryptedResource", payload: { @@ -117,7 +117,7 @@ export const publishGroupEncryptedResource = async ({ export const encryptSingleFunc = async (data: string, secretKeyObject: any) => { try { return new Promise((res, rej) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "encryptSingle", payload: { @@ -147,7 +147,8 @@ export const NewThread = ({ getSecretKey, closeCallback, postReply, - myName + myName, + setPostReply }: NewMessageProps) => { const { show } = React.useContext(MyContext); @@ -171,6 +172,7 @@ export const NewThread = ({ const closeModal = () => { setIsOpen(false); setValue(""); + setPostReply(null) }; async function publishQDNResource() { @@ -399,7 +401,8 @@ export const NewThread = ({ > setIsOpen(true)} > @@ -410,7 +413,7 @@ export const NewThread = ({ {isMessage ? "Post Message" : "New Thread"} - + @@ -463,7 +468,7 @@ export const NewThread = ({ color: "white", "& .MuiInput-input::placeholder": { color: "rgba(255,255,255, 0.70) !important", - fontSize: "20px", + fontSize: isMobile ? '14px' : "20px", fontStyle: "normal", fontWeight: 400, lineHeight: "120%", // 24px @@ -491,7 +496,10 @@ export const NewThread = ({
)} - + {!isMobile && ( + + + )} {/* diff --git a/src/components/Group/Forum/ReusableModal.tsx b/src/components/Group/Forum/ReusableModal.tsx index b61e83e..d081293 100644 --- a/src/components/Group/Forum/ReusableModal.tsx +++ b/src/components/Group/Forum/ReusableModal.tsx @@ -1,5 +1,6 @@ import React from 'react' import { Box, Modal, useTheme } from '@mui/material' +import { isMobile } from '../../../App' interface MyModalProps { open: boolean @@ -40,7 +41,7 @@ export const ReusableModal: React.FC = ({ top: '50%', left: '50%', transform: 'translate(-50%, -50%)', - width: '75%', + width: isMobile ? '95%' : '75%', bgcolor: theme.palette.primary.main, boxShadow: 24, p: 4, diff --git a/src/components/Group/Forum/ShowMessageWithoutModal.tsx b/src/components/Group/Forum/ShowMessageWithoutModal.tsx index c8fb5cb..9a079d6 100644 --- a/src/components/Group/Forum/ShowMessageWithoutModal.tsx +++ b/src/components/Group/Forum/ShowMessageWithoutModal.tsx @@ -19,8 +19,9 @@ import ReadOnlySlate from "./ReadOnlySlate"; import { MessageDisplay } from "../../Chat/MessageDisplay"; import { getBaseApi } from "../../../background"; import { getBaseApiReact } from "../../../App"; +import { WrapperUserAction } from "../../WrapperUserAction"; -export const ShowMessage = ({ message, openNewPostWithQuote }: any) => { +export const ShowMessage = ({ message, openNewPostWithQuote, myName }: any) => { const [expandAttachments, setExpandAttachments] = useState(false); let cleanHTML = ""; @@ -53,13 +54,17 @@ export const ShowMessage = ({ message, openNewPostWithQuote }: any) => { }} > - + {message?.name?.charAt(0)} + + + {message?.name} + {formatTimestampForum(message?.created)} diff --git a/src/components/Group/Forum/Thread copy.tsx b/src/components/Group/Forum/Thread copy.tsx index 7aadabd..9091a2c 100644 --- a/src/components/Group/Forum/Thread copy.tsx +++ b/src/components/Group/Forum/Thread copy.tsx @@ -24,7 +24,7 @@ import ReturnSVG from '../../../assets/svgs/Return.svg' import { NewThread } from './NewThread' import { decryptPublishes } from '../../Chat/GroupAnnouncements' import { getBaseApi } from '../../../background' -import { getBaseApiReact } from '../../../App' +import { getArbitraryEndpointReact, getBaseApiReact } from '../../../App' interface ThreadProps { currentThread: any groupInfo: any @@ -91,7 +91,7 @@ export const Thread = ({ const offset = messages.length const identifier = `thmsg-${threadId}` - const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true` + const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true` const response = await fetch(url, { method: 'GET', headers: { @@ -180,7 +180,7 @@ export const Thread = ({ let threadId = groupInfo.threadId const identifier = `thmsg-${threadId}` - const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=20&includemetadata=false&offset=${0}&reverse=true&prefix=true` + const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=20&includemetadata=false&offset=${0}&reverse=true&prefix=true` const response = await fetch(url, { method: 'GET', headers: { diff --git a/src/components/Group/Forum/Thread.tsx b/src/components/Group/Forum/Thread.tsx index 6dd0dbb..f393d5d 100644 --- a/src/components/Group/Forum/Thread.tsx +++ b/src/components/Group/Forum/Thread.tsx @@ -1,10 +1,21 @@ -import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "react"; - -import { Box, Button, IconButton, Skeleton } from "@mui/material"; +import React, { + FC, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { + Avatar, + Box, + Button, + ButtonBase, + IconButton, + Skeleton, + Typography, +} from "@mui/material"; import { ShowMessage } from "./ShowMessageWithoutModal"; -// import { -// setIsLoadingCustom, -// } from '../../state/features/globalSlice' import { ComposeP, GroupContainer, @@ -14,19 +25,38 @@ import { SingleThreadParent, ThreadContainer, ThreadContainerFullWidth, + ThreadInfoColumn, + ThreadInfoColumnNameP, + ThreadInfoColumnTime, } from "./Mail-styles"; import { Spacer } from "../../../common/Spacer"; import { threadIdentifier } from "./GroupMail"; import LazyLoad from "../../../common/LazyLoad"; import ReturnSVG from "../../../assets/svgs/Return.svg"; import { NewThread } from "./NewThread"; -import { decryptPublishes, getTempPublish } from "../../Chat/GroupAnnouncements"; +import { + decryptPublishes, + getTempPublish, +} from "../../Chat/GroupAnnouncements"; import { LoadingSnackbar } from "../../Snackbar/LoadingSnackbar"; import { subscribeToEvent, unsubscribeFromEvent } from "../../../utils/events"; import RefreshIcon from "@mui/icons-material/Refresh"; -import { getBaseApi } from "../../../background"; -import { getBaseApiReact } from "../../../App"; - +import { + getArbitraryEndpointReact, + getBaseApiReact, + isMobile, +} from "../../../App"; +import { + ArrowDownward as ArrowDownwardIcon, + ArrowUpward as ArrowUpwardIcon, +} from "@mui/icons-material"; +import { addDataPublishesFunc, getDataPublishesFunc } from "../Group"; +import { RequestQueueWithPromise } from "../../../utils/queue/queue"; +import { CustomLoader } from "../../../common/CustomLoader"; +import { WrapperUserAction } from "../../WrapperUserAction"; +import { formatTimestampForum } from "../../../utils/time"; +const requestQueueSaveToLocal = new RequestQueueWithPromise(1); +const requestQueueDownloadPost = new RequestQueueWithPromise(3); interface ThreadProps { currentThread: any; groupInfo: any; @@ -34,11 +64,41 @@ interface ThreadProps { members: any; } -const getEncryptedResource = async ({ name, identifier, secretKey }) => { - const res = await fetch( - `${getBaseApiReact()}/arbitrary/DOCUMENT/${name}/${identifier}?encoding=base64` - ); - const data = await res.text(); +const getEncryptedResource = async ({ + name, + identifier, + secretKey, + resource, + groupId, + dataPublishes, +}) => { + let data = dataPublishes[`${name}-${identifier}`]; + if ( + !data || + data?.update || + data?.created !== (resource?.updated || resource?.created) + ) { + const res = await requestQueueDownloadPost.enqueue(() => { + return fetch( + `${getBaseApiReact()}/arbitrary/DOCUMENT/${name}/${identifier}?encoding=base64` + ); + }); + if (!res.ok) { + const errorData = await res.json(); + + return { + error: errorData?.message, + }; + } + data = await res.text(); + + if (data?.error || typeof data !== "string") return; + await requestQueueSaveToLocal.enqueue(() => { + return addDataPublishesFunc({ ...resource, data }, groupId, "thmsg"); + }); + } else { + data = data.data; + } const response = await decryptPublishes([{ data }], secretKey); const messageData = response[0]; @@ -53,10 +113,9 @@ export const Thread = ({ userInfo, secretKey, getSecretKey, - updateThreadActivityCurrentThread + updateThreadActivityCurrentThread, }: ThreadProps) => { - const [tempPublishedList, setTempPublishedList] = useState([]) - + const [tempPublishedList, setTempPublishedList] = useState([]); const [messages, setMessages] = useState([]); const [hashMapMailMessages, setHashMapMailMessages] = useState({}); const [hasFirstPage, setHasFirstPage] = useState(false); @@ -66,9 +125,28 @@ export const Thread = ({ const [postReply, setPostReply] = useState(null); const [hasLastPage, setHasLastPage] = useState(false); + // Update: Use a new ref for the scrollable container + const threadContainerRef = useRef(null); + const threadBeginningRef = useRef(null) + // New state variables + const [showScrollButton, setShowScrollButton] = useState(false); + const [isAtBottom, setIsAtBottom] = useState(false); + const secretKeyRef = useRef(null); const currentThreadRef = useRef(null); const containerRef = useRef(null); + const dataPublishes = useRef({}); + + const getSavedData = useCallback(async (groupId) => { + const res = await getDataPublishesFunc(groupId, "thmsg"); + dataPublishes.current = res || {}; + }, []); + + useEffect(() => { + if (!groupInfo?.groupId) return; + getSavedData(groupInfo?.groupId); + }, [groupInfo?.groupId]); + useEffect(() => { currentThreadRef.current = currentThread; }, [currentThread]); @@ -83,8 +161,25 @@ export const Thread = ({ identifier: message.identifier, name: message.name, secretKey, + resource: message, + groupId: groupInfo?.groupId, + dataPublishes: dataPublishes.current, }); - + + if (responseDataMessage?.error) { + const fullObject = { + ...message, + error: responseDataMessage?.error, + id: message.identifier, + }; + setHashMapMailMessages((prev) => { + return { + ...prev, + [message.identifier]: fullObject, + }; + }); + return; + } const fullObject = { ...message, @@ -100,46 +195,40 @@ export const Thread = ({ } catch (error) {} }; - const setTempData = async ()=> { + const setTempData = async () => { try { let threadId = currentThread.threadId; - - const keyTemp = 'thread-post' - const getTempAnnouncements = await getTempPublish() - - if(getTempAnnouncements?.[keyTemp]){ - - let tempData = [] - Object.keys(getTempAnnouncements?.[keyTemp] || {}).map((key)=> { - const value = getTempAnnouncements?.[keyTemp][key] - - if(value.data?.threadId === threadId){ - tempData.push(value.data) - } - - }) - setTempPublishedList(tempData) - } - } catch (error) { - - } - - } + const keyTemp = "thread-post"; + const getTempAnnouncements = await getTempPublish(); + + if (getTempAnnouncements?.[keyTemp]) { + let tempData = []; + Object.keys(getTempAnnouncements?.[keyTemp] || {}).map((key) => { + const value = getTempAnnouncements?.[keyTemp][key]; + + if (value.data?.threadId === threadId) { + tempData.push(value.data); + } + }); + setTempPublishedList(tempData); + } + } catch (error) {} + }; const getMailMessages = React.useCallback( - async (groupInfo: any, before, after, isReverse) => { + async (groupInfo: any, before, after, isReverse, groupId) => { try { - setTempPublishedList([]) + setTempPublishedList([]); setIsLoading(true); setHasFirstPage(false); setHasPreviousPage(false); setHasLastPage(false); setHasNextPage(false); let threadId = groupInfo.threadId; - + const identifier = `thmsg-${threadId}`; - let url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=20&includemetadata=false&prefix=true`; + let url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=20&includemetadata=false&prefix=true`; if (!isReverse) { url = url + "&reverse=false"; } @@ -160,7 +249,6 @@ export const Thread = ({ }, }); const responseData = await response.json(); - let fullArrayMsg = [...responseData]; if (isReverse) { @@ -175,15 +263,22 @@ export const Thread = ({ setTimeout(() => { containerRef.current.scrollIntoView({ behavior: "smooth" }); }, 300); + } + if(after || before === null && after === null && !isReverse){ + setTimeout(() => { + threadBeginningRef.current.scrollIntoView(); + }, 100); } - - if (fullArrayMsg.length === 0){ - setTempData() + + if (fullArrayMsg.length === 0) { + setTempData(); return; - } + } // check if there are newer posts - const urlNewer = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=1&includemetadata=false&reverse=false&prefix=true&before=${fullArrayMsg[0].created}`; + const urlNewer = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=1&includemetadata=false&reverse=false&prefix=true&before=${ + fullArrayMsg[0].created + }`; const responseNewer = await fetch(urlNewer, { method: "GET", headers: { @@ -199,7 +294,7 @@ export const Thread = ({ setHasPreviousPage(false); } // check if there are older posts - const urlOlder = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=1&includemetadata=false&reverse=false&prefix=true&after=${ + const urlOlder = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=1&includemetadata=false&reverse=false&prefix=true&after=${ fullArrayMsg[fullArrayMsg.length - 1].created }`; const responseOlder = await fetch(urlOlder, { @@ -215,22 +310,22 @@ export const Thread = ({ } else { setHasLastPage(false); setHasNextPage(false); - setTempData() - updateThreadActivityCurrentThread() + setTempData(); + updateThreadActivityCurrentThread(); } } catch (error) { + console.log("error", error); } finally { setIsLoading(false); - + getSavedData(groupId); } }, [messages, secretKey] ); const getMessages = React.useCallback(async () => { - - if (!currentThread || !secretKey) return; - await getMailMessages(currentThread, null, null, false); - }, [getMailMessages, currentThread, secretKey]); + if (!currentThread || !secretKey || !groupInfo?.groupId) return; + await getMailMessages(currentThread, null, null, false, groupInfo?.groupId); + }, [getMailMessages, currentThread, secretKey, groupInfo?.groupId]); const firstMount = useRef(false); const saveTimestamp = useCallback((currentThread: any, username?: string) => { @@ -287,8 +382,6 @@ export const Thread = ({ } if (currentThread && secretKey && !firstMount.current) { getMessagesMiddleware(); - - // saveTimestamp(currentThread, user.name) } }, [currentThread, secretKey]); const messageCallback = useCallback((msg: any) => { @@ -304,7 +397,7 @@ export const Thread = ({ let threadId = groupInfo.threadId; const identifier = `thmsg-${threadId}`; - const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=20&includemetadata=false&offset=${0}&reverse=true&prefix=true`; + const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=20&includemetadata=false&offset=${0}&reverse=true&prefix=true`; const response = await fetch(url, { method: "GET", headers: { @@ -329,6 +422,9 @@ export const Thread = ({ identifier: message.identifier, name: message.name, secretKey: secretKeyRef.current, + resource: message, + groupId: groupInfo?.groupId, + dataPublishes: dataPublishes.current, }); const fullObject = { @@ -360,25 +456,6 @@ export const Thread = ({ [messages] ); - // const checkNewMessagesFunc = useCallback(() => { - // let isCalling = false - // interval.current = setInterval(async () => { - // if (isCalling) return - // isCalling = true - // const res = await checkNewMessages(currentThread) - // isCalling = false - // }, 8000) - // }, [checkNewMessages, currentThrefirstMount.current = truead]) - - // useEffect(() => { - // checkNewMessagesFunc() - // return () => { - // if (interval?.current) { - // clearInterval(interval.current) - // } - // } - // }, [checkNewMessagesFunc]) - const openNewPostWithQuote = useCallback((reply) => { setPostReply(reply); }, []); @@ -390,7 +467,7 @@ export const Thread = ({ const threadFetchModeFunc = (e) => { const mode = e.detail?.mode; if (mode === "last-page") { - getMailMessages(currentThread, null, null, true); + getMailMessages(currentThread, null, null, true, groupInfo?.groupId); } firstMount.current = true; }; @@ -406,44 +483,157 @@ export const Thread = ({ const combinedListTempAndReal = useMemo(() => { // Combine the two lists const combined = [...tempPublishedList, ...messages]; - + // Remove duplicates based on the "identifier" const uniqueItems = new Map(); - combined.forEach(item => { - uniqueItems.set(item.identifier, item); // This will overwrite duplicates, keeping the last occurrence + combined.forEach((item) => { + uniqueItems.set(item.identifier, item); // This will overwrite duplicates, keeping the last occurrence }); - + // Convert the map back to an array and sort by "created" timestamp in descending order - const sortedList = Array.from(uniqueItems.values()).sort((a, b) => a.created - b.created); - + const sortedList = Array.from(uniqueItems.values()).sort( + (a, b) => a.created - b.created + ); + return sortedList; }, [tempPublishedList, messages]); + // Updated useEffect to handle scroll and overflow + useEffect(() => { + const container = threadContainerRef.current; // Updated reference + if (!container) return; + + const handleScroll = () => { + const { scrollTop, scrollHeight, clientHeight } = container; + // Check if user is at the bottom + if (scrollTop + clientHeight >= scrollHeight - 5) { + setIsAtBottom(true); + } else { + setIsAtBottom(false); + } + + // Initial check if content overflows + if (container.scrollHeight > container.clientHeight) { + setShowScrollButton(true); + } else { + setShowScrollButton(false); + } + }; + setTimeout(() => { + handleScroll(); + }, 400); + + container.addEventListener("scroll", handleScroll); + + // Cleanup + return () => { + container.removeEventListener("scroll", handleScroll); + }; + }, [messages]); + + // Function to scroll to the top or bottom of the container + const scrollToPosition = () => { + const container = threadContainerRef.current; // Updated reference + if (!container) return; + + if (isAtBottom) { + container.scrollTo({ top: 0, behavior: "smooth" }); // Scroll to top + } else { + container.scrollTo({ top: container.scrollHeight, behavior: "smooth" }); // Scroll to bottom + } + }; + if (!currentThread) return null; return ( - - - - + + + + { + setMessages([]); + closeThread(); + }} + > + + {!isMobile && Return to Threads} + + {/* Conditionally render the scroll buttons */} + {showScrollButton && + (isAtBottom ? ( + + + + ) : ( + + + + ))} + + + + +
+ + - {currentThread?.threadData?.title} - - { - setMessages([]); - closeThread(); + - - Return to Threads - + {currentThread?.threadData?.title} + + + - - {combinedListTempAndReal.map((message) => { - let fullMessage = message; + + {combinedListTempAndReal.map((message, index, list) => { + let fullMessage = message; if (hashMapMailMessages[message?.identifier]) { fullMessage = hashMapMailMessages[message.identifier]; + + + if (fullMessage?.error) { + return ( + + + + + + {message?.name?.charAt(0)} + + + + + + {message?.name} + + + + {formatTimestampForum(message?.created)} + + + + + + {fullMessage?.error} + + + + + + ); + } return ( ); - } else if(message?.tempData){ + } else if (message?.tempData) { return ( ); } return ( - - + + > + + + + {message?.name?.charAt(0)} + + + + + + {message?.name} + + + + {formatTimestampForum(message?.created)} + + + + + + + Downloading from QDN + + + + ); })} -
+ {!hasLastPage && !isLoading && ( <> @@ -572,7 +942,13 @@ export const Thread = ({ variant="outlined" startIcon={} onClick={() => { - getMailMessages(currentThread, null, null, true); + getMailMessages( + currentThread, + null, + null, + true, + groupInfo?.groupId + ); }} sx={{ color: "white", @@ -584,8 +960,11 @@ export const Thread = ({ )} - {messages?.length > 4 && ( - <> + + 4 ? 'visible' : 'hidden' + }}> - - )} + + +
- {/* {messages.length >= 20 && ( - getMailMessages(currentThread, false, true)}> - - )} */} touchStartY) { +// event.preventDefault(); +// } +// }); interface GroupProps { myAddress: string; @@ -75,7 +123,7 @@ const timeDifferenceForNotificationChats = 900000; export const requestQueueMemberNames = new RequestQueueWithPromise(5); export const requestQueueAdminMemberNames = new RequestQueueWithPromise(5); -const audio = new Audio(chrome.runtime.getURL("msg-not1.wav")); +const audio = new Audio(chrome.runtime?.getURL("msg-not1.wav")); export const getGroupAdimnsAddress = async (groupNumber: number) => { // const validApi = await findUsableApi(); @@ -151,7 +199,7 @@ export const getGroupMembers = async (groupNumber: number) => { export const decryptResource = async (data: string) => { try { return new Promise((res, rej) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "decryptGroupEncryption", payload: { @@ -159,7 +207,52 @@ export const decryptResource = async (data: string) => { }, }, (response) => { - + if (!response?.error) { + res(response); + return; + } + rej(response.error); + } + ); + }); + } catch (error) {} +}; + +export const addDataPublishesFunc = async (data: string, groupId, type) => { + try { + return new Promise((res, rej) => { + chrome?.runtime?.sendMessage( + { + action: "addDataPublishes", + payload: { + data, + groupId, + type, + }, + }, + (response) => { + if (!response?.error) { + res(response); + } + rej(response.error); + } + ); + }); + } catch (error) {} +}; + +export const getDataPublishesFunc = async (groupId, type) => { + try { + return new Promise((res, rej) => { + chrome?.runtime?.sendMessage( + { + action: "getDataPublishes", + payload: { + groupId, + type, + }, + }, + (response) => { if (!response?.error) { res(response); } @@ -189,6 +282,8 @@ export const getGroupAdimns = async (groupNumber: number) => { ); const groupData = await response.json(); let members: any = []; + let membersAddresses = []; + let both = []; // if (groupData && Array.isArray(groupData?.members)) { // for (const member of groupData.members) { // if (member.member) { @@ -207,14 +302,16 @@ export const getGroupAdimns = async (groupNumber: number) => { }); if (name) { members.push(name); + both.push({ name, address: member.member }); } + membersAddresses.push(member.member); } return true; }); await Promise.all(getMemNames); - return members; + return { names: members, addresses: membersAddresses, both }; }; export const getNames = async (listOfMembers) => { @@ -276,10 +373,13 @@ export const Group = ({ isMain, userInfo, balance, + isOpenDrawerProfile, + setIsOpenDrawerProfile, + logoutFunc, }: GroupProps) => { const [secretKey, setSecretKey] = useState(null); const [secretKeyPublishDate, setSecretKeyPublishDate] = useState(null); - const lastFetchedSecretKey = useRef(null) + const lastFetchedSecretKey = useRef(null); const [secretKeyDetails, setSecretKeyDetails] = useState(null); const [newEncryptionNotification, setNewEncryptionNotification] = useState(null); @@ -293,14 +393,13 @@ export const Group = ({ const [directs, setDirects] = useState([]); const [admins, setAdmins] = useState([]); const [adminsWithNames, setAdminsWithNames] = useState([]); - const [members, setMembers] = useState([]); const [groupOwner, setGroupOwner] = useState(null); const [triedToFetchSecretKey, setTriedToFetchSecretKey] = useState(false); const [openAddGroup, setOpenAddGroup] = useState(false); const [isInitialGroups, setIsInitialGroups] = useState(false); const [openManageMembers, setOpenManageMembers] = useState(false); - const { setMemberGroups, memberGroups } = useContext(MyContext); + const { setMemberGroups, memberGroups, rootHeight } = useContext(MyContext); const lastGroupNotification = useRef(null); const [timestampEnterData, setTimestampEnterData] = useState({}); const [chatMode, setChatMode] = useState("groups"); @@ -308,13 +407,21 @@ export const Group = ({ const [openSnack, setOpenSnack] = React.useState(false); const [infoSnack, setInfoSnack] = React.useState(null); const [isLoadingNotifyAdmin, setIsLoadingNotifyAdmin] = React.useState(false); - const [isLoadingGroups, setIsLoadingGroups] = React.useState(false); + const [isLoadingGroups, setIsLoadingGroups] = React.useState(true); const [isLoadingGroup, setIsLoadingGroup] = React.useState(false); - const [firstSecretKeyInCreation, setFirstSecretKeyInCreation] = React.useState(false) + const [firstSecretKeyInCreation, setFirstSecretKeyInCreation] = + React.useState(false); const [groupSection, setGroupSection] = React.useState("home"); const [groupAnnouncements, setGroupAnnouncements] = React.useState({}); const [defaultThread, setDefaultThread] = React.useState(null); - + const [isOpenDrawer, setIsOpenDrawer] = React.useState(false); + const [hideCommonKeyPopup, setHideCommonKeyPopup] = React.useState(false); + const [isLoadingGroupMessage, setIsLoadingGroupMessage] = React.useState(""); + const [drawerMode, setDrawerMode] = React.useState("groups"); + const [mutedGroups, setMutedGroups] = useState([]); + const [mobileViewMode, setMobileViewMode] = useState("home"); + const [mobileViewModeKeepOpen, setMobileViewModeKeepOpen] = useState(""); + const [desktopSideView, setDesktopSideView] = useState('groups') const isFocusedRef = useRef(true); const selectedGroupRef = useRef(null); const selectedDirectRef = useRef(null); @@ -324,7 +431,10 @@ export const Group = ({ const setupGroupWebsocketInterval = useRef(null); const settimeoutForRefetchSecretKey = useRef(null); const { clearStatesMessageQueueProvider } = useMessageQueue(); - + const initiatedGetMembers = useRef(false); + // useEffect(()=> { + // setFullHeight() + // }, []) useEffect(() => { isFocusedRef.current = isFocused; @@ -341,12 +451,39 @@ export const Group = ({ selectedDirectRef.current = selectedDirect; }, [selectedDirect]); + const getUserSettings = async () => { + try { + return new Promise((res, rej) => { + chrome?.runtime?.sendMessage( + { + action: "getUserSettings", + payload: { + key: "mutedGroups", + }, + }, + (response) => { + if (!response?.error) { + setMutedGroups(response || []); + res(response); + return; + } + rej(response.error); + } + ); + }); + } catch (error) { + console.log("error", error); + } + }; + useEffect(() => { + getUserSettings(); + }, []); const getTimestampEnterChat = async () => { try { return new Promise((res, rej) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "getTimestampEnterChat", }, @@ -361,6 +498,29 @@ export const Group = ({ }); } catch (error) {} }; + const getGroupDataSingle = async (groupId) => { + try { + return new Promise((res, rej) => { + chrome?.runtime?.sendMessage( + { + action: "getGroupDataSingle", + payload: { + groupId, + }, + }, + (response) => { + if (!response?.error) { + res(response); + return; + } + rej(response.error); + } + ); + }); + } catch (error) { + return {}; + } + }; const refreshHomeDataFunc = () => { setGroupSection("default"); setTimeout(() => { @@ -371,12 +531,11 @@ export const Group = ({ const getGroupAnnouncements = async () => { try { return new Promise((res, rej) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "getGroupNotificationTimestamp", }, (response) => { - if (!response?.error) { setGroupAnnouncements(response); res(response); @@ -390,11 +549,10 @@ export const Group = ({ const getGroupOwner = async (groupId) => { try { - const url = `${getBaseApiReact()}/groups/${groupId}`; const response = await fetch(url); let data = await response.json(); - + const name = await getNameInfo(data?.owner); if (name) { data.name = name; @@ -458,6 +616,37 @@ export const Group = ({ return hasUnread; }, [timestampEnterData, directs, myAddress]); + const groupChatHasUnread = useMemo(() => { + let hasUnread = false; + groups.forEach((group) => { + if ( + group?.data && + isExtMsg(group?.data) && + group?.sender !== myAddress && + group?.timestamp && + ((!timestampEnterData[group?.groupId] && + Date.now() - group?.timestamp < timeDifferenceForNotificationChats) || + timestampEnterData[group?.groupId] < group?.timestamp) + ) { + hasUnread = true; + } + }); + return hasUnread; + }, [timestampEnterData, groups, myAddress]); + + const groupsAnnHasUnread = useMemo(() => { + let hasUnread = false; + groups.forEach((group) => { + if ( + groupAnnouncements[group?.groupId] && + !groupAnnouncements[group?.groupId]?.seentimestamp + ) { + hasUnread = true; + } + }); + return hasUnread; + }, [groupAnnouncements, groups]); + // useEffect(() => { // if (!myAddress) return; // checkGroupListFunc(myAddress); @@ -471,15 +660,15 @@ export const Group = ({ const getPublishesFromAdmins = async (admins: string[]) => { // const validApi = await findUsableApi(); const queryString = admins.map((name) => `name=${name}`).join("&"); - const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=DOCUMENT_PRIVATE&identifier=symmetric-qchat-group-${ + const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=symmetric-qchat-group-${ selectedGroup?.groupId - }&exactmatchnames=true&limit=0&reverse=true&${queryString}`; + }&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`; const response = await fetch(url); - if(!response.ok){ - throw new Error('network error') + if (!response.ok) { + throw new Error("network error"); } const adminData = await response.json(); - + const filterId = adminData.filter( (data: any) => data.identifier === `symmetric-qchat-group-${selectedGroup?.groupId}` @@ -491,18 +680,42 @@ export const Group = ({ // Get the most recent date for both a and b const dateA = a.updated ? new Date(a.updated) : new Date(a.created); const dateB = b.updated ? new Date(b.updated) : new Date(b.created); - + // Sort by most recent return dateB.getTime() - dateA.getTime(); }); - + return sortedData[0]; }; - const getSecretKey = async (loadingGroupParam?: boolean, secretKeyToPublish?: boolean) => { + const getSecretKey = async ( + loadingGroupParam?: boolean, + secretKeyToPublish?: boolean + ) => { try { - pauseAllQueues() - - if(secretKeyToPublish && secretKey && lastFetchedSecretKey.current && Date.now() - lastFetchedSecretKey.current < 1800000) return secretKey + setIsLoadingGroupMessage("Locating encryption keys"); + // setGroupDataLastSet(null) + pauseAllQueues(); + let dataFromStorage; + let publishFromStorage; + let adminsFromStorage; + // const groupData = await getGroupDataSingle(selectedGroup?.groupId); + // if ( + // groupData?.secretKeyData && + // Date.now() - groupData?.timestampLastSet < 3600000 + // ) { + // dataFromStorage = groupData.secretKeyData; + // publishFromStorage = groupData.secretKeyResource; + // adminsFromStorage = groupData.admins; + // // setGroupDataLastSet(groupData.timestampLastSet) + // } + + if ( + secretKeyToPublish && + secretKey && + lastFetchedSecretKey.current && + Date.now() - lastFetchedSecretKey.current < 1800000 + ) + return secretKey; if (loadingGroupParam) { setIsLoadingGroup(true); } @@ -514,13 +727,16 @@ export const Group = ({ } const prevGroupId = selectedGroupRef.current.groupId; // const validApi = await findUsableApi(); - const groupAdmins = await getGroupAdimns(selectedGroup?.groupId); - setAdmins(groupAdmins) - if(!groupAdmins.length){ - throw new Error('Network error') + const { names, addresses, both } = + adminsFromStorage || (await getGroupAdimns(selectedGroup?.groupId)); + setAdmins(addresses); + setAdminsWithNames(both); + if (!names.length) { + throw new Error("Network error"); } - const publish = await getPublishesFromAdmins(groupAdmins); - + const publish = + publishFromStorage || (await getPublishesFromAdmins(names)); + if (prevGroupId !== selectedGroupRef.current.groupId) { if (settimeoutForRefetchSecretKey.current) { clearTimeout(settimeoutForRefetchSecretKey.current); @@ -535,59 +751,76 @@ export const Group = ({ return false; } setSecretKeyPublishDate(publish?.updated || publish?.created); - - const res = await fetch( - `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ - publish.identifier - }?encoding=base64` - ); - const data = await res.text(); - + let data; + if (dataFromStorage) { + data = dataFromStorage; + } else { + setIsLoadingGroupMessage("Downloading encryption keys"); + const res = await fetch( + `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ + publish.identifier + }?encoding=base64` + ); + data = await res.text(); + } + const decryptedKey: any = await decryptResource(data); - + const dataint8Array = base64ToUint8Array(decryptedKey.data); const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); - + if (!validateSecretKey(decryptedKeyToObject)) throw new Error("SecretKey is not valid"); setSecretKeyDetails(publish); setSecretKey(decryptedKeyToObject); - lastFetchedSecretKey.current = Date.now() + lastFetchedSecretKey.current = Date.now(); setMemberCountFromSecretKeyData(decryptedKey.count); + chrome?.runtime?.sendMessage({ + action: "setGroupData", + payload: { + groupId: selectedGroup?.groupId, + secretKeyData: data, + secretKeyResource: publish, + admins: { names, addresses, both }, + }, + }); if (decryptedKeyToObject) { setTriedToFetchSecretKey(true); - setFirstSecretKeyInCreation(false) + setFirstSecretKeyInCreation(false); return decryptedKeyToObject; } else { setTriedToFetchSecretKey(true); } - } catch (error) { - if(error === 'Unable to decrypt data'){ + if (error === "Unable to decrypt data") { setTriedToFetchSecretKey(true); settimeoutForRefetchSecretKey.current = setTimeout(() => { getSecretKey(); }, 120000); } - } finally { setIsLoadingGroup(false); - if(!secretKeyToPublish){ - await getAdmins(selectedGroup?.groupId); - + setIsLoadingGroupMessage(""); + if (!secretKeyToPublish) { + // await getAdmins(selectedGroup?.groupId); } - resumeAllQueues() - + resumeAllQueues(); } }; + useEffect(()=> { + if(!selectedGroup) return + getGroupOwner(selectedGroup?.groupId); + }, [selectedGroup]) + + useEffect(() => { - if (selectedGroup) { + if (selectedGroup && groupOwner && groupOwner?.isOpen === false) { setTriedToFetchSecretKey(false); getSecretKey(true); - getGroupOwner(selectedGroup?.groupId); + // getGroupOwner(selectedGroup?.groupId); } - }, [selectedGroup]); + }, [selectedGroup, groupOwner]); // const handleNotification = async (data)=> { // try { @@ -596,7 +829,7 @@ export const Group = ({ // } // const newActiveChats= data // const oldActiveChats = await new Promise((res, rej) => { - // chrome.runtime.sendMessage( + // chrome?.runtime?.sendMessage( // { // action: "getChatHeads", // }, @@ -627,7 +860,7 @@ export const Group = ({ // if(results?.length > 0){ // if (!lastGroupNotification.current || (Date.now() - lastGroupNotification.current >= 60000)) { // console.log((Date.now() - lastGroupNotification.current >= 60000), lastGroupNotification.current) - // chrome.runtime.sendMessage( + // chrome?.runtime?.sendMessage( // { // action: "notification", // payload: { @@ -650,7 +883,7 @@ export const Group = ({ // } catch (error) { // console.log('error not', error) // if(!isFocusedRef.current){ - // chrome.runtime.sendMessage( + // chrome?.runtime?.sendMessage( // { // action: "notification", // payload: { @@ -670,7 +903,7 @@ export const Group = ({ // } finally { - // chrome.runtime.sendMessage( + // chrome?.runtime?.sendMessage( // { // action: "setChatHeads", // payload: { @@ -693,16 +926,15 @@ export const Group = ({ useEffect(() => { // Listen for messages from the background script - chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - + chrome?.runtime?.onMessage.addListener((message, sender, sendResponse) => { if (message.action === "SET_GROUPS") { // Update the component state with the received 'sendqort' state setGroups(message.payload); - + setMemberGroups(message.payload); if (selectedGroupRef.current && groupSectionRef.current === "chat") { - chrome.runtime.sendMessage({ + chrome?.runtime?.sendMessage({ action: "addTimestampEnterChat", payload: { timestamp: Date.now(), @@ -711,7 +943,7 @@ export const Group = ({ }); } if (selectedDirectRef.current) { - chrome.runtime.sendMessage({ + chrome?.runtime?.sendMessage({ action: "addTimestampEnterChat", payload: { timestamp: Date.now(), @@ -731,7 +963,7 @@ export const Group = ({ selectedGroupRef.current && groupSectionRef.current === "announcement" ) { - chrome.runtime.sendMessage({ + chrome?.runtime?.sendMessage({ action: "addGroupNotificationTimestamp", payload: { timestamp: Date.now(), @@ -748,7 +980,7 @@ export const Group = ({ setDirects(message.payload); // if (selectedGroupRef.current) { - // chrome.runtime.sendMessage({ + // chrome?.runtime?.sendMessage({ // action: "addTimestampEnterChat", // payload: { // timestamp: Date.now(), @@ -775,27 +1007,30 @@ export const Group = ({ ) return; - chrome.runtime.sendMessage({ action: "setupGroupWebsocket" }); + chrome?.runtime?.sendMessage({ action: "setupGroupWebsocket" }); hasInitializedWebsocket.current = true; }, [myAddress, groups]); - const getMembers = async (groupId) => { try { const res = await getGroupMembers(groupId); - if(groupId !== selectedGroupRef.current?.groupId) return + if (groupId !== selectedGroupRef.current?.groupId) return; setMembers(res); } catch (error) {} }; useEffect(() => { - if (selectedGroup?.groupId) { + if ( + !initiatedGetMembers.current && + selectedGroup?.groupId && + secretKey && + admins.includes(myAddress) + ) { // getAdmins(selectedGroup?.groupId); getMembers(selectedGroup?.groupId); + initiatedGetMembers.current = true; } - }, [selectedGroup?.groupId]); - - + }, [selectedGroup?.groupId, secretKey, myAddress, admins]); const shouldReEncrypt = useMemo(() => { if (triedToFetchSecretKey && !secretKeyPublishDate) return true; @@ -809,13 +1044,13 @@ export const Group = ({ memberCountFromSecretKeyData !== members?.memberCount && newEncryptionNotification?.text?.data?.numberOfMembers !== members?.memberCount; - + if (isDiffMemberNumber) return true; const latestJoined = members?.members.reduce((maxJoined, current) => { return current.joined > maxJoined ? current.joined : maxJoined; }, members?.members[0].joined); - + if ( secretKeyPublishDate < latestJoined && newEncryptionNotification?.data?.timestamp < latestJoined @@ -835,7 +1070,7 @@ export const Group = ({ try { setIsLoadingNotifyAdmin(true); await new Promise((res, rej) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "notifyAdminRegenerateSecretKey", payload: { @@ -844,7 +1079,6 @@ export const Group = ({ }, }, (response) => { - if (!response?.error) { res(response); } @@ -872,6 +1106,7 @@ export const Group = ({ .filter((group) => group?.sender !== myAddress) .find((gr) => gr?.groupId === selectedGroup?.groupId); if (!findGroup) return false; + if (!findGroup?.data || !isExtMsg(findGroup?.data)) return false; return ( findGroup?.timestamp && ((!timestampEnterData[selectedGroup?.groupId] && @@ -882,7 +1117,6 @@ export const Group = ({ }, [timestampEnterData, selectedGroup]); const isUnread = useMemo(() => { - if (!selectedGroup) return false; return ( groupAnnouncements?.[selectedGroup?.groupId]?.seentimestamp === false @@ -893,7 +1127,7 @@ export const Group = ({ if (isLoadingOpenSectionFromNotification.current) return; isLoadingOpenSectionFromNotification.current = true; const directAddress = e.detail?.from; - + const findDirect = directs?.find( (direct) => direct?.address === directAddress ); @@ -904,11 +1138,11 @@ export const Group = ({ if (findDirect) { setChatMode("directs"); setSelectedDirect(null); - setSelectedGroup(null); + // setSelectedGroup(null); setNewChat(false); - chrome.runtime.sendMessage({ + chrome?.runtime?.sendMessage({ action: "addTimestampEnterChat", payload: { timestamp: Date.now(), @@ -926,6 +1160,54 @@ export const Group = ({ } }; + const openDirectChatFromInternal = (e) => { + const directAddress = e.detail?.address; + const name = e.detail?.name; + const findDirect = directs?.find( + (direct) => direct?.address === directAddress || direct?.name === name + ); + + if (findDirect) { + setChatMode("directs"); + setSelectedDirect(null); + // setSelectedGroup(null); + + setNewChat(false); + + chrome?.runtime?.sendMessage({ + action: "addTimestampEnterChat", + payload: { + timestamp: Date.now(), + groupId: findDirect.address, + }, + }); + + setTimeout(() => { + setSelectedDirect(findDirect); + getTimestampEnterChat(); + }, 200); + } else { + setChatMode("directs"); + setNewChat(true); + setTimeout(() => { + executeEvent("setDirectToValueNewChat", { + directToValue: name || directAddress, + }); + }, 500); + } + }; + + useEffect(() => { + subscribeToEvent("openDirectMessageInternal", openDirectChatFromInternal); + + return () => { + unsubscribeFromEvent( + "openDirectMessageInternal", + openDirectChatFromInternal + ); + }; + }, [directs, selectedDirect]); + useEffect(() => { subscribeToEvent("openDirectMessage", openDirectChatFromNotification); @@ -934,10 +1216,41 @@ export const Group = ({ }; }, [directs, selectedDirect]); + const handleMarkAsRead = (e) => { + const { groupId } = e.detail; + chrome?.runtime?.sendMessage({ + action: "addTimestampEnterChat", + payload: { + timestamp: Date.now(), + groupId, + }, + }); + + chrome?.runtime?.sendMessage({ + action: "addGroupNotificationTimestamp", + payload: { + timestamp: Date.now(), + groupId, + }, + }); + setTimeout(() => { + getGroupAnnouncements(); + getTimestampEnterChat(); + }, 200); + }; + + useEffect(() => { + subscribeToEvent("markAsRead", handleMarkAsRead); + + return () => { + unsubscribeFromEvent("markAsRead", handleMarkAsRead); + }; + }, []); + const resetAllStatesAndRefs = () => { // Reset all useState values to their initial states setSecretKey(null); - lastFetchedSecretKey.current = null + lastFetchedSecretKey.current = null; setSecretKeyPublishDate(null); setSecretKeyDetails(null); setNewEncryptionNotification(null); @@ -951,10 +1264,11 @@ export const Group = ({ setMembers([]); setGroupOwner(null); setTriedToFetchSecretKey(false); + setHideCommonKeyPopup(false); setOpenAddGroup(false); setIsInitialGroups(false); setOpenManageMembers(false); - setMemberGroups([]); // Assuming you're clearing the context here as well + setMemberGroups([]); // Assuming you're clearing the context here as well setTimestampEnterData({}); setChatMode("groups"); setNewChat(false); @@ -967,7 +1281,7 @@ export const Group = ({ setGroupSection("home"); setGroupAnnouncements({}); setDefaultThread(null); - + setMobileViewMode("home"); // Reset all useRef values to their initial states hasInitialized.current = false; hasInitializedWebsocket.current = false; @@ -980,13 +1294,13 @@ export const Group = ({ isLoadingOpenSectionFromNotification.current = false; setupGroupWebsocketInterval.current = null; settimeoutForRefetchSecretKey.current = null; + initiatedGetMembers.current = false; }; - - const logoutEventFunc = ()=> { - resetAllStatesAndRefs() - clearStatesMessageQueueProvider() - } + const logoutEventFunc = () => { + resetAllStatesAndRefs(); + clearStatesMessageQueueProvider(); + }; useEffect(() => { subscribeToEvent("logout-event", logoutEventFunc); @@ -1013,7 +1327,8 @@ export const Group = ({ setNewChat(false); setSecretKey(null); - lastFetchedSecretKey.current = null + setGroupOwner(null) + lastFetchedSecretKey.current = null; setSecretKeyPublishDate(null); setAdmins([]); setSecretKeyDetails(null); @@ -1021,10 +1336,10 @@ export const Group = ({ setMembers([]); setMemberCountFromSecretKeyData(null); setTriedToFetchSecretKey(false); - setFirstSecretKeyInCreation(false) + setFirstSecretKeyInCreation(false); setGroupSection("chat"); - chrome.runtime.sendMessage({ + chrome?.runtime?.sendMessage({ action: "addTimestampEnterChat", payload: { timestamp: Date.now(), @@ -1034,7 +1349,7 @@ export const Group = ({ setTimeout(() => { setSelectedGroup(findGroup); - + setMobileViewMode("group"); getTimestampEnterChat(); isLoadingOpenSectionFromNotification.current = false; }, 200); @@ -1052,7 +1367,6 @@ export const Group = ({ }, [groups, selectedGroup]); const openGroupAnnouncementFromNotification = (e) => { - const groupId = e.detail?.from; const findGroup = groups?.find((group) => +group?.groupId === +groupId); @@ -1061,7 +1375,8 @@ export const Group = ({ setChatMode("groups"); setSelectedGroup(null); setSecretKey(null); - lastFetchedSecretKey.current = null + setGroupOwner(null) + lastFetchedSecretKey.current = null; setSecretKeyPublishDate(null); setAdmins([]); setSecretKeyDetails(null); @@ -1069,9 +1384,9 @@ export const Group = ({ setMembers([]); setMemberCountFromSecretKeyData(null); setTriedToFetchSecretKey(false); - setFirstSecretKeyInCreation(false) + setFirstSecretKeyInCreation(false); setGroupSection("announcement"); - chrome.runtime.sendMessage({ + chrome?.runtime?.sendMessage({ action: "addGroupNotificationTimestamp", payload: { timestamp: Date.now(), @@ -1080,6 +1395,7 @@ export const Group = ({ }); setTimeout(() => { setSelectedGroup(findGroup); + setMobileViewMode("group"); getGroupAnnouncements(); }, 200); @@ -1118,7 +1434,8 @@ export const Group = ({ setChatMode("groups"); setSelectedGroup(null); setSecretKey(null); - lastFetchedSecretKey.current = null + setGroupOwner(null) + lastFetchedSecretKey.current = null; setSecretKeyPublishDate(null); setAdmins([]); setSecretKeyDetails(null); @@ -1126,13 +1443,13 @@ export const Group = ({ setMembers([]); setMemberCountFromSecretKeyData(null); setTriedToFetchSecretKey(false); - setFirstSecretKeyInCreation(false) + setFirstSecretKeyInCreation(false); setGroupSection("forum"); setDefaultThread(data); setTimeout(() => { setSelectedGroup(findGroup); - + setMobileViewMode("group"); getGroupAnnouncements(); }, 200); } @@ -1146,257 +1463,586 @@ export const Group = ({ }; }, [groups, selectedGroup]); - const handleSecretKeyCreationInProgress = ()=> { - setFirstSecretKeyInCreation(true) - } + const handleSecretKeyCreationInProgress = () => { + setFirstSecretKeyInCreation(true); + }; + const goToHome = async () => { + if (isMobile) { + setMobileViewMode("home"); + } + if (!isMobile) { + } + setGroupSection("default"); + clearAllQueues(); + await new Promise((res) => { + setTimeout(() => { + res(null); + }, 200); + }); + setGroupSection("home"); + setSelectedGroup(null); + setNewChat(false); + setSelectedDirect(null); + setSecretKey(null); + setGroupOwner(null) + lastFetchedSecretKey.current = null; + setSecretKeyPublishDate(null); + setAdmins([]); + setSecretKeyDetails(null); + setAdminsWithNames([]); + setMembers([]); + setMemberCountFromSecretKeyData(null); + setTriedToFetchSecretKey(false); + setFirstSecretKeyInCreation(false); + }; + const goToAnnouncements = async () => { + setGroupSection("default"); + await new Promise((res) => { + setTimeout(() => { + res(null); + }, 200); + }); + setSelectedDirect(null); + setNewChat(false); + setGroupSection("announcement"); + chrome?.runtime?.sendMessage({ + action: "addGroupNotificationTimestamp", + payload: { + timestamp: Date.now(), + groupId: selectedGroupRef.current.groupId, + }, + }); + setTimeout(() => { + getGroupAnnouncements(); + }, 200); + }; - return ( - <> - - + const openDrawerGroups = () => { + setIsOpenDrawer(true); + setDrawerMode("groups"); + }; + const goToThreads = () => { + setSelectedDirect(null); + setNewChat(false); + setGroupSection("forum"); + }; + + const goToChat = async () => { + setGroupSection("default"); + await new Promise((res) => { + setTimeout(() => { + res(null); + }, 200); + }); + setGroupSection("chat"); + setNewChat(false); + setSelectedDirect(null); + if (selectedGroupRef.current) { + chrome?.runtime?.sendMessage({ + action: "addTimestampEnterChat", + payload: { + timestamp: Date.now(), + groupId: selectedGroupRef.current.groupId, + }, + }); + + setTimeout(() => { + getTimestampEnterChat(); + }, 200); + } + }; + + const renderDirects = () => { + return (
+ {isMobile && ( + + + + + + + + { + setMobileViewModeKeepOpen('') + }} + > + + + + + + )}
-
- { - setChatMode((prev) => - prev === "directs" ? "groups" : "directs" - ); - setNewChat(false); - setSelectedDirect(null); - setSelectedGroup(null); - setGroupSection("default"); + {directs.map((direct: any) => ( + - {chatMode === "groups" && ( - <> - + // + // + // } + onClick={() => { + setSelectedDirect(null); + setNewChat(false); + // setSelectedGroup(null); + setIsOpenDrawer(false); + chrome?.runtime?.sendMessage({ + action: "addTimestampEnterChat", + payload: { + timestamp: Date.now(), + groupId: direct.address, + }, + }); + setTimeout(() => { + setSelectedDirect(direct); + + getTimestampEnterChat(); + }, 200); + }} + sx={{ + display: "flex", + width: "100%", + flexDirection: "column", + cursor: "pointer", + border: "1px #232428 solid", + padding: "2px", + borderRadius: "2px", + background: + direct?.address === selectedDirect?.address && "white", + }} + > + + + + {(direct?.name || direct?.address)?.charAt(0)} + + + - - )} - {chatMode === "directs" ? "Switch to groups" : "Direct msgs"} - -
-
- {directs.map((direct: any) => ( - - - // - // - // } - onClick={() => { - setSelectedDirect(null); - setNewChat(false); - setSelectedGroup(null); - chrome.runtime.sendMessage({ - action: "addTimestampEnterChat", - payload: { - timestamp: Date.now(), - groupId: direct.address, - }, - }); - setTimeout(() => { - setSelectedDirect(direct); - - getTimestampEnterChat(); - }, 200); - }} - sx={{ - display: "flex", - width: "100%", - flexDirection: "column", - cursor: "pointer", - border: "1px #232428 solid", - padding: "2px", - borderRadius: "2px", - background: - direct?.address === selectedDirect?.address && "white", - }} - > - - - - {(direct?.name || direct?.address)?.charAt(0)} - - - - {direct?.sender !== myAddress && - direct?.timestamp && - ((!timestampEnterData[direct?.address] && - Date.now() - direct?.timestamp < - timeDifferenceForNotificationChats) || - timestampEnterData[direct?.address] < - direct?.timestamp) && ( - - )} - - - - ))} -
-
+ )} + + + + ))} +
+
+ { + setNewChat(true); + setSelectedDirect(null); + // setSelectedGroup(null); + setIsOpenDrawer(false); }} > - {groups.map((group: any) => ( - - - // - // - // } - onClick={() => { - clearAllQueues() - setSelectedDirect(null); + + New Chat + +
+
+ ); + }; - setNewChat(false); - setSelectedGroup(null); - setSecretKey(null); - lastFetchedSecretKey.current = null - setSecretKeyPublishDate(null); - setAdmins([]); - setSecretKeyDetails(null); - setAdminsWithNames([]); - setMembers([]); - setMemberCountFromSecretKeyData(null); - setTriedToFetchSecretKey(false); - setFirstSecretKeyInCreation(false) - setGroupSection("announcement"); - - setTimeout(() => { - setSelectedGroup(group); - - getTimestampEnterChat(); - }, 200); - - if (groupSectionRef.current === "announcement") { - chrome.runtime.sendMessage({ - action: "addGroupNotificationTimestamp", - payload: { - timestamp: Date.now(), - groupId: group.groupId, - }, - }); - } - - setTimeout(() => { - getGroupAnnouncements(); - }, 600); + const renderGroups = () => { + return ( +
+ {/*
+ {isMobile && ( + + { + setIsOpenDrawer(false); + }} + sx={{ + cursor: "pointer", + color: "white", + }} + /> + + )} + { + setChatMode((prev) => + prev === "directs" ? "groups" : "directs" + ); + + }} + sx={{ + backgroundColor: chatMode === 'directs' && ( groupChatHasUnread || groupsAnnHasUnread) ? 'red' : 'revert' + }} + > + {chatMode === "groups" && ( + <> + + + )} + + {chatMode === "directs" ? "Switch to groups" : "Direct msgs"} + +
*/} + {/*
+ {directs.map((direct: any) => ( + + + // + // + // } + onClick={() => { + setSelectedDirect(null); + setNewChat(false); + // setSelectedGroup(null); + setIsOpenDrawer(false); + chrome?.runtime?.sendMessage({ + action: "addTimestampEnterChat", + payload: { + timestamp: Date.now(), + groupId: direct.address, + }, + }); + setTimeout(() => { + setSelectedDirect(direct); + + getTimestampEnterChat(); + }, 200); + }} + sx={{ + display: "flex", + width: "100%", + flexDirection: "column", + cursor: "pointer", + border: "1px #232428 solid", + padding: "2px", + borderRadius: "2px", + background: + direct?.address === selectedDirect?.address && "white", + }} + > + + + + {(direct?.name || direct?.address)?.charAt(0)} + + + + {direct?.sender !== myAddress && + direct?.timestamp && + ((!timestampEnterData[direct?.address] && + Date.now() - direct?.timestamp < + timeDifferenceForNotificationChats) || + timestampEnterData[direct?.address] < + direct?.timestamp) && ( + + )} + + + + ))} +
*/} +
+ {groups.map((group: any) => ( + + + // + // + // } + onClick={() => { + setMobileViewMode("group"); + clearAllQueues(); + setSelectedDirect(null); + setTriedToFetchSecretKey(false); + setNewChat(false); + setSelectedGroup(null); + setSecretKey(null); + lastFetchedSecretKey.current = null; + setSecretKeyPublishDate(null); + setAdmins([]); + setSecretKeyDetails(null); + setAdminsWithNames([]); + setGroupOwner(null) + setMembers([]); + setMemberCountFromSecretKeyData(null); + setHideCommonKeyPopup(false); + setFirstSecretKeyInCreation(false); + // setGroupSection("announcement"); + setGroupSection("chat"); + setIsOpenDrawer(false); + setTimeout(() => { + setSelectedGroup(group); + + // getTimestampEnterChat(); + }, 200); + + chrome?.runtime?.sendMessage({ + action: "addTimestampEnterChat", + payload: { + timestamp: Date.now(), + groupId: group.groupId, + }, + }); + + setTimeout(() => { + getTimestampEnterChat(); + }, 200); + + // if (groupSectionRef.current === "announcement") { + // chrome?.runtime?.sendMessage({ + // action: "addGroupNotificationTimestamp", + // payload: { + // timestamp: Date.now(), + // groupId: group.groupId, + // }, + // }); + // } + + // setTimeout(() => { + // getGroupAnnouncements(); + // }, 600); + }} + sx={{ + display: "flex", + width: "100%", + flexDirection: "column", + cursor: "pointer", + border: "1px #232428 solid", + padding: "2px", + borderRadius: "2px", + background: + group?.groupId === selectedGroup?.groupId && "white", + }} + > + @@ -1442,7 +2088,9 @@ export const Group = ({ }} /> )} - {group?.sender !== myAddress && + {group?.data && + isExtMsg(group?.data) && + group?.sender !== myAddress && group?.timestamp && ((!timestampEnterData[group?.groupId] && Date.now() - group?.timestamp < @@ -1456,330 +2104,632 @@ export const Group = ({ /> )} - - - ))} -
-
- {chatMode === "groups" && ( - { - setOpenAddGroup(true); - }} - > - - Add Group - - )} - {chatMode === "directs" && ( - { - setNewChat(true); - setSelectedDirect(null); - setSelectedGroup(null); - }} - > - - New Chat - - )} -
+ + + + ))}
- - {newChat && ( - <> - - - )} - {selectedGroup && !newChat && ( - <> - + {chatMode === "groups" && ( + { + setOpenAddGroup(true); }} > - {triedToFetchSecretKey && ( - - )} - {firstSecretKeyInCreation && triedToFetchSecretKey && !secretKeyPublishDate && ( -
- {" "} - - The group's first common encryption key is in the process of creation. Please wait a few minutes for it to be retrieved by the network. Checking every 2 minutes... - -
- )} - {!admins.includes(myAddress) && - !secretKey && - triedToFetchSecretKey ? ( - <> - {(secretKeyPublishDate || !secretKeyPublishDate && !firstSecretKeyInCreation) ? ( -
+ Add Group + + )} + {chatMode === "directs" && ( + { + setNewChat(true); + setSelectedDirect(null); + // setSelectedGroup(null); + setIsOpenDrawer(false); + }} + > + + New Chat + + )} +
+
+ ); + }; + + return ( + <> + + + + {isMobile && ( +
+ )} + + +
+ {!isMobile && desktopSideView === 'groups' && renderGroups()} + {!isMobile && desktopSideView === 'directs' && renderDirects()} + + + + + {mobileViewMode === "groups" && !mobileViewModeKeepOpen && renderGroups()} + + {mobileViewModeKeepOpen === "messaging" && renderDirects()} + {newChat && ( + <> + {isMobile && ( + + - {" "} - - You are not part of the encrypted group of members. Wait - until an admin re-encrypts the keys. - - - - Try notifying an admin from the list of admins below: - - - {adminsWithNames.map((admin) => { - - return ( - - {admin?.name} - notifyAdmin(admin)} - > - Notify - - - ); - })} -
- ) : null} - - - ) : admins.includes(myAddress) && - !secretKey && - triedToFetchSecretKey ? null : !triedToFetchSecretKey ? null : ( - <> - + + { + close() + }} + > + + + + + + { + setSelectedDirect(null) + setMobileViewModeKeepOpen('') + }} + > + + + + + + )} + + { + setSelectedDirect(null); - + + + )} + {selectedGroup && ( + <> + {!isMobile && selectedGroup && ( + + + + )} + {isMobile && ( + + + + { + setMobileViewMode("groups"); + }} + > + + + + + {selectedGroup?.groupName} + + + {/* */} + + + + )} + + {isMobile && mobileViewMode === "group" && ( + <> + + + )} + + {triedToFetchSecretKey && ( + - - - )} - - - {admins.includes(myAddress) && - shouldReEncrypt && - triedToFetchSecretKey && !firstSecretKeyInCreation && ( - + )} + {firstSecretKeyInCreation && + triedToFetchSecretKey && + !secretKeyPublishDate && ( +
+ {" "} + + The group's first common encryption key is in the + process of creation. Please wait a few minutes for it to + be retrieved by the network. Checking every 2 minutes... + +
)} -
-
- {openManageMembers && ( - - )} + {!admins.includes(myAddress) && + !secretKey && + triedToFetchSecretKey ? ( + <> + {secretKeyPublishDate || + (!secretKeyPublishDate && !firstSecretKeyInCreation) ? ( +
+ {" "} + + You are not part of the encrypted group of members. + Wait until an admin re-encrypts the keys. + + + + Try notifying an admin from the list of admins below: + + + {adminsWithNames.map((admin) => { + return ( + + {admin?.name} + notifyAdmin(admin)} + > + Notify + + + ); + })} +
+ ) : null} + + ) : admins.includes(myAddress) && + !secretKey && + triedToFetchSecretKey ? null : !triedToFetchSecretKey ? null : ( + <> + + + + )} - - - )} - - {selectedDirect && !newChat && ( - <> - - - - - )} - {!selectedDirect && - !selectedGroup && - !newChat && - groupSection === "home" && ( - - - + {admins.includes(myAddress) && + shouldReEncrypt && + triedToFetchSecretKey && + !firstSecretKeyInCreation && + !hideCommonKeyPopup && ( + + )} + + {openManageMembers && ( + + )} + + )} + {selectedDirect && !newChat && ( + <> - - - - + + { + setSelectedDirect(null); + + setNewChat(false); + }} + setMobileViewModeKeepOpen={setMobileViewModeKeepOpen} + /> + - + )} - + )} + {isMobile && mobileViewMode === "home" && ( + + )} + { + !isMobile && !selectedGroup && + groupSection === "home" && ( + + + )} + + + {/* + + + + Home + + + {selectedGroup && ( + <> { - setGroupSection("default"); - clearAllQueues() - await new Promise((res) => { - setTimeout(() => { - res(null); - }, 200); - }); - setGroupSection("home"); - setSelectedGroup(null); - setNewChat(false); - setSelectedDirect(null); - setSecretKey(null); - lastFetchedSecretKey.current = null - setSecretKeyPublishDate(null); - setAdmins([]); - setSecretKeyDetails(null); - setAdminsWithNames([]); - setMembers([]); - setMemberCountFromSecretKeyData(null); - setTriedToFetchSecretKey(false); - setFirstSecretKeyInCreation(false) - }} - > - - - Home - - - {selectedGroup && ( - <> - - { - setGroupSection("default"); - await new Promise((res) => { - setTimeout(() => { - res(null); - }, 200); - }); - setGroupSection("announcement"); - chrome.runtime.sendMessage({ - action: "addGroupNotificationTimestamp", - payload: { - timestamp: Date.now(), - groupId: selectedGroupRef.current.groupId, - }, - }); - setTimeout(() => { - getGroupAnnouncements(); - }, 200); + cursor: "pointer", }} + onClick={goToAnnouncements} > Announcements @@ -1899,33 +2776,11 @@ export const Group = ({ alignItems: "center", justifyContent: "flex-start", width: "100%", - cursor: 'pointer' - }} - onClick={async () => { - setGroupSection("default"); - await new Promise((res) => { - setTimeout(() => { - res(null); - }, 200); - }); - setGroupSection("chat"); - if (selectedGroupRef.current) { - chrome.runtime.sendMessage({ - action: "addTimestampEnterChat", - payload: { - timestamp: Date.now(), - groupId: selectedGroupRef.current.groupId, - }, - }); - - setTimeout(() => { - getTimestampEnterChat(); - }, 200); - } + cursor: "pointer", }} + onClick={goToChat} > Chat @@ -1959,14 +2814,15 @@ export const Group = ({ alignItems: "center", justifyContent: "flex-start", width: "100%", - cursor: 'pointer' + cursor: "pointer", }} onClick={() => { setGroupSection("forum"); + setSelectedDirect(null); + setNewChat(false); }} > setOpenManageMembers(true)} + onClick={() => setOpenManageMembers(true)} sx={{ display: "flex", gap: "3px", alignItems: "center", justifyContent: "flex-start", width: "100%", - cursor: 'pointer' + cursor: "pointer", }} > - - + Members - - )} - - {/* + )} */} + + {/* */} - + -
+ + {isMobile && mobileViewMode === "home" && !mobileViewModeKeepOpen && ( + <> +
+ {/* + {renderGroups()} + */} + {isMobile && ( + + )} + + )} ); }; + +// {isMobile && ( +// +// +// {selectedGroup && ( +// <> +// +// +// +// +// +// +// +// +// +// +// +// +// +// )} + +// {/* Second row: Groups, Home, Profile */} +// +// +// +// +// +// +// +// +// +// setIsOpenDrawerProfile(true)} +// > +// +// +// +// +// +// )} diff --git a/src/components/Group/GroupInvites.tsx b/src/components/Group/GroupInvites.tsx index 06e176d..0c4b6f6 100644 --- a/src/components/Group/GroupInvites.tsx +++ b/src/components/Group/GroupInvites.tsx @@ -8,107 +8,165 @@ import Checkbox from "@mui/material/Checkbox"; import IconButton from "@mui/material/IconButton"; import CommentIcon from "@mui/icons-material/Comment"; import InfoIcon from "@mui/icons-material/Info"; -import GroupAddIcon from '@mui/icons-material/GroupAdd'; +import GroupAddIcon from "@mui/icons-material/GroupAdd"; import { executeEvent } from "../../utils/events"; import { Box, Typography } from "@mui/material"; import { Spacer } from "../../common/Spacer"; import { getGroupNames } from "./UserListOfInvites"; import { CustomLoader } from "../../common/CustomLoader"; -import { getBaseApiReact } from "../../App"; +import { getBaseApiReact, isMobile } from "../../App"; export const GroupInvites = ({ myAddress, setOpenAddGroup }) => { - const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState([]) - const [loading, setLoading] = React.useState(true) + const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState( + [] + ); + const [loading, setLoading] = React.useState(true); - const getJoinRequests = async ()=> { + const getJoinRequests = async () => { try { - setLoading(true) - const response = await fetch(`${getBaseApiReact()}/groups/invites/${myAddress}/?limit=0`); + setLoading(true); + const response = await fetch( + `${getBaseApiReact()}/groups/invites/${myAddress}/?limit=0` + ); const data = await response.json(); - const resMoreData = await getGroupNames(data) + const resMoreData = await getGroupNames(data); - setGroupsWithJoinRequests(resMoreData) + setGroupsWithJoinRequests(resMoreData); } catch (error) { - } finally { - setLoading(false) + setLoading(false); } - } + }; React.useEffect(() => { if (myAddress) { - getJoinRequests() + getJoinRequests(); } }, [myAddress]); - return ( - - Group Invites - - {loading && groupsWithJoinRequests.length === 0 && ( - - - - )} - {!loading && groupsWithJoinRequests.length === 0 && ( - - No invites - - )} - - {groupsWithJoinRequests?.map((group)=> { - return ( - { - setOpenAddGroup(true) - setTimeout(() => { - executeEvent("openGroupInvitesRequest", {}); - - }, 300); + + + - - - } > - - - - - - ) + Hub Invites: + + + - })} - - - - + + {loading && groupsWithJoinRequests.length === 0 && ( + + + + )} + {!loading && groupsWithJoinRequests.length === 0 && ( + + + Nothing to display + + + )} + + {groupsWithJoinRequests?.map((group) => { + return ( + { + setOpenAddGroup(true); + setTimeout(() => { + executeEvent("openGroupInvitesRequest", {}); + }, 300); + }} + disablePadding + secondaryAction={ + + + + } + > + + + + + ); + })} + + ); }; diff --git a/src/components/Group/GroupJoinRequests.tsx b/src/components/Group/GroupJoinRequests.tsx index 1123d57..1f9d2ca 100644 --- a/src/components/Group/GroupJoinRequests.tsx +++ b/src/components/Group/GroupJoinRequests.tsx @@ -15,10 +15,10 @@ import { Box, Typography } from "@mui/material"; import { Spacer } from "../../common/Spacer"; import { CustomLoader } from "../../common/CustomLoader"; import { getBaseApi } from "../../background"; -import { getBaseApiReact } from "../../App"; -export const requestQueueGroupJoinRequests = new RequestQueueWithPromise(3) +import { getBaseApiReact, isMobile } from "../../App"; +export const requestQueueGroupJoinRequests = new RequestQueueWithPromise(2) -export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, getTimestampEnterChat, setSelectedGroup, setGroupSection }) => { +export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, getTimestampEnterChat, setSelectedGroup, setGroupSection, setMobileViewMode }) => { const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState([]) const [loading, setLoading] = React.useState(true) @@ -93,19 +93,45 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get }, [myAddress, groups]); - return ( - - Join Requests - + + + Join Requests: + + + + + {loading && groupsWithJoinRequests.length === 0 && ( )} - {!loading && groupsWithJoinRequests.length === 0 && ( - - No join requests - + {!loading && (groupsWithJoinRequests.length === 0 || groupsWithJoinRequests?.filter((group)=> group?.data?.length > 0).length === 0) && ( + + + Nothing to display + + )} {groupsWithJoinRequests?.map((group)=> { @@ -134,6 +171,7 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get key={group?.groupId} onClick={()=> { setSelectedGroup(group?.group) + setMobileViewMode('group') getTimestampEnterChat() setGroupSection("announcement") setOpenManageMembers(true) @@ -142,20 +180,31 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get }, 300); }} + sx={{ + marginBottom: '20px' + }} disablePadding secondaryAction={ } > - + - + ) @@ -166,5 +215,6 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get + ); }; diff --git a/src/components/Group/GroupMenu.tsx b/src/components/Group/GroupMenu.tsx new file mode 100644 index 0000000..5bc99c6 --- /dev/null +++ b/src/components/Group/GroupMenu.tsx @@ -0,0 +1,200 @@ +import React, { useState } from "react"; +import { + Button, + Menu, + MenuItem, + ListItemIcon, + ListItemText, + Badge, + Box, +} from "@mui/material"; +import ForumIcon from "@mui/icons-material/Forum"; +import GroupIcon from "@mui/icons-material/Group"; +import { ArrowDownIcon } from "../../assets/Icons/ArrowDownIcon"; +import { NotificationIcon2 } from "../../assets/Icons/NotificationIcon2"; +import { ChatIcon } from "../../assets/Icons/ChatIcon"; +import { ThreadsIcon } from "../../assets/Icons/ThreadsIcon"; +import { MembersIcon } from "../../assets/Icons/MembersIcon"; + +export const GroupMenu = ({ setGroupSection, groupSection, setOpenManageMembers }) => { + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + return ( + + + + { + setGroupSection("chat"); + handleClose(); + }} + > + + + + + + { + setGroupSection("announcement"); + handleClose(); + }} + > + + + + + + { + setGroupSection("forum"); + handleClose(); + }} + > + + + + + + + { + setOpenManageMembers(true) + handleClose(); + }} + > + + + + + + + + + ); +}; diff --git a/src/components/Group/Home.tsx b/src/components/Group/Home.tsx new file mode 100644 index 0000000..4db465d --- /dev/null +++ b/src/components/Group/Home.tsx @@ -0,0 +1,110 @@ +import { Box, Button, Typography } from "@mui/material"; +import React from "react"; +import { Spacer } from "../../common/Spacer"; +import { ListOfThreadPostsWatched } from "./ListOfThreadPostsWatched"; +import { ThingsToDoInitial } from "./ThingsToDoInitial"; +import { GroupJoinRequests } from "./GroupJoinRequests"; +import { GroupInvites } from "./GroupInvites"; +import RefreshIcon from "@mui/icons-material/Refresh"; + +export const Home = ({ + refreshHomeDataFunc, + myAddress, + isLoadingGroups, + balance, + userInfo, + groups, + setGroupSection, + setSelectedGroup, + getTimestampEnterChat, + setOpenManageMembers, + setOpenAddGroup, + setMobileViewMode, +}) => { + return ( + + + 15 ? "16px" : "20px", + padding: '10px' + }} + > + Welcome{" "} + {userInfo?.name ? ( + {`, ${userInfo?.name}`} + ) : null} + + + + {/* + + */} + {!isLoadingGroups && ( + + + + + + + + )} + + + ); +}; diff --git a/src/components/Group/HomeDesktop.tsx b/src/components/Group/HomeDesktop.tsx new file mode 100644 index 0000000..2162a02 --- /dev/null +++ b/src/components/Group/HomeDesktop.tsx @@ -0,0 +1,150 @@ +import { Box, Button, Typography } from "@mui/material"; +import React from "react"; +import { Spacer } from "../../common/Spacer"; +import { ListOfThreadPostsWatched } from "./ListOfThreadPostsWatched"; +import { ThingsToDoInitial } from "./ThingsToDoInitial"; +import { GroupJoinRequests } from "./GroupJoinRequests"; +import { GroupInvites } from "./GroupInvites"; +import RefreshIcon from "@mui/icons-material/Refresh"; + +export const HomeDesktop = ({ + refreshHomeDataFunc, + myAddress, + isLoadingGroups, + balance, + userInfo, + groups, + setGroupSection, + setSelectedGroup, + getTimestampEnterChat, + setOpenManageMembers, + setOpenAddGroup, + setMobileViewMode, +}) => { + return ( + + + + 15 ? "16px" : "20px", + padding: '10px' + }} + > + Welcome{" "} + {userInfo?.name ? ( + {`, ${userInfo?.name}`} + ) : null} + + + {!isLoadingGroups && ( + + + + + + + + + + + + + + + )} + + + + + {/* + + */} + + + + ); +}; diff --git a/src/components/Group/InviteMember.tsx b/src/components/Group/InviteMember.tsx index 5a22678..12efdda 100644 --- a/src/components/Group/InviteMember.tsx +++ b/src/components/Group/InviteMember.tsx @@ -26,7 +26,7 @@ export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => { setIsLoadingInvite(true) if (!expiryTime || !value) return; new Promise((res, rej) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "inviteToGroup", payload: { diff --git a/src/components/Group/ListOfBans.tsx b/src/components/Group/ListOfBans.tsx index 6cd1236..41ef9f4 100644 --- a/src/components/Group/ListOfBans.tsx +++ b/src/components/Group/ListOfBans.tsx @@ -74,7 +74,7 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => { }) setIsLoadingUnban(true) new Promise((res, rej)=> { - chrome.runtime.sendMessage({ action: "cancelBan", payload: { + chrome?.runtime?.sendMessage({ action: "cancelBan", payload: { groupId, qortalAddress: address, }}, (response) => { @@ -168,7 +168,7 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => { return (

Ban list

-
+
{({ height, width }) => ( }) setIsLoadingCancelInvite(true) await new Promise((res, rej)=> { - chrome.runtime.sendMessage({ action: "cancelInvitationToGroup", payload: { + chrome?.runtime?.sendMessage({ action: "cancelInvitationToGroup", payload: { groupId, qortalAddress: address, }}, (response) => { @@ -169,7 +169,7 @@ export const ListOfInvites = ({ groupId, setInfoSnack, setOpenSnack, show }) => return (

Invitees list

-
+
{({ height, width }) => ( { - chrome.runtime.sendMessage({ action: "inviteToGroup", payload: { + chrome?.runtime?.sendMessage({ action: "inviteToGroup", payload: { groupId, qortalAddress: address, inviteTime: 10800, @@ -169,7 +169,7 @@ export const ListOfJoinRequests = ({ groupId, setInfoSnack, setOpenSnack, show } return (

Join request list

-
+
{({ height, width }) => ( { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "kickFromGroup", payload: { @@ -107,7 +107,7 @@ const ListOfMembers = ({ }); setIsLoadingBan(true); await new Promise((res, rej) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "banFromGroup", payload: { @@ -153,7 +153,7 @@ const ListOfMembers = ({ }); setIsLoadingMakeAdmin(true); await new Promise((res, rej) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "makeAdmin", payload: { @@ -198,7 +198,7 @@ const ListOfMembers = ({ }); setIsLoadingRemoveAdmin(true); await new Promise((res, rej) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "removeAdmin", payload: { @@ -357,7 +357,7 @@ const ListOfMembers = ({ style={{ position: "relative", height: "500px", - width: "600px", + width: "100%", display: "flex", flexDirection: "column", flexShrink: 1, diff --git a/src/components/Group/ListOfThreadPostsWatched.tsx b/src/components/Group/ListOfThreadPostsWatched.tsx index ed8e7d9..4817805 100644 --- a/src/components/Group/ListOfThreadPostsWatched.tsx +++ b/src/components/Group/ListOfThreadPostsWatched.tsx @@ -8,131 +8,174 @@ import Checkbox from "@mui/material/Checkbox"; import IconButton from "@mui/material/IconButton"; import CommentIcon from "@mui/icons-material/Comment"; import InfoIcon from "@mui/icons-material/Info"; -import GroupAddIcon from '@mui/icons-material/GroupAdd'; +import GroupAddIcon from "@mui/icons-material/GroupAdd"; import { executeEvent } from "../../utils/events"; import { Box, Typography } from "@mui/material"; import { Spacer } from "../../common/Spacer"; import { getGroupNames } from "./UserListOfInvites"; import { CustomLoader } from "../../common/CustomLoader"; -import VisibilityIcon from '@mui/icons-material/Visibility'; +import VisibilityIcon from "@mui/icons-material/Visibility"; +import { isMobile } from "../../App"; export const ListOfThreadPostsWatched = () => { - const [posts, setPosts] = React.useState([]) - const [loading, setLoading] = React.useState(true) + const [posts, setPosts] = React.useState([]); + const [loading, setLoading] = React.useState(true); - const getPosts = async ()=> { + const getPosts = async () => { try { await new Promise((res, rej) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "getThreadActivity", - payload: { - - }, + payload: {}, }, (response) => { - if (!response?.error) { - if(!response) { - res(null) - return + if (!response) { + res(null); + return; } const uniquePosts = response.reduce((acc, current) => { - const x = acc.find(item => item?.thread?.threadId === current?.thread?.threadId); + const x = acc.find( + (item) => item?.thread?.threadId === current?.thread?.threadId + ); if (!x) { return acc.concat([current]); } else { return acc; } }, []); - setPosts(uniquePosts) + setPosts(uniquePosts); res(uniquePosts); - return + return; } rej(response.error); } ); }); } catch (error) { - } finally { - setLoading(false) + setLoading(false); } - } + }; React.useEffect(() => { - - getPosts() - + getPosts(); }, []); - - return ( - - New Thread Posts - - {loading && posts.length === 0 && ( - - - - )} - {!loading && posts.length === 0 && ( - - No thread post notifications - - )} - - {posts?.map((post)=> { - return ( - { - executeEvent("openThreadNewPost", { - data: post - }); - }} - disablePadding - secondaryAction={ - - - - } - > - - - - - - ) + + }} + > + + New Thread Posts: + + + + + + {loading && posts.length === 0 && ( + + + + )} + {!loading && posts.length === 0 && ( + + + Nothing to display + + + )} + {posts?.length > 0 && ( + + {posts?.map((post) => { + return ( + { + executeEvent("openThreadNewPost", { + data: post, + }); + }} + disablePadding + secondaryAction={ + + + + } + > + + + + + ); + })} + + )} + + ); }; diff --git a/src/components/Group/ManageMembers.tsx b/src/components/Group/ManageMembers.tsx index aebb3fb..96a46ba 100644 --- a/src/components/Group/ManageMembers.tsx +++ b/src/components/Group/ManageMembers.tsx @@ -19,7 +19,7 @@ import { ListOfBans } from "./ListOfBans"; import { ListOfJoinRequests } from "./ListOfJoinRequests"; import { Box, Tab, Tabs } from "@mui/material"; import { CustomizedSnackbars } from "../Snackbar/Snackbar"; -import { MyContext } from "../../App"; +import { MyContext, isMobile } from "../../App"; import { getGroupMembers, getNames } from "./Group"; import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar"; import { getFee } from "../../background"; @@ -77,7 +77,7 @@ export const ManageMembers = ({ }) await new Promise((res, rej) => { - chrome.runtime.sendMessage( + chrome?.runtime?.sendMessage( { action: "leaveGroup", payload: { @@ -173,63 +173,72 @@ export const ManageMembers = ({ }} > - - - - - - - - + + + + + + + {selectedGroup?.groupId && !isOwner && ( @@ -244,6 +253,7 @@ export const ManageMembers = ({ sx={{ width: "100%", padding: "25px", + maxWidth: '750px' }} > @@ -272,7 +283,8 @@ export const ManageMembers = ({ @@ -284,7 +296,8 @@ export const ManageMembers = ({ @@ -295,7 +308,8 @@ export const ManageMembers = ({ diff --git a/src/components/Group/ThingsToDoInitial.tsx b/src/components/Group/ThingsToDoInitial.tsx index 18f75b5..28c3889 100644 --- a/src/components/Group/ThingsToDoInitial.tsx +++ b/src/components/Group/ThingsToDoInitial.tsx @@ -10,157 +10,226 @@ import CommentIcon from "@mui/icons-material/Comment"; import InfoIcon from "@mui/icons-material/Info"; import { Box, Typography } from "@mui/material"; import { Spacer } from "../../common/Spacer"; +import { isMobile } from "../../App"; export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance }) => { const [checked1, setChecked1] = React.useState(false); const [checked2, setChecked2] = React.useState(false); const [checked3, setChecked3] = React.useState(false); -// const getAddressInfo = async (address) => { -// const response = await fetch(getBaseApiReact() + "/addresses/" + address); -// const data = await response.json(); -// if (data.error && data.error === 124) { -// setChecked1(false); -// } else if (data.address) { -// setChecked1(true); -// } -// }; + // const getAddressInfo = async (address) => { + // const response = await fetch(getBaseApiReact() + "/addresses/" + address); + // const data = await response.json(); + // if (data.error && data.error === 124) { + // setChecked1(false); + // } else if (data.address) { + // setChecked1(true); + // } + // }; -// const checkInfo = async () => { -// try { -// getAddressInfo(myAddress); -// } catch (error) {} -// }; - - + // const checkInfo = async () => { + // try { + // getAddressInfo(myAddress); + // } catch (error) {} + // }; React.useEffect(() => { if (balance && +balance >= 6) { - setChecked1(true) + setChecked1(true); } }, [balance]); - React.useEffect(()=> { - if(hasGroups) setChecked3(true) - }, [hasGroups]) + React.useEffect(() => { + if (hasGroups) setChecked3(true); + }, [hasGroups]); - React.useEffect(()=> { - if(name) setChecked2(true) - }, [name]) + React.useEffect(() => { + if (name) setChecked2(true); + }, [name]); return ( - - Suggestion: Complete the following - - - - // - // - // } - disablePadding + + - - - - - - - - - // - // - // } - disablePadding + + Getting Started: + + + + + - - - + + // + // + // } + disablePadding + sx={{ + marginBottom: '20px' + }} + > + - - - - - - // - // - // } - disablePadding - > - - - - - - - - + role={undefined} + dense + > + + + + {/* */} + + + + + // + // + // } + disablePadding + > + + + + + + + + + + // + // + // } + disablePadding + > + + + + + + + + + + ); }; diff --git a/src/components/Group/UserListOfInvites.tsx b/src/components/Group/UserListOfInvites.tsx index d126024..20aa6fb 100644 --- a/src/components/Group/UserListOfInvites.tsx +++ b/src/components/Group/UserListOfInvites.tsx @@ -85,7 +85,7 @@ export const UserListOfInvites = ({myAddress, setInfoSnack, setOpenSnack}) => { setIsLoading(true); await new Promise((res, rej)=> { - chrome.runtime.sendMessage({ action: "joinGroup", payload: { + chrome?.runtime?.sendMessage({ action: "joinGroup", payload: { groupId, }}, (response) => { @@ -186,7 +186,7 @@ export const UserListOfInvites = ({myAddress, setInfoSnack, setOpenSnack}) => { return (

Invite list

-
+
{({ height, width }) => ( { ).sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); - chrome.runtime.sendMessage({ + chrome?.runtime?.sendMessage({ action: 'handleActiveGroupDataFromSocket', payload: { groups: sortedGroups, diff --git a/src/components/Loader.tsx b/src/components/Loader.tsx index fe0a05e..8db908e 100644 --- a/src/components/Loader.tsx +++ b/src/components/Loader.tsx @@ -14,7 +14,7 @@ export const Loader = () => { left:'0px', right: '0px', bottom: '0px', - zIndex: 2, + zIndex: 10, background: 'rgba(0, 0, 0, 0.4)' }}> diff --git a/src/components/Mobile/MobileFooter.tsx b/src/components/Mobile/MobileFooter.tsx new file mode 100644 index 0000000..ca31331 --- /dev/null +++ b/src/components/Mobile/MobileFooter.tsx @@ -0,0 +1,186 @@ +import * as React from "react"; +import { + BottomNavigation, + BottomNavigationAction, + Typography, +} from "@mui/material"; +import { Home, Groups, Message, ShowChart } from "@mui/icons-material"; +import Box from "@mui/material/Box"; +import BottomLogo from "../../assets/svgs/BottomLogo5.svg"; +import { CustomSvg } from "../../common/CustomSvg"; +import { WalletIcon } from "../../assets/Icons/WalletIcon"; +import { HubsIcon } from "../../assets/Icons/HubsIcon"; +import { TradingIcon } from "../../assets/Icons/TradingIcon"; +import { MessagingIcon } from "../../assets/Icons/MessagingIcon"; + +const IconWrapper = ({ children, label, color }) => { + return ( + + {children} + + {label} + + + ); +}; + +export const MobileFooter = ({ + selectedGroup, + groupSection, + isUnread, + goToAnnouncements, + isUnreadChat, + goToChat, + goToThreads, + setOpenManageMembers, + groupChatHasUnread, + groupsAnnHasUnread, + directChatHasUnread, + chatMode, + openDrawerGroups, + goToHome, + setIsOpenDrawerProfile, + mobileViewMode, + setMobileViewMode, + setMobileViewModeKeepOpen, + hasUnreadGroups, + hasUnreadDirects +}) => { + const [value, setValue] = React.useState(0); + return ( + + setValue(newValue)} + sx={{ backgroundColor: "transparent", flexGrow: 1 }} + > + { + // setMobileViewMode('wallet') + setIsOpenDrawerProfile(true); + }} + icon={ + + + + } + sx={{ color: value === 0 ? "white" : "gray", padding: "0px 10px" }} + /> + { + setMobileViewMode("groups"); + }} + icon={ + + + + } + sx={{ + color: value === 0 ? "white" : "gray", + paddingLeft: "10px", + paddingRight: "42px", + }} + /> + + + {/* Floating Center Button */} + + + {/* Custom Center Icon */} + center-icon + + + + setValue(newValue)} + sx={{ backgroundColor: "transparent", flexGrow: 1 }} + > + { + setMobileViewModeKeepOpen("messaging"); + }} + icon={ + + + + } + sx={{ + color: value === 2 ? "white" : "gray", + paddingLeft: "55px", + paddingRight: "10px", + }} + /> + { + chrome.tabs.create({ url: "https://www.qort.trade"}); + }} + icon={ + + + + } + sx={{ color: value === 3 ? "white" : "gray", padding: "0px 10px" }} + /> + + + ); +}; diff --git a/src/components/Mobile/MobileHeader.tsx b/src/components/Mobile/MobileHeader.tsx new file mode 100644 index 0000000..f49291e --- /dev/null +++ b/src/components/Mobile/MobileHeader.tsx @@ -0,0 +1,444 @@ +import React, { useState } from "react"; +import { + AppBar, + Toolbar, + IconButton, + Typography, + Box, + MenuItem, + Select, + ButtonBase, + Menu, + ListItemIcon, + ListItemText, +} from "@mui/material"; +import { HomeIcon } from "../../assets/Icons/HomeIcon"; +import { LogoutIcon } from "../../assets/Icons/LogoutIcon"; +import { NotificationIcon } from "../../assets/Icons/NotificationIcon"; +import { ArrowDownIcon } from "../../assets/Icons/ArrowDownIcon"; +import { MessagingIcon } from "../../assets/Icons/MessagingIcon"; +import { MessagingIcon2 } from "../../assets/Icons/MessagingIcon2"; +import { HubsIcon } from "../../assets/Icons/HubsIcon"; + +const Header = ({ + logoutFunc, + goToHome, + setIsOpenDrawerProfile, + isThin, + setMobileViewModeKeepOpen, + hasUnreadGroups, + hasUnreadDirects, + setMobileViewMode, + myName + // selectedGroup, + // onHomeClick, + // onLogoutClick, + // onGroupChange, + // onWalletClick, + // onNotificationClick, +}) => { + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + if (isThin) { + return ( + + + {/* Left Home Icon */} + + { + setMobileViewModeKeepOpen(""); + goToHome(); + }} + // onClick={onHomeClick} + > + + + + + + + + {/* Center Title */} + + QORTAL + + + {/* Right Logout Icon */} + + { + setMobileViewModeKeepOpen("messaging"); + }} + edge="end" + color="inherit" + aria-label="logout" + + // onClick={onLogoutClick} + > + + + + + + + + + { + setMobileViewMode("groups"); + setMobileViewModeKeepOpen("") + handleClose(); + }} + > + + + + + + { + setMobileViewModeKeepOpen("messaging"); + + handleClose(); + }} + > + + + + + + + + ); + } + return ( + <> + {/* Main Header */} + + + {/* Left Home Icon */} + + + + + {/* Center Title */} + + QORTAL + + + {/* Right Logout Icon */} + + + + + + + {/* Secondary Section */} + + + + {myName} + + {/* + */} + + + + + + + + + {/* Right Dropdown */} + {/* { + setIsOpenDrawerProfile(true); + }} + > + + + View Wallet + + + + + */} + + + { + setMobileViewMode("groups"); + setMobileViewModeKeepOpen("") + handleClose(); + }} + > + + + + + + { + setMobileViewModeKeepOpen("messaging"); + + handleClose(); + }} + > + + + + + + + + ); +}; + +export default Header; diff --git a/src/components/WrapperUserAction.tsx b/src/components/WrapperUserAction.tsx new file mode 100644 index 0000000..2d1f04a --- /dev/null +++ b/src/components/WrapperUserAction.tsx @@ -0,0 +1,108 @@ +import React, { useState } from 'react'; +import { Popover, Button, Box } from '@mui/material'; +import { executeEvent } from '../utils/events'; + +export const WrapperUserAction = ({ children, address, name, disabled }) => { + const [anchorEl, setAnchorEl] = useState(null); + + // Handle child element click to open Popover + const handleChildClick = (event) => { + event.stopPropagation(); // Prevent parent onClick from firing + setAnchorEl(event.currentTarget); + }; + + // Handle closing the Popover + const handleClose = () => { + setAnchorEl(null); + }; + + // Determine if the popover is open + const open = Boolean(anchorEl); + const id = open ? address || name : undefined; + + if(disabled){ + return children + } + + return ( + <> + + {/* Render the child without altering dimensions */} + {children} + + + {/* Popover */} + event.stopPropagation(), // Stop propagation inside popover + }, + }} + > + + {/* Option 1: Message */} + + + {/* Option 2: Send QORT */} + + + + + ); +}; diff --git a/src/index.css b/src/index.css index 853afa4..9d6fa90 100644 --- a/src/index.css +++ b/src/index.css @@ -25,10 +25,16 @@ padding: 0px; margin: 0px; box-sizing: border-box !important; + word-break: break-word; --color-instance : #1E1E20; --color-instance-popover-bg: #222222; --Mail-Background: rgba(49, 51, 56, 1); --new-message-text: black; + + --bg-primary : rgba(31, 32, 35, 1); + --bg-2: #27282c; + --bg-3: rgba(0, 0, 0, 0.1); + --unread: rgba(255, 0, 0, 1); } body { @@ -78,6 +84,23 @@ body { border: 4px solid transparent; } +/* Mobile-specific scrollbar styles */ +@media only screen and (max-width: 600px) { + ::-webkit-scrollbar { + width: 8px; /* Narrower scrollbar width on mobile */ + height: 6px; /* Narrower scrollbar height on mobile */ + } + + ::-webkit-scrollbar-thumb { + border-radius: 4px; /* Adjust the radius for a narrower thumb */ + border: 2px solid transparent; /* Narrower thumb border */ + } +} + .group-list::-webkit-scrollbar-thumb:hover { background-color: whitesmoke; +} + +html, body { + overscroll-behavior:none !important; } \ No newline at end of file diff --git a/src/transactions/TradeBotRespondMultipleRequest.ts b/src/transactions/TradeBotRespondMultipleRequest.ts new file mode 100644 index 0000000..a7eb4d8 --- /dev/null +++ b/src/transactions/TradeBotRespondMultipleRequest.ts @@ -0,0 +1,42 @@ +// @ts-nocheck + +/** + * CrossChain - TradeBot Respond Multiple Request (Buy Action) + * + * These are special types of transactions (JSON ENCODED) + */ + +export class TradeBotRespondMultipleRequest { + constructor() { + // ... + } + + createTransaction(txnReq) { + this.addresses(txnReq.addresses) + this.foreignKey(txnReq.foreignKey) + this.receivingAddress(txnReq.receivingAddress) + + return this.txnRequest() + } + + addresses(addresses) { + this._addresses = addresses + } + + foreignKey(foreignKey) { + this._foreignKey = foreignKey + } + + receivingAddress(receivingAddress) { + this._receivingAddress = receivingAddress + } + + txnRequest() { + return { + addresses: this._addresses, + foreignKey: this._foreignKey, + receivingAddress: this._receivingAddress + } + } +} + diff --git a/src/utils/mobile/mobileUtils.ts b/src/utils/mobile/mobileUtils.ts new file mode 100644 index 0000000..144c8c1 --- /dev/null +++ b/src/utils/mobile/mobileUtils.ts @@ -0,0 +1,5 @@ + +export const getRootHeight = ()=> { + + return document?.getElementById('root')?.style?.height || '100%' +} \ No newline at end of file