From b8d548163344e58c293e13e3c9f20678dcc2395a Mon Sep 17 00:00:00 2001 From: PhilReact Date: Mon, 14 Oct 2024 18:59:42 +0300 Subject: [PATCH 01/58] started infrustructure --- public/content-script.js | 768 +++++++++++++++++-------- src/App.tsx | 151 +++-- src/background.ts | 11 +- src/qdn/encryption/group-encryption.ts | 39 ++ src/qortalRequests.ts | 85 +++ src/qortalRequests/get.ts | 281 +++++++++ 6 files changed, 1054 insertions(+), 281 deletions(-) create mode 100644 src/qortalRequests.ts create mode 100644 src/qortalRequests/get.ts diff --git a/public/content-script.js b/public/content-script.js index e84cd7d..183b230 100644 --- a/public/content-script.js +++ b/public/content-script.js @@ -1,290 +1,584 @@ - - async function connection(hostname) { +async function connection(hostname) { const isConnected = await chrome.storage.local.get([hostname]); - let connected = false - if(isConnected && Object.keys(isConnected).length > 0 && isConnected[hostname]){ - connected = true + let connected = false; + if ( + isConnected && + Object.keys(isConnected).length > 0 && + isConnected[hostname] + ) { + connected = true; } - return connected + return connected; } // In your content script -document.addEventListener('qortalExtensionRequests', async (event) => { +document.addEventListener("qortalExtensionRequests", async (event) => { const { type, payload, requestId, timeout } = event.detail; // Capture the requestId - if (type === 'REQUEST_USER_INFO') { - const hostname = window.location.hostname - const res = await connection(hostname) + if (type === "REQUEST_USER_INFO") { + 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 + if (!res) { + document.dispatchEvent( + new CustomEvent("qortalExtensionResponses", { + detail: { + type: "USER_INFO", + data: { + error: "Not authorized", + }, + requestId, + }, + }) + ); + return; } chrome?.runtime?.sendMessage({ action: "userInfo" }, (response) => { if (response.error) { - document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { - detail: { type: "USER_INFO", data: { - error: response.error - }, requestId } - })); + document.dispatchEvent( + new CustomEvent("qortalExtensionResponses", { + detail: { + type: "USER_INFO", + data: { + error: response.error, + }, + requestId, + }, + }) + ); } else { // Include the requestId in the detail when dispatching the response - document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { - detail: { type: "USER_INFO", data: response, requestId } - })); + document.dispatchEvent( + new CustomEvent("qortalExtensionResponses", { + detail: { type: "USER_INFO", data: response, requestId }, + }) + ); } }); - } else if (type === 'REQUEST_IS_INSTALLED') { + } else if (type === "REQUEST_IS_INSTALLED") { chrome?.runtime?.sendMessage({ action: "version" }, (response) => { if (response.error) { console.error("Error:", response.error); } else { // Include the requestId in the detail when dispatching the response - document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { - detail: { type: "IS_INSTALLED", data: response, requestId } - })); + document.dispatchEvent( + new CustomEvent("qortalExtensionResponses", { + detail: { type: "IS_INSTALLED", data: response, requestId }, + }) + ); } }); - } else if (type === 'REQUEST_CONNECTION') { - const hostname = window.location.hostname - chrome?.runtime?.sendMessage({ action: "connection", payload: { - hostname - }, timeout }, (response) => { - if (response.error) { - console.error("Error:", response.error); - } else { - // Include the requestId in the detail when dispatching the response - document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { - detail: { type: "CONNECTION", data: response, requestId } - })); + } else if (type === "REQUEST_CONNECTION") { + const hostname = window.location.hostname; + chrome?.runtime?.sendMessage( + { + action: "connection", + payload: { + hostname, + }, + timeout, + }, + (response) => { + if (response.error) { + console.error("Error:", response.error); + } else { + // Include the requestId in the detail when dispatching the response + document.dispatchEvent( + new CustomEvent("qortalExtensionResponses", { + detail: { type: "CONNECTION", data: response, requestId }, + }) + ); + } } - }); - } else if (type === 'REQUEST_OAUTH') { - const hostname = window.location.hostname - const res = await connection(hostname) - if(!res){ - document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { - detail: { type: "OAUTH", data: { - error: "Not authorized" - }, requestId } - })); - return + ); + } else if (type === "REQUEST_OAUTH") { + const hostname = window.location.hostname; + const res = await connection(hostname); + if (!res) { + document.dispatchEvent( + new CustomEvent("qortalExtensionResponses", { + detail: { + type: "OAUTH", + data: { + error: "Not authorized", + }, + requestId, + }, + }) + ); + return; } - chrome?.runtime?.sendMessage({ action: "oauth", payload: { - nodeBaseUrl: payload.nodeBaseUrl, - senderAddress: payload.senderAddress, - senderPublicKey: payload.senderPublicKey, timestamp: payload.timestamp - }}, (response) => { - if (response.error) { - document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { - detail: { type: "OAUTH", data: { - error: response.error - }, requestId } - })); - } else { - // Include the requestId in the detail when dispatching the response - document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { - detail: { type: "OAUTH", data: response, requestId } - })); + chrome?.runtime?.sendMessage( + { + action: "oauth", + payload: { + nodeBaseUrl: payload.nodeBaseUrl, + senderAddress: payload.senderAddress, + senderPublicKey: payload.senderPublicKey, + timestamp: payload.timestamp, + }, + }, + (response) => { + if (response.error) { + document.dispatchEvent( + new CustomEvent("qortalExtensionResponses", { + detail: { + type: "OAUTH", + data: { + error: response.error, + }, + requestId, + }, + }) + ); + } else { + // Include the requestId in the detail when dispatching the response + document.dispatchEvent( + new CustomEvent("qortalExtensionResponses", { + detail: { type: "OAUTH", data: response, requestId }, + }) + ); + } } - }); - } else if (type === 'REQUEST_BUY_ORDER') { - const hostname = window.location.hostname - const res = await connection(hostname) - if(!res){ - document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { - detail: { type: "BUY_ORDER", data: { - error: "Not authorized" - }, requestId } - })); - return + ); + } else if (type === "REQUEST_BUY_ORDER") { + const hostname = window.location.hostname; + const res = await connection(hostname); + if (!res) { + document.dispatchEvent( + new CustomEvent("qortalExtensionResponses", { + detail: { + type: "BUY_ORDER", + data: { + error: "Not authorized", + }, + requestId, + }, + }) + ); + return; } - chrome?.runtime?.sendMessage({ action: "buyOrder", payload: { - qortalAtAddresses: payload.qortalAtAddresses, - hostname, - useLocal: payload?.useLocal - - }, timeout}, (response) => { - if (response.error) { - document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { - detail: { type: "BUY_ORDER", data: { - error: response.error - }, requestId } - })); - } else { - // Include the requestId in the detail when dispatching the response - document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { - detail: { type: "BUY_ORDER", data: response, requestId } - })); + chrome?.runtime?.sendMessage( + { + action: "buyOrder", + payload: { + qortalAtAddresses: payload.qortalAtAddresses, + hostname, + useLocal: payload?.useLocal, + }, + timeout, + }, + (response) => { + if (response.error) { + document.dispatchEvent( + new CustomEvent("qortalExtensionResponses", { + detail: { + type: "BUY_ORDER", + data: { + error: response.error, + }, + requestId, + }, + }) + ); + } else { + // Include the requestId in the detail when dispatching the response + document.dispatchEvent( + new CustomEvent("qortalExtensionResponses", { + detail: { type: "BUY_ORDER", data: response, requestId }, + }) + ); + } } - }); - } else if(type === 'REQUEST_LTC_BALANCE'){ - - - 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 + ); + } else if (type === "REQUEST_LTC_BALANCE") { + 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: "ltcBalance", payload: { - hostname - }, timeout }, (response) => { - - if (response.error) { - document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { - detail: { type: "LTC_BALANCE", data: { - error: response.error - }, requestId } - })); - } else { - // Include the requestId in the detail when dispatching the response - document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { - detail: { type: "LTC_BALANCE", data: response, requestId } - })); + chrome?.runtime?.sendMessage( + { + action: "ltcBalance", + payload: { + hostname, + }, + timeout, + }, + (response) => { + if (response.error) { + document.dispatchEvent( + new CustomEvent("qortalExtensionResponses", { + detail: { + type: "LTC_BALANCE", + data: { + error: response.error, + }, + requestId, + }, + }) + ); + } else { + // Include the requestId in the detail when dispatching the response + document.dispatchEvent( + new CustomEvent("qortalExtensionResponses", { + detail: { type: "LTC_BALANCE", data: response, requestId }, + }) + ); + } } - }); - } 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 + ); + } 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 } - })); + 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) - if(!res){ - document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { - detail: { type: "USER_INFO", data: { - error: "Not authorized" - }, requestId } - })); - return + ); + } else if (type === "REQUEST_AUTHENTICATION") { + 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: "authentication", payload: { - hostname - }, timeout }, (response) => { - if (response.error) { - document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { - detail: { type: "AUTHENTICATION", data: { - error: response.error - }, requestId } - })); - } else { - // Include the requestId in the detail when dispatching the response - document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { - detail: { type: "AUTHENTICATION", data: response, requestId } - })); + chrome?.runtime?.sendMessage( + { + action: "authentication", + payload: { + hostname, + }, + timeout, + }, + (response) => { + if (response.error) { + document.dispatchEvent( + new CustomEvent("qortalExtensionResponses", { + detail: { + type: "AUTHENTICATION", + data: { + error: response.error, + }, + requestId, + }, + }) + ); + } else { + // Include the requestId in the detail when dispatching the response + document.dispatchEvent( + new CustomEvent("qortalExtensionResponses", { + detail: { type: "AUTHENTICATION", data: response, requestId }, + }) + ); + } } - }); - } else if (type === 'REQUEST_SEND_QORT') { - 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 + ); + } else if (type === "REQUEST_SEND_QORT") { + 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: "sendQort", payload: { - hostname, - amount: payload.amount, - description: payload.description, - address: payload.address - }, timeout }, (response) => { - if (response.error) { - document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { - detail: { type: "SEND_QORT", data: { - error: response.error - }, requestId } - })); - } else { - // Include the requestId in the detail when dispatching the response - document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { - detail: { type: "SEND_QORT", data: response, requestId } - })); + chrome?.runtime?.sendMessage( + { + action: "sendQort", + payload: { + hostname, + amount: payload.amount, + description: payload.description, + address: payload.address, + }, + timeout, + }, + (response) => { + if (response.error) { + document.dispatchEvent( + new CustomEvent("qortalExtensionResponses", { + detail: { + type: "SEND_QORT", + data: { + error: response.error, + }, + requestId, + }, + }) + ); + } else { + // Include the requestId in the detail when dispatching the response + document.dispatchEvent( + new CustomEvent("qortalExtensionResponses", { + detail: { type: "SEND_QORT", data: response, requestId }, + }) + ); + } } - }); - } else if (type === 'REQUEST_CLOSE_POPUP') { - 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 + ); + } else if (type === "REQUEST_CLOSE_POPUP") { + 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: "closePopup" }, (response) => { if (response.error) { - - document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { - detail: { type: "CLOSE_POPUP", data: { - error: response.error - }, requestId } - })); + document.dispatchEvent( + new CustomEvent("qortalExtensionResponses", { + detail: { + type: "CLOSE_POPUP", + data: { + error: response.error, + }, + requestId, + }, + }) + ); } else { // Include the requestId in the detail when dispatching the response - document.dispatchEvent(new CustomEvent('qortalExtensionResponses', { - detail: { type: "CLOSE_POPUP", data: true, requestId } - })); + document.dispatchEvent( + new CustomEvent("qortalExtensionResponses", { + detail: { type: "CLOSE_POPUP", data: true, requestId }, + }) + ); } }); } // Handle other request types as needed... }); - -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({ - type: "LOGOUT", - from: 'qortal' - }, "*"); + // Notify the web page + window.postMessage( + { + type: "LOGOUT", + from: "qortal", + }, + "*" + ); } else if (message.type === "RESPONSE_FOR_TRADES") { // Notify the web page - window.postMessage({ + window.postMessage( + { type: "RESPONSE_FOR_TRADES", - from: 'qortal', - payload: message.message - }, "*"); -} + from: "qortal", + payload: message.message, + }, + "*" + ); + } }); +const lastActionTimestamps = {}; + +// Function to debounce actions based on message.action +function debounceAction(action, wait = 500) { + const currentTime = Date.now(); + + // Check if this action has been recently triggered + if (lastActionTimestamps[action] && currentTime - lastActionTimestamps[action] < wait) { + // Ignore this action if it occurred within the debounce time window + return false; + } + + // Update the last timestamp for this action + lastActionTimestamps[action] = currentTime; + return true; +} + +if (!window.hasAddedQortalListener) { + console.log("Listener added"); + window.hasAddedQortalListener = true; + //qortalRequests + const listener = (event) => { + + event.preventDefault(); // Prevent default behavior + event.stopImmediatePropagation(); // Stop other listeners from firing + // Verify that the message is from the web page and contains expected data + if (event.source !== window || !event.data || !event.data.action) return; + // // Check if this action should be processed (debounced) + // if (!debounceAction(event.data.action)) { + // console.log(`Debounced action: ${event.data.action}`); + // return; + // } + if(event?.data?.requestedHandler !== 'UI') return + if (event.data.action === "GET_USER_ACCOUNT") { + chrome?.runtime?.sendMessage( + { action: "GET_USER_ACCOUNT", type: "qortalRequest" }, + (response) => { + if (response.error) { + event.ports[0].postMessage({ + result: null, + error: response.error, + }); + } else { + event.ports[0].postMessage({ + result: response, + error: null, + }); + } + } + ); + } else if (event.data.action === "SEND_COIN") { + chrome?.runtime?.sendMessage( + { action: "SEND_COIN", type: "qortalRequest", payload: event.data }, + (response) => { + if (response.error) { + event.ports[0].postMessage({ + result: null, + error: response.error, + }); + } else { + event.ports[0].postMessage({ + result: response, + error: null, + }); + } + } + ); + } else if (event.data.action === "ENCRYPT_DATA") { + chrome?.runtime?.sendMessage( + { action: "ENCRYPT_DATA", type: "qortalRequest", payload: event.data }, + (response) => { + if (response.error) { + event.ports[0].postMessage({ + result: null, + error: response.error, + }); + } else { + event.ports[0].postMessage({ + result: response, + error: null, + }); + } + } + ); + } else if (event.data.action === "DECRYPT_DATA") { + chrome?.runtime?.sendMessage( + { action: "DECRYPT_DATA", type: "qortalRequest", payload: event.data }, + (response) => { + if (response.error) { + event.ports[0].postMessage({ + result: null, + error: response.error, + }); + } else { + event.ports[0].postMessage({ + result: response, + error: null, + }); + } + } + ); + } else if (event.data.action === "GET_LIST_ITEMS") { + console.log("message", event); + + chrome?.runtime?.sendMessage( + { + action: "GET_LIST_ITEMS", + type: "qortalRequest", + payload: event.data, + }, + (response) => { + console.log("response", response); + if (response.error) { + event.ports[0].postMessage({ + result: null, + error: response.error, + }); + } else { + event.ports[0].postMessage({ + result: response, + error: null, + }); + } + } + ); + } + }; + window.addEventListener("message", listener); +} diff --git a/src/App.tsx b/src/App.tsx index ac78985..a119d20 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -304,6 +304,8 @@ function App() { const holdRefExtState = useRef("not-authenticated"); const isFocusedRef = useRef(true); const { isShow, onCancel, onOk, show, message } = useModal(); + const { onCancel: onCancelQortalRequest, onOk: onOkQortalRequest, show: showQortalRequest, isShow: isShowQortalRequest, message: messageQortalRequest } = useModal(); + const [openRegisterName, setOpenRegisterName] = useState(false); const registerNamePopoverRef = useRef(null); const [isLoadingRegisterName, setIsLoadingRegisterName] = useState(false); @@ -561,55 +563,49 @@ function App() { setRequestAuthentication(null); }; + const qortalRequestPermisson = async (message, sender, sendResponse)=> { + if ( + message.action === "QORTAL_REQUEST_PERMISSION" && + !isMainWindow + ) { + try { + await showQortalRequest(message?.payload) + sendResponse(true) + } catch (error) { + sendResponse(false) + } finally { + window.close(); + } + } + } + useEffect(() => { // Listen for messages from the background script - chrome.runtime?.onMessage.addListener((message, sender, sendResponse) => { - // Check if the message is to update the state - if ( - message.action === "UPDATE_STATE_CONFIRM_SEND_QORT" && - !isMainWindow - ) { - // Update the component state with the received 'sendqort' state + const messageListener = (message, sender, sendResponse) => { + // Handle various actions + if (message.action === "UPDATE_STATE_CONFIRM_SEND_QORT" && !isMainWindow) { setSendqortState(message.payload); setExtstate("web-app-request-payment"); } else if (message.action === "closePopup" && !isMainWindow) { - // Update the component state with the received 'sendqort' state window.close(); - } else if ( - message.action === "UPDATE_STATE_REQUEST_CONNECTION" && - !isMainWindow - ) { - - // Update the component state with the received 'sendqort' state + } else if (message.action === "UPDATE_STATE_REQUEST_CONNECTION" && !isMainWindow) { setRequestConnection(message.payload); setExtstate("web-app-request-connection"); - } else if ( - message.action === "UPDATE_STATE_REQUEST_BUY_ORDER" && - !isMainWindow - ) { - // Update the component state with the received 'sendqort' state + } else if (message.action === "UPDATE_STATE_REQUEST_BUY_ORDER" && !isMainWindow) { setRequestBuyOrder(message.payload); setExtstate("web-app-request-buy-order"); - } else if ( - message.action === "UPDATE_STATE_REQUEST_AUTHENTICATION" && - !isMainWindow - ) { - // Update the component state with the received 'sendqort' state + } else if (message.action === "UPDATE_STATE_REQUEST_AUTHENTICATION" && !isMainWindow) { setRequestAuthentication(message.payload); setExtstate("web-app-request-authentication"); } else if (message.action === "SET_COUNTDOWN" && !isMainWindow) { setCountdown(message.payload); } else if (message.action === "INITIATE_MAIN") { - // Update the component state with the received 'sendqort' state setIsMain(true); isMainRef.current = true; } else if (message.action === "CHECK_FOCUS" && isMainWindow) { - - sendResponse(isFocusedRef.current); - } else if ( - message.action === "NOTIFICATION_OPEN_DIRECT" && - isMainWindow - ) { + sendResponse(isFocusedRef.current); // Synchronous response + return true; // Return true if you plan to send a response asynchronously + } else if (message.action === "NOTIFICATION_OPEN_DIRECT" && isMainWindow) { executeEvent("openDirectMessage", { from: message.payload.from, }); @@ -617,22 +613,34 @@ function App() { executeEvent("openGroupMessage", { from: message.payload.from, }); - } else if ( - message.action === "NOTIFICATION_OPEN_ANNOUNCEMENT_GROUP" && - isMainWindow - ) { + } else if (message.action === "NOTIFICATION_OPEN_ANNOUNCEMENT_GROUP" && isMainWindow) { executeEvent("openGroupAnnouncement", { from: message.payload.from, }); - } else if ( - message.action === "NOTIFICATION_OPEN_THREAD_NEW_POST" && - isMainWindow - ) { + } else if (message.action === "NOTIFICATION_OPEN_THREAD_NEW_POST" && isMainWindow) { executeEvent("openThreadNewPost", { data: message.payload.data, }); } - }); + + // Call the permission request handler for "QORTAL_REQUEST_PERMISSION" + qortalRequestPermisson(message, sender, sendResponse); + if (message.action === "QORTAL_REQUEST_PERMISSION" && !isMainWindow) { + console.log('isMainWindow', isMainWindow, window?.location?.href ) + return true; // Return true to indicate an async response is coming + } + if(message.action === "QORTAL_REQUEST_PERMISSION" && isMainWindow){ + return; + } + }; + + // Add message listener + chrome.runtime?.onMessage.addListener(messageListener); + + // Clean up the listener on component unmount + return () => { + chrome.runtime?.onMessage.removeListener(messageListener); + }; }, []); @@ -1800,6 +1808,68 @@ function App() { )} + +{isShowQortalRequest && !isMainWindow && ( + <> + + + + {messageQortalRequest?.text1} + + + + {messageQortalRequest?.text2} + + + + {messageQortalRequest?.text3} + + + + onOkQortalRequest("accepted")} + > + accept + + onCancelQortalRequest()} + > + decline + + + {sendPaymentError} + + )} {extState === "web-app-request-buy-order" && !isMainWindow && ( <> @@ -1890,6 +1960,7 @@ function App() { {sendPaymentError} )} + {extState === "web-app-request-payment" && !isMainWindow && ( <> diff --git a/src/background.ts b/src/background.ts index adccae7..e0058d0 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,5 +1,7 @@ // @ts-nocheck // import { encryptAndPublishSymmetricKeyGroupChat } from "./backgroundFunctions/encryption"; + +import './qortalRequests' import { constant, isArray } from "lodash"; import { decryptGroupEncryption, @@ -144,7 +146,7 @@ export const createEndpointSocket = async (endpoint) => { } }; -export const createEndpoint = async (endpoint, customApi) => { +export const createEndpoint = async (endpoint, customApi?: string) => { if (customApi) { return `${customApi}${endpoint}`; } @@ -949,7 +951,7 @@ async function getAddressInfo(address) { return data; } -async function getKeyPair() { +export async function getKeyPair() { const res = await chrome.storage.local.get(["keyPair"]); if (res?.keyPair) { return res.keyPair; @@ -958,7 +960,7 @@ async function getKeyPair() { } } -async function getSaveWallet() { +export async function getSaveWallet() { const res = await chrome.storage.local.get(["walletInfo"]); if (res?.walletInfo) { return res.walletInfo; @@ -2498,7 +2500,7 @@ async function listenForChatMessageForBuyOrder({ } } -function removeDuplicateWindow(popupUrl) { +export function removeDuplicateWindow(popupUrl) { chrome.windows.getAll( { populate: true, windowTypes: ["popup"] }, (windows) => { @@ -2800,6 +2802,7 @@ async function getChatHeadsDirect() { } chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { if (request) { + switch (request.action) { case "version": // Example: respond with the version diff --git a/src/qdn/encryption/group-encryption.ts b/src/qdn/encryption/group-encryption.ts index 1905d4d..24d53df 100644 --- a/src/qdn/encryption/group-encryption.ts +++ b/src/qdn/encryption/group-encryption.ts @@ -331,4 +331,43 @@ export function decryptGroupData(data64EncryptedData: string, privateKey: string } } throw new Error("Unable to decrypt data") +} + +export function uint8ArrayStartsWith(uint8Array, string) { + const stringEncoder = new TextEncoder() + const stringUint8Array = stringEncoder.encode(string) + if (uint8Array.length < stringUint8Array.length) { + return false + } + for (let i = 0; i < stringUint8Array.length; i++) { + if (uint8Array[i] !== stringUint8Array[i]) { + return false + } + } + return true +} + +export function decryptDeprecatedSingle(uint8Array, publicKey, privateKey) { + const combinedData = uint8Array + const str = "qortalEncryptedData" + const strEncoder = new TextEncoder() + const strUint8Array = strEncoder.encode(str) + const strData = combinedData.slice(0, strUint8Array.length) + const nonce = combinedData.slice(strUint8Array.length, strUint8Array.length + 24) + const _encryptedData = combinedData.slice(strUint8Array.length + 24) + + const _publicKey = window.parent.Base58.decode(publicKey) + if (!privateKey || !_publicKey) { + throw new Error("Unable to retrieve keys") + } + const convertedPrivateKey = ed2curve.convertSecretKey(privateKey) + const convertedPublicKey = ed2curve.convertPublicKey(_publicKey) + const sharedSecret = new Uint8Array(32) + nacl.lowlevel.crypto_scalarmult(sharedSecret, convertedPrivateKey, convertedPublicKey) + const _chatEncryptionSeed = new window.parent.Sha256().process(sharedSecret).finish().result + const _decryptedData = nacl.secretbox.open(_encryptedData, nonce, _chatEncryptionSeed) + if (!_decryptedData) { + throw new Error("Unable to decrypt") + } + return uint8ArrayToBase64(_decryptedData) } \ No newline at end of file diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts new file mode 100644 index 0000000..388ccf9 --- /dev/null +++ b/src/qortalRequests.ts @@ -0,0 +1,85 @@ +import { decryptData, encryptData, getListItems, getUserAccount, sendCoin } from "./qortalRequests/get"; + +chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { + if (request) { + switch (request.action) { + case "GET_USER_ACCOUNT": { + getUserAccount() + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: "Unable to get user account" }); + }); + + break; + } + case "ENCRYPT_DATA": { + const data = request.payload; + + encryptData(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } + case "DECRYPT_DATA": { + const data = request.payload; + + decryptData(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } + case "GET_LIST_ITEMS": { + const data = request.payload; + + getListItems(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } + case "SEND_COIN": { + const data = request.payload; + const requiredFields = ["coin", "destinationAddress", "amount"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + sendResponse({ error: errorMsg }); + break; + } + // Example: respond with the version + sendCoin() + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: "Unable to get user account" }); + }); + + break; + } + } + } + return true; +}); diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts new file mode 100644 index 0000000..2450a64 --- /dev/null +++ b/src/qortalRequests/get.ts @@ -0,0 +1,281 @@ +import { createEndpoint, getKeyPair, getSaveWallet, removeDuplicateWindow } from "../background"; +import Base58 from "../deps/Base58"; +import { + base64ToUint8Array, + decryptDeprecatedSingle, + decryptGroupData, + encryptDataGroup, + uint8ArrayStartsWith, + uint8ArrayToBase64, +} from "../qdn/encryption/group-encryption"; +import { fileToBase64 } from "../utils/fileReading"; + +async function getUserPermission(payload: any) { + + + function waitForWindowReady(windowId) { + return new Promise((resolve) => { + const checkInterval = setInterval(() => { + chrome.windows.get(windowId, (win) => { + if (chrome.runtime.lastError) { + clearInterval(checkInterval); // Stop polling if there's an error + resolve(false); + } else if (win.state === 'normal' || win.state === 'maximized') { + clearInterval(checkInterval); // Window is ready + resolve(true); + } + }); + }, 100); // Check every 100ms + }); + } + + await new Promise((res)=> { + const popupUrl = chrome.runtime.getURL( + "index.html?secondary=true" + ); + console.log('popupUrl', popupUrl) + chrome.windows.getAll( + { populate: true, windowTypes: ["popup"] }, + (windows) => { + console.log('windows', windows) + // Attempt to find an existing popup window that has a tab with the correct URL + const existingPopup = windows.find( + (w) => + 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", + }); + res(null) + } 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; + const windowHeight = 500; // Your window height + const windowWidth = 400; // Your window width + + // Calculate left position for the window to appear on the right of the screen + const leftPosition = screenWidth - windowWidth; + + // Calculate top position for the window, adjust as desired + const topPosition = + (primaryDisplay.bounds.height - windowHeight) / 2; + + chrome.windows.create( + { + url: popupUrl, + type: "popup", + width: windowWidth, + height: windowHeight, + left: leftPosition, + top: 0, + }, + async (newWindow) => { + await waitForWindowReady(newWindow.id); + removeDuplicateWindow(popupUrl); + res(null) + } + ); + + }); + } + + + + + } + ); + }) + + await new Promise((res)=> { + setTimeout(() => { + chrome.runtime.sendMessage({ + action: "SET_COUNTDOWN", + payload: 15, + }); + res(true) + }, 450); + }) + return new Promise((resolve) => { + // Set a timeout for 1 second + const timeout = setTimeout(() => { + resolve(false); // No response within 10 second, assume not focused + }, 15000); + + // Send message to the content script to check focus + console.log('send msg') + chrome.runtime.sendMessage({ action: "QORTAL_REQUEST_PERMISSION", payload }, (response) => { + console.log('permission response', response) + if(response === undefined) return + clearTimeout(timeout); // Clear the timeout if we get a response + + if (chrome.runtime.lastError) { + resolve(false); // Error occurred, assume not focused + } else { + resolve(response); // Resolve based on the response + } + }); + }); + } + +export const getUserAccount = async () => { + try { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const publicKey = wallet.publicKey; + return { + address, + publicKey, + }; + } catch (error) { + throw new Error("Unable to fetch user account"); + } +}; + +export const encryptData = async (data) => { + let data64 = data.data64; + let publicKeys = data.publicKeys || []; + if (data.file) { + data64 = await fileToBase64(data.file); + } + if (!data64) { + throw new Error("Please include data to encrypt"); + } + + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const privateKey = parsedData.privateKey; + const userPublicKey = parsedData.publicKey; + + const encryptDataResponse = encryptDataGroup({ + data64, + publicKeys: publicKeys, + privateKey, + userPublicKey, + }); + if (encryptDataResponse) { + return encryptDataResponse; + } else { + throw new Error("Unable to encrypt"); + } +}; +export const decryptData = async (data) => { + const { encryptedData, publicKey } = data; + + + if (!encryptedData) { + throw new Error(`Missing fields: encryptedData`); + } + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8Array = base64ToUint8Array(encryptedData); + const startsWithQortalEncryptedData = uint8ArrayStartsWith( + uint8Array, + "qortalEncryptedData" + ); + if (startsWithQortalEncryptedData) { + if (!publicKey) { + throw new Error(`Missing fields: publicKey`); + } + + const decryptedDataToBase64 = decryptDeprecatedSingle( + uint8Array, + publicKey, + uint8PrivateKey + ); + return decryptedDataToBase64; + } + const startsWithQortalGroupEncryptedData = uint8ArrayStartsWith( + uint8Array, + "qortalGroupEncryptedData" + ); + if (startsWithQortalGroupEncryptedData) { + const decryptedData = decryptGroupData( + encryptedData, + parsedData.privateKey + ); + const decryptedDataToBase64 = uint8ArrayToBase64(decryptedData); + return decryptedDataToBase64; + } + throw new Error("Unable to decrypt"); +}; + + + +export const getListItems = async (data) => { + const requiredFields = ['list_name'] + const missingFields: string[] = [] + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field) + } + }) + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', ') + const errorMsg = `Missing fields: ${missingFieldsString}` + throw new Error(errorMsg) + } + let skip = false + // if (window.parent.reduxStore.getState().app.qAPPAutoLists) { + // skip = true + // } + let resPermission + if (!skip) { + // res1 = await showModalAndWait( + // actions.GET_LIST_ITEMS, + // { + // list_name: data.list_name + // } + // ) + + resPermission = await getUserPermission({ + text1: 'Do you give this application permission to', + text2: 'Access the list', + text3: data.list_name + }) + + } + console.log('resPermission', resPermission) + if (resPermission || skip) { + try { + + const url = await createEndpoint(`/lists/${data.list_name}`); + console.log('url', url) + const response = await fetch(url); + console.log('response', response) + if (!response.ok) throw new Error("Failed to fetch"); + + const list = await response.json(); + return list + + } catch (error) { + throw new Error("Error in retrieving list") + } + } else { + const data = {} + throw new Error("User declined to share list") + } + }; + +export const sendCoin = async () => { + try { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const publicKey = wallet.publicKey; + return { + address, + publicKey, + }; + } catch (error) { + console.error(error); + } +}; From 771d6c16f1c06a7693308e1339169ec29a05ef00 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Mon, 14 Oct 2024 19:35:36 +0300 Subject: [PATCH 02/58] fix double screen --- src/qortalRequests/get.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index 2450a64..62a5da8 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -79,8 +79,9 @@ async function getUserPermission(payload: any) { top: 0, }, async (newWindow) => { + removeDuplicateWindow(popupUrl); await waitForWindowReady(newWindow.id); - removeDuplicateWindow(popupUrl); + res(null) } ); From 95219783359e4cef522b7069007a549bee932848 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Mon, 14 Oct 2024 20:20:15 +0300 Subject: [PATCH 03/58] add to list delete get --- public/content-script.js | 106 +--------- src/App.tsx | 10 +- src/qortalRequests.ts | 28 ++- src/qortalRequests/get.ts | 412 +++++++++++++++++++++++--------------- 4 files changed, 292 insertions(+), 264 deletions(-) diff --git a/public/content-script.js b/public/content-script.js index 183b230..82d97ca 100644 --- a/public/content-script.js +++ b/public/content-script.js @@ -453,22 +453,7 @@ chrome.runtime?.onMessage.addListener(function (message, sender, sendResponse) { } }); -const lastActionTimestamps = {}; - -// Function to debounce actions based on message.action -function debounceAction(action, wait = 500) { - const currentTime = Date.now(); - - // Check if this action has been recently triggered - if (lastActionTimestamps[action] && currentTime - lastActionTimestamps[action] < wait) { - // Ignore this action if it occurred within the debounce time window - return false; - } - - // Update the last timestamp for this action - lastActionTimestamps[action] = currentTime; - return true; -} +const UIQortalRequests = ['GET_USER_ACCOUNT', 'ENCRYPT_DATA', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM'] if (!window.hasAddedQortalListener) { console.log("Listener added"); @@ -480,91 +465,14 @@ if (!window.hasAddedQortalListener) { event.stopImmediatePropagation(); // Stop other listeners from firing // Verify that the message is from the web page and contains expected data if (event.source !== window || !event.data || !event.data.action) return; - // // Check if this action should be processed (debounced) - // if (!debounceAction(event.data.action)) { - // console.log(`Debounced action: ${event.data.action}`); - // return; - // } - if(event?.data?.requestedHandler !== 'UI') return - if (event.data.action === "GET_USER_ACCOUNT") { - chrome?.runtime?.sendMessage( - { action: "GET_USER_ACCOUNT", type: "qortalRequest" }, - (response) => { - if (response.error) { - event.ports[0].postMessage({ - result: null, - error: response.error, - }); - } else { - event.ports[0].postMessage({ - result: response, - error: null, - }); - } - } - ); - } else if (event.data.action === "SEND_COIN") { - chrome?.runtime?.sendMessage( - { action: "SEND_COIN", type: "qortalRequest", payload: event.data }, - (response) => { - if (response.error) { - event.ports[0].postMessage({ - result: null, - error: response.error, - }); - } else { - event.ports[0].postMessage({ - result: response, - error: null, - }); - } - } - ); - } else if (event.data.action === "ENCRYPT_DATA") { - chrome?.runtime?.sendMessage( - { action: "ENCRYPT_DATA", type: "qortalRequest", payload: event.data }, - (response) => { - if (response.error) { - event.ports[0].postMessage({ - result: null, - error: response.error, - }); - } else { - event.ports[0].postMessage({ - result: response, - error: null, - }); - } - } - ); - } else if (event.data.action === "DECRYPT_DATA") { - chrome?.runtime?.sendMessage( - { action: "DECRYPT_DATA", type: "qortalRequest", payload: event.data }, - (response) => { - if (response.error) { - event.ports[0].postMessage({ - result: null, - error: response.error, - }); - } else { - event.ports[0].postMessage({ - result: response, - error: null, - }); - } - } - ); - } else if (event.data.action === "GET_LIST_ITEMS") { - console.log("message", event); + if(event?.data?.requestedHandler !== 'UI') return + if (UIQortalRequests.includes(event.data.action)) { + chrome?.runtime?.sendMessage( - { - action: "GET_LIST_ITEMS", - type: "qortalRequest", - payload: event.data, - }, + { action: event.data.action, type: "qortalRequest", payload: event.data }, (response) => { - console.log("response", response); + console.log('response', response) if (response.error) { event.ports[0].postMessage({ result: null, @@ -578,7 +486,7 @@ if (!window.hasAddedQortalListener) { } } ); - } + } }; window.addEventListener("message", listener); } diff --git a/src/App.tsx b/src/App.tsx index a119d20..a8a1a51 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1816,7 +1816,8 @@ function App() { {messageQortalRequest?.text1} @@ -1825,8 +1826,10 @@ function App() { {messageQortalRequest?.text2} @@ -1838,6 +1841,7 @@ function App() { lineHeight: 1.2, fontSize: "16px", fontWeight: 700, + maxWidth: '90%' }} > {messageQortalRequest?.text3} diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index 388ccf9..08a2612 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -1,4 +1,4 @@ -import { decryptData, encryptData, getListItems, getUserAccount, sendCoin } from "./qortalRequests/get"; +import { addListItems, decryptData, deleteListItems, encryptData, getListItems, getUserAccount, sendCoin } from "./qortalRequests/get"; chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { if (request) { @@ -53,6 +53,32 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { break; } + case "ADD_LIST_ITEMS": { + const data = request.payload; + + addListItems(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } + case "DELETE_LIST_ITEM": { + const data = request.payload; + + deleteListItems(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } case "SEND_COIN": { const data = request.payload; const requiredFields = ["coin", "destinationAddress", "amount"]; diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index 62a5da8..62efe2d 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -1,4 +1,9 @@ -import { createEndpoint, getKeyPair, getSaveWallet, removeDuplicateWindow } from "../background"; +import { + createEndpoint, + getKeyPair, + getSaveWallet, + removeDuplicateWindow, +} from "../background"; import Base58 from "../deps/Base58"; import { base64ToUint8Array, @@ -11,121 +16,113 @@ import { import { fileToBase64 } from "../utils/fileReading"; async function getUserPermission(payload: any) { - - - function waitForWindowReady(windowId) { - return new Promise((resolve) => { - const checkInterval = setInterval(() => { - chrome.windows.get(windowId, (win) => { - if (chrome.runtime.lastError) { - clearInterval(checkInterval); // Stop polling if there's an error - resolve(false); - } else if (win.state === 'normal' || win.state === 'maximized') { - clearInterval(checkInterval); // Window is ready - resolve(true); - } - }); - }, 100); // Check every 100ms - }); - } - - await new Promise((res)=> { - const popupUrl = chrome.runtime.getURL( - "index.html?secondary=true" - ); - console.log('popupUrl', popupUrl) - chrome.windows.getAll( - { populate: true, windowTypes: ["popup"] }, - (windows) => { - console.log('windows', windows) - // Attempt to find an existing popup window that has a tab with the correct URL - const existingPopup = windows.find( - (w) => - 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", - }); - res(null) - } 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; - const windowHeight = 500; // Your window height - const windowWidth = 400; // Your window width - - // Calculate left position for the window to appear on the right of the screen - const leftPosition = screenWidth - windowWidth; - - // Calculate top position for the window, adjust as desired - const topPosition = - (primaryDisplay.bounds.height - windowHeight) / 2; - - chrome.windows.create( - { - url: popupUrl, - type: "popup", - width: windowWidth, - height: windowHeight, - left: leftPosition, - top: 0, - }, - async (newWindow) => { - removeDuplicateWindow(popupUrl); - await waitForWindowReady(newWindow.id); - - res(null) - } - ); - - }); - } - - - - - } - ); - }) - - await new Promise((res)=> { - setTimeout(() => { - chrome.runtime.sendMessage({ - action: "SET_COUNTDOWN", - payload: 15, - }); - res(true) - }, 450); - }) + function waitForWindowReady(windowId) { return new Promise((resolve) => { - // Set a timeout for 1 second - const timeout = setTimeout(() => { - resolve(false); // No response within 10 second, assume not focused - }, 15000); - - // Send message to the content script to check focus - console.log('send msg') - chrome.runtime.sendMessage({ action: "QORTAL_REQUEST_PERMISSION", payload }, (response) => { - console.log('permission response', response) - if(response === undefined) return + const checkInterval = setInterval(() => { + chrome.windows.get(windowId, (win) => { + if (chrome.runtime.lastError) { + clearInterval(checkInterval); // Stop polling if there's an error + resolve(false); + } else if (win.state === "normal" || win.state === "maximized") { + clearInterval(checkInterval); // Window is ready + resolve(true); + } + }); + }, 100); // Check every 100ms + }); + } + + await new Promise((res) => { + const popupUrl = chrome.runtime.getURL("index.html?secondary=true"); + console.log("popupUrl", popupUrl); + chrome.windows.getAll( + { populate: true, windowTypes: ["popup"] }, + (windows) => { + console.log("windows", windows); + // Attempt to find an existing popup window that has a tab with the correct URL + const existingPopup = windows.find( + (w) => + 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", + }); + res(null); + } 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; + const windowHeight = 500; // Your window height + const windowWidth = 400; // Your window width + + // Calculate left position for the window to appear on the right of the screen + const leftPosition = screenWidth - windowWidth; + + // Calculate top position for the window, adjust as desired + const topPosition = + (primaryDisplay.bounds.height - windowHeight) / 2; + + chrome.windows.create( + { + url: popupUrl, + type: "popup", + width: windowWidth, + height: windowHeight, + left: leftPosition, + top: 0, + }, + async (newWindow) => { + removeDuplicateWindow(popupUrl); + await waitForWindowReady(newWindow.id); + + res(null); + } + ); + }); + } + } + ); + }); + + await new Promise((res) => { + setTimeout(() => { + chrome.runtime.sendMessage({ + action: "SET_COUNTDOWN", + payload: 30, + }); + res(true); + }, 700); + }); + return new Promise((resolve) => { + // Set a timeout for 1 second + const timeout = setTimeout(() => { + resolve(false); // No response within 10 second, assume not focused + }, 30000); + + // Send message to the content script to check focus + console.log("send msg"); + chrome.runtime.sendMessage( + { action: "QORTAL_REQUEST_PERMISSION", payload }, + (response) => { + console.log("permission response", response); + if (response === undefined) return; clearTimeout(timeout); // Clear the timeout if we get a response - + if (chrome.runtime.lastError) { resolve(false); // Error occurred, assume not focused } else { resolve(response); // Resolve based on the response } - }); - }); - } + } + ); + }); +} export const getUserAccount = async () => { try { @@ -171,7 +168,6 @@ export const encryptData = async (data) => { export const decryptData = async (data) => { const { encryptedData, publicKey } = data; - if (!encryptedData) { throw new Error(`Missing fields: encryptedData`); } @@ -210,61 +206,155 @@ export const decryptData = async (data) => { throw new Error("Unable to decrypt"); }; - - export const getListItems = async (data) => { - const requiredFields = ['list_name'] - const missingFields: string[] = [] - requiredFields.forEach((field) => { - if (!data[field]) { - missingFields.push(field) - } - }) - if (missingFields.length > 0) { - const missingFieldsString = missingFields.join(', ') - const errorMsg = `Missing fields: ${missingFieldsString}` - throw new Error(errorMsg) - } - let skip = false - // if (window.parent.reduxStore.getState().app.qAPPAutoLists) { - // skip = true - // } - let resPermission - if (!skip) { - // res1 = await showModalAndWait( - // actions.GET_LIST_ITEMS, - // { - // list_name: data.list_name - // } - // ) + const requiredFields = ["list_name"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + let skip = false; + // if (window.parent.reduxStore.getState().app.qAPPAutoLists) { + // skip = true + // } + let resPermission; + if (!skip) { + // res1 = await showModalAndWait( + // actions.GET_LIST_ITEMS, + // { + // list_name: data.list_name + // } + // ) - resPermission = await getUserPermission({ - text1: 'Do you give this application permission to', - text2: 'Access the list', - text3: data.list_name - }) + resPermission = await getUserPermission({ + text1: "Do you give this application permission to", + text2: "Access the list", + text3: data.list_name, + }); + } + console.log("resPermission", resPermission); + if (resPermission || skip) { + const url = await createEndpoint(`/lists/${data.list_name}`); + console.log("url", url); + const response = await fetch(url); + console.log("response", response); + if (!response.ok) throw new Error("Failed to fetch"); - } - console.log('resPermission', resPermission) - if (resPermission || skip) { - try { - - const url = await createEndpoint(`/lists/${data.list_name}`); - console.log('url', url) - const response = await fetch(url); - console.log('response', response) - if (!response.ok) throw new Error("Failed to fetch"); - - const list = await response.json(); - return list + const list = await response.json(); + return list; + } else { + throw new Error("User declined to share list"); + } +}; - } catch (error) { - throw new Error("Error in retrieving list") - } - } else { - const data = {} - throw new Error("User declined to share list") - } +export const addListItems = async (data) => { + const requiredFields = ["list_name", "items"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + + const items = data.items; + const list_name = data.list_name; + + const resPermission = await getUserPermission({ + text1: "Do you give this application permission to", + text2: `Add the following to the list ${list_name}:`, + text3: items.join(', ') + }); + + if (resPermission) { + const url = await createEndpoint(`/lists/${list_name}`); + console.log("url", url); + const body = { + items: items, + }; + const bodyToString = JSON.stringify(body); + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: bodyToString, + }); + + console.log("response", response); + if (!response.ok) throw new Error("Failed to add to list"); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + return res + } else { + throw new Error("User declined add to list"); + } +}; + +export const deleteListItems = async (data) => { + const requiredFields = ['list_name', 'item'] + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + + const item = data.item; + const list_name = data.list_name; + + const resPermission = await getUserPermission({ + text1: "Do you give this application permission to", + text2: `Remove the following from the list ${list_name}:`, + text3: item + }); + + if (resPermission) { + const url = await createEndpoint(`/lists/${list_name}`); + console.log("url", url); + const body = { + items: [item], + }; + const bodyToString = JSON.stringify(body); + const response = await fetch(url, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: bodyToString, + }); + + console.log("response", response); + if (!response.ok) throw new Error("Failed to add to list"); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + return res + } else { + throw new Error("User declined add to list"); + } }; export const sendCoin = async () => { From 9660e04d8dca5c494e4c305ef01984ef85cc9c89 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Mon, 14 Oct 2024 23:27:11 +0300 Subject: [PATCH 04/58] added saved permissions local --- src/App.tsx | 54 ++++++++++++++++++++++++++++++++++--- src/qortalRequests.ts | 57 +++++++++++++++++++++++++++++++++++++++ src/qortalRequests/get.ts | 45 +++++++++++++++++++------------ 3 files changed, 136 insertions(+), 20 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index a8a1a51..ebc9166 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -322,6 +322,7 @@ function App() { const [isOpenSendQortSuccess, setIsOpenSendQortSuccess] = useState(false) const [rootHeight, setRootHeight] = useState('100%') const [isSettingsOpen, setIsSettingsOpen] = useState(false) + const qortalRequestCheckbox1Ref = useRef(null); useEffect(() => { if(!isMobile) return // Function to set the height of the app to the viewport height @@ -569,10 +570,21 @@ function App() { !isMainWindow ) { try { + console.log('payloadbefore', message.payload) + await showQortalRequest(message?.payload) - sendResponse(true) + console.log('payload', message.payload) + if(message?.payload?.checkbox1){ + console.log('qortalRequestCheckbox1Ref.current', qortalRequestCheckbox1Ref.current) + sendResponse({accepted: true, + checkbox1: qortalRequestCheckbox1Ref.current + }) + return + } + sendResponse({accepted: true}) } catch (error) { - sendResponse(false) + console.log('error', error) + sendResponse({accepted: false}) } finally { window.close(); } @@ -1811,7 +1823,7 @@ function App() { {isShowQortalRequest && !isMainWindow && ( <> - + {messageQortalRequest?.text3} + {messageQortalRequest?.checkbox1 && + ( + + { + qortalRequestCheckbox1Ref.current = e.target.checked + }} + edge="start" + tabIndex={-1} + disableRipple + defaultChecked={messageQortalRequest?.checkbox1?.value} + sx={{ + "&.Mui-checked": { + color: "white", // Customize the color when checked + }, + "& .MuiSvgIcon-root": { + color: "white", + }, + }} + /> + + {messageQortalRequest?.checkbox1?.label} + + )} + { + chrome.storage.local.get([key], function (result) { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + resolve(result[key]); + }); + }); + } + + // Promisify chrome.storage.local.set + function setLocalStorage(data) { + return new Promise((resolve, reject) => { + chrome.storage.local.set(data, function () { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + resolve(); + }); + }); + } + + + export async function setPermission(key, value) { + try { + // Get the existing qortalRequestPermissions object + const qortalRequestPermissions = (await getLocalStorage('qortalRequestPermissions')) || {}; + + // Update the permission + qortalRequestPermissions[key] = value; + + // Save the updated object back to storage + await setLocalStorage({ qortalRequestPermissions }); + + console.log('Permission set for', key); + } catch (error) { + console.error('Error setting permission:', error); + } + } + + export async function getPermission(key) { + try { + // Get the qortalRequestPermissions object from storage + const qortalRequestPermissions = (await getLocalStorage('qortalRequestPermissions')) || {}; + + // Return the value for the given key, or null if it doesn't exist + return qortalRequestPermissions[key] || null; + } catch (error) { + console.error('Error getting permission:', error); + return null; + } + } + chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { if (request) { switch (request.action) { diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index 62efe2d..70e18e1 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -13,6 +13,7 @@ import { uint8ArrayStartsWith, uint8ArrayToBase64, } from "../qdn/encryption/group-encryption"; +import { getPermission, setPermission } from "../qortalRequests"; import { fileToBase64 } from "../utils/fileReading"; async function getUserPermission(payload: any) { @@ -219,27 +220,34 @@ export const getListItems = async (data) => { const errorMsg = `Missing fields: ${missingFieldsString}`; throw new Error(errorMsg); } - let skip = false; - // if (window.parent.reduxStore.getState().app.qAPPAutoLists) { - // skip = true - // } - let resPermission; - if (!skip) { - // res1 = await showModalAndWait( - // actions.GET_LIST_ITEMS, - // { - // list_name: data.list_name - // } - // ) + const value = await getPermission('qAPPAutoLists') || false; + let skip = false; + if (value) { + skip = true + } + let resPermission; + let acceptedVar + let checkbox1Var + if (!skip) { resPermission = await getUserPermission({ text1: "Do you give this application permission to", text2: "Access the list", text3: data.list_name, + checkbox1: { + value: value, + label: 'Always allow lists to be retrieved automatically' + } }); + const {accepted, checkbox1} = resPermission + acceptedVar = accepted + checkbox1Var = checkbox1 + setPermission('qAPPAutoLists', checkbox1) + } - console.log("resPermission", resPermission); - if (resPermission || skip) { + + + if (acceptedVar || skip) { const url = await createEndpoint(`/lists/${data.list_name}`); console.log("url", url); const response = await fetch(url); @@ -247,6 +255,7 @@ export const getListItems = async (data) => { if (!response.ok) throw new Error("Failed to fetch"); const list = await response.json(); + console.log('list', list) return list; } else { throw new Error("User declined to share list"); @@ -275,8 +284,9 @@ export const addListItems = async (data) => { text2: `Add the following to the list ${list_name}:`, text3: items.join(', ') }); + const {accepted} = resPermission - if (resPermission) { + if (accepted) { const url = await createEndpoint(`/lists/${list_name}`); console.log("url", url); const body = { @@ -327,8 +337,9 @@ export const deleteListItems = async (data) => { text2: `Remove the following from the list ${list_name}:`, text3: item }); - - if (resPermission) { + const {accepted} = resPermission + + if (accepted) { const url = await createEndpoint(`/lists/${list_name}`); console.log("url", url); const body = { From 3694b4390a40832d8cb579540be407934e775c1e Mon Sep 17 00:00:00 2001 From: PhilReact Date: Tue, 15 Oct 2024 02:18:59 +0300 Subject: [PATCH 05/58] added publish qortalrequest --- public/content-script.js | 2 +- src/App.tsx | 60 ++++++- src/backgroundFunctions/encryption.ts | 2 +- src/qortalRequests.ts | 19 +- src/qortalRequests/get.ts | 240 +++++++++++++++++++------- 5 files changed, 248 insertions(+), 75 deletions(-) diff --git a/public/content-script.js b/public/content-script.js index 82d97ca..d60420b 100644 --- a/public/content-script.js +++ b/public/content-script.js @@ -453,7 +453,7 @@ chrome.runtime?.onMessage.addListener(function (message, sender, sendResponse) { } }); -const UIQortalRequests = ['GET_USER_ACCOUNT', 'ENCRYPT_DATA', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM'] +const UIQortalRequests = ['GET_USER_ACCOUNT', 'ENCRYPT_DATA', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'PUBLISH_QDN_RESOURCE'] if (!window.hasAddedQortalListener) { console.log("Listener added"); diff --git a/src/App.tsx b/src/App.tsx index ebc9166..5910f8c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1824,18 +1824,63 @@ function App() { {isShowQortalRequest && !isMainWindow && ( <> - + {messageQortalRequest?.text1} + + + {messageQortalRequest?.text2} + + + + {messageQortalRequest?.text3 && ( + + + {messageQortalRequest?.text3} + + + + )} + {messageQortalRequest?.text4 && ( + + - {messageQortalRequest?.text2} + {messageQortalRequest?.text4} - + + + )} + - {messageQortalRequest?.text3} + {messageQortalRequest?.highlightedText} {messageQortalRequest?.checkbox1 && ( diff --git a/src/backgroundFunctions/encryption.ts b/src/backgroundFunctions/encryption.ts index 54640f1..4456381 100644 --- a/src/backgroundFunctions/encryption.ts +++ b/src/backgroundFunctions/encryption.ts @@ -43,7 +43,7 @@ async function getSaveWallet() { throw new Error("No wallet saved"); } } -async function getNameInfo() { +export async function getNameInfo() { const wallet = await getSaveWallet(); const address = wallet.address0; const validApi = await getBaseApi() diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index 6fbc9b5..1990371 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -1,4 +1,4 @@ -import { addListItems, decryptData, deleteListItems, encryptData, getListItems, getUserAccount, sendCoin } from "./qortalRequests/get"; +import { addListItems, decryptData, deleteListItems, encryptData, getListItems, getUserAccount, publishQDNResource, sendCoin } from "./qortalRequests/get"; @@ -56,6 +56,10 @@ function getLocalStorage(key) { return null; } } + + + // TODO: GET_FRIENDS_LIST + // NOT SURE IF TO IMPLEMENT: LINK_TO_QDN_RESOURCE, QDN_RESOURCE_DISPLAYED, SET_TAB_NOTIFICATIONS chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { if (request) { @@ -136,6 +140,19 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { break; } + case "PUBLISH_QDN_RESOURCE": { + const data = request.payload; + + publishQDNResource(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } case "SEND_COIN": { const data = request.payload; const requiredFields = ["coin", "destinationAddress", "amount"]; diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index 70e18e1..5e141d1 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -1,9 +1,11 @@ import { createEndpoint, + getFee, getKeyPair, getSaveWallet, removeDuplicateWindow, } from "../background"; +import { getNameInfo } from "../backgroundFunctions/encryption"; import Base58 from "../deps/Base58"; import { base64ToUint8Array, @@ -13,6 +15,7 @@ import { uint8ArrayStartsWith, uint8ArrayToBase64, } from "../qdn/encryption/group-encryption"; +import { publishData } from "../qdn/publish/pubish"; import { getPermission, setPermission } from "../qortalRequests"; import { fileToBase64 } from "../utils/fileReading"; @@ -98,7 +101,7 @@ async function getUserPermission(payload: any) { payload: 30, }); res(true); - }, 700); + }, 1000); }); return new Promise((resolve) => { // Set a timeout for 1 second @@ -220,33 +223,31 @@ export const getListItems = async (data) => { const errorMsg = `Missing fields: ${missingFieldsString}`; throw new Error(errorMsg); } - const value = await getPermission('qAPPAutoLists') || false; + const value = (await getPermission("qAPPAutoLists")) || false; let skip = false; if (value) { - skip = true + skip = true; } let resPermission; - let acceptedVar - let checkbox1Var + let acceptedVar; + let checkbox1Var; if (!skip) { resPermission = await getUserPermission({ text1: "Do you give this application permission to", text2: "Access the list", - text3: data.list_name, + highlightedText: data.list_name, checkbox1: { value: value, - label: 'Always allow lists to be retrieved automatically' - } + label: "Always allow lists to be retrieved automatically", + }, }); - const {accepted, checkbox1} = resPermission - acceptedVar = accepted - checkbox1Var = checkbox1 - setPermission('qAPPAutoLists', checkbox1) - + const { accepted, checkbox1 } = resPermission; + acceptedVar = accepted; + checkbox1Var = checkbox1; + setPermission("qAPPAutoLists", checkbox1); } - if (acceptedVar || skip) { const url = await createEndpoint(`/lists/${data.list_name}`); console.log("url", url); @@ -255,7 +256,7 @@ export const getListItems = async (data) => { if (!response.ok) throw new Error("Failed to fetch"); const list = await response.json(); - console.log('list', list) + console.log("list", list); return list; } else { throw new Error("User declined to share list"); @@ -282,9 +283,9 @@ export const addListItems = async (data) => { const resPermission = await getUserPermission({ text1: "Do you give this application permission to", text2: `Add the following to the list ${list_name}:`, - text3: items.join(', ') + highlightedText: items.join(", "), }); - const {accepted} = resPermission + const { accepted } = resPermission; if (accepted) { const url = await createEndpoint(`/lists/${list_name}`); @@ -309,64 +310,171 @@ export const addListItems = async (data) => { } catch (e) { res = await response.text(); } - return res + return res; } else { throw new Error("User declined add to list"); } }; export const deleteListItems = async (data) => { - const requiredFields = ['list_name', 'item'] - const missingFields: string[] = []; - requiredFields.forEach((field) => { - if (!data[field]) { - missingFields.push(field); - } - }); - if (missingFields.length > 0) { - const missingFieldsString = missingFields.join(", "); - const errorMsg = `Missing fields: ${missingFieldsString}`; - throw new Error(errorMsg); + const requiredFields = ["list_name", "item"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); } - - const item = data.item; - const list_name = data.list_name; - - const resPermission = await getUserPermission({ - text1: "Do you give this application permission to", - text2: `Remove the following from the list ${list_name}:`, - text3: item - }); - const {accepted} = resPermission + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } - if (accepted) { - const url = await createEndpoint(`/lists/${list_name}`); - console.log("url", url); - const body = { - items: [item], - }; - const bodyToString = JSON.stringify(body); - const response = await fetch(url, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - body: bodyToString, - }); - - console.log("response", response); - if (!response.ok) throw new Error("Failed to add to list"); - let res; - try { - res = await response.clone().json(); - } catch (e) { - res = await response.text(); - } - return res - } else { - throw new Error("User declined add to list"); + const item = data.item; + const list_name = data.list_name; + + const resPermission = await getUserPermission({ + text1: "Do you give this application permission to", + text2: `Remove the following from the list ${list_name}:`, + highlightedText: item, + }); + const { accepted } = resPermission; + + if (accepted) { + const url = await createEndpoint(`/lists/${list_name}`); + console.log("url", url); + const body = { + items: [item], + }; + const bodyToString = JSON.stringify(body); + const response = await fetch(url, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: bodyToString, + }); + + console.log("response", response); + if (!response.ok) throw new Error("Failed to add to list"); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); } - }; + return res; + } else { + throw new Error("User declined add to list"); + } +}; + +export const publishQDNResource = async (data: any) => { + const requiredFields = ["service"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + if (!data.file && !data.data64) { + throw new Error("No data or file was submitted"); + } + // Use "default" if user hasn't specified an identifer + const service = data.service; + const registeredName = await getNameInfo(); + const name = registeredName; + let identifier = data.identifier; + let data64 = data.data64; + const filename = data.filename; + const title = data.title; + const description = data.description; + const category = data.category; + const tag1 = data.tag1; + const tag2 = data.tag2; + const tag3 = data.tag3; + const tag4 = data.tag4; + const tag5 = data.tag5; + let feeAmount = null; + if (data.identifier == null) { + identifier = "default"; + } + if ( + data.encrypt && + (!data.publicKeys || + (Array.isArray(data.publicKeys) && data.publicKeys.length === 0)) + ) { + throw new Error("Encrypting data requires public keys"); + } + if (!data.encrypt && data.service.endsWith("_PRIVATE")) { + throw new Error("Only encrypted data can go into private services"); + } + if (data.file) { + data64 = await fileToBase64(data.file); + } + if (data.encrypt) { + try { + const encryptDataResponse = encryptDataGroup({ + data64, + publicKeys: data.publicKeys, + }); + if (encryptDataResponse) { + data64 = encryptDataResponse; + } + } catch (error) { + throw new Error( + error.message || "Upload failed due to failed encryption" + ); + } + } + + const fee = await getFee('ARBITRARY') + + const resPermission = await getUserPermission({ + text1: "Do you give this application permission to publish to QDN?", + text2: `service: ${service}`, + text3: `identifier: ${identifier || null}`, + highlightedText: `isEncrypted: ${!!data.encrypt}`, + fee: fee.fee + }); + const { accepted } = resPermission; + if (accepted) { + if (data.file && !data.encrypt) { + data64 = await fileToBase64(data.file); + } + try { + const resPublish = await publishData({ + registeredName: encodeURIComponent(name), + file: data64, + service: service, + identifier: encodeURIComponent(identifier), + uploadType: "file", + isBase64: true, + filename: filename, + title, + description, + category, + tag1, + tag2, + tag3, + tag4, + tag5, + apiVersion: 2, + withFee: true, + }); + return resPublish; + } catch (error) { + throw new Error(error?.message || "Upload failed"); + } + } else { + throw new Error("User declined request"); + } +}; export const sendCoin = async () => { try { From 56fea6f41341a754447afacb5d2e8a56edfea64b Mon Sep 17 00:00:00 2001 From: PhilReact Date: Tue, 15 Oct 2024 17:48:03 +0300 Subject: [PATCH 06/58] added multi-publish --- public/content-script.js | 361 +++++++++- src/App.tsx | 1306 ++++++++++++++++++++----------------- src/qortalRequests.ts | 17 +- src/qortalRequests/get.ts | 268 +++++++- 4 files changed, 1308 insertions(+), 644 deletions(-) diff --git a/public/content-script.js b/public/content-script.js index d60420b..5b21b83 100644 --- a/public/content-script.js +++ b/public/content-script.js @@ -1,3 +1,61 @@ +class Semaphore { + constructor(count) { + this.count = count + this.waiting = [] + } + acquire() { + return new Promise(resolve => { + if (this.count > 0) { + this.count-- + resolve() + } else { + this.waiting.push(resolve) + } + }) + } + release() { + if (this.waiting.length > 0) { + const resolve = this.waiting.shift() + resolve() + } else { + this.count++ + } + } +} +let semaphore = new Semaphore(1) +let reader = new FileReader() + +const fileToBase64 = (file) => new Promise(async (resolve, reject) => { + if (!reader) { + reader = new FileReader() + } + await semaphore.acquire() + reader.readAsDataURL(file) + reader.onload = () => { + const dataUrl = reader.result + if (typeof dataUrl === "string") { + const base64String = dataUrl.split(',')[1] + reader.onload = null + reader.onerror = null + resolve(base64String) + } else { + reader.onload = null + reader.onerror = null + reject(new Error('Invalid data URL')) + } + semaphore.release() + } + reader.onerror = (error) => { + reader.onload = null + reader.onerror = null + reject(error) + semaphore.release() + } +}) + + + + async function connection(hostname) { const isConnected = await chrome.storage.local.get([hostname]); let connected = false; @@ -430,7 +488,70 @@ document.addEventListener("qortalExtensionRequests", async (event) => { // Handle other request types as needed... }); -chrome.runtime?.onMessage.addListener(function (message, sender, sendResponse) { +async function handleGetFileFromIndexedDB(fileId, sendResponse) { + try { + const db = await openIndexedDB(); + const transaction = db.transaction(["files"], "readonly"); + const objectStore = transaction.objectStore("files"); + + const getRequest = objectStore.get(fileId); + + getRequest.onsuccess = async function (event) { + if (getRequest.result) { + const file = getRequest.result.data; + + try { + const base64String = await fileToBase64(file); + + // Create a new transaction to delete the file + const deleteTransaction = db.transaction(["files"], "readwrite"); + const deleteObjectStore = deleteTransaction.objectStore("files"); + const deleteRequest = deleteObjectStore.delete(fileId); + + deleteRequest.onsuccess = function () { + console.log(`File with ID ${fileId} has been removed from IndexedDB`); + try { + sendResponse({ result: base64String }); + + } catch (error) { + console.log('error', error) + } + }; + + deleteRequest.onerror = function () { + console.error(`Error deleting file with ID ${fileId} from IndexedDB`); + sendResponse({ result: null, error: "Failed to delete file from IndexedDB" }); + }; + } catch (error) { + console.error("Error converting file to Base64:", error); + sendResponse({ result: null, error: "Failed to convert file to Base64" }); + } + } else { + console.error(`File with ID ${fileId} not found in IndexedDB`); + sendResponse({ result: null, error: "File not found in IndexedDB" }); + } + }; + + getRequest.onerror = function () { + console.error(`Error retrieving file with ID ${fileId} from IndexedDB`); + sendResponse({ result: null, error: "Error retrieving file from IndexedDB" }); + }; + } catch (error) { + console.error("Error opening IndexedDB:", error); + sendResponse({ result: null, error: "Error opening IndexedDB" }); + } +} + +const testAsync = async (sendResponse)=> { + await new Promise((res)=> { + setTimeout(() => { + res() + }, 2500); + }) + sendResponse({ result: null, error: "Testing" }); +} + +chrome.runtime?.onMessage.addListener( function (message, sender, sendResponse) { if (message.type === "LOGOUT") { // Notify the web page window.postMessage( @@ -451,42 +572,232 @@ chrome.runtime?.onMessage.addListener(function (message, sender, sendResponse) { "*" ); } + + else if (message.action === "getFileFromIndexedDB") { + handleGetFileFromIndexedDB(message.fileId, sendResponse); + return true; // Keep the message channel open for async response + } }); -const UIQortalRequests = ['GET_USER_ACCOUNT', 'ENCRYPT_DATA', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'PUBLISH_QDN_RESOURCE'] +function openIndexedDB() { + return new Promise((resolve, reject) => { + const request = indexedDB.open("fileStorageDB", 1); + + request.onupgradeneeded = function (event) { + const db = event.target.result; + if (!db.objectStoreNames.contains("files")) { + db.createObjectStore("files", { keyPath: "id" }); + } + }; + + request.onsuccess = function (event) { + resolve(event.target.result); + }; + + request.onerror = function () { + reject("Error opening IndexedDB"); + }; + }); +} + + +async function retrieveFileFromIndexedDB(fileId) { + const db = await openIndexedDB(); + const transaction = db.transaction(["files"], "readwrite"); + const objectStore = transaction.objectStore("files"); + + return new Promise((resolve, reject) => { + const getRequest = objectStore.get(fileId); + + getRequest.onsuccess = function (event) { + if (getRequest.result) { + // File found, resolve it and delete from IndexedDB + const file = getRequest.result.data; + objectStore.delete(fileId); + resolve(file); + } else { + reject("File not found in IndexedDB"); + } + }; + + getRequest.onerror = function () { + reject("Error retrieving file from IndexedDB"); + }; + }); +} + +async function deleteQortalFilesFromIndexedDB() { + try { + console.log("Opening IndexedDB for deleting files..."); + const db = await openIndexedDB(); + const transaction = db.transaction(["files"], "readwrite"); + const objectStore = transaction.objectStore("files"); + + // Create a request to get all keys + const getAllKeysRequest = objectStore.getAllKeys(); + + getAllKeysRequest.onsuccess = function (event) { + const keys = event.target.result; + + // Iterate through keys to find and delete those containing '_qortalfile' + for (let key of keys) { + if (key.includes("_qortalfile")) { + const deleteRequest = objectStore.delete(key); + + deleteRequest.onsuccess = function () { + console.log(`File with key '${key}' has been deleted from IndexedDB`); + }; + + deleteRequest.onerror = function () { + console.error(`Failed to delete file with key '${key}' from IndexedDB`); + }; + } + } + }; + + getAllKeysRequest.onerror = function () { + console.error("Failed to retrieve keys from IndexedDB"); + }; + + transaction.oncomplete = function () { + console.log("Transaction complete for deleting files from IndexedDB"); + }; + + transaction.onerror = function () { + console.error("Error occurred during transaction for deleting files"); + }; + } catch (error) { + console.error("Error opening IndexedDB:", error); + } +} + + +async function storeFilesInIndexedDB(obj) { + // First delete any existing files in IndexedDB with '_qortalfile' in their ID + await deleteQortalFilesFromIndexedDB(); + + // Open the IndexedDB + const db = await openIndexedDB(); + const transaction = db.transaction(["files"], "readwrite"); + const objectStore = transaction.objectStore("files"); + + // Handle the obj.file if it exists and is a File instance + if (obj.file instanceof File) { + const fileId = "objFile_qortalfile"; + + // Store the file in IndexedDB + const fileData = { + id: fileId, + data: obj.file, + }; + objectStore.put(fileData); + + // Replace the file object with the file ID in the original object + obj.fileId = fileId; + delete obj.file; + } + + // Iterate through resources to find files and save them to IndexedDB + for (let resource of obj.resources) { + if (resource.file instanceof File) { + const fileId = resource.identifier + "_qortalfile"; + + // Store the file in IndexedDB + const fileData = { + id: fileId, + data: resource.file, + }; + objectStore.put(fileData); + + // Replace the file object with the file ID in the original object + resource.fileId = fileId; + delete resource.file; + } + } + + // Set transaction completion handlers + transaction.oncomplete = function () { + console.log("Files saved successfully to IndexedDB"); + }; + + transaction.onerror = function () { + console.error("Error saving files to IndexedDB"); + }; + + return obj; // Updated object with references to stored files +} + + + +const UIQortalRequests = ['GET_USER_ACCOUNT', 'ENCRYPT_DATA', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM'] if (!window.hasAddedQortalListener) { console.log("Listener added"); window.hasAddedQortalListener = true; //qortalRequests - const listener = (event) => { - + const listener = async (event) => { event.preventDefault(); // Prevent default behavior event.stopImmediatePropagation(); // Stop other listeners from firing + // Verify that the message is from the web page and contains expected data if (event.source !== window || !event.data || !event.data.action) return; - - if(event?.data?.requestedHandler !== 'UI') return - if (UIQortalRequests.includes(event.data.action)) { - - chrome?.runtime?.sendMessage( - { action: event.data.action, type: "qortalRequest", payload: event.data }, - (response) => { - console.log('response', response) - if (response.error) { - event.ports[0].postMessage({ - result: null, - error: response.error, - }); - } else { - event.ports[0].postMessage({ - result: response, - error: null, - }); - } + + if (event?.data?.requestedHandler !== 'UI') return; + + const sendMessageToRuntime = (message, eventPort) => { + chrome?.runtime?.sendMessage(message, (response) => { + console.log('response', response); + if (response.error) { + eventPort.postMessage({ + result: null, + error: response.error, + }); + } else { + eventPort.postMessage({ + result: response, + error: null, + }); } + }); + }; + + // Check if action is included in the predefined list of UI requests + if (UIQortalRequests.includes(event.data.action)) { + console.log('event?.data', event?.data); + sendMessageToRuntime( + { action: event.data.action, type: 'qortalRequest', payload: event.data }, + event.ports[0] ); - } + } else if (event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' || event?.data?.action === 'PUBLISH_QDN_RESOURCE') { + let data; + try { + data = await storeFilesInIndexedDB(event.data); + } catch (error) { + console.error('Error storing files in IndexedDB:', error); + event.ports[0].postMessage({ + result: null, + error: 'Failed to store files in IndexedDB', + }); + return; + } + + if (data) { + sendMessageToRuntime( + { action: event.data.action, type: 'qortalRequest', payload: data }, + event.ports[0] + ); + } else { + event.ports[0].postMessage({ + result: null, + error: 'Failed to prepare data for publishing', + }); + } + } }; - window.addEventListener("message", listener); + + // Add the listener for messages coming from the window + window.addEventListener('message', listener); + } + + diff --git a/src/App.tsx b/src/App.tsx index 5910f8c..241e13e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -42,7 +42,7 @@ 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 CloseIcon from "@mui/icons-material/Close"; import { createAccount, @@ -70,13 +70,13 @@ 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"; import { Label } from "./components/Group/AddGroup"; import { CustomizedSnackbars } from "./components/Snackbar/Snackbar"; -import SettingsIcon from '@mui/icons-material/Settings'; +import SettingsIcon from "@mui/icons-material/Settings"; import { getFee, groupApi, @@ -84,8 +84,15 @@ import { groupApiSocket, groupApiSocketLocal, } from "./background"; -import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "./utils/events"; -import { requestQueueCommentCount, requestQueuePublishedAccouncements } from "./components/Chat/GroupAnnouncements"; +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"; import { AddressQRCode } from "./components/AddressQRCode"; @@ -134,11 +141,11 @@ const defaultValues: MyContextInterface = { message: "", }, }; -export let isMobile = false +export let isMobile = false; const isMobileDevice = () => { const userAgent = navigator.userAgent || navigator.vendor || window.opera; - + if (/android/i.test(userAgent)) { return true; // Android device } @@ -151,7 +158,7 @@ const isMobileDevice = () => { }; if (isMobileDevice()) { - isMobile = true + isMobile = true; console.log("Running on a mobile device"); } else { console.log("Running on a desktop"); @@ -161,14 +168,14 @@ export const allQueues = { requestQueueCommentCount: requestQueueCommentCount, requestQueuePublishedAccouncements: requestQueuePublishedAccouncements, requestQueueMemberNames: requestQueueMemberNames, - requestQueueGroupJoinRequests: requestQueueGroupJoinRequests -} + requestQueueGroupJoinRequests: requestQueueGroupJoinRequests, +}; 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) { @@ -186,38 +193,28 @@ export const clearAllQueues = () => { console.error(error); } }); -} +}; export const pauseAllQueues = () => { - controlAllQueues('pause'); - chrome?.runtime?.sendMessage( - { - action: "pauseAllQueues", - payload: { - - }, - } - ); -} + controlAllQueues("pause"); + chrome?.runtime?.sendMessage({ + action: "pauseAllQueues", + payload: {}, + }); +}; export const resumeAllQueues = () => { - controlAllQueues('resume'); - chrome?.runtime?.sendMessage( - { - action: "resumeAllQueues", - payload: { - - }, - } - ); -} - + controlAllQueues("resume"); + chrome?.runtime?.sendMessage({ + action: "resumeAllQueues", + payload: {}, + }); +}; export const MyContext = createContext(defaultValues); export let globalApiKey: string | null = null; export const getBaseApiReact = (customApi?: string) => { - if (customApi) { return customApi; } @@ -229,7 +226,6 @@ export const getBaseApiReact = (customApi?: string) => { } }; // export const getArbitraryEndpointReact = () => { - // if (globalApiKey) { // return `/arbitrary/resources/search`; @@ -238,8 +234,6 @@ export const getBaseApiReact = (customApi?: string) => { // } // }; export const getArbitraryEndpointReact = () => { - - if (globalApiKey) { return `/arbitrary/resources/search`; } else { @@ -247,7 +241,6 @@ export const getArbitraryEndpointReact = () => { } }; export const getBaseApiReactSocket = (customApi?: string) => { - if (customApi) { return customApi; } @@ -300,11 +293,17 @@ function App() { const [txList, setTxList] = useState([]); const [memberGroups, setMemberGroups] = useState([]); const [isFocused, setIsFocused] = useState(true); - + const holdRefExtState = useRef("not-authenticated"); const isFocusedRef = useRef(true); const { isShow, onCancel, onOk, show, message } = useModal(); - const { onCancel: onCancelQortalRequest, onOk: onOkQortalRequest, show: showQortalRequest, isShow: isShowQortalRequest, message: messageQortalRequest } = useModal(); + const { + onCancel: onCancelQortalRequest, + onOk: onOkQortalRequest, + show: showQortalRequest, + isShow: isShowQortalRequest, + message: messageQortalRequest, + } = useModal(); const [openRegisterName, setOpenRegisterName] = useState(false); const registerNamePopoverRef = useRef(null); @@ -318,43 +317,44 @@ function App() { 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%') - const [isSettingsOpen, setIsSettingsOpen] = useState(false) + const [isOpenSendQort, setIsOpenSendQort] = useState(false); + const [isOpenSendQortSuccess, setIsOpenSendQortSuccess] = useState(false); + const [rootHeight, setRootHeight] = useState("100%"); + const [isSettingsOpen, setIsSettingsOpen] = useState(false); const qortalRequestCheckbox1Ref = useRef(null); useEffect(() => { - if(!isMobile) return + 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; + 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") + 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); + 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); + window.removeEventListener("resize", resetHeight); + window.visualViewport?.removeEventListener("resize", resetHeight); }; }, []); useEffect(() => { chrome?.runtime?.sendMessage({ action: "getApiKey" }, (response) => { if (response) { - globalApiKey = response; setApiKey(response); - setUseLocalNode(true) - setConfirmUseOfLocal(true) - setOpenAdvancedSettings(true) + setUseLocalNode(true); + setConfirmUseOfLocal(true); + setOpenAdvancedSettings(true); } }); }, []); @@ -380,7 +380,7 @@ function App() { reader.readAsText(file); // Read the file as text } }; - + // const checkIfUserHasLocalNode = useCallback(async () => { // try { // const url = `http://127.0.0.1:12391/admin/status`; @@ -549,8 +549,8 @@ function App() { if (response?.error) { setSendPaymentError(response.error); } else { - setIsOpenSendQort(false) - setIsOpenSendQortSuccess(true) + setIsOpenSendQort(false); + setIsOpenSendQortSuccess(true); // setExtstate("transfer-success-regular"); // setSendPaymentSuccess("Payment successfully sent"); } @@ -564,49 +564,62 @@ function App() { setRequestAuthentication(null); }; - const qortalRequestPermisson = async (message, sender, sendResponse)=> { - if ( - message.action === "QORTAL_REQUEST_PERMISSION" && - !isMainWindow - ) { + const qortalRequestPermisson = async (message, sender, sendResponse) => { + if (message.action === "QORTAL_REQUEST_PERMISSION" && !isMainWindow) { try { - console.log('payloadbefore', message.payload) + console.log("payloadbefore", message.payload); - await showQortalRequest(message?.payload) - console.log('payload', message.payload) - if(message?.payload?.checkbox1){ - console.log('qortalRequestCheckbox1Ref.current', qortalRequestCheckbox1Ref.current) - sendResponse({accepted: true, - checkbox1: qortalRequestCheckbox1Ref.current - }) - return + await showQortalRequest(message?.payload); + console.log("payload", message.payload); + if (message?.payload?.checkbox1) { + console.log( + "qortalRequestCheckbox1Ref.current", + qortalRequestCheckbox1Ref.current + ); + sendResponse({ + accepted: true, + checkbox1: qortalRequestCheckbox1Ref.current, + }); + return; } - sendResponse({accepted: true}) + sendResponse({ accepted: true }); } catch (error) { - console.log('error', error) - sendResponse({accepted: false}) + console.log("error", error); + sendResponse({ accepted: false }); } finally { window.close(); } } - } + }; useEffect(() => { // Listen for messages from the background script const messageListener = (message, sender, sendResponse) => { // Handle various actions - if (message.action === "UPDATE_STATE_CONFIRM_SEND_QORT" && !isMainWindow) { + if ( + message.action === "UPDATE_STATE_CONFIRM_SEND_QORT" && + !isMainWindow + ) { setSendqortState(message.payload); setExtstate("web-app-request-payment"); } else if (message.action === "closePopup" && !isMainWindow) { window.close(); - } else if (message.action === "UPDATE_STATE_REQUEST_CONNECTION" && !isMainWindow) { + } else if ( + message.action === "UPDATE_STATE_REQUEST_CONNECTION" && + !isMainWindow + ) { setRequestConnection(message.payload); setExtstate("web-app-request-connection"); - } else if (message.action === "UPDATE_STATE_REQUEST_BUY_ORDER" && !isMainWindow) { + } else if ( + message.action === "UPDATE_STATE_REQUEST_BUY_ORDER" && + !isMainWindow + ) { setRequestBuyOrder(message.payload); setExtstate("web-app-request-buy-order"); - } else if (message.action === "UPDATE_STATE_REQUEST_AUTHENTICATION" && !isMainWindow) { + } else if ( + message.action === "UPDATE_STATE_REQUEST_AUTHENTICATION" && + !isMainWindow + ) { setRequestAuthentication(message.payload); setExtstate("web-app-request-authentication"); } else if (message.action === "SET_COUNTDOWN" && !isMainWindow) { @@ -615,9 +628,12 @@ function App() { setIsMain(true); isMainRef.current = true; } else if (message.action === "CHECK_FOCUS" && isMainWindow) { - sendResponse(isFocusedRef.current); // Synchronous response - return true; // Return true if you plan to send a response asynchronously - } else if (message.action === "NOTIFICATION_OPEN_DIRECT" && isMainWindow) { + sendResponse(isFocusedRef.current); // Synchronous response + return true; // Return true if you plan to send a response asynchronously + } else if ( + message.action === "NOTIFICATION_OPEN_DIRECT" && + isMainWindow + ) { executeEvent("openDirectMessage", { from: message.payload.from, }); @@ -625,37 +641,42 @@ function App() { executeEvent("openGroupMessage", { from: message.payload.from, }); - } else if (message.action === "NOTIFICATION_OPEN_ANNOUNCEMENT_GROUP" && isMainWindow) { + } else if ( + message.action === "NOTIFICATION_OPEN_ANNOUNCEMENT_GROUP" && + isMainWindow + ) { executeEvent("openGroupAnnouncement", { from: message.payload.from, }); - } else if (message.action === "NOTIFICATION_OPEN_THREAD_NEW_POST" && isMainWindow) { + } else if ( + message.action === "NOTIFICATION_OPEN_THREAD_NEW_POST" && + isMainWindow + ) { executeEvent("openThreadNewPost", { data: message.payload.data, }); } - + // Call the permission request handler for "QORTAL_REQUEST_PERMISSION" - qortalRequestPermisson(message, sender, sendResponse); + qortalRequestPermisson(message, sender, sendResponse); if (message.action === "QORTAL_REQUEST_PERMISSION" && !isMainWindow) { - console.log('isMainWindow', isMainWindow, window?.location?.href ) - return true; // Return true to indicate an async response is coming - } - if(message.action === "QORTAL_REQUEST_PERMISSION" && isMainWindow){ + console.log("isMainWindow", isMainWindow, window?.location?.href); + return true; // Return true to indicate an async response is coming + } + if (message.action === "QORTAL_REQUEST_PERMISSION" && isMainWindow) { return; } }; - + // Add message listener chrome.runtime?.onMessage.addListener(messageListener); - + // Clean up the listener on component unmount return () => { chrome.runtime?.onMessage.removeListener(messageListener); }; }, []); - //param = isDecline const confirmPayment = (isDecline: boolean) => { if (isDecline) { @@ -716,7 +737,7 @@ function App() { crosschainAtInfo: requestBuyOrder?.crosschainAtInfo, interactionId: requestBuyOrder?.interactionId, isDecline: true, - useLocal: requestBuyOrder?.useLocal + useLocal: requestBuyOrder?.useLocal, }, }, (response) => { @@ -734,7 +755,7 @@ function App() { crosschainAtInfo: requestBuyOrder?.crosschainAtInfo, interactionId: requestBuyOrder?.interactionId, isDecline: false, - useLocal: requestBuyOrder?.useLocal + useLocal: requestBuyOrder?.useLocal, }, }, (response) => { @@ -787,7 +808,6 @@ function App() { ) return; - setExtstate("authenticated"); } }); @@ -912,12 +932,15 @@ function App() { wallet, qortAddress: wallet.address0, }); - chrome?.runtime?.sendMessage({ action: "userInfo" }, (response2) => { - setIsLoading(false); - if (response2 && !response2.error) { - setUserInfo(response); + chrome?.runtime?.sendMessage( + { action: "userInfo" }, + (response2) => { + setIsLoading(false); + if (response2 && !response2.error) { + setUserInfo(response); + } } - }); + ); getBalanceFunc(); } else if (response?.error) { setIsLoading(false); @@ -952,8 +975,8 @@ function App() { setWalletToBeDownloaded(null); setWalletToBeDownloadedPassword(""); setExtstate("authenticated"); - setIsOpenSendQort(false) - setIsOpenSendQortSuccess(false) + setIsOpenSendQort(false); + setIsOpenSendQortSuccess(false); }; const resetAllStates = () => { @@ -984,9 +1007,9 @@ function App() { setUseLocalNode(false); setHasLocalNode(false); setOpenAdvancedSettings(false); - setConfirmUseOfLocal(false) - setTxList([]) - setMemberGroups([]) + setConfirmUseOfLocal(false); + setTxList([]); + setMemberGroups([]); }; function roundUpToDecimals(number, decimals = 8) { @@ -1068,7 +1091,7 @@ function App() { const handleBeforeUnload = (e) => { e.preventDefault(); e.returnValue = ""; // This is required for Chrome to display the confirmation dialog. - return ""; + return ""; }; // Add the event listener when the component mounts @@ -1085,17 +1108,13 @@ function App() { // Handler for when the window gains focus const handleFocus = () => { setIsFocused(true); - if(isMobile){ - chrome?.runtime?.sendMessage( - { - action: "clearAllNotifications", - payload: { - - }, - } - ); + if (isMobile) { + chrome?.runtime?.sendMessage({ + action: "clearAllNotifications", + payload: {}, + }); } - + console.log("Webview is focused"); }; @@ -1113,15 +1132,11 @@ function App() { const handleVisibilityChange = () => { if (document.visibilityState === "visible") { setIsFocused(true); - if(isMobile){ - chrome?.runtime?.sendMessage( - { - action: "clearAllNotifications", - payload: { - - }, - } - ); + if (isMobile) { + chrome?.runtime?.sendMessage({ + action: "clearAllNotifications", + payload: {}, + }); } console.log("Webview is visible"); } else { @@ -1140,12 +1155,11 @@ function App() { }; }, []); - const openPaymentInternal = (e) => { const directAddress = e.detail?.address; - const name = e.detail?.name - setIsOpenSendQort(true) - setPaymentTo(name || directAddress) + const name = e.detail?.name; + setIsOpenSendQort(true); + setPaymentTo(name || directAddress); }; useEffect(() => { @@ -1174,7 +1188,6 @@ function App() { }, }, (response) => { - if (!response?.error) { res(response); setIsLoadingRegisterName(false); @@ -1219,248 +1232,268 @@ function App() { } }; - const renderProfile = ()=> { + 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 && ( - + {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 + + + + )} + + + ) : ( + <> + + - {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" }); }} > - {userInfo?.name} + Get QORT at qort.trade - - - - {rawWallet?.address0?.slice(0, 6)}... - {rawWallet?.address0?.slice(-4)} - - - - {qortBalanceLoading && ( - - )} - {!qortBalanceLoading && balance >= 0 && ( - - + + + { + setExtstate("download-wallet"); + setIsOpenDrawerProfile(false); + }} + src={Download} + style={{ + cursor: "pointer", + }} + /> + {!isMobile && ( + <> + + { + logoutFunc(); + setIsOpenDrawerProfile(false); }} - > - {balance?.toFixed(2)} QORT - - - - )} - - - {userInfo && !userInfo?.name && ( - { - setOpenRegisterName(true); - }} - > - REGISTER NAME - + )} - { - setIsOpenSendQort(true) - // setExtstate("send-qort"); - setIsOpenDrawerProfile(false) + setIsSettingsOpen(true); }} > - 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", - }} - /> - - )} - - - { - setIsSettingsOpen(true) - }}> - - - - {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", - }} - /> - )} - - - - ) - } + + + + {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' && ( )} */} @@ -1533,41 +1566,40 @@ function App() { }} /> - - <> - - + + + { + setOpenAdvancedSettings(true); }} > - - { - setOpenAdvancedSettings(true); + Advanced settings + + + {openAdvancedSettings && ( + <> + - Advanced settings - - - {openAdvancedSettings && ( - <> - Use local node - - {useLocalNode && ( - <> - - - - {apiKey} - - - + + + {apiKey} + + + - - )} - - )} - - - + } + ); + }} + variant="contained" + sx={{ + color: "white", + }} + > + {!confirmUseOfLocal + ? "Confirm use of local node" + : "Switch back to gateway"} + + + )} + + )} + + )} {/* {extState !== "not-authenticated" && ( )} */} - {extState === "authenticated" && isMainWindow && ( + {extState === "authenticated" && isMainWindow && ( {!isMobile && renderProfile()} - - - - - - - + + + + )} {isOpenSendQort && isMainWindow && ( - + )} -{isShowQortalRequest && !isMainWindow && ( + {isShowQortalRequest && !isMainWindow && ( <> - - - - {messageQortalRequest?.text1} - - - - - - {messageQortalRequest?.text2} - - - - {messageQortalRequest?.text3 && ( - - - {messageQortalRequest?.text3} - - - - )} - {messageQortalRequest?.text4 && ( - - - {messageQortalRequest?.text4} - - - - )} - - - {messageQortalRequest?.highlightedText} - - {messageQortalRequest?.checkbox1 && - ( + + sx={{ + display: "flex", + justifyContent: "center", + width: "100%", + }} + > + + {messageQortalRequest?.text1} + + + {messageQortalRequest?.text2 && ( + <> + + + + {messageQortalRequest?.text2} + + + + )} + {messageQortalRequest?.text3 && ( + <> + + + {messageQortalRequest?.text3} + + + + + )} + + {messageQortalRequest?.text4 && ( + + + {messageQortalRequest?.text4} + + + )} + + {messageQortalRequest?.html && ( +
+ )} + + {messageQortalRequest?.fee && ( + <> + + {'Fee: '}{messageQortalRequest?.fee}{' QORT'} + + + + + )} + + {messageQortalRequest?.highlightedText} + + {messageQortalRequest?.checkbox1 && ( + { - qortalRequestCheckbox1Ref.current = e.target.checked + onChange={(e) => { + qortalRequestCheckbox1Ref.current = e.target.checked; }} edge="start" tabIndex={-1} @@ -1936,39 +2006,43 @@ function App() { }} /> - {messageQortalRequest?.checkbox1?.label} - - )} - - - - + {messageQortalRequest?.checkbox1?.label} + + + )} + + + onOkQortalRequest("accepted")} > - accept - - onCancelQortalRequest()} - > - decline - - - {sendPaymentError} - + onOkQortalRequest("accepted")} + > + accept + + onCancelQortalRequest()} + > + decline + + + {sendPaymentError} + )} {extState === "web-app-request-buy-order" && !isMainWindow && ( <> @@ -1982,7 +2056,12 @@ function App() { > The Application

{" "} {requestBuyOrder?.hostname}

- is requesting {requestBuyOrder?.crosschainAtInfo?.length} {`buy order${requestBuyOrder?.crosschainAtInfo.length === 1 ? '' : 's'}`} + + is requesting {requestBuyOrder?.crosschainAtInfo?.length}{" "} + {`buy order${ + requestBuyOrder?.crosschainAtInfo.length === 1 ? "" : "s" + }`} + - {requestBuyOrder?.crosschainAtInfo?.reduce((latest, cur)=> { - return latest + +cur?.qortAmount - }, 0)} QORT + {requestBuyOrder?.crosschainAtInfo?.reduce((latest, cur) => { + return latest + +cur?.qortAmount; + }, 0)}{" "} + QORT - {roundUpToDecimals(requestBuyOrder?.crosschainAtInfo?.reduce((latest, cur)=> { - return latest + +cur?.expectedForeignAmount - }, 0))} + {roundUpToDecimals( + requestBuyOrder?.crosschainAtInfo?.reduce((latest, cur) => { + return latest + +cur?.expectedForeignAmount; + }, 0) + )} {` ${requestBuyOrder?.crosschainAtInfo?.[0]?.foreignBlockchain}`} {/* @@ -2060,7 +2142,7 @@ function App() { {sendPaymentError} )} - + {extState === "web-app-request-payment" && !isMainWindow && ( <> @@ -2508,16 +2590,18 @@ function App() { )} {isOpenSendQortSuccess && ( - + @@ -2682,8 +2766,7 @@ function App() { {isSettingsOpen && ( - - + )} - {renderProfile()} + + {renderProfile()} + ); } diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index 1990371..4e316db 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -1,4 +1,4 @@ -import { addListItems, decryptData, deleteListItems, encryptData, getListItems, getUserAccount, publishQDNResource, sendCoin } from "./qortalRequests/get"; +import { addListItems, decryptData, deleteListItems, encryptData, getListItems, getUserAccount, publishMultipleQDNResources, publishQDNResource, sendCoin } from "./qortalRequests/get"; @@ -143,7 +143,20 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { case "PUBLISH_QDN_RESOURCE": { const data = request.payload; - publishQDNResource(data) + publishQDNResource(data, sender) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } + case "PUBLISH_MULTIPLE_QDN_RESOURCES": { + const data = request.payload; + + publishMultipleQDNResources(data, sender) .then((res) => { sendResponse(res); }) diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index 5e141d1..74b37c4 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -19,6 +19,30 @@ import { publishData } from "../qdn/publish/pubish"; import { getPermission, setPermission } from "../qortalRequests"; import { fileToBase64 } from "../utils/fileReading"; +function getFileFromContentScript(fileId, sender) { + console.log('sender', sender) + return new Promise((resolve, reject) => { + + + chrome.tabs.sendMessage( + sender.tab.id, + { action: "getFileFromIndexedDB", fileId: fileId }, + (response) => { + console.log('response2', response) + if (response && response.result) { + + resolve(response.result); + } else { + reject(response?.error || "Failed to retrieve file"); + } + } + ); + + + }); + } + + async function getUserPermission(payload: any) { function waitForWindowReady(windowId) { return new Promise((resolve) => { @@ -369,7 +393,7 @@ export const deleteListItems = async (data) => { } }; -export const publishQDNResource = async (data: any) => { +export const publishQDNResource = async (data: any, sender) => { const requiredFields = ["service"]; const missingFields: string[] = []; requiredFields.forEach((field) => { @@ -382,7 +406,7 @@ export const publishQDNResource = async (data: any) => { const errorMsg = `Missing fields: ${missingFieldsString}`; throw new Error(errorMsg); } - if (!data.file && !data.data64) { + if (!data.fileId && !data.data64) { throw new Error("No data or file was submitted"); } // Use "default" if user hasn't specified an identifer @@ -414,8 +438,8 @@ export const publishQDNResource = async (data: any) => { if (!data.encrypt && data.service.endsWith("_PRIVATE")) { throw new Error("Only encrypted data can go into private services"); } - if (data.file) { - data64 = await fileToBase64(data.file); + if (data.fileId) { + data64 = await getFileFromContentScript(data.fileId, sender); } if (data.encrypt) { try { @@ -433,19 +457,19 @@ export const publishQDNResource = async (data: any) => { } } - const fee = await getFee('ARBITRARY') + const fee = await getFee("ARBITRARY"); const resPermission = await getUserPermission({ text1: "Do you give this application permission to publish to QDN?", text2: `service: ${service}`, text3: `identifier: ${identifier || null}`, highlightedText: `isEncrypted: ${!!data.encrypt}`, - fee: fee.fee + fee: fee.fee, }); const { accepted } = resPermission; if (accepted) { - if (data.file && !data.encrypt) { - data64 = await fileToBase64(data.file); + if (data.fileId && !data.encrypt) { + data64 = await getFileFromContentScript(data.fileId, sender); } try { const resPublish = await publishData({ @@ -476,6 +500,234 @@ export const publishQDNResource = async (data: any) => { } }; +export const publishMultipleQDNResources = async (data: any, sender) => { + const requiredFields = ["resources"]; + const missingFields: string[] = []; + let feeAmount = null; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + const resources = data.resources; + if (!Array.isArray(resources)) { + throw new Error("Invalid data"); + } + if (resources.length === 0) { + throw new Error("No resources to publish"); + } + if ( + data.encrypt && + (!data.publicKeys || + (Array.isArray(data.publicKeys) && data.publicKeys.length === 0)) + ) { + throw new Error("Encrypting data requires public keys"); + } + const fee = await getFee("ARBITRARY"); + const registeredName = await getNameInfo(); + const name = registeredName; + const resPermission = await getUserPermission({ + text1: "Do you give this application permission to publish to QDN?", + html: ` +
+ + + ${data.resources + .map( + (resource) => ` +
+
Service: ${resource.service}
+
Name: ${resource.name}
+
Identifier: ${resource.identifier}
+ ${ + resource.filename + ? `
Filename: ${resource.filename}
` + : "" + } +
` + ) + .join("")} +
+ + `, + highlightedText: `isEncrypted: ${!!data.encrypt}`, + fee: fee.fee * resources.length, + }); + const { accepted } = resPermission; + console.log('accepted', accepted) + if (!accepted) { + throw new Error("User declined request"); + } + let failedPublishesIdentifiers = []; + console.log('resources', resources) + for (const resource of resources) { + try { + const requiredFields = ["service"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!resource[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + failedPublishesIdentifiers.push({ + reason: errorMsg, + identifier: resource.identifier, + }); + continue; + } + if (!resource.fileId && !resource.data64) { + const errorMsg = "No data or file was submitted"; + failedPublishesIdentifiers.push({ + reason: errorMsg, + identifier: resource.identifier, + }); + continue; + } + const service = resource.service; + let identifier = resource.identifier; + let data64 = resource.data64; + const filename = resource.filename; + const title = resource.title; + const description = resource.description; + const category = resource.category; + const tag1 = resource.tag1; + const tag2 = resource.tag2; + const tag3 = resource.tag3; + const tag4 = resource.tag4; + const tag5 = resource.tag5; + if (resource.identifier == null) { + identifier = "default"; + } + if (!data.encrypt && service.endsWith("_PRIVATE")) { + const errorMsg = "Only encrypted data can go into private services"; + failedPublishesIdentifiers.push({ + reason: errorMsg, + identifier: resource.identifier, + }); + continue; + } + // if (data.file) { + // data64 = await getFileFromContentScript(resource.identifier + "_file"); + // } + if (data.encrypt) { + try { + const encryptDataResponse = encryptDataGroup({ + data64, + publicKeys: data.publicKeys, + }); + if (encryptDataResponse) { + data64 = encryptDataResponse; + } + } catch (error) { + const errorMsg = + error.message || "Upload failed due to failed encryption"; + failedPublishesIdentifiers.push({ + reason: errorMsg, + identifier: resource.identifier, + }); + continue; + } + } + if (resource.fileId && !data.encrypt) { + + data64 = await getFileFromContentScript(resource.fileId, sender); + + } + try { + + await publishData({ + registeredName: encodeURIComponent(name), + file: data64, + service: service, + identifier: encodeURIComponent(identifier), + uploadType: "file", + isBase64: true, + filename: filename, + title, + description, + category, + tag1, + tag2, + tag3, + tag4, + tag5, + apiVersion: 2, + withFee: true, + }); + await new Promise((res) => { + setTimeout(() => { + res(); + }, 1000); + }); + } catch (error) { + const errorMsg = error.message || "Upload failed"; + failedPublishesIdentifiers.push({ + reason: errorMsg, + identifier: resource.identifier, + }); + } + } catch (error) { + console.log('error', error) + failedPublishesIdentifiers.push({ + reason: "Unknown error", + identifier: resource.identifier, + }); + } + } + if (failedPublishesIdentifiers.length > 0) { + const obj = {}; + obj["error"] = { + unsuccessfulPublishes: failedPublishesIdentifiers, + }; + return obj; + } + return true; +}; + export const sendCoin = async () => { try { const wallet = await getSaveWallet(); From 68940c5c2564a2f6182792d3d93cd45164ceb529 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Tue, 15 Oct 2024 17:59:34 +0300 Subject: [PATCH 07/58] fix duplicate --- src/qortalRequests/get.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index 74b37c4..acfbd64 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -650,9 +650,9 @@ export const publishMultipleQDNResources = async (data: any, sender) => { }); continue; } - // if (data.file) { - // data64 = await getFileFromContentScript(resource.identifier + "_file"); - // } + if (resource.fileId) { + data64 = await getFileFromContentScript(resource.fileId, sender); + } if (data.encrypt) { try { const encryptDataResponse = encryptDataGroup({ @@ -672,11 +672,7 @@ export const publishMultipleQDNResources = async (data: any, sender) => { continue; } } - if (resource.fileId && !data.encrypt) { - - data64 = await getFileFromContentScript(resource.fileId, sender); - - } + try { await publishData({ From 470b5548227e343b1d4d5a2e4b7a3bb933c6ae42 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Tue, 15 Oct 2024 19:47:08 +0300 Subject: [PATCH 08/58] added qortalrequset for polls --- public/content-script.js | 2 +- src/App.tsx | 2 - src/background.ts | 4 +- src/qortalRequests.ts | 28 +++- src/qortalRequests/get.ts | 157 ++++++++++++++++++++++ src/transactions/CreatePollTransaction.ts | 73 ++++++++++ src/transactions/VoteOnPollTransaction.ts | 38 ++++++ src/transactions/transactions.ts | 4 + 8 files changed, 302 insertions(+), 6 deletions(-) create mode 100644 src/transactions/CreatePollTransaction.ts create mode 100644 src/transactions/VoteOnPollTransaction.ts diff --git a/public/content-script.js b/public/content-script.js index 5b21b83..cc56687 100644 --- a/public/content-script.js +++ b/public/content-script.js @@ -729,7 +729,7 @@ async function storeFilesInIndexedDB(obj) { -const UIQortalRequests = ['GET_USER_ACCOUNT', 'ENCRYPT_DATA', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM'] +const UIQortalRequests = ['GET_USER_ACCOUNT', 'ENCRYPT_DATA', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL'] if (!window.hasAddedQortalListener) { console.log("Listener added"); diff --git a/src/App.tsx b/src/App.tsx index 241e13e..0e818fc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1931,11 +1931,9 @@ function App() { > {messageQortalRequest?.text4} diff --git a/src/background.ts b/src/background.ts index e0058d0..d829ae3 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1059,7 +1059,7 @@ const processTransactionVersion2Chat = async (body: any, customApi) => { }); }; -const processTransactionVersion2 = async (body: any) => { +export const processTransactionVersion2 = async (body: any) => { const url = await createEndpoint(`/transactions/process?apiVersion=2`); try { @@ -1141,7 +1141,7 @@ const makeTransactionRequest = async ( return myTxnrequest; }; -const getLastRef = async () => { +export const getLastRef = async () => { const wallet = await getSaveWallet(); const address = wallet.address0; const validApi = await getBaseApi(); diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index 4e316db..4ac0dbc 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -1,4 +1,4 @@ -import { addListItems, decryptData, deleteListItems, encryptData, getListItems, getUserAccount, publishMultipleQDNResources, publishQDNResource, sendCoin } from "./qortalRequests/get"; +import { addListItems, createPoll, decryptData, deleteListItems, encryptData, getListItems, getUserAccount, publishMultipleQDNResources, publishQDNResource, sendCoin, voteOnPoll } from "./qortalRequests/get"; @@ -166,6 +166,32 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { break; } + case "VOTE_ON_POLL": { + const data = request.payload; + + voteOnPoll(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } + case "CREATE_POLL": { + const data = request.payload; + + createPoll(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } case "SEND_COIN": { const data = request.payload; const requiredFields = ["coin", "destinationAddress", "amount"]; diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index acfbd64..ef00c53 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -2,7 +2,9 @@ import { createEndpoint, getFee, getKeyPair, + getLastRef, getSaveWallet, + processTransactionVersion2, removeDuplicateWindow, } from "../background"; import { getNameInfo } from "../backgroundFunctions/encryption"; @@ -17,8 +19,98 @@ import { } from "../qdn/encryption/group-encryption"; import { publishData } from "../qdn/publish/pubish"; import { getPermission, setPermission } from "../qortalRequests"; +import { createTransaction } from "../transactions/transactions"; import { fileToBase64 } from "../utils/fileReading"; + +const _createPoll = async (pollName, pollDescription, options) => { + + const fee = await getFee("CREATE_POLL"); + + const resPermission = await getUserPermission({ + text1: "You are requesting to create the poll below:", + text2: `Poll: ${pollName}`, + text3: `Description: ${pollDescription}`, + text4: `Options: ${options?.join(', ')}`, + fee: fee.fee, + }); + const { accepted } = resPermission; + + if(accepted){ + const wallet = await getSaveWallet(); + const address = wallet.address0; + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + let lastRef = await getLastRef() + + const tx = await createTransaction(8, keyPair, { + fee: fee.fee, + ownerAddress: address, + rPollName: pollName, + rPollDesc: pollDescription, + rOptions: options, + lastReference: lastRef + }); + 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; + } else { + throw new Error("User declined request"); + } + +} + +const _voteOnPoll =async (pollName, optionIndex, optionName)=> { + + const fee = await getFee("VOTE_ON_POLL"); + + const resPermission = await getUserPermission({ + text1: "You are being requested to vote on the poll below:", + text2: `Poll: ${pollName}`, + text3: `Option: ${optionName}`, + fee: fee.fee, + }); + const { accepted } = resPermission; + + if(accepted){ + const wallet = await getSaveWallet(); + const address = wallet.address0; + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + let lastRef = await getLastRef() + + const tx = await createTransaction(9, keyPair, { + fee: fee.fee, + voterAddress: address, + rPollName: pollName, + rOptionIndex: optionIndex, + lastReference: lastRef + }); + 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; + } else { + throw new Error("User declined request"); + } + +} + function getFileFromContentScript(fileId, sender) { console.log('sender', sender) return new Promise((resolve, reject) => { @@ -724,6 +816,71 @@ export const publishMultipleQDNResources = async (data: any, sender) => { return true; }; +export const voteOnPoll = async (data) => { + const requiredFields = ['pollName', 'optionIndex'] + const missingFields: string[] = [] + requiredFields.forEach((field) => { + if (!data[field] && data[field] !== 0) { + missingFields.push(field) + } + }) + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', ') + const errorMsg = `Missing fields: ${missingFieldsString}` + throw new Error(errorMsg) + } + const pollName = data.pollName + const optionIndex = data.optionIndex + let pollInfo = null + try { + const url = await createEndpoint(`/polls/${encodeURIComponent(pollName)}`); + const response = await fetch(url); + if (!response.ok) throw new Error("Failed to fetch poll"); + + pollInfo = await response.json(); + } catch (error) { + const errorMsg = (error && error.message) || 'Poll not found' + throw new Error(errorMsg) + } + if (!pollInfo || pollInfo.error) { + const errorMsg = (pollInfo && pollInfo.message) || 'Poll not found' + throw new Error(errorMsg) + } + try { + const optionName = pollInfo.pollOptions[optionIndex].optionName + const resVoteOnPoll = await _voteOnPoll(pollName, optionIndex, optionName) + return resVoteOnPoll + } catch (error) { + + throw new Error(error?.message || 'Failed to vote on the poll.') + } + }; + + export const createPoll = async (data) => { + const requiredFields = ['pollName', 'pollDescription', 'pollOptions', 'pollOwnerAddress'] + const missingFields: string[] = [] + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field) + } + }) + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', ') + const errorMsg = `Missing fields: ${missingFieldsString}` + throw new Error(errorMsg) + } + const pollName = data.pollName + const pollDescription = data.pollDescription + const pollOptions = data.pollOptions + const pollOwnerAddress = data.pollOwnerAddress + try { + const resCreatePoll = await _createPoll(pollName, pollDescription, pollOptions, pollOwnerAddress) + return resCreatePoll + } catch (error) { + throw new Error(error?.message || 'Failed to created poll.') + } + }; + export const sendCoin = async () => { try { const wallet = await getSaveWallet(); diff --git a/src/transactions/CreatePollTransaction.ts b/src/transactions/CreatePollTransaction.ts new file mode 100644 index 0000000..a2a9cc0 --- /dev/null +++ b/src/transactions/CreatePollTransaction.ts @@ -0,0 +1,73 @@ +// @ts-nocheck +import { QORT_DECIMALS } from '../constants/constants' +import TransactionBase from './TransactionBase' + +export default class CreatePollTransaction extends TransactionBase { + constructor() { + super() + this.type = 8 + this._options = [] + } + + addOption(option) { + const optionBytes = this.constructor.utils.stringtoUTF8Array(option) + const optionLength = this.constructor.utils.int32ToBytes(optionBytes.length) + this._options.push({ length: optionLength, bytes: optionBytes }) + } + + + set fee(fee) { + this._fee = fee * QORT_DECIMALS + this._feeBytes = this.constructor.utils.int64ToBytes(this._fee) + } + + set ownerAddress(ownerAddress) { + this._ownerAddress = ownerAddress instanceof Uint8Array ? ownerAddress : this.constructor.Base58.decode(ownerAddress) + } + + set rPollName(rPollName) { + this._rPollName = rPollName + this._rPollNameBytes = this.constructor.utils.stringtoUTF8Array(this._rPollName) + this._rPollNameLength = this.constructor.utils.int32ToBytes(this._rPollNameBytes.length) + + } + + set rPollDesc(rPollDesc) { + this._rPollDesc = rPollDesc + this._rPollDescBytes = this.constructor.utils.stringtoUTF8Array(this._rPollDesc) + this._rPollDescLength = this.constructor.utils.int32ToBytes(this._rPollDescBytes.length) + } + + set rOptions(rOptions) { + const optionsArray = rOptions[0].split(', ').map(opt => opt.trim()) + this._pollOptions = optionsArray + + for (let i = 0; i < optionsArray.length; i++) { + this.addOption(optionsArray[i]) + } + + this._rNumberOfOptionsBytes = this.constructor.utils.int32ToBytes(optionsArray.length) + } + + + get params() { + const params = super.params + params.push( + this._ownerAddress, + this._rPollNameLength, + this._rPollNameBytes, + this._rPollDescLength, + this._rPollDescBytes, + this._rNumberOfOptionsBytes + ) + + // Push the dynamic options + for (let i = 0; i < this._options.length; i++) { + params.push(this._options[i].length, this._options[i].bytes) + } + + params.push(this._feeBytes) + + return params + } +} diff --git a/src/transactions/VoteOnPollTransaction.ts b/src/transactions/VoteOnPollTransaction.ts new file mode 100644 index 0000000..327295e --- /dev/null +++ b/src/transactions/VoteOnPollTransaction.ts @@ -0,0 +1,38 @@ +// @ts-nocheck +import { QORT_DECIMALS } from '../constants/constants' +import TransactionBase from './TransactionBase' + +export default class VoteOnPollTransaction extends TransactionBase { + constructor() { + super() + this.type = 9 + } + + + set fee(fee) { + this._fee = fee * QORT_DECIMALS + this._feeBytes = this.constructor.utils.int64ToBytes(this._fee) + } + + set rPollName(rPollName) { + this._rPollName = rPollName + this._rPollNameBytes = this.constructor.utils.stringtoUTF8Array(this._rPollName) + this._rPollNameLength = this.constructor.utils.int32ToBytes(this._rPollNameBytes.length) + } + + set rOptionIndex(rOptionIndex) { + this._rOptionIndex = rOptionIndex + this._rOptionIndexBytes = this.constructor.utils.int32ToBytes(this._rOptionIndex) + } + + get params() { + const params = super.params + params.push( + this._rPollNameLength, + this._rPollNameBytes, + this._rOptionIndexBytes, + this._feeBytes + ) + return params + } +} diff --git a/src/transactions/transactions.ts b/src/transactions/transactions.ts index 25ddbf5..02854c6 100644 --- a/src/transactions/transactions.ts +++ b/src/transactions/transactions.ts @@ -14,11 +14,15 @@ import JoinGroupTransaction from './JoinGroupTransaction.js' import AddGroupAdminTransaction from './AddGroupAdminTransaction.js' import RemoveGroupAdminTransaction from './RemoveGroupAdminTransaction.js' import RegisterNameTransaction from './RegisterNameTransaction.js' +import VoteOnPollTransaction from './VoteOnPollTransaction.js' +import CreatePollTransaction from './CreatePollTransaction.js' export const transactionTypes = { 3: RegisterNameTransaction, 2: PaymentTransaction, + 8: CreatePollTransaction, + 9: VoteOnPollTransaction, 18: ChatTransaction, 181: GroupChatTransaction, 22: CreateGroupTransaction, From 638c673845d49ae56b68eaa8a54011f5f946ce5c Mon Sep 17 00:00:00 2001 From: PhilReact Date: Tue, 15 Oct 2024 23:51:14 +0300 Subject: [PATCH 09/58] join group qortalrequest --- public/content-script.js | 2 +- src/App.tsx | 27 +- src/background.ts | 8 +- src/qortalRequests.ts | 28 +- src/qortalRequests/get.ts | 547 +++++++++++++++++++++++++++----------- 5 files changed, 438 insertions(+), 174 deletions(-) diff --git a/public/content-script.js b/public/content-script.js index cc56687..5010905 100644 --- a/public/content-script.js +++ b/public/content-script.js @@ -729,7 +729,7 @@ async function storeFilesInIndexedDB(obj) { -const UIQortalRequests = ['GET_USER_ACCOUNT', 'ENCRYPT_DATA', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL'] +const UIQortalRequests = ['GET_USER_ACCOUNT', 'ENCRYPT_DATA', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL', 'SEND_CHAT_MESSAGE', 'JOIN_GROUP'] if (!window.hasAddedQortalListener) { console.log("Listener added"); diff --git a/src/App.tsx b/src/App.tsx index 0e818fc..00cb3a0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1896,6 +1896,7 @@ function App() { {messageQortalRequest?.text2}
+ )} {messageQortalRequest?.text3 && ( @@ -1947,8 +1948,23 @@ function App() { /> )} + + + {messageQortalRequest?.highlightedText} + + {messageQortalRequest?.fee && ( <> + + )} - - {messageQortalRequest?.highlightedText} - {messageQortalRequest?.checkbox1 && ( { +export const computePow = async ({ chatBytes, path, difficulty }) => { let response = null; await new Promise((resolve, reject) => { const _chatBytesArray = Object.keys(chatBytes).map(function (key) { @@ -1973,7 +1973,7 @@ async function leaveGroup({ groupId }) { return res; } -async function joinGroup({ groupId }) { +export async function joinGroup({ groupId }) { const wallet = await getSaveWallet(); const address = wallet.address0; const lastReference = await getLastRef(); diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index 4ac0dbc..91a3737 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -1,4 +1,4 @@ -import { addListItems, createPoll, decryptData, deleteListItems, encryptData, getListItems, getUserAccount, publishMultipleQDNResources, publishQDNResource, sendCoin, voteOnPoll } from "./qortalRequests/get"; +import { addListItems, createPoll, decryptData, deleteListItems, encryptData, getListItems, getUserAccount, joinGroup, publishMultipleQDNResources, publishQDNResource, sendChatMessage, sendCoin, voteOnPoll } from "./qortalRequests/get"; @@ -192,6 +192,32 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { break; } + case "SEND_CHAT_MESSAGE": { + const data = request.payload; + console.log('data', data) + sendChatMessage(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } + case "JOIN_GROUP": { + const data = request.payload; + console.log('data', data) + joinGroup(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } case "SEND_COIN": { const data = request.payload; const requiredFields = ["coin", "destinationAddress", "amount"]; diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index ef00c53..49fad5a 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -1,11 +1,15 @@ import { + computePow, createEndpoint, + getBalanceInfo, getFee, getKeyPair, getLastRef, getSaveWallet, processTransactionVersion2, removeDuplicateWindow, + signChatFunc, + joinGroup as joinGroupFunc } from "../background"; import { getNameInfo } from "../backgroundFunctions/encryption"; import Base58 from "../deps/Base58"; @@ -22,118 +26,107 @@ import { getPermission, setPermission } from "../qortalRequests"; import { createTransaction } from "../transactions/transactions"; import { fileToBase64 } from "../utils/fileReading"; +const _createPoll = async (pollName, pollDescription, options) => { + const fee = await getFee("CREATE_POLL"); -const _createPoll = async (pollName, pollDescription, options) => { + const resPermission = await getUserPermission({ + text1: "You are requesting to create the poll below:", + text2: `Poll: ${pollName}`, + text3: `Description: ${pollDescription}`, + text4: `Options: ${options?.join(", ")}`, + fee: fee.fee, + }); + const { accepted } = resPermission; - const fee = await getFee("CREATE_POLL"); + if (accepted) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + let lastRef = await getLastRef(); - const resPermission = await getUserPermission({ - text1: "You are requesting to create the poll below:", - text2: `Poll: ${pollName}`, - text3: `Description: ${pollDescription}`, - text4: `Options: ${options?.join(', ')}`, + const tx = await createTransaction(8, keyPair, { fee: fee.fee, + ownerAddress: address, + rPollName: pollName, + rPollDesc: pollDescription, + rOptions: options, + lastReference: lastRef, }); - const { accepted } = resPermission; + 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; + } else { + throw new Error("User declined request"); + } +}; - if(accepted){ - const wallet = await getSaveWallet(); - const address = wallet.address0; - const resKeyPair = await getKeyPair(); - const parsedData = JSON.parse(resKeyPair); - const uint8PrivateKey = Base58.decode(parsedData.privateKey); - const uint8PublicKey = Base58.decode(parsedData.publicKey); - const keyPair = { - privateKey: uint8PrivateKey, - publicKey: uint8PublicKey, - }; - let lastRef = await getLastRef() +const _voteOnPoll = async (pollName, optionIndex, optionName) => { + const fee = await getFee("VOTE_ON_POLL"); - const tx = await createTransaction(8, keyPair, { - fee: fee.fee, - ownerAddress: address, - rPollName: pollName, - rPollDesc: pollDescription, - rOptions: options, - lastReference: lastRef - }); - 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; - } else { - throw new Error("User declined request"); - } - -} + const resPermission = await getUserPermission({ + text1: "You are being requested to vote on the poll below:", + text2: `Poll: ${pollName}`, + text3: `Option: ${optionName}`, + fee: fee.fee, + }); + const { accepted } = resPermission; -const _voteOnPoll =async (pollName, optionIndex, optionName)=> { + if (accepted) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + let lastRef = await getLastRef(); - const fee = await getFee("VOTE_ON_POLL"); - - const resPermission = await getUserPermission({ - text1: "You are being requested to vote on the poll below:", - text2: `Poll: ${pollName}`, - text3: `Option: ${optionName}`, + const tx = await createTransaction(9, keyPair, { fee: fee.fee, + voterAddress: address, + rPollName: pollName, + rOptionIndex: optionIndex, + lastReference: lastRef, }); - const { accepted } = resPermission; - - if(accepted){ - const wallet = await getSaveWallet(); - const address = wallet.address0; - const resKeyPair = await getKeyPair(); - const parsedData = JSON.parse(resKeyPair); - const uint8PrivateKey = Base58.decode(parsedData.privateKey); - const uint8PublicKey = Base58.decode(parsedData.publicKey); - const keyPair = { - privateKey: uint8PrivateKey, - publicKey: uint8PublicKey, - }; - let lastRef = await getLastRef() - - const tx = await createTransaction(9, keyPair, { - fee: fee.fee, - voterAddress: address, - rPollName: pollName, - rOptionIndex: optionIndex, - lastReference: lastRef - }); - 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; - } else { - throw new Error("User declined request"); - } - -} + 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; + } else { + throw new Error("User declined request"); + } +}; function getFileFromContentScript(fileId, sender) { - console.log('sender', sender) - return new Promise((resolve, reject) => { - - - chrome.tabs.sendMessage( - sender.tab.id, - { action: "getFileFromIndexedDB", fileId: fileId }, - (response) => { - console.log('response2', response) - if (response && response.result) { - - resolve(response.result); - } else { - reject(response?.error || "Failed to retrieve file"); - } - } - ); - - - }); - } - + console.log("sender", sender); + return new Promise((resolve, reject) => { + chrome.tabs.sendMessage( + sender.tab.id, + { action: "getFileFromIndexedDB", fileId: fileId }, + (response) => { + console.log("response2", response); + if (response && response.result) { + resolve(response.result); + } else { + reject(response?.error || "Failed to retrieve file"); + } + } + ); + }); +} async function getUserPermission(payload: any) { function waitForWindowReady(windowId) { @@ -531,7 +524,7 @@ export const publishQDNResource = async (data: any, sender) => { throw new Error("Only encrypted data can go into private services"); } if (data.fileId) { - data64 = await getFileFromContentScript(data.fileId, sender); + data64 = await getFileFromContentScript(data.fileId, sender); } if (data.encrypt) { try { @@ -561,7 +554,7 @@ export const publishQDNResource = async (data: any, sender) => { const { accepted } = resPermission; if (accepted) { if (data.fileId && !data.encrypt) { - data64 = await getFileFromContentScript(data.fileId, sender); + data64 = await getFileFromContentScript(data.fileId, sender); } try { const resPublish = await publishData({ @@ -669,9 +662,13 @@ export const publishMultipleQDNResources = async (data: any, sender) => { .map( (resource) => `
-
Service: ${resource.service}
+
Service: ${ + resource.service + }
Name: ${resource.name}
-
Identifier: ${resource.identifier}
+
Identifier: ${ + resource.identifier + }
${ resource.filename ? `
Filename: ${resource.filename}
` @@ -687,12 +684,12 @@ export const publishMultipleQDNResources = async (data: any, sender) => { fee: fee.fee * resources.length, }); const { accepted } = resPermission; - console.log('accepted', accepted) + console.log("accepted", accepted); if (!accepted) { throw new Error("User declined request"); } let failedPublishesIdentifiers = []; - console.log('resources', resources) + console.log("resources", resources); for (const resource of resources) { try { const requiredFields = ["service"]; @@ -764,9 +761,8 @@ export const publishMultipleQDNResources = async (data: any, sender) => { continue; } } - + try { - await publishData({ registeredName: encodeURIComponent(name), file: data64, @@ -799,7 +795,7 @@ export const publishMultipleQDNResources = async (data: any, sender) => { }); } } catch (error) { - console.log('error', error) + console.log("error", error); failedPublishesIdentifiers.push({ reason: "Unknown error", identifier: resource.identifier, @@ -817,10 +813,258 @@ export const publishMultipleQDNResources = async (data: any, sender) => { }; export const voteOnPoll = async (data) => { - const requiredFields = ['pollName', 'optionIndex'] + const requiredFields = ["pollName", "optionIndex"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field] && data[field] !== 0) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + const pollName = data.pollName; + const optionIndex = data.optionIndex; + let pollInfo = null; + try { + const url = await createEndpoint(`/polls/${encodeURIComponent(pollName)}`); + const response = await fetch(url); + if (!response.ok) throw new Error("Failed to fetch poll"); + + pollInfo = await response.json(); + } catch (error) { + const errorMsg = (error && error.message) || "Poll not found"; + throw new Error(errorMsg); + } + if (!pollInfo || pollInfo.error) { + const errorMsg = (pollInfo && pollInfo.message) || "Poll not found"; + throw new Error(errorMsg); + } + try { + const optionName = pollInfo.pollOptions[optionIndex].optionName; + const resVoteOnPoll = await _voteOnPoll(pollName, optionIndex, optionName); + return resVoteOnPoll; + } catch (error) { + throw new Error(error?.message || "Failed to vote on the poll."); + } +}; + +export const createPoll = async (data) => { + const requiredFields = [ + "pollName", + "pollDescription", + "pollOptions", + "pollOwnerAddress", + ]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + const pollName = data.pollName; + const pollDescription = data.pollDescription; + const pollOptions = data.pollOptions; + const pollOwnerAddress = data.pollOwnerAddress; + try { + const resCreatePoll = await _createPoll( + pollName, + pollDescription, + pollOptions, + pollOwnerAddress + ); + return resCreatePoll; + } catch (error) { + throw new Error(error?.message || "Failed to created poll."); + } +}; + +export const sendChatMessage = async (data) => { + const message = data.message; + const recipient = data.destinationAddress; + const groupId = data.groupId; + const isRecipient = !groupId; + const resPermission = await getUserPermission({ + text1: "Do you give this application permission to send this chat message?", + text2: `To: ${isRecipient ? recipient : `group ${groupId}`}`, + text3: `${message?.slice(0, 25)}${message?.length > 25 ? "..." : ""}`, + }); + + const { accepted } = resPermission; + if (accepted) { + const tiptapJson = { + type: "doc", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: message, + }, + ], + }, + ], + }; + const messageObject = { + messageText: tiptapJson, + images: [""], + repliedTo: "", + version: 3, + }; + try { + JSON.stringify(messageObject); + + } catch (error) { + console.log('my error', error) + } + const stringifyMessageObject = JSON.stringify(messageObject); + + const balance = await getBalanceInfo(); + const hasEnoughBalance = +balance < 4 ? false : true; + if (!hasEnoughBalance) { + throw new Error("You need at least 4 QORT to send a message"); + } + if (isRecipient && recipient) { + const url = await createEndpoint(`/addresses/publickey/${recipient}`); + const response = await fetch(url); + if (!response.ok) + throw new Error("Failed to fetch recipient's public key"); + + let key + let hasPublicKey; + let res + const contentType = response.headers.get("content-type"); + + // If the response is JSON, parse it as JSON + if (contentType && contentType.includes("application/json")) { + res = await response.json(); + } else { + // Otherwise, treat it as plain text + res = await response.text(); + } + console.log('res', res) + if (res?.error === 102) { + key = ""; + hasPublicKey = false; + } else if (res !== false) { + key = res; + hasPublicKey = true; + } else { + key = ""; + hasPublicKey = false; + } + + if (!hasPublicKey && isRecipient) { + throw new Error( + "Cannot send an encrypted message to this user since they do not have their publickey on chain." + ); + } + let _reference = new Uint8Array(64); + self.crypto.getRandomValues(_reference); + + let sendTimestamp = Date.now(); + + let reference = Base58.encode(_reference); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + + const difficulty = 8; + const tx = await createTransaction(18, keyPair, { + timestamp: sendTimestamp, + recipient: recipient, + recipientPublicKey: key, + hasChatReference: 0, + message: stringifyMessageObject, + lastReference: reference, + proofOfWorkNonce: 0, + isEncrypted: 1, + isText: 1, + }); + 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); + if (_response?.error) { + throw new Error(_response?.message); + } + return _response; + } else if (!isRecipient && groupId) { + let _reference = new Uint8Array(64); + self.crypto.getRandomValues(_reference); + + let reference = Base58.encode(_reference); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + + const difficulty = 8; + + const txBody = { + timestamp: Date.now(), + groupID: Number(groupId), + hasReceipient: 0, + hasChatReference: 0, + message: stringifyMessageObject, + lastReference: reference, + proofOfWorkNonce: 0, + isEncrypted: 0, // Set default to not encrypted for groups + isText: 1, + } + + const tx = await createTransaction(181, 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); + if (_response?.error) { + throw new Error(_response?.message); + } + return _response; + } else { + throw new Error("Please enter a recipient or groupId"); + } + } else { + throw new Error("User declined add to list"); + } +}; + +export const joinGroup = async (data) => { + const requiredFields = ['groupId'] const missingFields: string[] = [] requiredFields.forEach((field) => { - if (!data[field] && data[field] !== 0) { + if (!data[field]) { missingFields.push(field) } }) @@ -829,56 +1073,45 @@ export const voteOnPoll = async (data) => { const errorMsg = `Missing fields: ${missingFieldsString}` throw new Error(errorMsg) } - const pollName = data.pollName - const optionIndex = data.optionIndex - let pollInfo = null + let groupInfo = null try { - const url = await createEndpoint(`/polls/${encodeURIComponent(pollName)}`); - const response = await fetch(url); - if (!response.ok) throw new Error("Failed to fetch poll"); - - pollInfo = await response.json(); - } catch (error) { - const errorMsg = (error && error.message) || 'Poll not found' - throw new Error(errorMsg) - } - if (!pollInfo || pollInfo.error) { - const errorMsg = (pollInfo && pollInfo.message) || 'Poll not found' - throw new Error(errorMsg) - } - try { - const optionName = pollInfo.pollOptions[optionIndex].optionName - const resVoteOnPoll = await _voteOnPoll(pollName, optionIndex, optionName) - return resVoteOnPoll - } catch (error) { - throw new Error(error?.message || 'Failed to vote on the poll.') - } - }; + const url = await createEndpoint(`/groups/${data.groupId}`); + const response = await fetch(url); + if (!response.ok) throw new Error("Failed to fetch group"); - export const createPoll = async (data) => { - const requiredFields = ['pollName', 'pollDescription', 'pollOptions', 'pollOwnerAddress'] - const missingFields: string[] = [] - requiredFields.forEach((field) => { - if (!data[field]) { - missingFields.push(field) - } - }) - if (missingFields.length > 0) { - const missingFieldsString = missingFields.join(', ') - const errorMsg = `Missing fields: ${missingFieldsString}` - throw new Error(errorMsg) - } - const pollName = data.pollName - const pollDescription = data.pollDescription - const pollOptions = data.pollOptions - const pollOwnerAddress = data.pollOwnerAddress - try { - const resCreatePoll = await _createPoll(pollName, pollDescription, pollOptions, pollOwnerAddress) - return resCreatePoll - } catch (error) { - throw new Error(error?.message || 'Failed to created poll.') - } + groupInfo = await response.json(); + } catch (error) { + const errorMsg = (error && error.message) || 'Group not found' + throw new Error(errorMsg) + } + const fee = await getFee("JOIN_GROUP"); + + const resPermission = await getUserPermission({ + text1: "Confirm joining the group:", + highlightedText: `${groupInfo.groupName}`, + fee: fee.fee + }); + const { accepted } = resPermission; + + if(accepted){ + const groupId = data.groupId + + if (!groupInfo || groupInfo.error) { + const errorMsg = (groupInfo && groupInfo.message) || 'Group not found' + throw new Error(errorMsg) + } + try { + const resJoinGroup = await joinGroupFunc({groupId}) + return resJoinGroup + } catch (error) { + + throw new Error(error?.message || 'Failed to join the group.') + } + } else { + throw new Error("User declined add to list"); + } + }; export const sendCoin = async () => { From 12013947afffd13773b3d00072d6f502df13d5d7 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Wed, 16 Oct 2024 02:34:10 +0300 Subject: [PATCH 10/58] save file qortalrequest --- public/content-script.js | 51 +++++++++++++++++++++++-- src/qortalRequests.ts | 19 ++++++++-- src/qortalRequests/get.ts | 80 +++++++++++++++++++++++++++++++++++++-- src/utils/memeTypes.ts | 56 +++++++++++++++++++++++++++ 4 files changed, 196 insertions(+), 10 deletions(-) create mode 100644 src/utils/memeTypes.ts diff --git a/public/content-script.js b/public/content-script.js index 5010905..698e67c 100644 --- a/public/content-script.js +++ b/public/content-script.js @@ -551,7 +551,32 @@ const testAsync = async (sendResponse)=> { sendResponse({ result: null, error: "Testing" }); } +const showSaveFilePicker = async (data) => { + try { + const {filename, mimeType, fileHandleOptions, fileId} = data + const blob = await retrieveFileFromIndexedDB(fileId) + const fileHandle = await window.showSaveFilePicker({ + suggestedName: filename, + types: [ + { + description: mimeType, + ...fileHandleOptions + } + ] + }) + const writeFile = async (fileHandle, contents) => { + const writable = await fileHandle.createWritable() + await writable.write(contents) + await writable.close() + } + writeFile(fileHandle, blob).then(() => console.log("FILE SAVED")) +} catch (error) { + FileSaver.saveAs(blob, filename) +} +} + chrome.runtime?.onMessage.addListener( function (message, sender, sendResponse) { + console.log('message', message) if (message.type === "LOGOUT") { // Notify the web page window.postMessage( @@ -571,6 +596,8 @@ chrome.runtime?.onMessage.addListener( function (message, sender, sendResponse) }, "*" ); + } else if(message.action === "SHOW_SAVE_FILE_PICKER"){ + showSaveFilePicker(message?.data) } else if (message.action === "getFileFromIndexedDB") { @@ -696,9 +723,27 @@ async function storeFilesInIndexedDB(obj) { obj.fileId = fileId; delete obj.file; } + if (obj.blob instanceof Blob) { + const fileId = "objFile_qortalfile"; + + // Store the file in IndexedDB + const fileData = { + id: fileId, + data: obj.blob, + }; + objectStore.put(fileData); + + // Replace the file object with the file ID in the original object + let blobObj = { + type: obj.blob?.type + } + obj.fileId = fileId; + delete obj.blob; + obj.blob = blobObj + } // Iterate through resources to find files and save them to IndexedDB - for (let resource of obj.resources) { + for (let resource of (obj?.resources || [])) { if (resource.file instanceof File) { const fileId = resource.identifier + "_qortalfile"; @@ -729,7 +774,7 @@ async function storeFilesInIndexedDB(obj) { -const UIQortalRequests = ['GET_USER_ACCOUNT', 'ENCRYPT_DATA', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL', 'SEND_CHAT_MESSAGE', 'JOIN_GROUP'] +const UIQortalRequests = ['GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL', 'SEND_CHAT_MESSAGE', 'JOIN_GROUP'] if (!window.hasAddedQortalListener) { console.log("Listener added"); @@ -768,7 +813,7 @@ if (!window.hasAddedQortalListener) { { action: event.data.action, type: 'qortalRequest', payload: event.data }, event.ports[0] ); - } else if (event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' || event?.data?.action === 'PUBLISH_QDN_RESOURCE') { + } else if (event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' || event?.data?.action === 'PUBLISH_QDN_RESOURCE' || event?.data?.action === 'ENCRYPT_DATA' || event?.data?.action === 'SAVE_FILE') { let data; try { data = await storeFilesInIndexedDB(event.data); diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index 91a3737..20db7b0 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -1,4 +1,4 @@ -import { addListItems, createPoll, decryptData, deleteListItems, encryptData, getListItems, getUserAccount, joinGroup, publishMultipleQDNResources, publishQDNResource, sendChatMessage, sendCoin, voteOnPoll } from "./qortalRequests/get"; +import { addListItems, createPoll, decryptData, deleteListItems, encryptData, getListItems, getUserAccount, joinGroup, publishMultipleQDNResources, publishQDNResource, saveFile, sendChatMessage, sendCoin, voteOnPoll } from "./qortalRequests/get"; @@ -78,7 +78,7 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { case "ENCRYPT_DATA": { const data = request.payload; - encryptData(data) + encryptData(data, sender) .then((res) => { sendResponse(res); }) @@ -207,7 +207,7 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { } case "JOIN_GROUP": { const data = request.payload; - console.log('data', data) + joinGroup(data) .then((res) => { sendResponse(res); @@ -218,6 +218,19 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { break; } + case "SAVE_FILE": { + const data = request.payload; + + saveFile(data, sender) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } case "SEND_COIN": { const data = request.payload; const requiredFields = ["coin", "destinationAddress", "amount"]; diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index 49fad5a..d155337 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -25,6 +25,7 @@ import { publishData } from "../qdn/publish/pubish"; import { getPermission, setPermission } from "../qortalRequests"; import { createTransaction } from "../transactions/transactions"; import { fileToBase64 } from "../utils/fileReading"; +import { mimeToExtensionMap } from "../utils/memeTypes"; const _createPoll = async (pollName, pollDescription, options) => { const fee = await getFee("CREATE_POLL"); @@ -127,6 +128,15 @@ function getFileFromContentScript(fileId, sender) { ); }); } +function sendToSaveFilePicker(data, sender) { + console.log("sender", sender); + + chrome.tabs.sendMessage( + sender.tab.id, + { action: "SHOW_SAVE_FILE_PICKER", data } + ); + + } async function getUserPermission(payload: any) { function waitForWindowReady(windowId) { @@ -251,11 +261,11 @@ export const getUserAccount = async () => { } }; -export const encryptData = async (data) => { +export const encryptData = async (data, sender) => { let data64 = data.data64; let publicKeys = data.publicKeys || []; - if (data.file) { - data64 = await fileToBase64(data.file); + if (data.fileId) { + data64 = await getFileFromContentScript(data.fileId, sender) } if (!data64) { throw new Error("Please include data to encrypt"); @@ -509,7 +519,6 @@ export const publishQDNResource = async (data: any, sender) => { const tag3 = data.tag3; const tag4 = data.tag4; const tag5 = data.tag5; - let feeAmount = null; if (data.identifier == null) { identifier = "default"; } @@ -1114,6 +1123,69 @@ export const joinGroup = async (data) => { }; + export const saveFile = async (data, sender) => { + try { + const requiredFields = ['filename', 'fileId'] + const missingFields: string[] = [] + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field) + } + }) + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', ') + const errorMsg = `Missing fields: ${missingFieldsString}` + throw new Error(errorMsg) + } + const filename = data.filename + const blob = data.blob + const fileId = data.fileId + const resPermission = await getUserPermission({ + text1: "Would you like to download:", + highlightedText: `${filename}`, + }); + const { accepted } = resPermission; + + if(accepted){ + + const mimeType = blob.type || data.mimeType + let backupExention = filename.split('.').pop() + if (backupExention) { + backupExention = '.' + backupExention + } + const fileExtension = mimeToExtensionMap[mimeType] || backupExention + let fileHandleOptions = {} + if (!mimeType) { + + throw new Error('A mimeType could not be derived') + } + if (!fileExtension) { + const obj = {} + throw new Error('A file extension could not be derived') + } + if (fileExtension && mimeType) { + fileHandleOptions = { + accept: { + [mimeType]: [fileExtension] + } + } + } + sendToSaveFilePicker( { + filename, mimeType, blob, fileId, fileHandleOptions + } ,sender) + return true + } else { + throw new Error("User declined add to list"); + + } + + } catch (error) { + + throw new Error(error?.message || 'Failed to initiate download') + } + + }; + export const sendCoin = async () => { try { const wallet = await getSaveWallet(); diff --git a/src/utils/memeTypes.ts b/src/utils/memeTypes.ts new file mode 100644 index 0000000..2bc5873 --- /dev/null +++ b/src/utils/memeTypes.ts @@ -0,0 +1,56 @@ +export const mimeToExtensionMap = { + // Documents + "application/pdf": ".pdf", + "application/msword": ".doc", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx", + "application/vnd.ms-excel": ".xls", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx", + "application/vnd.ms-powerpoint": ".ppt", + "application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx", + "application/vnd.oasis.opendocument.text": ".odt", + "application/vnd.oasis.opendocument.spreadsheet": ".ods", + "application/vnd.oasis.opendocument.presentation": ".odp", + "text/plain": ".txt", + "text/csv": ".csv", + "text/html": ".html", + "application/xhtml+xml": ".xhtml", + "application/xml": ".xml", + "application/json": ".json", + + // Images + "image/jpeg": ".jpg", + "image/png": ".png", + "image/gif": ".gif", + "image/webp": ".webp", + "image/svg+xml": ".svg", + "image/tiff": ".tif", + "image/bmp": ".bmp", + + // Audio + "audio/mpeg": ".mp3", + "audio/ogg": ".ogg", + "audio/wav": ".wav", + "audio/webm": ".weba", + "audio/aac": ".aac", + + // Video + "video/mp4": ".mp4", + "video/webm": ".webm", + "video/ogg": ".ogv", + "video/x-msvideo": ".avi", + "video/quicktime": ".mov", + "video/x-ms-wmv": ".wmv", + "video/mpeg": ".mpeg", + "video/3gpp": ".3gp", + "video/3gpp2": ".3g2", + "video/x-matroska": ".mkv", + "video/x-flv": ".flv", + + // Archives + "application/zip": ".zip", + "application/x-rar-compressed": ".rar", + "application/x-tar": ".tar", + "application/x-7z-compressed": ".7z", + "application/x-gzip": ".gz", + "application/x-bzip2": ".bz2", +} \ No newline at end of file From 82fba4383849e5c10caa01d18528a28c9a29c344 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Wed, 16 Oct 2024 03:19:55 +0300 Subject: [PATCH 11/58] added deploy at qortalrequest --- public/content-script.js | 2 +- src/qortalRequests.ts | 15 ++++- src/qortalRequests/get.ts | 80 +++++++++++++++++++++++-- src/transactions/DeployAtTransaction.ts | 78 ++++++++++++++++++++++++ src/transactions/transactions.ts | 2 + 5 files changed, 171 insertions(+), 6 deletions(-) create mode 100644 src/transactions/DeployAtTransaction.ts diff --git a/public/content-script.js b/public/content-script.js index 698e67c..807dfff 100644 --- a/public/content-script.js +++ b/public/content-script.js @@ -774,7 +774,7 @@ async function storeFilesInIndexedDB(obj) { -const UIQortalRequests = ['GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL', 'SEND_CHAT_MESSAGE', 'JOIN_GROUP'] +const UIQortalRequests = ['GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL', 'SEND_CHAT_MESSAGE', 'JOIN_GROUP', 'DEPLOY_AT'] if (!window.hasAddedQortalListener) { console.log("Listener added"); diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index 20db7b0..c4b5f4f 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -1,4 +1,4 @@ -import { addListItems, createPoll, decryptData, deleteListItems, encryptData, getListItems, getUserAccount, joinGroup, publishMultipleQDNResources, publishQDNResource, saveFile, sendChatMessage, sendCoin, voteOnPoll } from "./qortalRequests/get"; +import { addListItems, createPoll, decryptData, deleteListItems, deployAt, encryptData, getListItems, getUserAccount, joinGroup, publishMultipleQDNResources, publishQDNResource, saveFile, sendChatMessage, sendCoin, voteOnPoll } from "./qortalRequests/get"; @@ -231,6 +231,19 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { break; } + case "DEPLOY_AT": { + const data = request.payload; + + deployAt(data, sender) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } case "SEND_COIN": { const data = request.payload; const requiredFields = ["coin", "destinationAddress", "amount"]; diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index d155337..db141b2 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -70,6 +70,57 @@ const _createPoll = async (pollName, pollDescription, options) => { } }; +const _deployAt= async(name, description, tags, creationBytes, amount, assetId, atType)=> { + const fee = await getFee("DEPLOY_AT"); + + const resPermission = await getUserPermission({ + text1: "Would you like to deploy this AT?", + text2: `Name: ${name}`, + text3: `Description: ${description}`, + fee: fee.fee, + }); + + const { accepted } = resPermission; + + if (accepted) { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const lastReference = await getLastRef(); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + + const tx = await createTransaction(16, keyPair, { + fee: fee.fee, + rName: name, + rDescription: description, + rTags: tags, + rAmount: amount, + rAssetId: assetId, + rCreationBytes: creationBytes, + atType: atType, + 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; + } else { + throw new Error("User declined transaction"); + } + + + +} + const _voteOnPoll = async (pollName, optionIndex, optionName) => { const fee = await getFee("VOTE_ON_POLL"); @@ -484,7 +535,7 @@ export const deleteListItems = async (data) => { } return res; } else { - throw new Error("User declined add to list"); + throw new Error("User declined delete from list"); } }; @@ -1065,7 +1116,7 @@ export const sendChatMessage = async (data) => { throw new Error("Please enter a recipient or groupId"); } } else { - throw new Error("User declined add to list"); + throw new Error("User declined to send message"); } }; @@ -1118,7 +1169,7 @@ export const joinGroup = async (data) => { throw new Error(error?.message || 'Failed to join the group.') } } else { - throw new Error("User declined add to list"); + throw new Error("User declined to join group"); } }; @@ -1175,7 +1226,7 @@ export const joinGroup = async (data) => { } ,sender) return true } else { - throw new Error("User declined add to list"); + throw new Error("User declined to save file"); } @@ -1186,6 +1237,27 @@ export const joinGroup = async (data) => { }; +export const deployAt = async (data)=> { + const requiredFields = ['name', 'description', 'tags', 'creationBytes', 'amount', 'assetId', 'type'] + const missingFields: string[] = [] + requiredFields.forEach((field) => { + if (!data[field] && data[field] !== 0) { + missingFields.push(field) + } + }) + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', ') + const errorMsg = `Missing fields: ${missingFieldsString}` + throw new Error(errorMsg) + } + try { + const resDeployAt = await _deployAt(data.name, data.description, data.tags, data.creationBytes, data.amount, data.assetId, data.type) + return resDeployAt + } catch (error) { + throw new Error(error?.message || 'Failed to join the group.') + } +} + export const sendCoin = async () => { try { const wallet = await getSaveWallet(); diff --git a/src/transactions/DeployAtTransaction.ts b/src/transactions/DeployAtTransaction.ts new file mode 100644 index 0000000..8a20553 --- /dev/null +++ b/src/transactions/DeployAtTransaction.ts @@ -0,0 +1,78 @@ +// @ts-nocheck + + +import TransactionBase from './TransactionBase' +import { QORT_DECIMALS } from '../constants/constants' + +export default class DeployAtTransaction extends TransactionBase { + constructor() { + super() + this.type = 16 + } + + + + set fee(fee) { + this._fee = fee * QORT_DECIMALS + this._feeBytes = this.constructor.utils.int64ToBytes(this._fee) + } + + set rAmount(rAmount) { + this._rAmount = Math.round(rAmount * QORT_DECIMALS) + this._rAmountBytes = this.constructor.utils.int64ToBytes(this._rAmount) + } + + set rName(rName) { + this._rName = rName + this._rNameBytes = this.constructor.utils.stringtoUTF8Array(this._rName.toLocaleLowerCase()) + this._rNameLength = this.constructor.utils.int32ToBytes(this._rNameBytes.length) + } + + set rDescription(rDescription) { + this._rDescription = rDescription + this._rDescriptionBytes = this.constructor.utils.stringtoUTF8Array(this._rDescription.toLocaleLowerCase()) + this._rDescriptionLength = this.constructor.utils.int32ToBytes(this._rDescriptionBytes.length) + } + + set atType(atType) { + this._atType = atType + this._atTypeBytes = this.constructor.utils.stringtoUTF8Array(this._atType) + this._atTypeLength = this.constructor.utils.int32ToBytes(this._atTypeBytes.length) + } + + set rTags(rTags) { + this._rTags = rTags + this._rTagsBytes = this.constructor.utils.stringtoUTF8Array(this._rTags.toLocaleLowerCase()) + this._rTagsLength = this.constructor.utils.int32ToBytes(this._rTagsBytes.length) + } + + set rCreationBytes(rCreationBytes) { + const decode = this.constructor.Base58.decode(rCreationBytes) + this._rCreationBytes = this.constructor.utils.stringtoUTF8Array(decode) + this._rCreationBytesLength = this.constructor.utils.int32ToBytes(this._rCreationBytes.length) + } + + set rAssetId(rAssetId) { + this._rAssetId = this.constructor.utils.int64ToBytes(rAssetId) + } + + get params() { + const params = super.params + params.push( + this._rNameLength, + this._rNameBytes, + this._rDescriptionLength, + this._rDescriptionBytes, + this._atTypeLength, + this._atTypeBytes, + this._rTagsLength, + this._rTagsBytes, + this._rCreationBytesLength, + this._rCreationBytes, + this._rAmountBytes, + this._rAssetId, + this._feeBytes + ) + return params + } +} diff --git a/src/transactions/transactions.ts b/src/transactions/transactions.ts index 02854c6..400418c 100644 --- a/src/transactions/transactions.ts +++ b/src/transactions/transactions.ts @@ -16,6 +16,7 @@ import RemoveGroupAdminTransaction from './RemoveGroupAdminTransaction.js' import RegisterNameTransaction from './RegisterNameTransaction.js' import VoteOnPollTransaction from './VoteOnPollTransaction.js' import CreatePollTransaction from './CreatePollTransaction.js' +import DeployAtTransaction from './DeployAtTransaction.js' export const transactionTypes = { @@ -23,6 +24,7 @@ export const transactionTypes = { 2: PaymentTransaction, 8: CreatePollTransaction, 9: VoteOnPollTransaction, + 16: DeployAtTransaction, 18: ChatTransaction, 181: GroupChatTransaction, 22: CreateGroupTransaction, From 1ec2dce2b6980034f978d905b5d81e09f52c7b71 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Wed, 16 Oct 2024 04:29:56 +0300 Subject: [PATCH 12/58] add getuserwallet qortalrequest --- public/content-script.js | 2 +- src/background.ts | 10 + src/qortalRequests.ts | 17 +- src/qortalRequests/get.ts | 560 ++++++++++++++++++++++---------------- 4 files changed, 358 insertions(+), 231 deletions(-) diff --git a/public/content-script.js b/public/content-script.js index 807dfff..75a77c6 100644 --- a/public/content-script.js +++ b/public/content-script.js @@ -774,7 +774,7 @@ async function storeFilesInIndexedDB(obj) { -const UIQortalRequests = ['GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL', 'SEND_CHAT_MESSAGE', 'JOIN_GROUP', 'DEPLOY_AT'] +const UIQortalRequests = ['GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL', 'SEND_CHAT_MESSAGE', 'JOIN_GROUP', 'DEPLOY_AT', 'GET_USER_WALLET'] if (!window.hasAddedQortalListener) { console.log("Listener added"); diff --git a/src/background.ts b/src/background.ts index bc91a66..60ec22c 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1352,6 +1352,16 @@ async function decryptWallet({ password, wallet, walletVersion }) { publicKey: Base58.encode(keyPair.publicKey), ltcPrivateKey: ltcPrivateKey, ltcPublicKey: ltcPublicKey, + arrrSeed58: wallet2._addresses[0].arrrWallet.seed58, + btcAddress: wallet2._addresses[0].btcWallet.address, + btcPublicKey: wallet2._addresses[0].btcWallet.derivedMasterPublicKey, + ltcAddress: wallet2._addresses[0].ltcWallet.address, + dogeAddress: wallet2._addresses[0].dogeWallet.address, + dogePublicKey: wallet2._addresses[0].dogeWallet.derivedMasterPublicKey, + dgbAddress: wallet2._addresses[0].dgbWallet.address, + dgbPublicKey: wallet2._addresses[0].dgbWallet.derivedMasterPublicKey, + rvnAddress: wallet2._addresses[0].rvnWallet.address, + rvnPublicKey: wallet2._addresses[0].rvnWallet.derivedMasterPublicKey }; const dataString = JSON.stringify(toSave); await new Promise((resolve, reject) => { diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index c4b5f4f..210e4f9 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -1,4 +1,4 @@ -import { addListItems, createPoll, decryptData, deleteListItems, deployAt, encryptData, getListItems, getUserAccount, joinGroup, publishMultipleQDNResources, publishQDNResource, saveFile, sendChatMessage, sendCoin, voteOnPoll } from "./qortalRequests/get"; +import { addListItems, createPoll, decryptData, deleteListItems, deployAt, encryptData, getListItems, getUserAccount, getUserWallet, joinGroup, publishMultipleQDNResources, publishQDNResource, saveFile, sendChatMessage, sendCoin, voteOnPoll } from "./qortalRequests/get"; @@ -234,7 +234,20 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { case "DEPLOY_AT": { const data = request.payload; - deployAt(data, sender) + deployAt(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } + case "GET_USER_WALLET": { + const data = request.payload; + + getUserWallet(data) .then((res) => { sendResponse(res); }) diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index db141b2..c422f36 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -9,7 +9,7 @@ import { processTransactionVersion2, removeDuplicateWindow, signChatFunc, - joinGroup as joinGroupFunc + joinGroup as joinGroupFunc, } from "../background"; import { getNameInfo } from "../backgroundFunctions/encryption"; import Base58 from "../deps/Base58"; @@ -70,8 +70,16 @@ const _createPoll = async (pollName, pollDescription, options) => { } }; -const _deployAt= async(name, description, tags, creationBytes, amount, assetId, atType)=> { - const fee = await getFee("DEPLOY_AT"); +const _deployAt = async ( + name, + description, + tags, + creationBytes, + amount, + assetId, + atType +) => { + const fee = await getFee("DEPLOY_AT"); const resPermission = await getUserPermission({ text1: "Would you like to deploy this AT?", @@ -84,42 +92,41 @@ const _deployAt= async(name, description, tags, creationBytes, amount, assetId, if (accepted) { const wallet = await getSaveWallet(); - const address = wallet.address0; - const lastReference = await getLastRef(); - const resKeyPair = await getKeyPair(); - const parsedData = JSON.parse(resKeyPair); - const uint8PrivateKey = Base58.decode(parsedData.privateKey); - const uint8PublicKey = Base58.decode(parsedData.publicKey); - const keyPair = { - privateKey: uint8PrivateKey, - publicKey: uint8PublicKey, - }; + const address = wallet.address0; + const lastReference = await getLastRef(); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; - const tx = await createTransaction(16, keyPair, { - fee: fee.fee, - rName: name, - rDescription: description, - rTags: tags, - rAmount: amount, - rAssetId: assetId, - rCreationBytes: creationBytes, - atType: atType, - lastReference: lastReference, - }); + const tx = await createTransaction(16, keyPair, { + fee: fee.fee, + rName: name, + rDescription: description, + rTags: tags, + rAmount: amount, + rAssetId: assetId, + rCreationBytes: creationBytes, + atType: atType, + lastReference: lastReference, + }); - const signedBytes = Base58.encode(tx.signedBytes); + 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; } else { throw new Error("User declined transaction"); } - - - -} +}; const _voteOnPoll = async (pollName, optionIndex, optionName) => { const fee = await getFee("VOTE_ON_POLL"); @@ -180,14 +187,13 @@ function getFileFromContentScript(fileId, sender) { }); } function sendToSaveFilePicker(data, sender) { - console.log("sender", sender); - - chrome.tabs.sendMessage( - sender.tab.id, - { action: "SHOW_SAVE_FILE_PICKER", data } - ); + console.log("sender", sender); - } + chrome.tabs.sendMessage(sender.tab.id, { + action: "SHOW_SAVE_FILE_PICKER", + data, + }); +} async function getUserPermission(payload: any) { function waitForWindowReady(windowId) { @@ -316,7 +322,7 @@ export const encryptData = async (data, sender) => { let data64 = data.data64; let publicKeys = data.publicKeys || []; if (data.fileId) { - data64 = await getFileFromContentScript(data.fileId, sender) + data64 = await getFileFromContentScript(data.fileId, sender); } if (!data64) { throw new Error("Please include data to encrypt"); @@ -980,10 +986,9 @@ export const sendChatMessage = async (data) => { version: 3, }; try { - JSON.stringify(messageObject); - + JSON.stringify(messageObject); } catch (error) { - console.log('my error', error) + console.log("my error", error); } const stringifyMessageObject = JSON.stringify(messageObject); @@ -997,20 +1002,20 @@ export const sendChatMessage = async (data) => { const response = await fetch(url); if (!response.ok) throw new Error("Failed to fetch recipient's public key"); - - let key + + let key; let hasPublicKey; - let res + let res; const contentType = response.headers.get("content-type"); - - // If the response is JSON, parse it as JSON - if (contentType && contentType.includes("application/json")) { - res = await response.json(); - } else { - // Otherwise, treat it as plain text - res = await response.text(); - } - console.log('res', res) + + // If the response is JSON, parse it as JSON + if (contentType && contentType.includes("application/json")) { + res = await response.json(); + } else { + // Otherwise, treat it as plain text + res = await response.text(); + } + console.log("res", res); if (res?.error === 102) { key = ""; hasPublicKey = false; @@ -1041,7 +1046,7 @@ export const sendChatMessage = async (data) => { privateKey: uint8PrivateKey, publicKey: uint8PublicKey, }; - + const difficulty = 8; const tx = await createTransaction(18, keyPair, { timestamp: sendTimestamp, @@ -1068,50 +1073,50 @@ export const sendChatMessage = async (data) => { } return _response; } else if (!isRecipient && groupId) { - let _reference = new Uint8Array(64); - self.crypto.getRandomValues(_reference); - - let reference = Base58.encode(_reference); - const resKeyPair = await getKeyPair(); - const parsedData = JSON.parse(resKeyPair); - const uint8PrivateKey = Base58.decode(parsedData.privateKey); - const uint8PublicKey = Base58.decode(parsedData.publicKey); - const keyPair = { - privateKey: uint8PrivateKey, - publicKey: uint8PublicKey, - }; - - const difficulty = 8; - - const txBody = { - timestamp: Date.now(), - groupID: Number(groupId), - hasReceipient: 0, - hasChatReference: 0, - message: stringifyMessageObject, - lastReference: reference, - proofOfWorkNonce: 0, - isEncrypted: 0, // Set default to not encrypted for groups - isText: 1, - } - - const tx = await createTransaction(181, 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); - if (_response?.error) { - throw new Error(_response?.message); - } - return _response; + let _reference = new Uint8Array(64); + self.crypto.getRandomValues(_reference); + + let reference = Base58.encode(_reference); + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + + const difficulty = 8; + + const txBody = { + timestamp: Date.now(), + groupID: Number(groupId), + hasReceipient: 0, + hasChatReference: 0, + message: stringifyMessageObject, + lastReference: reference, + proofOfWorkNonce: 0, + isEncrypted: 0, // Set default to not encrypted for groups + isText: 1, + }; + + const tx = await createTransaction(181, 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); + if (_response?.error) { + throw new Error(_response?.message); + } + return _response; } else { throw new Error("Please enter a recipient or groupId"); } @@ -1121,142 +1126,241 @@ export const sendChatMessage = async (data) => { }; export const joinGroup = async (data) => { - const requiredFields = ['groupId'] - const missingFields: string[] = [] - requiredFields.forEach((field) => { - if (!data[field]) { - missingFields.push(field) - } - }) - if (missingFields.length > 0) { - const missingFieldsString = missingFields.join(', ') - const errorMsg = `Missing fields: ${missingFieldsString}` - throw new Error(errorMsg) - } - let groupInfo = null - try { - - const url = await createEndpoint(`/groups/${data.groupId}`); + const requiredFields = ["groupId"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + let groupInfo = null; + try { + const url = await createEndpoint(`/groups/${data.groupId}`); const response = await fetch(url); if (!response.ok) throw new Error("Failed to fetch group"); groupInfo = await response.json(); - } catch (error) { - const errorMsg = (error && error.message) || 'Group not found' - throw new Error(errorMsg) - } - const fee = await getFee("JOIN_GROUP"); + } catch (error) { + const errorMsg = (error && error.message) || "Group not found"; + throw new Error(errorMsg); + } + const fee = await getFee("JOIN_GROUP"); - const resPermission = await getUserPermission({ - text1: "Confirm joining the group:", - highlightedText: `${groupInfo.groupName}`, - fee: fee.fee - }); - const { accepted } = resPermission; + const resPermission = await getUserPermission({ + text1: "Confirm joining the group:", + highlightedText: `${groupInfo.groupName}`, + fee: fee.fee, + }); + const { accepted } = resPermission; - if(accepted){ - const groupId = data.groupId - - if (!groupInfo || groupInfo.error) { - const errorMsg = (groupInfo && groupInfo.message) || 'Group not found' - throw new Error(errorMsg) - } - try { - const resJoinGroup = await joinGroupFunc({groupId}) - return resJoinGroup - } catch (error) { - - throw new Error(error?.message || 'Failed to join the group.') - } - } else { - throw new Error("User declined to join group"); - } - - }; + if (accepted) { + const groupId = data.groupId; - export const saveFile = async (data, sender) => { - try { - const requiredFields = ['filename', 'fileId'] - const missingFields: string[] = [] - requiredFields.forEach((field) => { - if (!data[field]) { - missingFields.push(field) - } - }) - if (missingFields.length > 0) { - const missingFieldsString = missingFields.join(', ') - const errorMsg = `Missing fields: ${missingFieldsString}` - throw new Error(errorMsg) - } - const filename = data.filename - const blob = data.blob - const fileId = data.fileId - const resPermission = await getUserPermission({ - text1: "Would you like to download:", - highlightedText: `${filename}`, - }); - const { accepted } = resPermission; - - if(accepted){ - - const mimeType = blob.type || data.mimeType - let backupExention = filename.split('.').pop() - if (backupExention) { - backupExention = '.' + backupExention - } - const fileExtension = mimeToExtensionMap[mimeType] || backupExention - let fileHandleOptions = {} - if (!mimeType) { - - throw new Error('A mimeType could not be derived') - } - if (!fileExtension) { - const obj = {} - throw new Error('A file extension could not be derived') - } - if (fileExtension && mimeType) { - fileHandleOptions = { - accept: { - [mimeType]: [fileExtension] - } - } - } - sendToSaveFilePicker( { - filename, mimeType, blob, fileId, fileHandleOptions - } ,sender) - return true - } else { - throw new Error("User declined to save file"); - - } - - } catch (error) { - - throw new Error(error?.message || 'Failed to initiate download') + if (!groupInfo || groupInfo.error) { + const errorMsg = (groupInfo && groupInfo.message) || "Group not found"; + throw new Error(errorMsg); } - - }; + try { + const resJoinGroup = await joinGroupFunc({ groupId }); + return resJoinGroup; + } catch (error) { + throw new Error(error?.message || "Failed to join the group."); + } + } else { + throw new Error("User declined to join group"); + } +}; -export const deployAt = async (data)=> { - const requiredFields = ['name', 'description', 'tags', 'creationBytes', 'amount', 'assetId', 'type'] - const missingFields: string[] = [] - requiredFields.forEach((field) => { - if (!data[field] && data[field] !== 0) { - missingFields.push(field) - } - }) - if (missingFields.length > 0) { - const missingFieldsString = missingFields.join(', ') - const errorMsg = `Missing fields: ${missingFieldsString}` - throw new Error(errorMsg) - } - try { - const resDeployAt = await _deployAt(data.name, data.description, data.tags, data.creationBytes, data.amount, data.assetId, data.type) - return resDeployAt - } catch (error) { - throw new Error(error?.message || 'Failed to join the group.') - } -} +export const saveFile = async (data, sender) => { + try { + const requiredFields = ["filename", "fileId"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + const filename = data.filename; + const blob = data.blob; + const fileId = data.fileId; + const resPermission = await getUserPermission({ + text1: "Would you like to download:", + highlightedText: `${filename}`, + }); + const { accepted } = resPermission; + + if (accepted) { + const mimeType = blob.type || data.mimeType; + let backupExention = filename.split(".").pop(); + if (backupExention) { + backupExention = "." + backupExention; + } + const fileExtension = mimeToExtensionMap[mimeType] || backupExention; + let fileHandleOptions = {}; + if (!mimeType) { + throw new Error("A mimeType could not be derived"); + } + if (!fileExtension) { + const obj = {}; + throw new Error("A file extension could not be derived"); + } + if (fileExtension && mimeType) { + fileHandleOptions = { + accept: { + [mimeType]: [fileExtension], + }, + }; + } + sendToSaveFilePicker( + { + filename, + mimeType, + blob, + fileId, + fileHandleOptions, + }, + sender + ); + return true; + } else { + throw new Error("User declined to save file"); + } + } catch (error) { + throw new Error(error?.message || "Failed to initiate download"); + } +}; + +export const deployAt = async (data) => { + const requiredFields = [ + "name", + "description", + "tags", + "creationBytes", + "amount", + "assetId", + "type", + ]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field] && data[field] !== 0) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + try { + const resDeployAt = await _deployAt( + data.name, + data.description, + data.tags, + data.creationBytes, + data.amount, + data.assetId, + data.type + ); + return resDeployAt; + } catch (error) { + throw new Error(error?.message || "Failed to join the group."); + } +}; + +export const getUserWallet = async (data) => { + const requiredFields = ["coin"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + const resPermission = await getUserPermission({ + text1: "Do you give this application permission to get your wallet information?", + }); + const { accepted } = resPermission; + + if (accepted) { + let coin = data.coin; + let userWallet = {}; + let arrrAddress = ""; + const wallet = await getSaveWallet(); + const address = wallet.address0; + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const arrrSeed58 = parsedData.arrrSeed58; + if (coin === "ARRR") { + const bodyToString = arrrSeed58 + const url = await createEndpoint(`/crosschain/arrr/walletaddress`); + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: bodyToString, + }); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + if(res?.error && res?.message){ + throw new Error(res.message) + } + arrrAddress = res; + } + switch (coin) { + case "QORT": + userWallet["address"] = address; + userWallet["publickey"] = parsedData.publicKey; + break; + case "BTC": + userWallet["address"] = parsedData.btcAddress; + userWallet["publickey"] = parsedData.derivedMasterPublicKey; + break; + case "LTC": + userWallet["address"] = parsedData.ltcAddress; + userWallet["publickey"] = parsedData.ltcPublicKey; + break; + case "DOGE": + userWallet["address"] = parsedData.dogeAddress; + userWallet["publickey"] = parsedData.dogePublicKey; + break; + case "DGB": + userWallet["address"] = parsedData.dgbAddress; + userWallet["publickey"] = parsedData.dgbPublicKey; + break; + case "RVN": + userWallet["address"] = parsedData.rvnAddress; + userWallet["publickey"] = parsedData.rvnPublicKey; + break; + case "ARRR": + userWallet["address"] = arrrAddress; + break; + default: + break; + } + return userWallet; + } else { + throw new Error("User declined request"); + } +}; export const sendCoin = async () => { try { From f1dc7210f448cf70a32444892a5e73c42a40b207 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Wed, 16 Oct 2024 06:38:33 +0300 Subject: [PATCH 13/58] qortalrequest wallet balance --- public/content-script.js | 2 +- src/qortalRequests.ts | 15 ++++- src/qortalRequests/get.ts | 116 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 127 insertions(+), 6 deletions(-) diff --git a/public/content-script.js b/public/content-script.js index 75a77c6..e12ac3e 100644 --- a/public/content-script.js +++ b/public/content-script.js @@ -774,7 +774,7 @@ async function storeFilesInIndexedDB(obj) { -const UIQortalRequests = ['GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL', 'SEND_CHAT_MESSAGE', 'JOIN_GROUP', 'DEPLOY_AT', 'GET_USER_WALLET'] +const UIQortalRequests = ['GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL', 'SEND_CHAT_MESSAGE', 'JOIN_GROUP', 'DEPLOY_AT', 'GET_USER_WALLET', 'GET_WALLET_BALANCE'] if (!window.hasAddedQortalListener) { console.log("Listener added"); diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index 210e4f9..ab54557 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -1,4 +1,4 @@ -import { addListItems, createPoll, decryptData, deleteListItems, deployAt, encryptData, getListItems, getUserAccount, getUserWallet, joinGroup, publishMultipleQDNResources, publishQDNResource, saveFile, sendChatMessage, sendCoin, voteOnPoll } from "./qortalRequests/get"; +import { addListItems, createPoll, decryptData, deleteListItems, deployAt, encryptData, getListItems, getUserAccount, getUserWallet, getWalletBalance, joinGroup, publishMultipleQDNResources, publishQDNResource, saveFile, sendChatMessage, sendCoin, voteOnPoll } from "./qortalRequests/get"; @@ -257,6 +257,19 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { break; } + case "GET_WALLET_BALANCE": { + const data = request.payload; + + getWalletBalance(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } case "SEND_COIN": { const data = request.payload; const requiredFields = ["coin", "destinationAddress", "amount"]; diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index c422f36..171ebaa 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -1291,7 +1291,8 @@ export const getUserWallet = async (data) => { throw new Error(errorMsg); } const resPermission = await getUserPermission({ - text1: "Do you give this application permission to get your wallet information?", + text1: + "Do you give this application permission to get your wallet information?", }); const { accepted } = resPermission; @@ -1305,7 +1306,7 @@ export const getUserWallet = async (data) => { const parsedData = JSON.parse(resKeyPair); const arrrSeed58 = parsedData.arrrSeed58; if (coin === "ARRR") { - const bodyToString = arrrSeed58 + const bodyToString = arrrSeed58; const url = await createEndpoint(`/crosschain/arrr/walletaddress`); const response = await fetch(url, { method: "POST", @@ -1320,8 +1321,8 @@ export const getUserWallet = async (data) => { } catch (e) { res = await response.text(); } - if(res?.error && res?.message){ - throw new Error(res.message) + if (res?.error && res?.message) { + throw new Error(res.message); } arrrAddress = res; } @@ -1362,6 +1363,113 @@ export const getUserWallet = async (data) => { } }; +export const getWalletBalance = async (data) => { + const requiredFields = ["coin"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + + const resPermission = await getUserPermission({ + text1: "Do you give this application permission to fetch your", + highlightedText: `${data.coin} balance`, + }); + const { accepted } = resPermission; + + if (accepted) { + let coin = data.coin; + const wallet = await getSaveWallet(); + const address = wallet.address0; + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + if (coin === "QORT") { + let qortAddress = address; + try { + const url = await createEndpoint(`/addresses/balance/${qortAddress}`); + const response = await fetch(url); + if (!response.ok) throw new Error("Failed to fetch"); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + return res; + } catch (error) { + throw new Error( + error?.message || "Fetch Wallet Failed. Please try again" + ); + } + } else { + let _url = ``; + let _body = null; + switch (coin) { + case "BTC": + _url = await createEndpoint(`/crosschain/btc/walletbalance`); + + _body = parsedData.derivedMasterPublicKey; + break; + case "LTC": + _url = await createEndpoint(`/crosschain/ltc/walletbalance`); + _body = parsedData.ltcPublicKey; + break; + case "DOGE": + _url = await createEndpoint(`/crosschain/doge/walletbalance`); + _body = parsedData.dogePublicKey; + break; + case "DGB": + _url = await createEndpoint(`/crosschain/dgb/walletbalance`); + _body = parsedData.dgbPublicKey; + break; + case "RVN": + _url = await createEndpoint(`/crosschain/rvn/walletbalance`); + _body = parsedData.rvnPublicKey; + break; + case "ARRR": + _url = await createEndpoint(`/crosschain/arrr/walletbalance`); + _body = parsedData.arrrSeed58; + break; + default: + break; + } + try { + const response = await fetch(_url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: _body, + }); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + if (res?.error && res?.message) { + throw new Error(res.message); + } + if (isNaN(Number(res))) { + throw new Error("Unable to fetch balance"); + } else { + return (Number(res) / 1e8).toFixed(8); + } + } catch (error) { + throw new Error(error?.message || "Unable to fetch balance"); + } + } + } else { + throw new Error("User declined request"); + } +}; + export const sendCoin = async () => { try { const wallet = await getSaveWallet(); From d26d6584b81b0ffb8637e12f8df9af97dec5291d Mon Sep 17 00:00:00 2001 From: PhilReact Date: Wed, 16 Oct 2024 07:18:46 +0300 Subject: [PATCH 14/58] wallet info qortalRequest --- public/content-script.js | 2 +- src/qortalRequests.ts | 16 ++++++- src/qortalRequests/get.ts | 95 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 2 deletions(-) diff --git a/public/content-script.js b/public/content-script.js index e12ac3e..0585cd8 100644 --- a/public/content-script.js +++ b/public/content-script.js @@ -774,7 +774,7 @@ async function storeFilesInIndexedDB(obj) { -const UIQortalRequests = ['GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL', 'SEND_CHAT_MESSAGE', 'JOIN_GROUP', 'DEPLOY_AT', 'GET_USER_WALLET', 'GET_WALLET_BALANCE'] +const UIQortalRequests = ['GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL', 'SEND_CHAT_MESSAGE', 'JOIN_GROUP', 'DEPLOY_AT', 'GET_USER_WALLET', 'GET_WALLET_BALANCE', 'GET_USER_WALLET_INFO'] if (!window.hasAddedQortalListener) { console.log("Listener added"); diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index ab54557..4f21978 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -1,4 +1,4 @@ -import { addListItems, createPoll, decryptData, deleteListItems, deployAt, encryptData, getListItems, getUserAccount, getUserWallet, getWalletBalance, joinGroup, publishMultipleQDNResources, publishQDNResource, saveFile, sendChatMessage, sendCoin, voteOnPoll } from "./qortalRequests/get"; +import { addListItems, createPoll, decryptData, deleteListItems, deployAt, encryptData, getListItems, getUserAccount, getUserWallet, getUserWalletInfo, getWalletBalance, joinGroup, publishMultipleQDNResources, publishQDNResource, saveFile, sendChatMessage, sendCoin, voteOnPoll } from "./qortalRequests/get"; @@ -270,6 +270,20 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { break; } + + case "GET_USER_WALLET_INFO": { + const data = request.payload; + + getUserWalletInfo(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } case "SEND_COIN": { const data = request.payload; const requiredFields = ["coin", "destinationAddress", "amount"]; diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index 171ebaa..31572a1 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -1470,6 +1470,101 @@ export const getWalletBalance = async (data) => { } }; +const getUserWalletFunc = async (coin) => { + let userWallet = {}; + const wallet = await getSaveWallet(); + const address = wallet.address0; + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + console.log('coin', coin) + switch (coin) { + case "QORT": + userWallet["address"] = address; + userWallet["publickey"] = parsedData.publicKey; + break; + case "BTC": + userWallet["address"] = parsedData.btcAddress; + userWallet["publickey"] = parsedData.btcPublicKey; + break; + case "LTC": + userWallet["address"] = parsedData.ltcAddress; + userWallet["publickey"] = parsedData.ltcPublicKey; + break; + case "DOGE": + userWallet["address"] = parsedData.dogeAddress; + userWallet["publickey"] = parsedData.dogePublicKey; + break; + case "DGB": + userWallet["address"] = parsedData.dgbAddress; + userWallet["publickey"] = parsedData.dgbPublicKey; + break; + case "RVN": + userWallet["address"] = parsedData.rvnAddress; + userWallet["publickey"] = parsedData.rvnPublicKey; + break; + case "ARRR": + break; + default: + break; + } + return userWallet; +}; + +export const getUserWalletInfo = async (data) => { + const requiredFields = ["coin"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + const resPermission = await getUserPermission({ + text1: "Do you give this application permission to retrieve your wallet information", + }); + const { accepted } = resPermission; + + if (accepted) { + let coin = data.coin; + let walletKeys = await getUserWalletFunc(coin); + console.log('walletKeys', walletKeys) + console.log('walletKeys["publickey"]', walletKeys["publickey"]) + const _url = await createEndpoint( + `/crosschain/` + data.coin.toLowerCase() + `/addressinfos` + ); + let _body = { xpub58: walletKeys["publickey"] }; + try { + const response = await fetch(_url, { + method: "POST", + headers: { + Accept: "*/*", + "Content-Type": "application/json", + }, + body: JSON.stringify(_body), + }); + if(!response?.ok) throw new Error('Unable to fetch wallet information') + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + if (res?.error && res?.message) { + throw new Error(res.message); + } + + return res; + } catch (error) { + throw new Error(error?.message || "Fetch Wallet Failed"); + } + } else { + throw new Error("User declined request"); + } +}; export const sendCoin = async () => { try { const wallet = await getSaveWallet(); From 2e030049381b0072dc7abd10b9e15df058527899 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Wed, 16 Oct 2024 07:50:18 +0300 Subject: [PATCH 15/58] added crosschain qortalRequests --- public/content-script.js | 2 +- src/qortalRequests.ts | 127 +++++++++++- src/qortalRequests/get.ts | 421 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 548 insertions(+), 2 deletions(-) diff --git a/public/content-script.js b/public/content-script.js index 0585cd8..6301404 100644 --- a/public/content-script.js +++ b/public/content-script.js @@ -774,7 +774,7 @@ async function storeFilesInIndexedDB(obj) { -const UIQortalRequests = ['GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL', 'SEND_CHAT_MESSAGE', 'JOIN_GROUP', 'DEPLOY_AT', 'GET_USER_WALLET', 'GET_WALLET_BALANCE', 'GET_USER_WALLET_INFO'] +const UIQortalRequests = ['GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL', 'SEND_CHAT_MESSAGE', 'JOIN_GROUP', 'DEPLOY_AT', 'GET_USER_WALLET', 'GET_WALLET_BALANCE', 'GET_USER_WALLET_INFO', 'GET_CROSSCHAIN_SERVER_INFO', 'GET_TX_ACTIVITY_SUMMARY', 'GET_FOREIGN_FEE', 'UPDATE_FOREIGN_FEE', 'GET_SERVER_CONNECTION_HISTORY', 'SET_CURRENT_FOREIGN_SERVER', 'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY'] if (!window.hasAddedQortalListener) { console.log("Listener added"); diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index 4f21978..7557a41 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -1,4 +1,4 @@ -import { addListItems, createPoll, decryptData, deleteListItems, deployAt, encryptData, getListItems, getUserAccount, getUserWallet, getUserWalletInfo, getWalletBalance, joinGroup, publishMultipleQDNResources, publishQDNResource, saveFile, sendChatMessage, sendCoin, voteOnPoll } from "./qortalRequests/get"; +import { addForeignServer, addListItems, createPoll, decryptData, deleteListItems, deployAt, encryptData, getCrossChainServerInfo, getDaySummary, getForeignFee, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getWalletBalance, joinGroup, publishMultipleQDNResources, publishQDNResource, removeForeignServer, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, updateForeignFee, voteOnPoll } from "./qortalRequests/get"; @@ -284,6 +284,131 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { break; } + case "GET_CROSSCHAIN_SERVER_INFO": { + const data = request.payload; + + getCrossChainServerInfo(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } + case "GET_TX_ACTIVITY_SUMMARY": { + const data = request.payload; + + getTxActivitySummary(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } + + case "GET_FOREIGN_FEE": { + const data = request.payload; + + getForeignFee(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } + + case "UPDATE_FOREIGN_FEE": { + const data = request.payload; + + updateForeignFee(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } + + case "GET_SERVER_CONNECTION_HISTORY": { + const data = request.payload; + + getServerConnectionHistory(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } + + case "SET_CURRENT_FOREIGN_SERVER": { + const data = request.payload; + + setCurrentForeignServer(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } + + case "ADD_FOREIGN_SERVER": { + const data = request.payload; + + addForeignServer(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } + + case "REMOVE_FOREIGN_SERVER": { + const data = request.payload; + + removeForeignServer(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } + + case "GET_DAY_SUMMARY": { + const data = request.payload; + + getDaySummary(data) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + + break; + } + case "SEND_COIN": { const data = request.payload; const requiredFields = ["coin", "destinationAddress", "amount"]; diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index 31572a1..eeaf871 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -1565,6 +1565,427 @@ export const getUserWalletInfo = async (data) => { throw new Error("User declined request"); } }; + +export const getCrossChainServerInfo = async (data)=> { + const requiredFields = ['coin'] + const missingFields: string[] = [] + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field) + } + }) + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', ') + const errorMsg = `Missing fields: ${missingFieldsString}` + throw new Error(errorMsg) + } + let _url = `/crosschain/` + data.coin.toLowerCase() + `/serverinfos` + try { + + + const url = await createEndpoint(_url); + const response = await fetch(url); + if (!response.ok) throw new Error("Failed to fetch"); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + if (res?.error && res?.message) { + throw new Error(res.message); + } + return res.servers + } catch (error) { + + throw new Error(error?.message || 'Error in retrieving server info') + } +} + +export const getTxActivitySummary = async (data) => { + const requiredFields = ['coin']; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + + const coin = data.coin; + const url = `/crosschain/txactivity?foreignBlockchain=${coin}`; // No apiKey here + + try { + const endpoint = await createEndpoint(url); + const response = await fetch(endpoint, { + method: 'POST', + headers: { + Accept: '*/*', + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) throw new Error('Failed to fetch'); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + if (res?.error && res?.message) { + throw new Error(res.message); + } + return res; // Return full response here + } catch (error) { + throw new Error(error?.message || 'Error in tx activity summary'); + } + }; + + export const getForeignFee = async (data) => { + const requiredFields = ['coin', 'type']; + const missingFields: string[] = []; + + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + + const { coin, type } = data; + const url = `/crosschain/${coin}/${type}`; + + try { + const endpoint = await createEndpoint(url); + const response = await fetch(endpoint, { + method: 'GET', + headers: { + Accept: '*/*', + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) throw new Error('Failed to fetch'); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + if (res?.error && res?.message) { + throw new Error(res.message); + } + return res; // Return full response here + } catch (error) { + throw new Error(error?.message || 'Error in get foreign fee'); + } + }; + + export const updateForeignFee = async (data) => { + const requiredFields = ['coin', 'type', 'value']; + const missingFields: string[] = []; + + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + + const { coin, type, value } = data; + const url = `/crosschain/${coin}/update${type}`; + + try { + const endpoint = await createEndpoint(url); + const response = await fetch(endpoint, { + method: 'POST', + headers: { + Accept: '*/*', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ value }), + }); + + if (!response.ok) throw new Error('Failed to update foreign fee'); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + if (res?.error && res?.message) { + throw new Error(res.message); + } + return res; // Return full response here + } catch (error) { + throw new Error(error?.message || 'Error in update foreign fee'); + } + }; + + export const getServerConnectionHistory = async (data) => { + const requiredFields = ['coin']; + const missingFields: string[] = []; + + // Validate required fields + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + + const coin = data.coin.toLowerCase(); + const url = `/crosschain/${coin}/serverconnectionhistory`; + + try { + const endpoint = await createEndpoint(url); // Assuming createEndpoint is available + const response = await fetch(endpoint, { + method: 'GET', + headers: { + Accept: '*/*', + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) throw new Error('Failed to fetch server connection history'); + + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + + if (res?.error && res?.message) { + throw new Error(res.message); + } + + return res; // Return full response here + } catch (error) { + throw new Error(error?.message || 'Error in get server connection history'); + } + }; + + export const setCurrentForeignServer = async (data) => { + const requiredFields = ['coin']; + const missingFields: string[] = []; + + // Validate required fields + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + + const { coin, host, port, type } = data; + const body = { + hostName: host, + port: port, + connectionType: type, + }; + + const url = `/crosschain/${coin}/setcurrentserver`; + + try { + const endpoint = await createEndpoint(url); // Assuming createEndpoint is available + const response = await fetch(endpoint, { + method: 'POST', + headers: { + Accept: '*/*', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!response.ok) throw new Error('Failed to set current server'); + + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + + if (res?.error && res?.message) { + throw new Error(res.message); + } + + return res; // Return the full response + } catch (error) { + throw new Error(error?.message || 'Error in set current server'); + } + }; + + + export const addForeignServer = async (data) => { + const requiredFields = ['coin']; + const missingFields: string[] = []; + + // Validate required fields + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + + const { coin, host, port, type } = data; + const body = { + hostName: host, + port: port, + connectionType: type, + }; + + const url = `/crosschain/${coin}/addserver`; + + try { + const endpoint = await createEndpoint(url); // Assuming createEndpoint is available + const response = await fetch(endpoint, { + method: 'POST', + headers: { + Accept: '*/*', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!response.ok) throw new Error('Failed to add server'); + + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + + if (res?.error && res?.message) { + throw new Error(res.message); + } + + return res; // Return the full response + } catch (error) { + throw new Error(error.message || 'Error in adding server'); + } + }; + + export const removeForeignServer = async (data) => { + const requiredFields = ['coin']; + const missingFields: string[] = []; + + // Validate required fields + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + + const { coin, host, port, type } = data; + const body = { + hostName: host, + port: port, + connectionType: type, + }; + + const url = `/crosschain/${coin}/removeserver`; + + try { + const endpoint = await createEndpoint(url); // Assuming createEndpoint is available + const response = await fetch(endpoint, { + method: 'POST', + headers: { + Accept: '*/*', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!response.ok) throw new Error('Failed to remove server'); + + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + + if (res?.error && res?.message) { + throw new Error(res.message); + } + + return res; // Return the full response + } catch (error) { + throw new Error(error?.message || 'Error in removing server'); + } + }; + + export const getDaySummary = async () => { + const url = `/admin/summary`; // Simplified endpoint URL + + try { + const endpoint = await createEndpoint(url); // Assuming createEndpoint is available for constructing the full URL + const response = await fetch(endpoint, { + method: 'GET', + headers: { + Accept: '*/*', + }, + }); + + if (!response.ok) throw new Error('Failed to retrieve summary'); + + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + + if (res?.error && res?.message) { + throw new Error(res.message); + } + + return res; // Return the full response + } catch (error) { + throw new Error(error?.message || 'Error in retrieving summary'); + } + }; + export const sendCoin = async () => { try { const wallet = await getSaveWallet(); From ca00025c862162aba1d55fb24c054c38434fdb23 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Wed, 16 Oct 2024 08:15:27 +0300 Subject: [PATCH 16/58] added auth request --- public/content-script.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/public/content-script.js b/public/content-script.js index 6301404..caeefd1 100644 --- a/public/content-script.js +++ b/public/content-script.js @@ -788,6 +788,27 @@ if (!window.hasAddedQortalListener) { if (event.source !== window || !event.data || !event.data.action) return; if (event?.data?.requestedHandler !== 'UI') return; + + await new Promise((res)=> { + chrome?.runtime?.sendMessage( + { + action: "authentication", + timeout: 60, + }, + (response) => { + if (response.error) { + eventPort.postMessage({ + result: null, + error: 'User not authenticated', + }); + res() + return + } else { + res() + } + } + ); + }) const sendMessageToRuntime = (message, eventPort) => { chrome?.runtime?.sendMessage(message, (response) => { From f1e2192096e50615394785dbf039513208d5612b Mon Sep 17 00:00:00 2001 From: PhilReact Date: Wed, 16 Oct 2024 19:57:19 +0300 Subject: [PATCH 17/58] allow ext to work with gateways --- public/disable-gateway-message.js | 26 ++++++++++++++++++++++++++ public/disable-gateway-popup.js | 21 +++++++++++++++++++++ public/document_end.js | 7 +++++++ public/document_start.js | 7 +++++++ public/manifest.json | 22 ++++++++++++++++++++++ 5 files changed, 83 insertions(+) create mode 100644 public/disable-gateway-message.js create mode 100644 public/disable-gateway-popup.js create mode 100644 public/document_end.js create mode 100644 public/document_start.js diff --git a/public/disable-gateway-message.js b/public/disable-gateway-message.js new file mode 100644 index 0000000..c395aff --- /dev/null +++ b/public/disable-gateway-message.js @@ -0,0 +1,26 @@ +(function() { + // Immediately disable qdnGatewayShowModal if it exists + + + // Now, let's wrap the handleResponse function with the new condition + const originalHandleResponse = window.handleResponse; // Save the original handleResponse function + + if (typeof originalHandleResponse === 'function') { + // Create the wrapper function to enhance the original handleResponse + window.handleResponse = function(event, response) { + // Check if the response contains the specific error message + if (response && typeof response === 'string' && response.includes("Interactive features were requested")) { + console.log('Response contains "Interactive features were requested", skipping processing.'); + return; // Skip further processing + } + + // Call the original handleResponse for normal processing + originalHandleResponse(event, response); + }; + + console.log('handleResponse has been enhanced to skip specific error handling.'); + } else { + console.log('No handleResponse function found to enhance.'); + } + +})(); diff --git a/public/disable-gateway-popup.js b/public/disable-gateway-popup.js new file mode 100644 index 0000000..58ee0b3 --- /dev/null +++ b/public/disable-gateway-popup.js @@ -0,0 +1,21 @@ +(function() { + console.log('External script loaded to disable qdnGatewayShowModal'); + + // Poll for qdnGatewayShowModal and disable it once it's defined + const checkQdnGatewayInterval = setInterval(() => { + if (typeof window.qdnGatewayShowModal === 'function') { + console.log('Disabling qdnGatewayShowModal'); + + // Disable qdnGatewayShowModal function + window.qdnGatewayShowModal = function(message) { + console.log('qdnGatewayShowModal function has been disabled.'); + }; + + // Stop checking once qdnGatewayShowModal has been disabled + clearInterval(checkQdnGatewayInterval); + } else { + console.log('Waiting for qdnGatewayShowModal to be defined...'); + } + }, 100); // Check every 100ms + +})(); diff --git a/public/document_end.js b/public/document_end.js new file mode 100644 index 0000000..748d513 --- /dev/null +++ b/public/document_end.js @@ -0,0 +1,7 @@ + +const script2 = document.createElement('script'); +script2.src = chrome.runtime.getURL('disable-gateway-message.js'); // Reference the external script +document.documentElement.appendChild(script2); // Inject it into the page +script2.onload = function() { + script2.remove(); // Clean up after the script has been injected and run +}; \ No newline at end of file diff --git a/public/document_start.js b/public/document_start.js new file mode 100644 index 0000000..19c7adf --- /dev/null +++ b/public/document_start.js @@ -0,0 +1,7 @@ + +const script = document.createElement('script'); +script.src = chrome.runtime.getURL('disable-gateway-popup.js'); // Reference the external script +document.documentElement.appendChild(script); // Inject it into the page +script.onload = function() { + script.remove(); // Clean up after the script has been injected and run +}; \ No newline at end of file diff --git a/public/manifest.json b/public/manifest.json index 70c4ce3..5d88b27 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -18,11 +18,33 @@ ], "content_scripts": [ + { + "matches": [""], + "js": ["document_start.js"], + "run_at": "document_start" + }, { "matches": [""], "js": ["content-script.js"] + }, + + { + "matches": [""], + "js": ["document_end.js"], + "run_at": "document_end" } ], + "web_accessible_resources": [ + { + "resources": ["disable-gateway-popup.js"], + "matches": [""] + }, + { + "resources": ["disable-gateway-message.js"], + "matches": [""] + } + + ], "content_security_policy": { "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; connect-src 'self' https://api.qortal.org https://api2.qortal.org https://appnode.qortal.org https://apinode.qortalnodes.live https://apinode1.qortalnodes.live https://apinode2.qortalnodes.live https://apinode3.qortalnodes.live https://apinode4.qortalnodes.live https://ext-node.qortal.link wss://appnode.qortal.org wss://ext-node.qortal.link ws://127.0.0.1:12391 http://127.0.0.1:12391 https://ext-node.qortal.link; " } From d7a226e47c36dea494b2afd015a1c96c7ceab93e Mon Sep 17 00:00:00 2001 From: PhilReact Date: Wed, 16 Oct 2024 20:20:22 +0300 Subject: [PATCH 18/58] fix bug encryption qortalrequest --- public/content-script.js | 1 + src/qortalRequests/get.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/public/content-script.js b/public/content-script.js index caeefd1..9864e06 100644 --- a/public/content-script.js +++ b/public/content-script.js @@ -835,6 +835,7 @@ if (!window.hasAddedQortalListener) { event.ports[0] ); } else if (event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' || event?.data?.action === 'PUBLISH_QDN_RESOURCE' || event?.data?.action === 'ENCRYPT_DATA' || event?.data?.action === 'SAVE_FILE') { + console.log('event?.data?', event?.data) let data; try { data = await storeFilesInIndexedDB(event.data); diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index eeaf871..8972ff9 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -594,9 +594,15 @@ export const publishQDNResource = async (data: any, sender) => { } if (data.encrypt) { try { + const resKeyPair = await getKeyPair() + const parsedData = JSON.parse(resKeyPair) + const privateKey = parsedData.privateKey + const userPublicKey = parsedData.publicKey const encryptDataResponse = encryptDataGroup({ data64, publicKeys: data.publicKeys, + privateKey, + userPublicKey }); if (encryptDataResponse) { data64 = encryptDataResponse; @@ -810,9 +816,15 @@ export const publishMultipleQDNResources = async (data: any, sender) => { } if (data.encrypt) { try { + const resKeyPair = await getKeyPair() + const parsedData = JSON.parse(resKeyPair) + const privateKey = parsedData.privateKey + const userPublicKey = parsedData.publicKey const encryptDataResponse = encryptDataGroup({ data64, publicKeys: data.publicKeys, + privateKey, + userPublicKey }); if (encryptDataResponse) { data64 = encryptDataResponse; From 425602d9e2c6e2b3a566a99f2234298a0320257f Mon Sep 17 00:00:00 2001 From: PhilReact Date: Wed, 16 Oct 2024 23:42:55 +0300 Subject: [PATCH 19/58] added send coin --- public/disable-gateway-popup.js | 12 +- src/background.ts | 23 +- src/qortalRequests.ts | 19 +- src/qortalRequests/get.ts | 451 ++++++++++++++++++++++++++++++-- 4 files changed, 467 insertions(+), 38 deletions(-) diff --git a/public/disable-gateway-popup.js b/public/disable-gateway-popup.js index 58ee0b3..7cf89e4 100644 --- a/public/disable-gateway-popup.js +++ b/public/disable-gateway-popup.js @@ -1,8 +1,13 @@ (function() { console.log('External script loaded to disable qdnGatewayShowModal'); + const timeoutDuration = 5000; // Set timeout duration to 5 seconds (5000ms) + let elapsedTime = 0; // Track the time that has passed + // Poll for qdnGatewayShowModal and disable it once it's defined const checkQdnGatewayInterval = setInterval(() => { + elapsedTime += 100; // Increment elapsed time by the polling interval (100ms) + if (typeof window.qdnGatewayShowModal === 'function') { console.log('Disabling qdnGatewayShowModal'); @@ -13,9 +18,10 @@ // Stop checking once qdnGatewayShowModal has been disabled clearInterval(checkQdnGatewayInterval); - } else { - console.log('Waiting for qdnGatewayShowModal to be defined...'); - } + } else if (elapsedTime >= timeoutDuration) { + console.log('Timeout reached, stopping polling for qdnGatewayShowModal.'); + clearInterval(checkQdnGatewayInterval); // Stop checking after 5 seconds + } }, 100); // Check every 100ms })(); diff --git a/src/background.ts b/src/background.ts index 60ec22c..40e6e30 100644 --- a/src/background.ts +++ b/src/background.ts @@ -135,6 +135,15 @@ export const getBaseApi = async (customApi?: string) => { return groupApi; } }; +export const isUsingLocal = async () => { + + const apiKey = await getApiKeyFromStorage(); // Retrieve apiKey asynchronously + if (apiKey) { + return true + } else { + return false; + } +}; export const createEndpointSocket = async (endpoint) => { const apiKey = await getApiKeyFromStorage(); // Retrieve apiKey asynchronously @@ -1152,7 +1161,7 @@ export const getLastRef = async () => { const data = await response.text(); return data; }; -const sendQortFee = async () => { +export const sendQortFee = async (): Promise => { const validApi = await getBaseApi(); const response = await fetch( validApi + "/transactions/unitfee?txType=PAYMENT" @@ -1355,13 +1364,21 @@ async function decryptWallet({ password, wallet, walletVersion }) { arrrSeed58: wallet2._addresses[0].arrrWallet.seed58, btcAddress: wallet2._addresses[0].btcWallet.address, btcPublicKey: wallet2._addresses[0].btcWallet.derivedMasterPublicKey, + btcPrivateKey: wallet2._addresses[0].btcWallet.derivedMasterPrivateKey, + ltcAddress: wallet2._addresses[0].ltcWallet.address, + dogeAddress: wallet2._addresses[0].dogeWallet.address, dogePublicKey: wallet2._addresses[0].dogeWallet.derivedMasterPublicKey, + dogePrivateKey: wallet2._addresses[0].dogeWallet.derivedMasterPrivateKey, + dgbAddress: wallet2._addresses[0].dgbWallet.address, dgbPublicKey: wallet2._addresses[0].dgbWallet.derivedMasterPublicKey, + dgbPrivateKey: wallet2._addresses[0].dgbWallet.derivedMasterPrivateKey, + rvnAddress: wallet2._addresses[0].rvnWallet.address, - rvnPublicKey: wallet2._addresses[0].rvnWallet.derivedMasterPublicKey + rvnPublicKey: wallet2._addresses[0].rvnWallet.derivedMasterPublicKey, + rvnPrivateKey: wallet2._addresses[0].rvnWallet.derivedMasterPrivateKey }; const dataString = JSON.stringify(toSave); await new Promise((resolve, reject) => { @@ -2278,7 +2295,7 @@ async function inviteToGroup({ groupId, qortalAddress, inviteTime }) { return res; } -async function sendCoin({ password, amount, receiver }, skipConfirmPassword) { +export async function sendCoin({ password, amount, receiver }, skipConfirmPassword) { try { const confirmReceiver = await getNameOrAddress(receiver); if (confirmReceiver.error) diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index 7557a41..8f1490a 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -411,26 +411,13 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { case "SEND_COIN": { const data = request.payload; - const requiredFields = ["coin", "destinationAddress", "amount"]; - const missingFields: string[] = []; - requiredFields.forEach((field) => { - if (!data[field]) { - missingFields.push(field); - } - }); - if (missingFields.length > 0) { - const missingFieldsString = missingFields.join(", "); - const errorMsg = `Missing fields: ${missingFieldsString}`; - sendResponse({ error: errorMsg }); - break; - } - // Example: respond with the version - sendCoin() + + sendCoin(data) .then((res) => { sendResponse(res); }) .catch((error) => { - sendResponse({ error: "Unable to get user account" }); + sendResponse({ error: error.message }); }); break; diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index 8972ff9..c961f5c 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -10,8 +10,12 @@ import { removeDuplicateWindow, signChatFunc, joinGroup as joinGroupFunc, + sendQortFee, + sendCoin as sendCoinFunc, + isUsingLocal } from "../background"; import { getNameInfo } from "../backgroundFunctions/encryption"; +import { QORT_DECIMALS } from "../constants/constants"; import Base58 from "../deps/Base58"; import { base64ToUint8Array, @@ -27,6 +31,14 @@ import { createTransaction } from "../transactions/transactions"; import { fileToBase64 } from "../utils/fileReading"; import { mimeToExtensionMap } from "../utils/memeTypes"; + +const btcFeePerByte = 0.00000100 + const ltcFeePerByte = 0.00000030 + const dogeFeePerByte = 0.00001000 + const dgbFeePerByte = 0.00000010 + const rvnFeePerByte = 0.00001125 + + const _createPoll = async (pollName, pollDescription, options) => { const fee = await getFee("CREATE_POLL"); @@ -1375,7 +1387,7 @@ export const getUserWallet = async (data) => { } }; -export const getWalletBalance = async (data) => { +export const getWalletBalance = async (data, bypassPermission?: boolean) => { const requiredFields = ["coin"]; const missingFields: string[] = []; requiredFields.forEach((field) => { @@ -1388,14 +1400,22 @@ export const getWalletBalance = async (data) => { const errorMsg = `Missing fields: ${missingFieldsString}`; throw new Error(errorMsg); } + let resPermission - const resPermission = await getUserPermission({ - text1: "Do you give this application permission to fetch your", - highlightedText: `${data.coin} balance`, - }); + if(!bypassPermission){ + resPermission = await getUserPermission({ + text1: "Do you give this application permission to fetch your", + highlightedText: `${data.coin} balance`, + }); + } else { + resPermission = { + accepted: false + } + } + const { accepted } = resPermission; - if (accepted) { + if (accepted || bypassPermission) { let coin = data.coin; const wallet = await getSaveWallet(); const address = wallet.address0; @@ -1998,16 +2018,415 @@ export const getTxActivitySummary = async (data) => { } }; -export const sendCoin = async () => { - try { +export const sendCoin = async (data) => { + const requiredFields = ['coin', 'destinationAddress', 'amount'] + const missingFields: string[] = [] + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field) + } + }) + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', ') + const errorMsg = `Missing fields: ${missingFieldsString}` + throw new Error(errorMsg) + } + let checkCoin = data.coin const wallet = await getSaveWallet(); const address = wallet.address0; - const publicKey = wallet.publicKey; - return { - address, - publicKey, - }; - } catch (error) { - console.error(error); - } + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const localNodeAvailable = await isUsingLocal() + if(checkCoin !== 'QORT' && !localNodeAvailable) throw new Error('Cannot send a non-QORT coin through the gateway. Please use your local node.') + if (checkCoin === "QORT") { + // Params: data.coin, data.destinationAddress, data.amount, data.fee + // TODO: prompt user to send. If they confirm, call `POST /crosschain/:coin/send`, or for QORT, broadcast a PAYMENT transaction + // then set the response string from the core to the `response` variable (defined above) + // If they decline, send back JSON that includes an `error` key, such as `{"error": "User declined request"}` + const amount = Number(data.amount) + const recipient = data.destinationAddress + + const url = await createEndpoint(`/addresses/balance/${address}`); + console.log("url", url); + const response = await fetch(url); + console.log("response", response); + if (!response.ok) throw new Error("Failed to fetch"); + let walletBalance; + try { + walletBalance = await response.clone().json(); + } catch (e) { + walletBalance = await response.text(); + } + if (isNaN(Number(walletBalance))) { + let errorMsg = "Failed to Fetch QORT Balance. Try again!" + throw new Error(errorMsg) + } + + const transformDecimals = (Number(walletBalance) * QORT_DECIMALS).toFixed(0) + const walletBalanceDecimals = Number(transformDecimals) + const amountDecimals = Number(amount) * QORT_DECIMALS + const fee: number = await sendQortFee() + if (amountDecimals + (fee * QORT_DECIMALS) > walletBalanceDecimals) { + let errorMsg = "Insufficient Funds!" + throw new Error(errorMsg) + } + if (amount <= 0) { + let errorMsg = "Invalid Amount!" + throw new Error(errorMsg) + } + if (recipient.length === 0) { + let errorMsg = "Receiver cannot be empty!" + throw new Error(errorMsg) + } + + const resPermission = await getUserPermission({ + text1: "Do you give this application permission to send coins?", + text2: `To: ${recipient}`, + highlightedText: `${amount} ${checkCoin}`, + }); + const { accepted } = resPermission; + + if (accepted) { + const makePayment = await sendCoinFunc({amount, password: null, receiver: recipient }, true) + return makePayment.res + } else { + throw new Error("User declined request") + } + + } else if (checkCoin === "BTC") { + const amount = Number(data.amount) + const recipient = data.destinationAddress + const xprv58 = parsedData.btcPrivateKey + const feePerByte = data.fee ? data.fee : btcFeePerByte + + const btcWalletBalance = await getWalletBalance({coin: checkCoin}, true) + + if (isNaN(Number(btcWalletBalance))) { + throw new Error('Unable to fetch BTC balance') + } + const btcWalletBalanceDecimals = Number(btcWalletBalance) + const btcAmountDecimals = Number(amount) * QORT_DECIMALS + const fee = feePerByte * 500 // default 0.00050000 + if (btcAmountDecimals + (fee * QORT_DECIMALS) > btcWalletBalanceDecimals) { + throw new Error("INSUFFICIENT_FUNDS") + } + + const resPermission = await getUserPermission({ + text1: "Do you give this application permission to send coins?", + text2: `To: ${recipient}`, + highlightedText: `${amount} ${checkCoin}`, + fee: fee + }); + const { accepted } = resPermission; + + if (accepted) { + const opts = { + xprv58: xprv58, + receivingAddress: recipient, + bitcoinAmount: amount, + feePerByte: feePerByte * QORT_DECIMALS + } + const url = await createEndpoint(`/crosschain/btc/send`); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(opts) + }) + if (!response.ok) throw new Error("Failed to send"); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + return res; + + } else { + throw new Error("User declined request") + } + + } else if (checkCoin === "LTC") { + + const amount = Number(data.amount) + const recipient = data.destinationAddress + const xprv58 = parsedData.ltcPrivateKey + const feePerByte = data.fee ? data.fee : ltcFeePerByte + const ltcWalletBalance = await getWalletBalance({coin: checkCoin}, true) + + if (isNaN(Number(ltcWalletBalance))) { + let errorMsg = "Failed to Fetch LTC Balance. Try again!" + throw new Error(errorMsg) + } + const ltcWalletBalanceDecimals = Number(ltcWalletBalance) + const ltcAmountDecimals = Number(amount) * QORT_DECIMALS + const balance = (Number(ltcWalletBalance) / 1e8).toFixed(8) + const fee = feePerByte * 1000 // default 0.00030000 + if (ltcAmountDecimals + (fee * QORT_DECIMALS) > ltcWalletBalanceDecimals) { + throw new Error("Insufficient Funds!") + } + const resPermission = await getUserPermission({ + text1: "Do you give this application permission to send coins?", + text2: `To: ${recipient}`, + highlightedText: `${amount} ${checkCoin}`, + fee: fee + }); + const { accepted } = resPermission; + + if (accepted) { + const url = await createEndpoint(`/crosschain/ltc/send`); + const opts = { + xprv58: xprv58, + receivingAddress: recipient, + litecoinAmount: amount, + feePerByte: feePerByte * QORT_DECIMALS + } + const response = await fetch(url, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(opts) + }) + if (!response.ok) throw new Error("Failed to send"); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + return res; + } else { + throw new Error("User declined request") + } + + } else if (checkCoin === "DOGE") { + + const amount = Number(data.amount) + const recipient = data.destinationAddress + const coin = data.coin + const xprv58 = parsedData.dogePrivateKey + const feePerByte = data.fee ? data.fee : dogeFeePerByte + const dogeWalletBalance = await getWalletBalance({coin: checkCoin}, true) + if (isNaN(Number(dogeWalletBalance))) { + let errorMsg = "Failed to Fetch DOGE Balance. Try again!" + throw new Error(errorMsg) + } + const dogeWalletBalanceDecimals = Number(dogeWalletBalance) + const dogeAmountDecimals = Number(amount) * QORT_DECIMALS + const balance = (Number(dogeWalletBalance) / 1e8).toFixed(8) + const fee = feePerByte * 5000 // default 0.05000000 + if (dogeAmountDecimals + (fee * QORT_DECIMALS) > dogeWalletBalanceDecimals) { + let errorMsg = "Insufficient Funds!" + throw new Error(errorMsg) + } + + const resPermission = await getUserPermission({ + text1: "Do you give this application permission to send coins?", + text2: `To: ${recipient}`, + highlightedText: `${amount} ${checkCoin}`, + fee: fee + }); + const { accepted } = resPermission; + + if (accepted) { + const opts = { + xprv58: xprv58, + receivingAddress: recipient, + dogecoinAmount: amount, + feePerByte: feePerByte * QORT_DECIMALS + } + const url = await createEndpoint(`/crosschain/doge/send`); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(opts) + }) + if (!response.ok) throw new Error("Failed to send"); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + return res; + } else { + throw new Error("User declined request") + } + + } else if (checkCoin === "DGB") { + const amount = Number(data.amount) + const recipient = data.destinationAddress + const xprv58 = parsedData.dbgPrivateKey + const feePerByte = data.fee ? data.fee : dgbFeePerByte + const dgbWalletBalance = await getWalletBalance({coin: checkCoin}, true) + if (isNaN(Number(dgbWalletBalance))) { + let errorMsg = "Failed to Fetch DGB Balance. Try again!" + throw new Error(errorMsg) + } + const dgbWalletBalanceDecimals = Number(dgbWalletBalance) + const dgbAmountDecimals = Number(amount) * QORT_DECIMALS + const fee = feePerByte * 500 // default 0.00005000 + if (dgbAmountDecimals + (fee * QORT_DECIMALS) > dgbWalletBalanceDecimals) { + let errorMsg = "Insufficient Funds!" + throw new Error(errorMsg) + } + + const resPermission = await getUserPermission({ + text1: "Do you give this application permission to send coins?", + text2: `To: ${recipient}`, + highlightedText: `${amount} ${checkCoin}`, + fee: fee + }); + const { accepted } = resPermission; + + if (accepted) { + const opts = { + xprv58: xprv58, + receivingAddress: recipient, + digibyteAmount: amount, + feePerByte: feePerByte * QORT_DECIMALS + } + const url = await createEndpoint(`/crosschain/dgb/send`); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(opts) + }) + if (!response.ok) throw new Error("Failed to send"); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + return res; + } else { + throw new Error("User declined request") + } + + } else if (checkCoin === "RVN") { + const amount = Number(data.amount) + const recipient = data.destinationAddress + const coin = data.coin + const xprv58 = parsedData.rvnPrivateKey + const feePerByte = data.fee ? data.fee : rvnFeePerByte + const rvnWalletBalance = await getWalletBalance({coin: checkCoin}, true) + if (isNaN(Number(rvnWalletBalance))) { + let errorMsg = "Failed to Fetch RVN Balance. Try again!" + throw new Error(errorMsg) + } + const rvnWalletBalanceDecimals = Number(rvnWalletBalance) + const rvnAmountDecimals = Number(amount) * QORT_DECIMALS + const balance = (Number(rvnWalletBalance) / 1e8).toFixed(8) + const fee = feePerByte * 500 // default 0.00562500 + if (rvnAmountDecimals + (fee * QORT_DECIMALS) > rvnWalletBalanceDecimals) { + + let errorMsg = "Insufficient Funds!" + throw new Error(errorMsg) + } + + const resPermission = await getUserPermission({ + text1: "Do you give this application permission to send coins?", + text2: `To: ${recipient}`, + highlightedText: `${amount} ${checkCoin}`, + fee: fee + }); + const { accepted } = resPermission; + + if (accepted) { + const opts = { + xprv58: xprv58, + receivingAddress: recipient, + ravencoinAmount: amount, + feePerByte: feePerByte * QORT_DECIMALS + } + const url = await createEndpoint(`/crosschain/rvn/send`); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(opts) + }) + if (!response.ok) throw new Error("Failed to send"); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + return res; + } else { + throw new Error("User declined request") + } + } else if (checkCoin === "ARRR") { + const amount = Number(data.amount) + const recipient = data.destinationAddress + const memo = data.memo + const arrrWalletBalance = await getWalletBalance({coin: checkCoin}, true) + + if (isNaN(Number(arrrWalletBalance))) { + let errorMsg = "Failed to Fetch ARRR Balance. Try again!" + throw new Error(errorMsg) + } + const arrrWalletBalanceDecimals = Number(arrrWalletBalance) + const arrrAmountDecimals = Number(amount) * QORT_DECIMALS + const fee = 0.00010000 + if (arrrAmountDecimals + (fee * QORT_DECIMALS) > arrrWalletBalanceDecimals) { + let errorMsg = "Insufficient Funds!" + throw new Error(errorMsg) + } + + const resPermission = await getUserPermission({ + text1: "Do you give this application permission to send coins?", + text2: `To: ${recipient}`, + highlightedText: `${amount} ${checkCoin}`, + fee: fee + }); + const { accepted } = resPermission; + + if (accepted) { + const opts = { + entropy58: parsedData.arrrSeed58, + receivingAddress: recipient, + arrrAmount: amount, + memo: memo + } + const url = await createEndpoint(`/crosschain/btc/send`); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(opts) + }) + if (!response.ok) throw new Error("Failed to send"); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + return res; + } else { + throw new Error("User declined request") + } + } }; From 3f1a035b7c5c584fd775ff76f9b1292416b41335 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Thu, 17 Oct 2024 01:12:34 +0300 Subject: [PATCH 20/58] fixes --- public/content-script.js | 2 +- src/qdn/encryption/group-encryption.ts | 57 ++++++++++++++++++++++++-- src/qortalRequests/get.ts | 6 ++- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/public/content-script.js b/public/content-script.js index 9864e06..b1a7569 100644 --- a/public/content-script.js +++ b/public/content-script.js @@ -816,7 +816,7 @@ if (!window.hasAddedQortalListener) { if (response.error) { eventPort.postMessage({ result: null, - error: response.error, + error: response, }); } else { eventPort.postMessage({ diff --git a/src/qdn/encryption/group-encryption.ts b/src/qdn/encryption/group-encryption.ts index 24d53df..f210182 100644 --- a/src/qdn/encryption/group-encryption.ts +++ b/src/qdn/encryption/group-encryption.ts @@ -67,7 +67,7 @@ export const createSymmetricKeyAndNonce = () => { export const encryptDataGroup = ({ data64, publicKeys, privateKey, userPublicKey }: any) => { - let combinedPublicKeys = publicKeys + let combinedPublicKeys = [...publicKeys, userPublicKey] const decodedPrivateKey = Base58.decode(privateKey) const publicKeysDuplicateFree = [...new Set(combinedPublicKeys)] @@ -275,11 +275,62 @@ export const decodeBase64ForUIChatMessages = (messages)=> { - + export function decryptGroupDataQortalRequest(data64EncryptedData, privateKey) { + const allCombined = base64ToUint8Array(data64EncryptedData) + const str = "qortalGroupEncryptedData" + const strEncoder = new TextEncoder() + const strUint8Array = strEncoder.encode(str) + // Extract the nonce + const nonceStartPosition = strUint8Array.length + const nonceEndPosition = nonceStartPosition + 24 // Nonce is 24 bytes + const nonce = allCombined.slice(nonceStartPosition, nonceEndPosition) + // Extract the shared keyNonce + const keyNonceStartPosition = nonceEndPosition + const keyNonceEndPosition = keyNonceStartPosition + 24 // Nonce is 24 bytes + const keyNonce = allCombined.slice(keyNonceStartPosition, keyNonceEndPosition) + // Extract the sender's public key + const senderPublicKeyStartPosition = keyNonceEndPosition + const senderPublicKeyEndPosition = senderPublicKeyStartPosition + 32 // Public keys are 32 bytes + const senderPublicKey = allCombined.slice(senderPublicKeyStartPosition, senderPublicKeyEndPosition) + // Calculate count first + const countStartPosition = allCombined.length - 4 // 4 bytes before the end, since count is stored in Uint32 (4 bytes) + const countArray = allCombined.slice(countStartPosition, countStartPosition + 4) + const count = new Uint32Array(countArray.buffer)[0] + // Then use count to calculate encryptedData + const encryptedDataStartPosition = senderPublicKeyEndPosition // start position of encryptedData + const encryptedDataEndPosition = allCombined.length - ((count * (32 + 16)) + 4) + const encryptedData = allCombined.slice(encryptedDataStartPosition, encryptedDataEndPosition) + // Extract the encrypted keys + // 32+16 = 48 + const combinedKeys = allCombined.slice(encryptedDataEndPosition, encryptedDataEndPosition + (count * 48)) + if (!privateKey) { + throw new Error("Unable to retrieve keys") + } + const decodedPrivateKey = Base58.decode(privateKey) + const convertedPrivateKey = ed2curve.convertSecretKey(decodedPrivateKey) + const convertedSenderPublicKey = ed2curve.convertPublicKey(senderPublicKey) + const sharedSecret = new Uint8Array(32) + nacl.lowlevel.crypto_scalarmult(sharedSecret, convertedPrivateKey, convertedSenderPublicKey) + for (let i = 0; i < count; i++) { + const encryptedKey = combinedKeys.slice(i * 48, (i + 1) * 48) + // Decrypt the symmetric key. + const decryptedKey = nacl.secretbox.open(encryptedKey, keyNonce, sharedSecret) + // If decryption was successful, decryptedKey will not be null. + if (decryptedKey) { + // Decrypt the data using the symmetric key. + const decryptedData = nacl.secretbox.open(encryptedData, nonce, decryptedKey) + console.log('decryptedData', decryptedData) + // If decryption was successful, decryptedData will not be null. + if (decryptedData) { + return decryptedData + } + } + } + throw new Error("Unable to decrypt data") +} export function decryptGroupData(data64EncryptedData: string, privateKey: string) { - const allCombined = base64ToUint8Array(data64EncryptedData) const str = "qortalGroupEncryptedData" const strEncoder = new TextEncoder() diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index c961f5c..12f76a3 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -21,6 +21,7 @@ import { base64ToUint8Array, decryptDeprecatedSingle, decryptGroupData, + decryptGroupDataQortalRequest, encryptDataGroup, uint8ArrayStartsWith, uint8ArrayToBase64, @@ -344,6 +345,7 @@ export const encryptData = async (data, sender) => { const parsedData = JSON.parse(resKeyPair); const privateKey = parsedData.privateKey; const userPublicKey = parsedData.publicKey; + console.log('data', data) const encryptDataResponse = encryptDataGroup({ data64, @@ -388,7 +390,7 @@ export const decryptData = async (data) => { "qortalGroupEncryptedData" ); if (startsWithQortalGroupEncryptedData) { - const decryptedData = decryptGroupData( + const decryptedData = decryptGroupDataQortalRequest( encryptedData, parsedData.privateKey ); @@ -843,7 +845,7 @@ export const publishMultipleQDNResources = async (data: any, sender) => { } } catch (error) { const errorMsg = - error.message || "Upload failed due to failed encryption"; + error?.message || "Upload failed due to failed encryption"; failedPublishesIdentifiers.push({ reason: errorMsg, identifier: resource.identifier, From e9860ae7be9e247ab106bf537c2c77aa0f5fa925 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Thu, 17 Oct 2024 01:29:50 +0300 Subject: [PATCH 21/58] added is local condition --- src/qortalRequests/get.ts | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index 12f76a3..78f5469 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -20,7 +20,6 @@ import Base58 from "../deps/Base58"; import { base64ToUint8Array, decryptDeprecatedSingle, - decryptGroupData, decryptGroupDataQortalRequest, encryptDataGroup, uint8ArrayStartsWith, @@ -29,15 +28,14 @@ import { import { publishData } from "../qdn/publish/pubish"; import { getPermission, setPermission } from "../qortalRequests"; import { createTransaction } from "../transactions/transactions"; -import { fileToBase64 } from "../utils/fileReading"; import { mimeToExtensionMap } from "../utils/memeTypes"; const btcFeePerByte = 0.00000100 - const ltcFeePerByte = 0.00000030 - const dogeFeePerByte = 0.00001000 - const dgbFeePerByte = 0.00000010 - const rvnFeePerByte = 0.00001125 +const ltcFeePerByte = 0.00000030 +const dogeFeePerByte = 0.00001000 +const dgbFeePerByte = 0.00000010 +const rvnFeePerByte = 0.00001125 const _createPoll = async (pollName, pollDescription, options) => { @@ -401,6 +399,8 @@ export const decryptData = async (data) => { }; export const getListItems = async (data) => { + const localNodeAvailable = await isUsingLocal() + if(!localNodeAvailable) throw new Error('Please use your local node.') const requiredFields = ["list_name"]; const missingFields: string[] = []; requiredFields.forEach((field) => { @@ -454,6 +454,8 @@ export const getListItems = async (data) => { }; export const addListItems = async (data) => { + const localNodeAvailable = await isUsingLocal() + if(!localNodeAvailable) throw new Error('Please use your local node.') const requiredFields = ["list_name", "items"]; const missingFields: string[] = []; requiredFields.forEach((field) => { @@ -507,6 +509,8 @@ export const addListItems = async (data) => { }; export const deleteListItems = async (data) => { + const localNodeAvailable = await isUsingLocal() + if(!localNodeAvailable) throw new Error('Please use your local node.') const requiredFields = ["list_name", "item"]; const missingFields: string[] = []; requiredFields.forEach((field) => { @@ -1726,6 +1730,8 @@ export const getTxActivitySummary = async (data) => { }; export const updateForeignFee = async (data) => { + const localNodeAvailable = await isUsingLocal() + if(!localNodeAvailable) throw new Error('Please use your local node.') const requiredFields = ['coin', 'type', 'value']; const missingFields: string[] = []; @@ -1821,6 +1827,8 @@ export const getTxActivitySummary = async (data) => { }; export const setCurrentForeignServer = async (data) => { + const localNodeAvailable = await isUsingLocal() + if(!localNodeAvailable) throw new Error('Please use your local node.') const requiredFields = ['coin']; const missingFields: string[] = []; @@ -1878,6 +1886,8 @@ export const getTxActivitySummary = async (data) => { export const addForeignServer = async (data) => { + const localNodeAvailable = await isUsingLocal() + if(!localNodeAvailable) throw new Error('Please use your local node.') const requiredFields = ['coin']; const missingFields: string[] = []; @@ -1934,6 +1944,8 @@ export const getTxActivitySummary = async (data) => { }; export const removeForeignServer = async (data) => { + const localNodeAvailable = await isUsingLocal() + if(!localNodeAvailable) throw new Error('Please use your local node.') const requiredFields = ['coin']; const missingFields: string[] = []; From bd170d848128fb55ed0a9e17225d46954232a3aa Mon Sep 17 00:00:00 2001 From: PhilReact Date: Thu, 17 Oct 2024 06:09:20 +0300 Subject: [PATCH 22/58] started app section --- public/manifest.json | 2 +- src/App.tsx | 2 +- src/assets/svgs/ClearInput.svg | 6 + src/assets/svgs/LogoSelected.svg | 9 ++ src/assets/svgs/Search.svg | 3 + src/components/Apps/AppInfo.tsx | 22 ++++ src/components/Apps/Apps-styles.tsx | 108 ++++++++++++++++ src/components/Apps/Apps.tsx | 62 ++++++++++ src/components/Apps/AppsHome.tsx | 56 +++++++++ src/components/Apps/AppsLibrary.tsx | 163 +++++++++++++++++++++++++ src/components/Group/Group.tsx | 6 +- src/components/Mobile/MobileFooter.tsx | 15 ++- src/index.css | 1 + 13 files changed, 451 insertions(+), 4 deletions(-) create mode 100644 src/assets/svgs/ClearInput.svg create mode 100644 src/assets/svgs/LogoSelected.svg create mode 100644 src/assets/svgs/Search.svg create mode 100644 src/components/Apps/AppInfo.tsx create mode 100644 src/components/Apps/Apps-styles.tsx create mode 100644 src/components/Apps/Apps.tsx create mode 100644 src/components/Apps/AppsHome.tsx create mode 100644 src/components/Apps/AppsLibrary.tsx diff --git a/public/manifest.json b/public/manifest.json index 5d88b27..b37d461 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -46,6 +46,6 @@ ], "content_security_policy": { - "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; connect-src 'self' https://api.qortal.org https://api2.qortal.org https://appnode.qortal.org https://apinode.qortalnodes.live https://apinode1.qortalnodes.live https://apinode2.qortalnodes.live https://apinode3.qortalnodes.live https://apinode4.qortalnodes.live https://ext-node.qortal.link wss://appnode.qortal.org wss://ext-node.qortal.link ws://127.0.0.1:12391 http://127.0.0.1:12391 https://ext-node.qortal.link; " + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; connect-src 'self' https://*:* http://*:* wss://*:* ws://*:*;" } } diff --git a/src/App.tsx b/src/App.tsx index 00cb3a0..53d88e8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -141,7 +141,7 @@ const defaultValues: MyContextInterface = { message: "", }, }; -export let isMobile = false; +export let isMobile = true; const isMobileDevice = () => { const userAgent = navigator.userAgent || navigator.vendor || window.opera; diff --git a/src/assets/svgs/ClearInput.svg b/src/assets/svgs/ClearInput.svg new file mode 100644 index 0000000..a4595df --- /dev/null +++ b/src/assets/svgs/ClearInput.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/svgs/LogoSelected.svg b/src/assets/svgs/LogoSelected.svg new file mode 100644 index 0000000..fec7e1e --- /dev/null +++ b/src/assets/svgs/LogoSelected.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/svgs/Search.svg b/src/assets/svgs/Search.svg new file mode 100644 index 0000000..b6cb06b --- /dev/null +++ b/src/assets/svgs/Search.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Apps/AppInfo.tsx b/src/components/Apps/AppInfo.tsx new file mode 100644 index 0000000..0e3be5a --- /dev/null +++ b/src/components/Apps/AppInfo.tsx @@ -0,0 +1,22 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { + + AppsLibraryContainer, + +} from "./Apps-styles"; +import { Avatar, Box, ButtonBase, InputBase } from "@mui/material"; +import { Add } from "@mui/icons-material"; +import { getBaseApiReact } from "../../App"; +import LogoSelected from "../../assets/svgs/LogoSelected.svg"; + + +import { Spacer } from "../../common/Spacer"; + +export const AppInfo = ({ app }) => { + + return ( + + + + ); +}; diff --git a/src/components/Apps/Apps-styles.tsx b/src/components/Apps/Apps-styles.tsx new file mode 100644 index 0000000..1549596 --- /dev/null +++ b/src/components/Apps/Apps-styles.tsx @@ -0,0 +1,108 @@ +import { + AppBar, + Button, + Toolbar, + Typography, + Box, + TextField, + InputLabel, + } from "@mui/material"; + import { styled } from "@mui/system"; + + export const AppsParent = styled(Box)(({ theme }) => ({ + display: "flex", + width: "100%", + flexDirection: "column", + height: "100%", + alignItems: "center", + overflow: 'auto', + // For WebKit-based browsers (Chrome, Safari, etc.) + "::-webkit-scrollbar": { + width: "0px", // Set the width to 0 to hide the scrollbar + height: "0px", // Set the height to 0 for horizontal scrollbar + }, + + // For Firefox + scrollbarWidth: "none", // Hides the scrollbar in Firefox + + // Optional for better cross-browser consistency + "-ms-overflow-style": "none" // Hides scrollbar in IE and Edge + })); + export const AppsContainer = styled(Box)(({ theme }) => ({ + display: "flex", + width: "90%", + justifyContent: 'space-evenly', + gap: '24px', + flexWrap: 'wrap', + alignItems: 'flex-start', + + })); + export const AppsLibraryContainer = styled(Box)(({ theme }) => ({ + display: "flex", + width: "90%", + flexDirection: 'column', + justifyContent: 'flex-start', + alignItems: 'flex-start', + })); + export const AppsSearchContainer = styled(Box)(({ theme }) => ({ + display: "flex", + width: "90%", + justifyContent: 'space-between', + alignItems: 'center', + backgroundColor: '#434343', + borderRadius: '8px', + padding: '0px 10px', + height: '36px' + })); + export const AppsSearchLeft = styled(Box)(({ theme }) => ({ + display: "flex", + width: "90%", + justifyContent: 'flex-start', + alignItems: 'center', + gap: '10px' + })); + export const AppsSearchRight = styled(Box)(({ theme }) => ({ + display: "flex", + width: "90%", + justifyContent: 'flex-end', + alignItems: 'center', + })); + export const AppCircleContainer = styled(Box)(({ theme }) => ({ + display: "flex", + flexDirection: "column", + gap: '5px', + alignItems: 'center', + width: '100%' + })); + export const Add = styled(Typography)(({ theme }) => ({ + fontSize: '36px', + fontWeight: 500, + lineHeight: '43.57px', + textAlign: 'left' + + })); + export const AppCircleLabel = styled(Typography)(({ theme }) => ({ + fontSize: '12px', + fontWeight: 500, + lineHeight: 1.2, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + width: '100%' + })); + export const AppLibrarySubTitle = styled(Typography)(({ theme }) => ({ + fontSize: '16px', + fontWeight: 500, + lineHeight: 1.2, + })); + export const AppCircle = styled(Box)(({ theme }) => ({ + display: "flex", + width: "60px", + flexDirection: "column", + height: "60px", + alignItems: 'center', + justifyContent: 'center', + borderRadius: '50%', + backgroundColor: "var(--apps-circle)", + border: '1px solid #FFFFFF' + })); \ No newline at end of file diff --git a/src/components/Apps/Apps.tsx b/src/components/Apps/Apps.tsx new file mode 100644 index 0000000..9ffe412 --- /dev/null +++ b/src/components/Apps/Apps.tsx @@ -0,0 +1,62 @@ +import React, { useEffect, useState } from 'react' +import { AppsHome } from './AppsHome' +import { Spacer } from '../../common/Spacer' +import { getBaseApiReact } from '../../App' +import { AppsLibrary } from './AppsLibrary' + +export const Apps = () => { + const [mode, setMode] = useState('home') + + const [availableQapps, setAvailableQapps] = useState([]) + const [downloadedQapps, setDownloadedQapps] = useState([]) + + const getQapps = React.useCallback( + async () => { + try { + let apps = [] + let websites = [] + // dispatch(setIsLoadingGlobal(true)) + const url = `${getBaseApiReact()}/arbitrary/resources/search?service=APP&mode=ALL&includestatus=true&limit=0&includemetadata=true`; + + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + if(!response?.ok) return + const responseData = await response.json(); + const urlWebsites = `${getBaseApiReact()}/arbitrary/resources/search?service=WEBSITE&mode=ALL&includestatus=true&limit=0&includemetadata=true`; + + const responseWebsites = await fetch(urlWebsites, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + if(!responseWebsites?.ok) return + const responseDataWebsites = await responseWebsites.json(); + apps = responseData + websites = responseDataWebsites + const combine = [...apps, ...websites] + setAvailableQapps(combine) + setDownloadedQapps(combine.filter((qapp)=> qapp?.status?.status === 'READY')) + } catch (error) { + } finally { + // dispatch(setIsLoadingGlobal(false)) + } + }, + [] + ); + useEffect(()=> { + getQapps() + }, [getQapps]) + + return ( + <> + + {mode === 'home' && } + {mode === 'library' && } + + ) +} diff --git a/src/components/Apps/AppsHome.tsx b/src/components/Apps/AppsHome.tsx new file mode 100644 index 0000000..775d7e0 --- /dev/null +++ b/src/components/Apps/AppsHome.tsx @@ -0,0 +1,56 @@ +import React, { useMemo, useState } from 'react' +import { AppCircle, AppCircleContainer, AppCircleLabel, AppsContainer, AppsParent } from './Apps-styles' +import { Avatar, ButtonBase } from '@mui/material' +import { Add } from '@mui/icons-material' +import { getBaseApiReact } from '../../App' +import LogoSelected from "../../assets/svgs/LogoSelected.svg"; + +export const AppsHome = ({downloadedQapps, setMode}) => { + + + return ( + + + { + setMode('library') + }}> + + + + + + Add + + + {downloadedQapps?.map((qapp)=> { + return ( + + + + + center-icon + + + {qapp?.metadata?.title || qapp?.name} + + + ) + })} + + + ) +} diff --git a/src/components/Apps/AppsLibrary.tsx b/src/components/Apps/AppsLibrary.tsx new file mode 100644 index 0000000..d801bc0 --- /dev/null +++ b/src/components/Apps/AppsLibrary.tsx @@ -0,0 +1,163 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { + AppCircle, + AppCircleContainer, + AppCircleLabel, + AppLibrarySubTitle, + AppsContainer, + AppsLibraryContainer, + AppsParent, + AppsSearchContainer, + AppsSearchLeft, + AppsSearchRight, +} from "./Apps-styles"; +import { Avatar, Box, ButtonBase, InputBase } from "@mui/material"; +import { Add } from "@mui/icons-material"; +import { getBaseApiReact } from "../../App"; +import LogoSelected from "../../assets/svgs/LogoSelected.svg"; +import IconSearch from "../../assets/svgs/Search.svg"; +import IconClearInput from "../../assets/svgs/ClearInput.svg"; + +import { Spacer } from "../../common/Spacer"; +const officialAppList = [ + "q-tube", + "q-blog", + "q-share", + "q-support", + "q-mail", + "qombo", + "q-fund", + "q-shop", +]; + +export const AppsLibrary = ({ downloadedQapps, availableQapps }) => { + const [searchValue, setSearchValue] = useState('') + const officialApps = useMemo(() => { + return availableQapps.filter((app) => app.service === 'APP' && + officialAppList.includes(app?.name?.toLowerCase()) + ); + }, [availableQapps]); + + const [debouncedValue, setDebouncedValue] = useState(''); // Debounced value + + // Debounce logic + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(searchValue); // Update debounced value after delay + }, 500); // 500ms debounce time (adjustable) + + // Cleanup timeout if searchValue changes before the timeout completes + return () => { + clearTimeout(handler); + }; + }, [searchValue]); // Runs effect when searchValue changes + + // Example: Perform search or other actions based on debouncedValue + + const searchedList = useMemo(()=> { + if(!debouncedValue) return [] + return availableQapps.filter((app)=> app.name.toLowerCase().includes(debouncedValue.toLowerCase())) + }, [debouncedValue]) + console.log('officialApps', searchedList) + + + return ( + + + + + + + setSearchValue(e.target.value)} + sx={{ ml: 1, flex: 1 }} + placeholder="Search for apps" + inputProps={{ 'aria-label': 'Search for apps', fontSize: '16px', fontWeight: 400 }} + /> + + + {searchValue && ( + { + setSearchValue('') + }}> + + + )} + + + + + + {searchedList?.length > 0 ? ( + <> + {searchedList.map((app)=> { + + return ( + + ) + })} + + ) : ( + <> + Official Apps + + + {officialApps?.map((qapp) => { + return ( + + + + + center-icon + + + + {qapp?.metadata?.title || qapp?.name} + + + + ); + })} + + + Featured + + + Categories + + )} + + + + ); +}; diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index 45d9c57..3e09a3a 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -88,6 +88,7 @@ import { ExitIcon } from "../../assets/Icons/ExitIcon"; import { HomeDesktop } from "./HomeDesktop"; import { DesktopFooter } from "../Desktop/DesktopFooter"; import { DesktopHeader } from "../Desktop/DesktopHeader"; +import { Apps } from "../Apps/Apps"; // let touchStartY = 0; // let disablePullToRefresh = false; @@ -2733,6 +2734,9 @@ export const Group = ({ setMobileViewMode={setMobileViewMode} /> )} + {isMobile && mobileViewMode === "apps" && ( + + )} { !isMobile && !selectedGroup && groupSection === "home" && ( @@ -2958,7 +2962,7 @@ export const Group = ({ />
- {isMobile && mobileViewMode === "home" && !mobileViewModeKeepOpen && ( + {(isMobile && mobileViewMode === "home" || isMobile && mobileViewMode === "apps") && !mobileViewModeKeepOpen && ( <>
+ { + if(mobileViewMode === 'home'){ + setMobileViewMode('apps') + + } else { + setMobileViewMode('home') + + } + }}> {/* Custom Center Icon */} - center-icon + center-icon + Date: Fri, 18 Oct 2024 02:50:19 +0300 Subject: [PATCH 23/58] started app browser --- package-lock.json | 11 + package.json | 1 + src/assets/svgs/NavAdd.svg | 4 + src/assets/svgs/NavBack.svg | 10 + src/assets/svgs/NavCloseTab.svg | 6 + src/assets/svgs/NavMoreMenu.svg | 10 + src/components/Apps/AppInfo.tsx | 90 ++++++- src/components/Apps/AppInfoSnippet.tsx | 100 ++++++++ src/components/Apps/AppViewer.tsx | 46 ++++ src/components/Apps/AppViewerContainer.tsx | 40 +++ src/components/Apps/Apps-styles.tsx | 90 ++++++- src/components/Apps/Apps.tsx | 110 ++++++++- src/components/Apps/AppsHome.tsx | 123 ++++++---- src/components/Apps/AppsLibrary.tsx | 270 +++++++++++++-------- src/components/Apps/AppsNavBar.tsx | 43 ++++ src/components/Group/Group.tsx | 13 +- 16 files changed, 795 insertions(+), 172 deletions(-) create mode 100644 src/assets/svgs/NavAdd.svg create mode 100644 src/assets/svgs/NavBack.svg create mode 100644 src/assets/svgs/NavCloseTab.svg create mode 100644 src/assets/svgs/NavMoreMenu.svg create mode 100644 src/components/Apps/AppInfoSnippet.tsx create mode 100644 src/components/Apps/AppViewer.tsx create mode 100644 src/components/Apps/AppViewerContainer.tsx create mode 100644 src/components/Apps/AppsNavBar.tsx diff --git a/package-lock.json b/package-lock.json index fe43916..1e8c9c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "react-countdown-circle-timer": "^3.2.1", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", + "react-frame-component": "^5.2.7", "react-infinite-scroller": "^1.2.6", "react-intersection-observer": "^9.13.0", "react-qr-code": "^2.0.15", @@ -9256,6 +9257,16 @@ "react": ">= 16.8 || 18.0.0" } }, + "node_modules/react-frame-component": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/react-frame-component/-/react-frame-component-5.2.7.tgz", + "integrity": "sha512-ROjHtSLoSVYUBfTieazj/nL8jIX9rZFmHC0yXEU+dx6Y82OcBEGgU9o7VyHMrBFUN9FuQ849MtIPNNLsb4krbg==", + "peerDependencies": { + "prop-types": "^15.5.9", + "react": ">= 16.3", + "react-dom": ">= 16.3" + } + }, "node_modules/react-infinite-scroller": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/react-infinite-scroller/-/react-infinite-scroller-1.2.6.tgz", diff --git a/package.json b/package.json index 4d0fa0c..07e3164 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "react-countdown-circle-timer": "^3.2.1", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", + "react-frame-component": "^5.2.7", "react-infinite-scroller": "^1.2.6", "react-intersection-observer": "^9.13.0", "react-qr-code": "^2.0.15", diff --git a/src/assets/svgs/NavAdd.svg b/src/assets/svgs/NavAdd.svg new file mode 100644 index 0000000..1d38c05 --- /dev/null +++ b/src/assets/svgs/NavAdd.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/svgs/NavBack.svg b/src/assets/svgs/NavBack.svg new file mode 100644 index 0000000..07df29e --- /dev/null +++ b/src/assets/svgs/NavBack.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/svgs/NavCloseTab.svg b/src/assets/svgs/NavCloseTab.svg new file mode 100644 index 0000000..a4595df --- /dev/null +++ b/src/assets/svgs/NavCloseTab.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/svgs/NavMoreMenu.svg b/src/assets/svgs/NavMoreMenu.svg new file mode 100644 index 0000000..f64cdab --- /dev/null +++ b/src/assets/svgs/NavMoreMenu.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/components/Apps/AppInfo.tsx b/src/components/Apps/AppInfo.tsx index 0e3be5a..eb1c686 100644 --- a/src/components/Apps/AppInfo.tsx +++ b/src/components/Apps/AppInfo.tsx @@ -1,22 +1,102 @@ import React, { useEffect, useMemo, useState } from "react"; import { - + AppCircle, + AppCircleContainer, + AppCircleLabel, + AppDownloadButton, + AppDownloadButtonText, + AppInfoAppName, + AppInfoSnippetContainer, + AppInfoSnippetLeft, + AppInfoSnippetMiddle, + AppInfoSnippetRight, + AppInfoUserName, AppsLibraryContainer, - + AppsParent, } from "./Apps-styles"; import { Avatar, Box, ButtonBase, InputBase } from "@mui/material"; import { Add } from "@mui/icons-material"; import { getBaseApiReact } from "../../App"; import LogoSelected from "../../assets/svgs/LogoSelected.svg"; - import { Spacer } from "../../common/Spacer"; +import { executeEvent } from "../../utils/events"; export const AppInfo = ({ app }) => { - + + + const isInstalled = app?.status?.status === 'READY' return ( + + + + + + + center-icon + + + + - + + {app?.metadata?.title || app?.name} + + + + { app?.name} + + + + + + + + + + + + + {isInstalled ? 'Open' : 'Download'} + + + + ); }; diff --git a/src/components/Apps/AppInfoSnippet.tsx b/src/components/Apps/AppInfoSnippet.tsx new file mode 100644 index 0000000..26d0935 --- /dev/null +++ b/src/components/Apps/AppInfoSnippet.tsx @@ -0,0 +1,100 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { + AppCircle, + AppCircleContainer, + AppCircleLabel, + AppDownloadButton, + AppDownloadButtonText, + AppInfoAppName, + AppInfoSnippetContainer, + AppInfoSnippetLeft, + AppInfoSnippetMiddle, + AppInfoSnippetRight, + AppInfoUserName, + AppsLibraryContainer, +} from "./Apps-styles"; +import { Avatar, Box, ButtonBase, InputBase } from "@mui/material"; +import { Add } from "@mui/icons-material"; +import { getBaseApiReact } from "../../App"; +import LogoSelected from "../../assets/svgs/LogoSelected.svg"; + +import { Spacer } from "../../common/Spacer"; +import { executeEvent } from "../../utils/events"; + +export const AppInfoSnippet = ({ app }) => { + + + const isInstalled = app?.status?.status === 'READY' + return ( + + + { + executeEvent("selectedAppInfo", { + data: app, + }); + }} + > + + + + center-icon + + + + + + { + executeEvent("selectedAppInfo", { + data: app, + }); + }}> + + {app?.metadata?.title || app?.name} + + + + + { app?.name} + + + + + + + {isInstalled ? 'Open' : 'Download'} + + + + ); +}; diff --git a/src/components/Apps/AppViewer.tsx b/src/components/Apps/AppViewer.tsx new file mode 100644 index 0000000..6992127 --- /dev/null +++ b/src/components/Apps/AppViewer.tsx @@ -0,0 +1,46 @@ +import React, { useContext, useEffect, useMemo, useRef, useState } from "react"; +import { + AppCircle, + AppCircleContainer, + AppCircleLabel, + AppDownloadButton, + AppDownloadButtonText, + AppInfoAppName, + AppInfoSnippetContainer, + AppInfoSnippetLeft, + AppInfoSnippetMiddle, + AppInfoSnippetRight, + AppInfoUserName, + AppsLibraryContainer, + AppsParent, +} from "./Apps-styles"; +import { Avatar, Box, ButtonBase, InputBase } from "@mui/material"; +import { Add } from "@mui/icons-material"; +import { MyContext, getBaseApiReact } from "../../App"; +import LogoSelected from "../../assets/svgs/LogoSelected.svg"; + +import { Spacer } from "../../common/Spacer"; +import { executeEvent } from "../../utils/events"; + +export const AppViewer = ({ app }) => { + const { rootHeight } = useContext(MyContext); + const iframeRef = useRef(null); + + + const url = useMemo(()=> { + return `${getBaseApiReact()}/render/${app?.service}/${app?.name}${app?.path != null ? app?.path : ''}?theme=dark&identifier=${(app?.identifier != null && app?.identifier != 'null') ? app?.identifier : ''}` + }, [app?.service, app?.name, app?.identifier, app?.path]) + + + + + return ( + + ); +}; diff --git a/src/components/Apps/AppViewerContainer.tsx b/src/components/Apps/AppViewerContainer.tsx new file mode 100644 index 0000000..ef04760 --- /dev/null +++ b/src/components/Apps/AppViewerContainer.tsx @@ -0,0 +1,40 @@ +import React, { useContext, useRef } from 'react' +import { AppViewer } from './AppViewer' +import Frame from 'react-frame-component'; +import { MyContext } from '../../App'; + +const AppViewerContainer = ({app, isSelected}) => { + const { rootHeight } = useContext(MyContext); + const frameRef = useRef(null); + + return ( + + {/* Inject styles directly into the iframe */} + + + } style={{ + height: `calc(${rootHeight} - 60px - 45px)`, + border: 'none', + width: '100%', + display: !isSelected && 'none' + }} > + ) +} + +export default AppViewerContainer \ No newline at end of file diff --git a/src/components/Apps/Apps-styles.tsx b/src/components/Apps/Apps-styles.tsx index 1549596..6f67658 100644 --- a/src/components/Apps/Apps-styles.tsx +++ b/src/components/Apps/Apps-styles.tsx @@ -6,6 +6,7 @@ import { Box, TextField, InputLabel, + ButtonBase, } from "@mui/material"; import { styled } from "@mui/system"; @@ -15,7 +16,7 @@ import { flexDirection: "column", height: "100%", alignItems: "center", - overflow: 'auto', + overflow: 'auto', // For WebKit-based browsers (Chrome, Safari, etc.) "::-webkit-scrollbar": { width: "0px", // Set the width to 0 to hide the scrollbar @@ -35,6 +36,7 @@ import { gap: '24px', flexWrap: 'wrap', alignItems: 'flex-start', + alignSelf: 'center' })); export const AppsLibraryContainer = styled(Box)(({ theme }) => ({ @@ -59,13 +61,16 @@ import { width: "90%", justifyContent: 'flex-start', alignItems: 'center', - gap: '10px' + gap: '10px', + flexGrow: 1, + flexShrink: 0 })); export const AppsSearchRight = styled(Box)(({ theme }) => ({ display: "flex", width: "90%", justifyContent: 'flex-end', alignItems: 'center', + flexShrink: 1 })); export const AppCircleContainer = styled(Box)(({ theme }) => ({ display: "flex", @@ -105,4 +110,85 @@ import { borderRadius: '50%', backgroundColor: "var(--apps-circle)", border: '1px solid #FFFFFF' + })); + + export const AppInfoSnippetContainer = styled(Box)(({ theme }) => ({ + display: "flex", + justifyContent: 'space-between', + alignItems: 'center', + width: '100%' + })); + + export const AppInfoSnippetLeft = styled(Box)(({ theme }) => ({ + display: "flex", + justifyContent: 'flex-start', + alignItems: 'center', + gap: '12px' + })); + export const AppInfoSnippetRight = styled(Box)(({ theme }) => ({ + display: "flex", + justifyContent: 'flex-end', + alignItems: 'center', + })); + + export const AppDownloadButton = styled(ButtonBase)(({ theme }) => ({ + backgroundColor: "#247C0E", + width: '101px', + height: '29px', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + borderRadius: '25px' + })); + + export const AppDownloadButtonText = styled(Typography)(({ theme }) => ({ + fontSize: '14px', + fontWeight: 500, + lineHeight: 1.2, + })); + + + + export const AppInfoSnippetMiddle = styled(Box)(({ theme }) => ({ + display: "flex", + flexDirection: "column", + justifyContent: 'center', + alignItems: 'flex-start', + })); + + export const AppInfoAppName = styled(Typography)(({ theme }) => ({ + fontSize: '16px', + fontWeight: 500, + lineHeight: 1.2, + })); + export const AppInfoUserName = styled(Typography)(({ theme }) => ({ + fontSize: '13px', + fontWeight: 400, + lineHeight: 1.2, + color: '#8D8F93' + })); + + + export const AppsNavBarParent = styled(Box)(({ theme }) => ({ + display: "flex", + justifyContent: 'space-between', + alignItems: 'center', + width: '100%', + height: '60px', + backgroundColor: '#1F2023', + padding: '0px 10px', + position: "fixed", + bottom: 0, + zIndex: 1, + })); + + export const AppsNavBarLeft = styled(Box)(({ theme }) => ({ + display: "flex", + justifyContent: 'flex-start', + alignItems: 'center', + })); + export const AppsNavBarRight = styled(Box)(({ theme }) => ({ + display: "flex", + justifyContent: 'flex-end', + alignItems: 'center', })); \ No newline at end of file diff --git a/src/components/Apps/Apps.tsx b/src/components/Apps/Apps.tsx index 9ffe412..0f4a1f0 100644 --- a/src/components/Apps/Apps.tsx +++ b/src/components/Apps/Apps.tsx @@ -1,14 +1,26 @@ -import React, { useEffect, useState } from 'react' +import React, { useContext, useEffect, useRef, useState } from 'react' import { AppsHome } from './AppsHome' import { Spacer } from '../../common/Spacer' -import { getBaseApiReact } from '../../App' +import { MyContext, getBaseApiReact } from '../../App' import { AppsLibrary } from './AppsLibrary' +import { AppInfo } from './AppInfo' +import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events' +import { AppsNavBar } from './AppsNavBar' +import { AppsParent } from './Apps-styles' +import { AppViewer } from './AppViewer' +import AppViewerContainer from './AppViewerContainer' +import ShortUniqueId from "short-unique-id"; -export const Apps = () => { - const [mode, setMode] = useState('home') +const uid = new ShortUniqueId({ length: 8 }); + +export const Apps = ({mode, setMode}) => { const [availableQapps, setAvailableQapps] = useState([]) const [downloadedQapps, setDownloadedQapps] = useState([]) + const [selectedAppInfo, setSelectedAppInfo] = useState(null) + const [tabs, setTabs] = useState([]) + const [selectedTab, setSelectedTab] = useState(null) + const getQapps = React.useCallback( async () => { @@ -52,11 +64,95 @@ export const Apps = () => { getQapps() }, [getQapps]) + + const selectedAppInfoFunc = (e) => { + const data = e.detail?.data; + setSelectedAppInfo(data) + setMode('appInfo') + }; + + useEffect(() => { + subscribeToEvent("selectedAppInfo", selectedAppInfoFunc); + + return () => { + unsubscribeFromEvent("selectedAppInfo", selectedAppInfoFunc); + }; + }, []); + + const navigateBackFunc = (e) => { + if(mode === 'appInfo'){ + setMode('library') + } else if(mode === 'library'){ + setMode('home') + } else { + + const iframe = document.getElementById('browser-iframe2'); + console.log('iframe', iframe) +// Go Back in the iframe's history +if (iframe) { + console.log(iframe.contentWindow) + if (iframe && iframe.contentWindow) { + const iframeWindow = iframe.contentWindow; + if (iframeWindow && iframeWindow.history) { + iframeWindow.history.back(); + } + } + } + + } + }; + + useEffect(() => { + subscribeToEvent("navigateBack", navigateBackFunc); + + return () => { + unsubscribeFromEvent("navigateBack", navigateBackFunc); + }; + }, [mode]); + + + const addTabFunc = (e) => { + const data = e.detail?.data; + const newTab = { + ...data, + tabId: uid.rnd() + } + setTabs((prev)=> [...prev, newTab]) + setSelectedTab(newTab) + }; + + useEffect(() => { + subscribeToEvent("addTab", addTabFunc); + + return () => { + unsubscribeFromEvent("addTab", addTabFunc); + }; + }, [mode]); + + + + return ( - <> - + + {mode !== 'viewer' && ( + + + )} {mode === 'home' && } {mode === 'library' && } - + {mode === 'appInfo' && } + {mode === 'viewer' && ( + <> + {tabs.map((tab)=> { + return + })} + + ) } + + {mode !== 'viewer' && ( + + + )} + ) } diff --git a/src/components/Apps/AppsHome.tsx b/src/components/Apps/AppsHome.tsx index 775d7e0..ab5dc31 100644 --- a/src/components/Apps/AppsHome.tsx +++ b/src/components/Apps/AppsHome.tsx @@ -1,56 +1,81 @@ -import React, { useMemo, useState } from 'react' -import { AppCircle, AppCircleContainer, AppCircleLabel, AppsContainer, AppsParent } from './Apps-styles' -import { Avatar, ButtonBase } from '@mui/material' -import { Add } from '@mui/icons-material' -import { getBaseApiReact } from '../../App' +import React, { useMemo, useState } from "react"; +import { + AppCircle, + AppCircleContainer, + AppCircleLabel, + AppsContainer, + AppsParent, +} from "./Apps-styles"; +import { Avatar, ButtonBase } from "@mui/material"; +import { Add } from "@mui/icons-material"; +import { getBaseApiReact } from "../../App"; import LogoSelected from "../../assets/svgs/LogoSelected.svg"; +import { executeEvent } from "../../utils/events"; -export const AppsHome = ({downloadedQapps, setMode}) => { - - +export const AppsHome = ({ downloadedQapps, setMode }) => { return ( - - - { - setMode('library') - }}> - + + { + setMode("library"); + }} + > + - + + + Add - + + + {downloadedQapps?.map((app) => { + return ( + { + executeEvent("addTab", { + data: app + }) + }} + > + + + + center-icon + + + + {app?.name} + + - {downloadedQapps?.map((qapp)=> { - return ( - - - - - center-icon - - - {qapp?.metadata?.title || qapp?.name} - - - ) - })} - - - ) -} + ); + })} + + ); +}; diff --git a/src/components/Apps/AppsLibrary.tsx b/src/components/Apps/AppsLibrary.tsx index d801bc0..03a8b47 100644 --- a/src/components/Apps/AppsLibrary.tsx +++ b/src/components/Apps/AppsLibrary.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from "react"; +import React, { useContext, useEffect, useMemo, useRef, useState } from "react"; import { AppCircle, AppCircleContainer, @@ -11,14 +11,16 @@ import { AppsSearchLeft, AppsSearchRight, } from "./Apps-styles"; -import { Avatar, Box, ButtonBase, InputBase } from "@mui/material"; +import { Avatar, Box, ButtonBase, InputBase, styled } from "@mui/material"; import { Add } from "@mui/icons-material"; -import { getBaseApiReact } from "../../App"; +import { MyContext, getBaseApiReact } from "../../App"; import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import IconSearch from "../../assets/svgs/Search.svg"; import IconClearInput from "../../assets/svgs/ClearInput.svg"; import { Spacer } from "../../common/Spacer"; +import { AppInfoSnippet } from "./AppInfoSnippet"; +import { Virtuoso } from "react-virtuoso"; const officialAppList = [ "q-tube", "q-blog", @@ -30,21 +32,41 @@ const officialAppList = [ "q-shop", ]; +const ScrollerStyled = styled('div')({ + // Hide scrollbar for WebKit browsers (Chrome, Safari) + "::-webkit-scrollbar": { + width: "0px", + height: "0px", + }, + + // Hide scrollbar for Firefox + scrollbarWidth: "none", + + // Hide scrollbar for IE and older Edge + "-ms-overflow-style": "none", + }); + + export const AppsLibrary = ({ downloadedQapps, availableQapps }) => { - const [searchValue, setSearchValue] = useState('') + const [searchValue, setSearchValue] = useState(""); + const virtuosoRef = useRef(); + const { rootHeight } = useContext(MyContext); + const officialApps = useMemo(() => { - return availableQapps.filter((app) => app.service === 'APP' && - officialAppList.includes(app?.name?.toLowerCase()) + return availableQapps.filter( + (app) => + app.service === "APP" && + officialAppList.includes(app?.name?.toLowerCase()) ); }, [availableQapps]); - const [debouncedValue, setDebouncedValue] = useState(''); // Debounced value + const [debouncedValue, setDebouncedValue] = useState(""); // Debounced value // Debounce logic useEffect(() => { const handler = setTimeout(() => { - setDebouncedValue(searchValue); // Update debounced value after delay - }, 500); // 500ms debounce time (adjustable) + setDebouncedValue(searchValue); + }, 250); // Cleanup timeout if searchValue changes before the timeout completes return () => { @@ -53,111 +75,147 @@ export const AppsLibrary = ({ downloadedQapps, availableQapps }) => { }, [searchValue]); // Runs effect when searchValue changes // Example: Perform search or other actions based on debouncedValue - - const searchedList = useMemo(()=> { - if(!debouncedValue) return [] - return availableQapps.filter((app)=> app.name.toLowerCase().includes(debouncedValue.toLowerCase())) - }, [debouncedValue]) - console.log('officialApps', searchedList) - - - return ( - - - - - - - setSearchValue(e.target.value)} - sx={{ ml: 1, flex: 1 }} - placeholder="Search for apps" - inputProps={{ 'aria-label': 'Search for apps', fontSize: '16px', fontWeight: 400 }} - /> - - - {searchValue && ( - { - setSearchValue('') - }}> - - - )} - - - - - - {searchedList?.length > 0 ? ( - <> - {searchedList.map((app)=> { - return ( - - ) - })} - - ) : ( - <> - Official Apps - - - {officialApps?.map((qapp) => { - return ( - { + if (!debouncedValue) return []; + return availableQapps.filter((app) => + app.name.toLowerCase().includes(debouncedValue.toLowerCase()) + ); + }, [debouncedValue]); + console.log("officialApps", searchedList); + + const rowRenderer = (index) => { + + let app = searchedList[index]; + console.log('appi', app) + return ; + }; + + const StyledVirtuosoContainer = styled('div')({ + position: 'relative', + height: rootHeight, + width: '100%', + display: 'flex', + flexDirection: 'column', + + // Hide scrollbar for WebKit browsers (Chrome, Safari) + "::-webkit-scrollbar": { + width: "0px", + height: "0px", + }, + + // Hide scrollbar for Firefox + scrollbarWidth: "none", + + // Hide scrollbar for IE and older Edge + "-ms-overflow-style": "none", + }); + + return ( + + + + + + setSearchValue(e.target.value)} + sx={{ ml: 1, flex: 1 }} + placeholder="Search for apps" + inputProps={{ + "aria-label": "Search for apps", + fontSize: "16px", + fontWeight: 400, }} - > - - + + + {searchValue && ( + { + setSearchValue(""); + }} + > + + + )} + + + + + {searchedList?.length > 0 ? ( + + + + ) : ( + <> + Official Apps + + + {officialApps?.map((qapp) => { + return ( + - - + - - - - {qapp?.metadata?.title || qapp?.name} - - - - ); - })} - - - Featured + > + + center-icon + + + + {qapp?.metadata?.title || qapp?.name} + + + + ); + })} + + + Featured - - Categories - + + Categories + )} - - ); }; diff --git a/src/components/Apps/AppsNavBar.tsx b/src/components/Apps/AppsNavBar.tsx new file mode 100644 index 0000000..01dc0e6 --- /dev/null +++ b/src/components/Apps/AppsNavBar.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { + AppsNavBarLeft, + AppsNavBarParent, + AppsNavBarRight, +} from "./Apps-styles"; +import NavBack from "../../assets/svgs/NavBack.svg"; +import NavCloseTab from "../../assets/svgs/NavCloseTab.svg"; +import NavAdd from "../../assets/svgs/NavAdd.svg"; +import NavMoreMenu from "../../assets/svgs/NavMoreMenu.svg"; +import { ButtonBase } from "@mui/material"; +import { executeEvent } from "../../utils/events"; + +export const AppsNavBar = () => { + return ( + + + { + executeEvent("navigateBack", { + }); + }}> + + + + + + + + + + + + + ); +}; diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index 3e09a3a..719ebe9 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -89,6 +89,7 @@ import { HomeDesktop } from "./HomeDesktop"; import { DesktopFooter } from "../Desktop/DesktopFooter"; import { DesktopHeader } from "../Desktop/DesktopHeader"; import { Apps } from "../Apps/Apps"; +import { AppsNavBar } from "../Apps/AppsNavBar"; // let touchStartY = 0; // let disablePullToRefresh = false; @@ -432,6 +433,7 @@ export const Group = ({ const { clearStatesMessageQueueProvider } = useMessageQueue(); const initiatedGetMembers = useRef(false); const [groupChatTimestamps, setGroupChatTimestamps] = React.useState({}); + const [appsMode, setAppsMode] = useState('viewer') useEffect(()=> { timestampEnterDataRef.current = timestampEnterData @@ -2229,7 +2231,7 @@ export const Group = ({ isThin={ mobileViewMode === "groups" || mobileViewMode === "group" || - mobileViewModeKeepOpen === "messaging" + mobileViewModeKeepOpen === "messaging" || (mobileViewMode === "apps" && appsMode !== 'home') } logoutFunc={logoutFunc} goToHome={goToHome} @@ -2735,7 +2737,7 @@ export const Group = ({ /> )} {isMobile && mobileViewMode === "apps" && ( - + )} { !isMobile && !selectedGroup && @@ -2962,7 +2964,7 @@ export const Group = ({ />
- {(isMobile && mobileViewMode === "home" || isMobile && mobileViewMode === "apps") && !mobileViewModeKeepOpen && ( + {(isMobile && mobileViewMode === "home" || (isMobile && mobileViewMode === "apps" && appsMode === 'home')) && !mobileViewModeKeepOpen && ( <>
)} + {(isMobile && isMobile && mobileViewMode === "apps" && appsMode !== 'home') && !mobileViewModeKeepOpen && ( + <> + + + )} ); }; From 54b1725585776486d89cb8a3861da2997f31013c Mon Sep 17 00:00:00 2001 From: PhilReact Date: Fri, 18 Oct 2024 04:51:16 +0300 Subject: [PATCH 24/58] added tabs --- src/components/Apps/AppViewerContainer.tsx | 4 +- src/components/Apps/Apps-styles.tsx | 16 ++- src/components/Apps/Apps.tsx | 108 ++++++++++++++++++--- src/components/Apps/AppsNavBar.tsx | 73 +++++++++++++- src/components/Apps/TabComponent.tsx | 61 ++++++++++++ src/components/Group/Group.tsx | 6 +- 6 files changed, 243 insertions(+), 25 deletions(-) create mode 100644 src/components/Apps/TabComponent.tsx diff --git a/src/components/Apps/AppViewerContainer.tsx b/src/components/Apps/AppViewerContainer.tsx index ef04760..c566ee4 100644 --- a/src/components/Apps/AppViewerContainer.tsx +++ b/src/components/Apps/AppViewerContainer.tsx @@ -3,7 +3,7 @@ import { AppViewer } from './AppViewer' import Frame from 'react-frame-component'; import { MyContext } from '../../App'; -const AppViewerContainer = ({app, isSelected}) => { +const AppViewerContainer = ({app, isSelected, hide}) => { const { rootHeight } = useContext(MyContext); const frameRef = useRef(null); @@ -32,7 +32,7 @@ const AppViewerContainer = ({app, isSelected}) => { height: `calc(${rootHeight} - 60px - 45px)`, border: 'none', width: '100%', - display: !isSelected && 'none' + display: (!isSelected || hide) && 'none' }} > ) } diff --git a/src/components/Apps/Apps-styles.tsx b/src/components/Apps/Apps-styles.tsx index 6f67658..d219bcb 100644 --- a/src/components/Apps/Apps-styles.tsx +++ b/src/components/Apps/Apps-styles.tsx @@ -186,9 +186,23 @@ import { display: "flex", justifyContent: 'flex-start', alignItems: 'center', + flexGrow: 1 })); export const AppsNavBarRight = styled(Box)(({ theme }) => ({ display: "flex", justifyContent: 'flex-end', alignItems: 'center', - })); \ No newline at end of file + })); + + export const TabParent = styled(Box)(({ theme }) => ({ + height: '36px', + width: '36px', + backgroundColor: '#434343', + position: 'relative', + borderRadius: '50%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + })); + + \ No newline at end of file diff --git a/src/components/Apps/Apps.tsx b/src/components/Apps/Apps.tsx index 0f4a1f0..79684c2 100644 --- a/src/components/Apps/Apps.tsx +++ b/src/components/Apps/Apps.tsx @@ -4,7 +4,7 @@ import { Spacer } from '../../common/Spacer' import { MyContext, getBaseApiReact } from '../../App' import { AppsLibrary } from './AppsLibrary' import { AppInfo } from './AppInfo' -import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events' +import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from '../../utils/events' import { AppsNavBar } from './AppsNavBar' import { AppsParent } from './Apps-styles' import { AppViewer } from './AppViewer' @@ -14,13 +14,24 @@ import ShortUniqueId from "short-unique-id"; const uid = new ShortUniqueId({ length: 8 }); -export const Apps = ({mode, setMode}) => { +export const Apps = ({mode, setMode, show}) => { const [availableQapps, setAvailableQapps] = useState([]) const [downloadedQapps, setDownloadedQapps] = useState([]) const [selectedAppInfo, setSelectedAppInfo] = useState(null) const [tabs, setTabs] = useState([]) const [selectedTab, setSelectedTab] = useState(null) - + const [isNewTabWindow, setIsNewTabWindow] = useState(false) + + useEffect(()=> { + setTimeout(() => { + executeEvent('setTabsToNav', { + data: { + tabs: tabs, + selectedTab: selectedTab + } + }) + }, 100); + }, [show, tabs, selectedTab]) const getQapps = React.useCallback( async () => { @@ -85,8 +96,8 @@ export const Apps = ({mode, setMode}) => { } else if(mode === 'library'){ setMode('home') } else { - - const iframe = document.getElementById('browser-iframe2'); + const iframeId = `browser-iframe-${selectedTab?.tabId}` + const iframe = document.getElementById(iframeId); console.log('iframe', iframe) // Go Back in the iframe's history if (iframe) { @@ -108,7 +119,7 @@ if (iframe) { return () => { unsubscribeFromEvent("navigateBack", navigateBackFunc); }; - }, [mode]); + }, [mode, selectedTab]); const addTabFunc = (e) => { @@ -119,6 +130,9 @@ if (iframe) { } setTabs((prev)=> [...prev, newTab]) setSelectedTab(newTab) + setMode('viewer') + + setIsNewTabWindow(false) }; useEffect(() => { @@ -127,10 +141,76 @@ if (iframe) { return () => { unsubscribeFromEvent("addTab", addTabFunc); }; - }, [mode]); - + }, [tabs]); + const setSelectedTabFunc = (e) => { + const data = e.detail?.data; + + + setSelectedTab(data) + setTimeout(() => { + executeEvent('setTabsToNav', { + data: { + tabs: tabs, + selectedTab: data + } + }) + }, 100); + setIsNewTabWindow(false) + }; + useEffect(() => { + subscribeToEvent("setSelectedTab", setSelectedTabFunc); + + return () => { + unsubscribeFromEvent("setSelectedTab", setSelectedTabFunc); + }; + }, [tabs]); + + const removeTabFunc = (e) => { + const data = e.detail?.data; + const copyTabs = [...tabs].filter((tab)=> tab?.tabId !== data?.tabId) + if(copyTabs?.length === 0){ + setMode('home') + } + else{ + setSelectedTab(copyTabs[0]) + } + setTabs(copyTabs) + setSelectedTab(copyTabs[0]) + setTimeout(() => { + executeEvent('setTabsToNav', { + data: { + tabs: copyTabs, + selectedTab: copyTabs[0] + } + }) + }, 400); + + }; + + useEffect(() => { + subscribeToEvent("removeTab", removeTabFunc); + + return () => { + unsubscribeFromEvent("removeTab", removeTabFunc); + }; + }, [tabs]); + + const setNewTabWindowFunc = (e) => { + setIsNewTabWindow(true) + + }; + + useEffect(() => { + subscribeToEvent("newTabWindow", setNewTabWindowFunc); + + return () => { + unsubscribeFromEvent("newTabWindow", setNewTabWindowFunc); + }; + }, [tabs]); + + if(!show) return null return ( @@ -141,14 +221,14 @@ if (iframe) { {mode === 'home' && } {mode === 'library' && } {mode === 'appInfo' && } - {mode === 'viewer' && ( - <> + {tabs.map((tab)=> { - return + return })} - - ) } - + + {isNewTabWindow && ( + + )} {mode !== 'viewer' && ( diff --git a/src/components/Apps/AppsNavBar.tsx b/src/components/Apps/AppsNavBar.tsx index 01dc0e6..2e1ec5c 100644 --- a/src/components/Apps/AppsNavBar.tsx +++ b/src/components/Apps/AppsNavBar.tsx @@ -1,17 +1,48 @@ -import React from "react"; +import React, { useEffect, useRef, useState } from "react"; import { AppsNavBarLeft, AppsNavBarParent, AppsNavBarRight, } from "./Apps-styles"; import NavBack from "../../assets/svgs/NavBack.svg"; -import NavCloseTab from "../../assets/svgs/NavCloseTab.svg"; import NavAdd from "../../assets/svgs/NavAdd.svg"; import NavMoreMenu from "../../assets/svgs/NavMoreMenu.svg"; -import { ButtonBase } from "@mui/material"; -import { executeEvent } from "../../utils/events"; +import { ButtonBase, Tab, Tabs } from "@mui/material"; +import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; +import TabComponent from "./TabComponent"; export const AppsNavBar = () => { + const [tabs, setTabs] = useState([]) + const [selectedTab, setSelectedTab] = useState([]) + + const tabsRef = useRef(null); + + useEffect(() => { + // Scroll to the last tab whenever the tabs array changes (e.g., when a new tab is added) + if (tabsRef.current) { + const tabElements = tabsRef.current.querySelectorAll('.MuiTab-root'); + console.log('tabElements', tabElements) + if (tabElements.length > 0) { + const lastTab = tabElements[tabElements.length - 1]; + lastTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'end' }); + } + } + }, [tabs.length]); // Dependency on the number of tabs + + const setTabsToNav = (e) => { + const {tabs, selectedTab} = e.detail?.data; + + setTabs([...tabs]) + setSelectedTab({...selectedTab}) + }; + + useEffect(() => { + subscribeToEvent("setTabsToNav", setTabsToNav); + + return () => { + unsubscribeFromEvent("setTabsToNav", setTabsToNav); + }; + }, []); return ( @@ -21,11 +52,43 @@ export const AppsNavBar = () => { }}> + + {tabs?.map((tab) => ( + } // Pass custom component + sx={{ + "&.Mui-selected": { + color: "white", + }, + padding: '0px', + margin: '0px', + minWidth: '0px', + width: '50px' + }} + /> + ))} + - + { + executeEvent("newTabWindow", { + }); + }}> { + return ( + { + if(isSelected){ + executeEvent('removeTab', { + data: app + }) + return + } + executeEvent('setSelectedTab', { + data: app + }) + }}> + + {isSelected && ( + + + + ) } + + center-icon + + + + ) +} + +export default TabComponent \ No newline at end of file diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index 719ebe9..3a147b3 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -433,7 +433,7 @@ export const Group = ({ const { clearStatesMessageQueueProvider } = useMessageQueue(); const initiatedGetMembers = useRef(false); const [groupChatTimestamps, setGroupChatTimestamps] = React.useState({}); - const [appsMode, setAppsMode] = useState('viewer') + const [appsMode, setAppsMode] = useState('home') useEffect(()=> { timestampEnterDataRef.current = timestampEnterData @@ -2736,8 +2736,8 @@ export const Group = ({ setMobileViewMode={setMobileViewMode} /> )} - {isMobile && mobileViewMode === "apps" && ( - + {isMobile && ( + )} { !isMobile && !selectedGroup && From 020366477a6551a3136df71070646f3c9bd6390a Mon Sep 17 00:00:00 2001 From: PhilReact Date: Fri, 18 Oct 2024 08:36:10 +0300 Subject: [PATCH 25/58] added qortal requests for browser --- src/background.ts | 2 + src/components/Apps/AppInfo.tsx | 6 +- src/components/Apps/AppInfoSnippet.tsx | 6 +- src/components/Apps/AppViewer.tsx | 11 +- src/components/Apps/AppViewerContainer.tsx | 3 +- src/components/Apps/Apps.tsx | 7 +- src/components/Apps/TabComponent.tsx | 6 +- .../Apps/useQortalMessageListener.tsx | 405 ++++++++++++++++++ src/qortalRequests/get.ts | 1 + 9 files changed, 436 insertions(+), 11 deletions(-) create mode 100644 src/components/Apps/useQortalMessageListener.tsx diff --git a/src/background.ts b/src/background.ts index 40e6e30..68443d3 100644 --- a/src/background.ts +++ b/src/background.ts @@ -2830,6 +2830,8 @@ async function getChatHeadsDirect() { chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { if (request) { + console.log('REQUEST MESSAGE', request) + switch (request.action) { case "version": // Example: respond with the version diff --git a/src/components/Apps/AppInfo.tsx b/src/components/Apps/AppInfo.tsx index eb1c686..af4787f 100644 --- a/src/components/Apps/AppInfo.tsx +++ b/src/components/Apps/AppInfo.tsx @@ -87,7 +87,11 @@ export const AppInfo = ({ app }) => { - { + executeEvent("addTab", { + data: app + }) + }} sx={{ backgroundColor: isInstalled ? '#0091E1' : '#247C0E', width: '100%', maxWidth: '320px', diff --git a/src/components/Apps/AppInfoSnippet.tsx b/src/components/Apps/AppInfoSnippet.tsx index 26d0935..9153945 100644 --- a/src/components/Apps/AppInfoSnippet.tsx +++ b/src/components/Apps/AppInfoSnippet.tsx @@ -88,7 +88,11 @@ export const AppInfoSnippet = ({ app }) => { - { + executeEvent("addTab", { + data: app + }) + }} sx={{ backgroundColor: isInstalled ? '#0091E1' : '#247C0E', }}> diff --git a/src/components/Apps/AppViewer.tsx b/src/components/Apps/AppViewer.tsx index 6992127..3f9486d 100644 --- a/src/components/Apps/AppViewer.tsx +++ b/src/components/Apps/AppViewer.tsx @@ -21,22 +21,27 @@ import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import { Spacer } from "../../common/Spacer"; import { executeEvent } from "../../utils/events"; +import { useFrame } from "react-frame-component"; +import { useQortalMessageListener } from "./useQortalMessageListener"; + + + export const AppViewer = ({ app }) => { const { rootHeight } = useContext(MyContext); const iframeRef = useRef(null); - + const { document, window } = useFrame(); + useQortalMessageListener(window) const url = useMemo(()=> { return `${getBaseApiReact()}/render/${app?.service}/${app?.name}${app?.path != null ? app?.path : ''}?theme=dark&identifier=${(app?.identifier != null && app?.identifier != 'null') ? app?.identifier : ''}` }, [app?.service, app?.name, app?.identifier, app?.path]) - return ( ); diff --git a/src/components/Apps/AppViewerContainer.tsx b/src/components/Apps/AppViewerContainer.tsx index 4152ab2..98303ad 100644 --- a/src/components/Apps/AppViewerContainer.tsx +++ b/src/components/Apps/AppViewerContainer.tsx @@ -2,12 +2,24 @@ import React, { useContext, useEffect, useRef } from 'react' import { AppViewer } from './AppViewer' import Frame from 'react-frame-component'; import { MyContext } from '../../App'; +import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events'; const AppViewerContainer = ({app, isSelected, hide}) => { const { rootHeight } = useContext(MyContext); const frameRef = useRef(null); - + const refreshAppFunc = (e) => { + console.log('getting refresh', e) + }; + + // useEffect(() => { + // subscribeToEvent("refreshAPp", refreshAppFunc); + + // return () => { + // unsubscribeFromEvent("refreshApp", refreshAppFunc); + // }; + // }, []); + return ( diff --git a/src/components/Apps/Apps.tsx b/src/components/Apps/Apps.tsx index 1c532c2..0a6ea90 100644 --- a/src/components/Apps/Apps.tsx +++ b/src/components/Apps/Apps.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useRef, useState } from "react"; +import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { AppsHome } from "./AppsHome"; import { Spacer } from "../../common/Spacer"; import { MyContext, getBaseApiReact } from "../../App"; @@ -15,19 +15,27 @@ import { AppViewer } from "./AppViewer"; import AppViewerContainer from "./AppViewerContainer"; import ShortUniqueId from "short-unique-id"; import { AppPublish } from "./AppPublish"; +import { useRecoilState } from "recoil"; const uid = new ShortUniqueId({ length: 8 }); export const Apps = ({ mode, setMode, show , myName}) => { const [availableQapps, setAvailableQapps] = useState([]); - const [downloadedQapps, setDownloadedQapps] = useState([]); const [selectedAppInfo, setSelectedAppInfo] = useState(null); const [tabs, setTabs] = useState([]); const [selectedTab, setSelectedTab] = useState(null); const [isNewTabWindow, setIsNewTabWindow] = useState(false); const [categories, setCategories] = useState([]) - const [myApp, setMyApp] = useState(null) - const [myWebsite, setMyWebsite] = useState(null) + + + const myApp = useMemo(()=> { + + return availableQapps.find((app)=> app.name === myName && app.service === 'APP') + }, [myName, availableQapps]) + const myWebsite = useMemo(()=> { + + return availableQapps.find((app)=> app.name === myName && app.service === 'WEBSITE') + }, [myName, availableQapps]) useEffect(() => { setTimeout(() => { @@ -62,12 +70,12 @@ export const Apps = ({ mode, setMode, show , myName}) => { } }, []); - const getQapps = React.useCallback(async (myName) => { + const getQapps = React.useCallback(async () => { try { let apps = []; let websites = []; // dispatch(setIsLoadingGlobal(true)) - const url = `${getBaseApiReact()}/arbitrary/resources/search?service=APP&mode=ALL&includestatus=true&limit=0&includemetadata=true`; + const url = `${getBaseApiReact()}/arbitrary/resources/search?service=APP&mode=ALL&limit=0&includestatus=true&includemetadata=true`; const response = await fetch(url, { method: "GET", @@ -77,7 +85,8 @@ export const Apps = ({ mode, setMode, show , myName}) => { }); if (!response?.ok) return; const responseData = await response.json(); - const urlWebsites = `${getBaseApiReact()}/arbitrary/resources/search?service=WEBSITE&mode=ALL&includestatus=true&limit=0&includemetadata=true`; + console.log('responseData', responseData) + const urlWebsites = `${getBaseApiReact()}/arbitrary/resources/search?service=WEBSITE&mode=ALL&limit=0&includestatus=true&includemetadata=true`; const responseWebsites = await fetch(urlWebsites, { method: "GET", @@ -87,31 +96,20 @@ export const Apps = ({ mode, setMode, show , myName}) => { }); if (!responseWebsites?.ok) return; const responseDataWebsites = await responseWebsites.json(); - const findMyWebsite = responseDataWebsites.find((web)=> web.name === myName) - if(findMyWebsite){ - setMyWebsite(findMyWebsite) - } - const findMyApp = responseData.find((web)=> web.name === myName) - console.log('findMyApp', findMyApp) - if(findMyApp){ - setMyWebsite(findMyApp) - } + apps = responseData; websites = responseDataWebsites; const combine = [...apps, ...websites]; setAvailableQapps(combine); - setDownloadedQapps( - combine.filter((qapp) => qapp?.status?.status === "READY") - ); } catch (error) { } finally { // dispatch(setIsLoadingGlobal(false)) } }, []); useEffect(() => { - getQapps(myName); + getQapps(); getCategories() - }, [getQapps, getCategories, myName]); + }, [getQapps, getCategories]); const selectedAppInfoFunc = (e) => { const data = e.detail?.data; @@ -256,11 +254,10 @@ export const Apps = ({ mode, setMode, show , myName}) => { > {mode !== "viewer" && } {mode === "home" && ( - + )} {mode === "library" && ( { {isNewTabWindow && mode === "viewer" && ( <> - + )} {mode !== "viewer" && } diff --git a/src/components/Apps/AppsHome.tsx b/src/components/Apps/AppsHome.tsx index d9142a1..2927694 100644 --- a/src/components/Apps/AppsHome.tsx +++ b/src/components/Apps/AppsHome.tsx @@ -11,8 +11,9 @@ import { Add } from "@mui/icons-material"; import { getBaseApiReact } from "../../App"; import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import { executeEvent } from "../../utils/events"; +import { SortablePinnedApps } from "./SortablePinnedApps"; -export const AppsHome = ({ downloadedQapps, setMode, myApp, myWebsite, myName }) => { +export const AppsHome = ({ setMode, myApp, myWebsite, availableQapps }) => { return ( Add - {myApp &&( - { - executeEvent("addTab", { - data: myApp - }) - }} - > - - - - center-icon - - - - {myApp?.name} - - - - )} - {myWebsite &&( - { - executeEvent("addTab", { - data: myWebsite - }) - }} - > - - - - center-icon - - - - {myWebsite?.name} - - - - )} - {downloadedQapps?.filter((item)=> item?.name !== myName).map((app) => { - return ( - { - executeEvent("addTab", { - data: app - }) - }} - > - - - - center-icon - - - - {app?.name} - - - - ); - })} + + + ); }; diff --git a/src/components/Apps/AppsLibrary.tsx b/src/components/Apps/AppsLibrary.tsx index d220a4e..78f083a 100644 --- a/src/components/Apps/AppsLibrary.tsx +++ b/src/components/Apps/AppsLibrary.tsx @@ -74,7 +74,7 @@ const ScrollerStyled = styled('div')({ "-ms-overflow-style": "none", }); -export const AppsLibrary = ({ downloadedQapps, availableQapps, setMode, myName, hasPublishApp }) => { +export const AppsLibrary = ({ availableQapps, setMode, myName, hasPublishApp }) => { const [searchValue, setSearchValue] = useState(""); const virtuosoRef = useRef(); const { rootHeight } = useContext(MyContext); diff --git a/src/components/Apps/AppsNavBar.tsx b/src/components/Apps/AppsNavBar.tsx index 09c4e22..0a703cf 100644 --- a/src/components/Apps/AppsNavBar.tsx +++ b/src/components/Apps/AppsNavBar.tsx @@ -7,15 +7,41 @@ import { import NavBack from "../../assets/svgs/NavBack.svg"; import NavAdd from "../../assets/svgs/NavAdd.svg"; import NavMoreMenu from "../../assets/svgs/NavMoreMenu.svg"; -import { ButtonBase, Tab, Tabs } from "@mui/material"; +import { ButtonBase, ListItemIcon, ListItemText, Menu, MenuItem, Tab, Tabs } from "@mui/material"; import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; import TabComponent from "./TabComponent"; +import PushPinIcon from '@mui/icons-material/PushPin'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import { useRecoilState } from "recoil"; +import { sortablePinnedAppsAtom } from "../../atoms/global"; + +export function saveToLocalStorage(key, value) { + try { + const serializedValue = JSON.stringify(value); + localStorage.setItem(key, serializedValue); + console.log(`Data saved to localStorage with key: ${key}`); + } catch (error) { + console.error('Error saving to localStorage:', error); + } +} + export const AppsNavBar = () => { const [tabs, setTabs] = useState([]) const [selectedTab, setSelectedTab] = useState([]) const [isNewTabWindow, setIsNewTabWindow] = useState(false) const tabsRef = useRef(null); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom); + + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; useEffect(() => { // Scroll to the last tab whenever the tabs array changes (e.g., when a new tab is added) @@ -44,6 +70,8 @@ export const AppsNavBar = () => { unsubscribeFromEvent("setTabsToNav", setTabsToNav); }; }, []); + + const isSelectedAppPinned = !!sortablePinnedApps?.find((item)=> item?.name === selectedTab?.name && item?.service === selectedTab?.service) return ( @@ -95,13 +123,119 @@ export const AppsNavBar = () => { width: '40px' }} src={NavAdd} /> - + { + handleClick(e) + }}> + + { + if (!selectedTab) return; + + setSortablePinnedApps((prev) => { + let updatedApps; + + if (isSelectedAppPinned) { + // Remove the selected app if it is pinned + updatedApps = prev.filter( + (item) => !(item?.name === selectedTab?.name && item?.service === selectedTab?.service) + ); + } else { + // Add the selected app if it is not pinned + updatedApps = [...prev, { + name: selectedTab?.name, + service: selectedTab?.service, + }]; + } + + saveToLocalStorage('sortablePinnedApps', updatedApps) + return updatedApps; + }); + + handleClose(); + }} + > + + + + + + { + executeEvent('refreshApp', { + tabId: selectedTab?.tabId + }) + handleClose(); + }} + > + + + + + + ); }; diff --git a/src/components/Apps/SortablePinnedApps.tsx b/src/components/Apps/SortablePinnedApps.tsx new file mode 100644 index 0000000..374cb1d --- /dev/null +++ b/src/components/Apps/SortablePinnedApps.tsx @@ -0,0 +1,166 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { DndContext, closestCenter } from '@dnd-kit/core'; +import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable'; +import { KeyboardSensor, PointerSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core'; +import { CSS } from '@dnd-kit/utilities'; +import { Avatar, ButtonBase } from '@mui/material'; +import { AppCircle, AppCircleContainer, AppCircleLabel } from './Apps-styles'; +import { getBaseApiReact } from '../../App'; +import { executeEvent } from '../../utils/events'; +import { sortablePinnedAppsAtom } from '../../atoms/global'; +import { useRecoilState } from 'recoil'; +import { saveToLocalStorage } from './AppsNavBar'; + +const SortableItem = ({ id, name, app }) => { + const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id }); + console.log('namednd', name) + const style = { + transform: CSS.Transform.toString(transform), + transition, + padding: '10px', + border: '1px solid #ccc', + marginBottom: '5px', + borderRadius: '4px', + backgroundColor: '#f9f9f9', + cursor: 'grab', + color: 'black' + }; + + return ( + { + executeEvent("addTab", { + data: app + }) + }} + > + + + + center-icon + + + + {app?.metadata?.title || app?.name} + + + + ); +}; + +export const SortablePinnedApps = ({ myWebsite, myApp, availableQapps = [] }) => { + const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom); + + const transformPinnedApps = useMemo(()=> { + console.log({myWebsite, myApp, availableQapps, pinnedApps}) + let pinned = [...pinnedApps] + const findMyWebsite = pinned?.find((item)=> item?.service === myWebsite?.service && item?.name === myWebsite?.name) + const findMyApp = pinned?.find((item)=> item?.service === myApp?.service && item?.name === myApp?.name) + + if(myWebsite && !findMyWebsite){ + pinned.unshift(myWebsite) + } + if(myApp && !findMyApp){ + pinned.unshift(myApp) + } + pinned = pinned.map((pin)=> { + const findIndex = availableQapps?.findIndex((item)=> item?.service === pin?.service && item?.name === pin?.name) + if(findIndex !== -1) return availableQapps[findIndex] + + return pin + }) + return pinned + }, [myApp, myWebsite, pinnedApps, availableQapps]) + console.log('transformPinnedApps', transformPinnedApps) + // const hasSetPinned = useRef(false) + // useEffect(() => { + // if (!apps || apps.length === 0) return; + + // setPinnedApps((prevPinnedApps) => { + // // Create a map of the previous pinned apps for easy lookup + // const pinnedAppsMap = new Map(prevPinnedApps.map(app => [`${app?.service}-${app?.name}`, app])); + + // // Update the pinnedApps list based on new apps + // const updatedPinnedApps = apps.map(app => { + // const id = `${app?.service}-${app?.name}`; + // // Keep the existing app from pinnedApps if it exists + // return pinnedAppsMap.get(id) || app; + // }); + + // return updatedPinnedApps; + // }); + // }, [apps]); + + console.log('dnd',{pinnedApps}) + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 10, // Set a distance to avoid triggering drag on small movements + }, + }), + useSensor(TouchSensor, { + activationConstraint: { + distance: 10, // Also apply to touch + }, + }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + const handleDragEnd = (event) => { + const { active, over } = event; + + if (!over) return; // Make sure the drop target exists + + if (active.id !== over.id) { + const oldIndex = transformPinnedApps.findIndex((item) => `${item?.service}-${item?.name}` === active.id); + const newIndex = transformPinnedApps.findIndex((item) => `${item?.service}-${item?.name}` === over.id); + + const newOrder = arrayMove(transformPinnedApps, oldIndex, newIndex); + setPinnedApps(newOrder); + saveToLocalStorage('sortablePinnedApps', newOrder) + + } + }; + return ( + + `${app?.service}-${app?.name}`)}> + {transformPinnedApps.map((app) => ( + + ))} + + + ); +}; + diff --git a/src/components/Apps/useQortalMessageListener.tsx b/src/components/Apps/useQortalMessageListener.tsx index 270a540..c9ace3f 100644 --- a/src/components/Apps/useQortalMessageListener.tsx +++ b/src/components/Apps/useQortalMessageListener.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; class Semaphore { constructor(count) { @@ -313,6 +313,7 @@ const UIQortalRequests = [ } export const useQortalMessageListener = (frameWindow) => { + const [path, setPath] = useState('') useEffect(() => { console.log("Listener added react"); @@ -376,7 +377,11 @@ export const useQortalMessageListener = (frameWindow) => { error: 'Failed to prepare data for publishing', }); } - } + } else if(event?.data?.action === 'LINK_TO_QDN_RESOURCE' || + event?.data?.action === 'QDN_RESOURCE_DISPLAYED'){ + const pathUrl = event?.data?.path != null ? (event?.data?.path.startsWith('/') ? '' : '/') + event?.data?.path : null + setPath(pathUrl) + } }; // Add the listener for messages coming from the frameWindow @@ -401,5 +406,7 @@ export const useQortalMessageListener = (frameWindow) => { return true; // Keep the message channel open for async response } }); + + return {path} }; diff --git a/src/components/Mobile/MobileHeader.tsx b/src/components/Mobile/MobileHeader.tsx index 38e6f73..00918cb 100644 --- a/src/components/Mobile/MobileHeader.tsx +++ b/src/components/Mobile/MobileHeader.tsx @@ -19,6 +19,7 @@ import { ArrowDownIcon } from "../../assets/Icons/ArrowDownIcon"; import { MessagingIcon } from "../../assets/Icons/MessagingIcon"; import { MessagingIcon2 } from "../../assets/Icons/MessagingIcon2"; import { HubsIcon } from "../../assets/Icons/HubsIcon"; +import { Save } from "../Save/Save"; const Header = ({ logoutFunc, @@ -135,6 +136,7 @@ const Header = ({ /> + { + const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom); + const [oldPinnedApps, setOldPinnedApps] = useState(pinnedApps) + console.log('oldpin', {oldPinnedApps, pinnedApps}) + + const hasChanged = useMemo(()=> { + return !isEqual(pinnedApps, oldPinnedApps) + }, [oldPinnedApps, pinnedApps]) + return ( +
{hasChanged && 'Save'}
+ ) +} diff --git a/src/main.tsx b/src/main.tsx index 8a477a3..2bdf4b3 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,7 +6,7 @@ import './index.css' import { ThemeProvider, createTheme } from '@mui/material/styles'; import { CssBaseline } from '@mui/material'; import { MessageQueueProvider } from './MessageQueueContext.tsx'; - +import { RecoilRoot } from 'recoil'; const theme = createTheme({ palette: { primary: { @@ -50,7 +50,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render( + + , diff --git a/src/useRetrieveDataLocalStorage.tsx b/src/useRetrieveDataLocalStorage.tsx new file mode 100644 index 0000000..93831e0 --- /dev/null +++ b/src/useRetrieveDataLocalStorage.tsx @@ -0,0 +1,34 @@ +import React, { useCallback, useEffect } from 'react' +import { useSetRecoilState } from 'recoil'; +import { sortablePinnedAppsAtom } from './atoms/global'; + +function fetchFromLocalStorage(key) { + try { + const serializedValue = localStorage.getItem(key); + if (serializedValue === null) { + console.log(`No data found for key: ${key}`); + return null; + } + return JSON.parse(serializedValue); + } catch (error) { + console.error('Error fetching from localStorage:', error); + return null; + } +} + +export const useRetrieveDataLocalStorage = () => { + const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom); + + + const getSortablePinnedApps = useCallback(()=> { + const pinnedAppsLocal = fetchFromLocalStorage('sortablePinnedApps') + if(pinnedAppsLocal){ + setSortablePinnedApps(pinnedAppsLocal) + } + }, []) + useEffect(()=> { + + getSortablePinnedApps() + }, [getSortablePinnedApps]) + +} From 082c9c11cf1837dee81a3ce2107b4f29e7e75f1a Mon Sep 17 00:00:00 2001 From: PhilReact Date: Mon, 21 Oct 2024 01:20:06 +0300 Subject: [PATCH 30/58] added save settings to qdn --- src/App.tsx | 2 + src/assets/svgs/SaveIcon.tsx | 10 ++ src/atoms/global.ts | 5 + src/components/Apps/AppViewerContainer.tsx | 2 +- src/components/Apps/Apps.tsx | 10 +- src/components/Apps/AppsLibrary.tsx | 6 +- src/components/Apps/AppsNavBar.tsx | 1 + src/components/Mobile/MobileHeader.tsx | 18 +-- src/components/Save/Save.tsx | 127 ++++++++++++++++++++- src/useQortalGetSaveSettings.tsx | 77 +++++++++++++ 10 files changed, 233 insertions(+), 25 deletions(-) create mode 100644 src/assets/svgs/SaveIcon.tsx create mode 100644 src/useQortalGetSaveSettings.tsx diff --git a/src/App.tsx b/src/App.tsx index 48aaf6a..5200bec 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -99,6 +99,7 @@ import { AddressQRCode } from "./components/AddressQRCode"; import { Settings } from "./components/Group/Settings"; import { MainAvatar } from "./components/MainAvatar"; import { useRetrieveDataLocalStorage } from "./useRetrieveDataLocalStorage"; +import { useQortalGetSaveSettings } from "./useQortalGetSaveSettings"; type extStates = | "not-authenticated" @@ -324,6 +325,7 @@ function App() { const [isSettingsOpen, setIsSettingsOpen] = useState(false); const qortalRequestCheckbox1Ref = useRef(null); useRetrieveDataLocalStorage() + useQortalGetSaveSettings(userInfo?.name) useEffect(() => { if (!isMobile) return; // Function to set the height of the app to the viewport height diff --git a/src/assets/svgs/SaveIcon.tsx b/src/assets/svgs/SaveIcon.tsx new file mode 100644 index 0000000..12c4999 --- /dev/null +++ b/src/assets/svgs/SaveIcon.tsx @@ -0,0 +1,10 @@ +import React from 'react' + +export const SaveIcon = ({color = '#8F8F91'}) => { + return ( + + + + + ) +} diff --git a/src/atoms/global.ts b/src/atoms/global.ts index 54d4186..2ad90d4 100644 --- a/src/atoms/global.ts +++ b/src/atoms/global.ts @@ -4,4 +4,9 @@ import { atom } from 'recoil'; export const sortablePinnedAppsAtom = atom({ key: 'sortablePinnedAppsFromAtom', default: [], +}); + +export const canSaveSettingToQdnAtom = atom({ + key: 'canSaveSettingToQdnAtom', + default: false, }); \ No newline at end of file diff --git a/src/components/Apps/AppViewerContainer.tsx b/src/components/Apps/AppViewerContainer.tsx index 98303ad..7f01c15 100644 --- a/src/components/Apps/AppViewerContainer.tsx +++ b/src/components/Apps/AppViewerContainer.tsx @@ -42,7 +42,7 @@ const AppViewerContainer = ({app, isSelected, hide}) => { } style={{ - height: `calc(${rootHeight} - 60px - 45px)`, + height: `calc(${rootHeight} - 60px - 45px - 20px)`, border: 'none', width: '100%', display: (!isSelected || hide) && 'none' diff --git a/src/components/Apps/Apps.tsx b/src/components/Apps/Apps.tsx index 0a6ea90..bc1af29 100644 --- a/src/components/Apps/Apps.tsx +++ b/src/components/Apps/Apps.tsx @@ -236,6 +236,7 @@ export const Apps = ({ mode, setMode, show , myName}) => { const setNewTabWindowFunc = (e) => { setIsNewTabWindow(true); + setSelectedTab(null) }; useEffect(() => { @@ -252,18 +253,19 @@ export const Apps = ({ mode, setMode, show , myName}) => { display: !show && "none", }} > - {mode !== "viewer" && } + {mode !== "viewer" && !selectedTab && } {mode === "home" && ( )} - {mode === "library" && ( + - )} + {mode === "appInfo" && } {mode === "publish" && } @@ -283,7 +285,7 @@ export const Apps = ({ mode, setMode, show , myName}) => { )} - {mode !== "viewer" && } + {mode !== "viewer" && !selectedTab && }
); }; diff --git a/src/components/Apps/AppsLibrary.tsx b/src/components/Apps/AppsLibrary.tsx index 78f083a..c1cc3ef 100644 --- a/src/components/Apps/AppsLibrary.tsx +++ b/src/components/Apps/AppsLibrary.tsx @@ -74,7 +74,7 @@ const ScrollerStyled = styled('div')({ "-ms-overflow-style": "none", }); -export const AppsLibrary = ({ availableQapps, setMode, myName, hasPublishApp }) => { +export const AppsLibrary = ({ availableQapps, setMode, myName, hasPublishApp, isShow }) => { const [searchValue, setSearchValue] = useState(""); const virtuosoRef = useRef(); const { rootHeight } = useContext(MyContext); @@ -133,7 +133,9 @@ export const AppsLibrary = ({ availableQapps, setMode, myName, hasPublishApp }) return ( - + { }} src={NavAdd} /> { + if(!selectedTab) return handleClick(e) }}> {/* Right Logout Icon */} - { setMobileViewModeKeepOpen("messaging"); }} - edge="end" - color="inherit" - aria-label="logout" - - // onClick={onLogoutClick} > - + - - + { const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom); + const [canSave, _] = useRecoilState(canSaveSettingToQdnAtom); + const [openSnack, setOpenSnack] = useState(false); + const [isLoading, setIsLoading] = useState(false) + const [infoSnack, setInfoSnack] = useState(null); const [oldPinnedApps, setOldPinnedApps] = useState(pinnedApps) console.log('oldpin', {oldPinnedApps, pinnedApps}) + const { show } = useContext(MyContext); const hasChanged = useMemo(()=> { - return !isEqual(pinnedApps, oldPinnedApps) + const newChanges = { + sortablePinnedApps: pinnedApps.map((item)=> { + return { + name: item?.name, + service: item?.service + } + }) + } + const oldChanges = { + sortablePinnedApps: oldPinnedApps.map((item)=> { + return { + name: item?.name, + service: item?.service + } + }) + } + return !isEqual(oldChanges, newChanges) }, [oldPinnedApps, pinnedApps]) + + const saveToQdn = async ()=> { + try { + setIsLoading(true) + const data64 = await objectToBase64({ + sortablePinnedApps: pinnedApps + }) + const encryptData = await new Promise((res, rej) => { + chrome?.runtime?.sendMessage( + { + action: "ENCRYPT_DATA", + type: "qortalRequest", + payload: { + data64 + }, + }, + (response) => { + console.log("response", response); + if (response.error) { + rej(response?.message); + return; + } else { + res(response); + + } + } + ); + }); + if(encryptData && !encryptData?.error){ + const fee = await getFee('ARBITRARY') + + await show({ + message: "Would you like to publish your settings to QDN (encrypted) ?" , + publishFee: fee.fee + ' QORT' + }) + const response = await new Promise((res, rej) => { + chrome?.runtime?.sendMessage( + { + action: "publishOnQDN", + payload: { + data: encryptData, + identifier: "ext_saved_settings", + service: 'DOCUMENT_PRIVATE' + }, + }, + (response) => { + + if (!response?.error) { + res(response); + return + } + rej(response.error); + } + ); + }); + console.log('saved', response) + if(response?.identifier){ + setOldPinnedApps(pinnedApps) + setInfoSnack({ + type: "success", + message: + "Sucessfully published to QDN", + }); + setOpenSnack(true); + } + } + console.log('save encryptedData', encryptData) + } catch (error) { + setInfoSnack({ + type: "error", + message: + error?.message || "Unable to save to QDN", + }); + setOpenSnack(true); + } finally { + setIsLoading(false) + } + } return ( -
{hasChanged && 'Save'}
+ <> + + + + + + ) } diff --git a/src/useQortalGetSaveSettings.tsx b/src/useQortalGetSaveSettings.tsx new file mode 100644 index 0000000..e330077 --- /dev/null +++ b/src/useQortalGetSaveSettings.tsx @@ -0,0 +1,77 @@ +import React, { useCallback, useEffect } from 'react' +import { useSetRecoilState } from 'recoil'; +import { canSaveSettingToQdnAtom, sortablePinnedAppsAtom } from './atoms/global'; +import { getArbitraryEndpointReact, getBaseApiReact } from './App'; +import { decryptResource } from './components/Group/Group'; +import { base64ToUint8Array, uint8ArrayToObject } from './backgroundFunctions/encryption'; + +function fetchFromLocalStorage(key) { + try { + const serializedValue = localStorage.getItem(key); + if (serializedValue === null) { + console.log(`No data found for key: ${key}`); + return null; + } + return JSON.parse(serializedValue); + } catch (error) { + console.error('Error fetching from localStorage:', error); + return null; + } +} + +const getPublishRecord = async (myName) => { + // const validApi = await findUsableApi(); + const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=ext_saved_settings&exactmatchnames=true&limit=1&prefix=true&name=${myName}`; + const response = await fetch(url); + if (!response.ok) { + throw new Error("network error"); + } + const publishData = await response.json(); + + if(publishData?.length > 0) return true + + return false + }; + const getPublish = async (myName) => { + let data + const res = await fetch( + `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${myName}/ext_saved_settings?encoding=base64` + ); + data = await res.text(); + + + if(!data) throw new Error('Unable to fetch publish') + + const decryptedKey: any = await decryptResource(data); + + const dataint8Array = base64ToUint8Array(decryptedKey.data); + const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); + return decryptedKeyToObject + }; + +export const useQortalGetSaveSettings = (myName) => { + const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom); + const setCanSave = useSetRecoilState(canSaveSettingToQdnAtom); + + + const getSavedSettings = useCallback(async (myName)=> { + try { + const hasPublishRecord = await getPublishRecord(myName) + if(hasPublishRecord){ + const settings = await getPublish(myName) + if(settings?.sortablePinnedApps){ + fetchFromLocalStorage('sortablePinnedApps', settings.sortablePinnedApps) + setSortablePinnedApps(settings.sortablePinnedApps) + } + } + setCanSave(true) + } catch (error) { + + } + }, []) + useEffect(()=> { + if(!myName) return + getSavedSettings(myName) + }, [getSavedSettings, myName]) + +} From 7091e0b5366e10e8ed3d5177ebeabc96daa18650 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Mon, 21 Oct 2024 04:46:45 +0300 Subject: [PATCH 31/58] change save indicator --- src/atoms/global.ts | 10 ++ src/components/Apps/AppsNavBar.tsx | 50 ++++++-- src/components/Apps/SortablePinnedApps.tsx | 12 +- src/components/ContextMenuPinnedApps.tsx | 136 +++++++++++++++++++++ src/components/Mobile/MobileHeader.tsx | 38 ++++-- src/components/Save/Save.tsx | 28 +++-- src/useQortalGetSaveSettings.tsx | 39 +++--- src/useRetrieveDataLocalStorage.tsx | 11 +- 8 files changed, 270 insertions(+), 54 deletions(-) create mode 100644 src/components/ContextMenuPinnedApps.tsx diff --git a/src/atoms/global.ts b/src/atoms/global.ts index 2ad90d4..5e212c2 100644 --- a/src/atoms/global.ts +++ b/src/atoms/global.ts @@ -9,4 +9,14 @@ export const sortablePinnedAppsAtom = atom({ export const canSaveSettingToQdnAtom = atom({ key: 'canSaveSettingToQdnAtom', default: false, +}); + +export const settingsQDNLastUpdatedAtom = atom({ + key: 'settingsQDNLastUpdatedAtom', + default: -100, +}); + +export const settingsLocalLastUpdatedAtom = atom({ + key: 'settingsLocalLastUpdatedAtom', + default: 0, }); \ No newline at end of file diff --git a/src/components/Apps/AppsNavBar.tsx b/src/components/Apps/AppsNavBar.tsx index a069c8a..8d87fca 100644 --- a/src/components/Apps/AppsNavBar.tsx +++ b/src/components/Apps/AppsNavBar.tsx @@ -12,28 +12,52 @@ import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../util import TabComponent from "./TabComponent"; import PushPinIcon from '@mui/icons-material/PushPin'; import RefreshIcon from '@mui/icons-material/Refresh'; -import { useRecoilState } from "recoil"; -import { sortablePinnedAppsAtom } from "../../atoms/global"; +import { useRecoilState, useSetRecoilState } from "recoil"; +import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from "../../atoms/global"; -export function saveToLocalStorage(key, value) { +export function saveToLocalStorage(key, subKey, newValue) { try { - const serializedValue = JSON.stringify(value); - localStorage.setItem(key, serializedValue); - console.log(`Data saved to localStorage with key: ${key}`); + // Fetch existing data + const existingData = localStorage.getItem(key); + let combinedData = {}; + + if (existingData) { + // Parse the existing data + const parsedData = JSON.parse(existingData); + // Merge with the new data under the subKey + combinedData = { + ...parsedData, + timestamp: Date.now(), // Update the root timestamp + [subKey]: newValue // Assuming the data is an array + }; + } else { + // If no existing data, just use the new data under the subKey + combinedData = { + timestamp: Date.now(), // Set the initial root timestamp + [subKey]: newValue + }; + } + + // Save combined data back to localStorage + const serializedValue = JSON.stringify(combinedData); + localStorage.setItem(key, serializedValue); + console.log(`Data saved to localStorage with key: ${key} and subKey: ${subKey}`); } catch (error) { - console.error('Error saving to localStorage:', error); + console.error('Error saving to localStorage:', error); } } + export const AppsNavBar = () => { const [tabs, setTabs] = useState([]) - const [selectedTab, setSelectedTab] = useState([]) + const [selectedTab, setSelectedTab] = useState(null) const [isNewTabWindow, setIsNewTabWindow] = useState(false) const tabsRef = useRef(null); const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom); + const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom); const handleClick = (event) => { setAnchorEl(event.currentTarget); @@ -59,7 +83,7 @@ export const AppsNavBar = () => { const {tabs, selectedTab, isNewTabWindow} = e.detail?.data; setTabs([...tabs]) - setSelectedTab({...selectedTab}) + setSelectedTab(!selectedTab ? nulll : {...selectedTab}) setIsNewTabWindow(isNewTabWindow) }; @@ -71,6 +95,8 @@ export const AppsNavBar = () => { }; }, []); + console.log('selectedTab', selectedTab) + const isSelectedAppPinned = !!sortablePinnedApps?.find((item)=> item?.name === selectedTab?.name && item?.service === selectedTab?.service) return ( @@ -115,6 +141,7 @@ export const AppsNavBar = () => { gap: '10px' }}> { + setSelectedTab(null) executeEvent("newTabWindow", { }); }}> @@ -186,10 +213,11 @@ export const AppsNavBar = () => { }]; } - saveToLocalStorage('sortablePinnedApps', updatedApps) + saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps) return updatedApps; }); - + setSettingsLocalLastUpdated(Date.now()) + handleClose(); }} > diff --git a/src/components/Apps/SortablePinnedApps.tsx b/src/components/Apps/SortablePinnedApps.tsx index 374cb1d..d758997 100644 --- a/src/components/Apps/SortablePinnedApps.tsx +++ b/src/components/Apps/SortablePinnedApps.tsx @@ -7,9 +7,10 @@ import { Avatar, ButtonBase } from '@mui/material'; import { AppCircle, AppCircleContainer, AppCircleLabel } from './Apps-styles'; import { getBaseApiReact } from '../../App'; import { executeEvent } from '../../utils/events'; -import { sortablePinnedAppsAtom } from '../../atoms/global'; -import { useRecoilState } from 'recoil'; +import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global'; +import { useRecoilState, useSetRecoilState } from 'recoil'; import { saveToLocalStorage } from './AppsNavBar'; +import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps'; const SortableItem = ({ id, name, app }) => { const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id }); @@ -27,6 +28,7 @@ const SortableItem = ({ id, name, app }) => { }; return ( + { + ); }; export const SortablePinnedApps = ({ myWebsite, myApp, availableQapps = [] }) => { const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom); + const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom); const transformPinnedApps = useMemo(()=> { console.log({myWebsite, myApp, availableQapps, pinnedApps}) @@ -149,8 +153,8 @@ export const SortablePinnedApps = ({ myWebsite, myApp, availableQapps = [] }) = const newOrder = arrayMove(transformPinnedApps, oldIndex, newIndex); setPinnedApps(newOrder); - saveToLocalStorage('sortablePinnedApps', newOrder) - + saveToLocalStorage('ext_saved_settings','sortablePinnedApps', newOrder) + setSettingsLocalLastUpdated(Date.now()) } }; return ( diff --git a/src/components/ContextMenuPinnedApps.tsx b/src/components/ContextMenuPinnedApps.tsx new file mode 100644 index 0000000..7075f4c --- /dev/null +++ b/src/components/ContextMenuPinnedApps.tsx @@ -0,0 +1,136 @@ +import React, { useState, useRef } from 'react'; +import { ListItemIcon, Menu, MenuItem, Typography, styled } from '@mui/material'; +import PushPinIcon from '@mui/icons-material/PushPin'; +import { saveToLocalStorage } from './Apps/AppsNavBar'; +import { useRecoilState } from 'recoil'; +import { sortablePinnedAppsAtom } from '../atoms/global'; + +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', + color: '#444', + transition: '0.3s background-color', + '&:hover': { + backgroundColor: '#f0f0f0', + }, + }, +})); + +export const ContextMenuPinnedApps = ({ children, app, setEnableDrag }) => { + const [menuPosition, setMenuPosition] = useState(null); + const longPressTimeout = useRef(null); + const maxHoldTimeout = useRef(null); + const preventClick = useRef(false); + const startTouchPosition = useRef({ x: 0, y: 0 }); // Track initial touch position + const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom); + + const handleContextMenu = (event) => { + event.preventDefault(); + event.stopPropagation(); + preventClick.current = true; + setMenuPosition({ + mouseX: event.clientX, + mouseY: event.clientY, + }); + }; + + const handleTouchStart = (event) => { + const { clientX, clientY } = event.touches[0]; + startTouchPosition.current = { x: clientX, y: clientY }; + + longPressTimeout.current = setTimeout(() => { + preventClick.current = true; + setEnableDrag(false); + event.stopPropagation(); + setMenuPosition({ + mouseX: clientX, + mouseY: clientY, + }); + }, 500); + + // Set a maximum hold duration (e.g., 1.5 seconds) + maxHoldTimeout.current = setTimeout(() => { + clearTimeout(longPressTimeout.current); + }, 1500); + }; + + const handleTouchMove = (event) => { + const { clientX, clientY } = event.touches[0]; + const { x, y } = startTouchPosition.current; + + // Determine if the touch has moved beyond a small threshold (e.g., 10px) + const movedEnough = Math.abs(clientX - x) > 10 || Math.abs(clientY - y) > 10; + + if (movedEnough) { + clearTimeout(longPressTimeout.current); + clearTimeout(maxHoldTimeout.current); + } + }; + + const handleTouchEnd = (event) => { + clearTimeout(longPressTimeout.current); + clearTimeout(maxHoldTimeout.current); + setEnableDrag(true); + if (preventClick.current) { + event.preventDefault(); + event.stopPropagation(); + preventClick.current = false; + } + }; + + const handleClose = (e) => { + e.preventDefault(); + e.stopPropagation(); + setMenuPosition(null); + }; + + return ( +
+ {children} + { + e.stopPropagation(); + }} + > + { + handleClose(e); + setSortablePinnedApps((prev) => { + const updatedApps = prev.filter( + (item) => !(item?.name === app?.name && item?.service === app?.service) + ); + saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps); + return updatedApps; + }); + }}> + + + + + Unpin app + + + +
+ ); +}; diff --git a/src/components/Mobile/MobileHeader.tsx b/src/components/Mobile/MobileHeader.tsx index 8b95391..0feeb7b 100644 --- a/src/components/Mobile/MobileHeader.tsx +++ b/src/components/Mobile/MobileHeader.tsx @@ -239,16 +239,22 @@ const Header = ({ }} > {/* Left Home Icon */} - + - - +
+ {/* Center Title */} QORTAL - + {/* Right Logout Icon */} - + - + + diff --git a/src/components/Save/Save.tsx b/src/components/Save/Save.tsx index 9781ded..8a11f52 100644 --- a/src/components/Save/Save.tsx +++ b/src/components/Save/Save.tsx @@ -1,7 +1,7 @@ import React, { useContext, useMemo, useState } from 'react' import { useRecoilState } from 'recoil'; import isEqual from 'lodash/isEqual'; // Import deep comparison utility -import { canSaveSettingToQdnAtom, sortablePinnedAppsAtom } from '../../atoms/global'; +import { canSaveSettingToQdnAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global'; import { ButtonBase } from '@mui/material'; import { objectToBase64 } from '../../qdn/encryption/group-encryption'; import { MyContext } from '../../App'; @@ -10,12 +10,15 @@ import { CustomizedSnackbars } from '../Snackbar/Snackbar'; import { SaveIcon } from '../../assets/svgs/SaveIcon'; export const Save = () => { const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom); - const [canSave, _] = useRecoilState(canSaveSettingToQdnAtom); + const [settingsQdnLastUpdated, setSettingsQdnLastUpdated] = useRecoilState(settingsQDNLastUpdatedAtom); + const [settingsLocalLastUpdated] = useRecoilState(settingsLocalLastUpdatedAtom); + + const [canSave] = useRecoilState(canSaveSettingToQdnAtom); const [openSnack, setOpenSnack] = useState(false); const [isLoading, setIsLoading] = useState(false) const [infoSnack, setInfoSnack] = useState(null); const [oldPinnedApps, setOldPinnedApps] = useState(pinnedApps) - console.log('oldpin', {oldPinnedApps, pinnedApps}) + console.log('oldpin', {oldPinnedApps, pinnedApps}, settingsQdnLastUpdated, settingsLocalLastUpdated, settingsQdnLastUpdated < settingsLocalLastUpdated,) const { show } = useContext(MyContext); const hasChanged = useMemo(()=> { @@ -35,14 +38,21 @@ export const Save = () => { } }) } - return !isEqual(oldChanges, newChanges) - }, [oldPinnedApps, pinnedApps]) + console.log('!isEqual(oldChanges, newChanges)', !isEqual(oldChanges, newChanges)) + if(settingsQdnLastUpdated === -100) return false + return !isEqual(oldChanges, newChanges) || settingsQdnLastUpdated < settingsLocalLastUpdated + }, [oldPinnedApps, pinnedApps, settingsQdnLastUpdated, settingsLocalLastUpdated]) const saveToQdn = async ()=> { try { setIsLoading(true) const data64 = await objectToBase64({ - sortablePinnedApps: pinnedApps + sortablePinnedApps: pinnedApps.map((item)=> { + return { + name: item?.name, + service: item?.service + } + }) }) const encryptData = await new Promise((res, rej) => { chrome?.runtime?.sendMessage( @@ -95,6 +105,7 @@ export const Save = () => { console.log('saved', response) if(response?.identifier){ setOldPinnedApps(pinnedApps) + setSettingsQdnLastUpdated(Date.now()) setInfoSnack({ type: "success", message: @@ -115,11 +126,12 @@ export const Save = () => { setIsLoading(false) } } + console.log('settingsQdnLastUpdated', settingsQdnLastUpdated) return ( <> - + { } const publishData = await response.json(); - if(publishData?.length > 0) return true + if(publishData?.length > 0) return {hasPublishRecord: false, timestamp: publishData[0]?.updated || publishData[0].created} - return false + return {hasPublishRecord: false} }; const getPublish = async (myName) => { - let data + try { + let data const res = await fetch( `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${myName}/ext_saved_settings?encoding=base64` ); @@ -47,22 +48,32 @@ const getPublishRecord = async (myName) => { const dataint8Array = base64ToUint8Array(decryptedKey.data); const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); return decryptedKeyToObject + } catch (error) { + return null + } }; export const useQortalGetSaveSettings = (myName) => { const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom); const setCanSave = useSetRecoilState(canSaveSettingToQdnAtom); - - - const getSavedSettings = useCallback(async (myName)=> { + const setSettingsQDNLastUpdated = useSetRecoilState(settingsQDNLastUpdatedAtom); + const [settingsLocalLastUpdated] = useRecoilState(settingsLocalLastUpdatedAtom); + const getSavedSettings = useCallback(async (myName, settingsLocalLastUpdated)=> { try { - const hasPublishRecord = await getPublishRecord(myName) + const {hasPublishRecord, timestamp} = await getPublishRecord(myName) if(hasPublishRecord){ const settings = await getPublish(myName) - if(settings?.sortablePinnedApps){ - fetchFromLocalStorage('sortablePinnedApps', settings.sortablePinnedApps) + if(settings?.sortablePinnedApps && timestamp > settingsLocalLastUpdated){ setSortablePinnedApps(settings.sortablePinnedApps) + setSettingsQDNLastUpdated(timestamp || 0) } + if(!settings){ + // set -100 to indicate that it couldn't fetch the publish + setSettingsQDNLastUpdated(-100) + + } + } else { + setSettingsQDNLastUpdated( 0) } setCanSave(true) } catch (error) { @@ -70,8 +81,8 @@ export const useQortalGetSaveSettings = (myName) => { } }, []) useEffect(()=> { - if(!myName) return - getSavedSettings(myName) - }, [getSavedSettings, myName]) + if(!myName || !settingsLocalLastUpdated) return + getSavedSettings(myName, settingsLocalLastUpdated) + }, [getSavedSettings, myName, settingsLocalLastUpdated]) } diff --git a/src/useRetrieveDataLocalStorage.tsx b/src/useRetrieveDataLocalStorage.tsx index 93831e0..6d7b05e 100644 --- a/src/useRetrieveDataLocalStorage.tsx +++ b/src/useRetrieveDataLocalStorage.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect } from 'react' import { useSetRecoilState } from 'recoil'; -import { sortablePinnedAppsAtom } from './atoms/global'; +import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global'; function fetchFromLocalStorage(key) { try { @@ -18,13 +18,14 @@ function fetchFromLocalStorage(key) { export const useRetrieveDataLocalStorage = () => { const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom); - + const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom); const getSortablePinnedApps = useCallback(()=> { - const pinnedAppsLocal = fetchFromLocalStorage('sortablePinnedApps') - if(pinnedAppsLocal){ - setSortablePinnedApps(pinnedAppsLocal) + const pinnedAppsLocal = fetchFromLocalStorage('ext_saved_settings') + if(pinnedAppsLocal?.sortablePinnedApps){ + setSortablePinnedApps(pinnedAppsLocal?.sortablePinnedApps) } + setSettingsLocalLastUpdated(pinnedAppsLocal?.timestamp || -1) }, []) useEffect(()=> { From 0cc2bc240e19bf9660d1ec0625a3f79ef344a906 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Mon, 21 Oct 2024 05:20:17 +0300 Subject: [PATCH 32/58] fix issue with saving --- src/useQortalGetSaveSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/useQortalGetSaveSettings.tsx b/src/useQortalGetSaveSettings.tsx index 3765af5..da1fb37 100644 --- a/src/useQortalGetSaveSettings.tsx +++ b/src/useQortalGetSaveSettings.tsx @@ -28,7 +28,7 @@ const getPublishRecord = async (myName) => { } const publishData = await response.json(); - if(publishData?.length > 0) return {hasPublishRecord: false, timestamp: publishData[0]?.updated || publishData[0].created} + if(publishData?.length > 0) return {hasPublishRecord: true, timestamp: publishData[0]?.updated || publishData[0].created} return {hasPublishRecord: false} }; From 203226d3d9bee379b3cedb31d1ee57556ceb2204 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Mon, 21 Oct 2024 06:56:20 +0300 Subject: [PATCH 33/58] reset recoils on logout --- src/App.tsx | 37 +++++++++++++ src/atoms/global.ts | 5 ++ src/components/Apps/AppsLibrary.tsx | 9 ++-- .../Apps/useQortalMessageListener.tsx | 4 +- src/components/Save/Save.tsx | 6 +-- src/qortalRequests.ts | 4 +- src/qortalRequests/get.ts | 54 ++++++++++++++++--- src/useQortalGetSaveSettings.tsx | 8 ++- 8 files changed, 111 insertions(+), 16 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 5200bec..0ebc465 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -100,6 +100,8 @@ import { Settings } from "./components/Group/Settings"; import { MainAvatar } from "./components/MainAvatar"; import { useRetrieveDataLocalStorage } from "./useRetrieveDataLocalStorage"; import { useQortalGetSaveSettings } from "./useQortalGetSaveSettings"; +import { useResetRecoilState } from "recoil"; +import { canSaveSettingToQdnAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from "./atoms/global"; type extStates = | "not-authenticated" @@ -326,6 +328,21 @@ function App() { const qortalRequestCheckbox1Ref = useRef(null); useRetrieveDataLocalStorage() useQortalGetSaveSettings(userInfo?.name) + + //resets for recoil + const resetAtomSortablePinnedAppsAtom = useResetRecoilState(sortablePinnedAppsAtom); + const resetAtomCanSaveSettingToQdnAtom = useResetRecoilState(canSaveSettingToQdnAtom); + const resetAtomSettingsQDNLastUpdatedAtom = useResetRecoilState(settingsQDNLastUpdatedAtom); + const resetAtomSettingsLocalLastUpdatedAtom = useResetRecoilState(settingsLocalLastUpdatedAtom); + const resetAtomOldPinnedAppsAtom = useResetRecoilState(oldPinnedAppsAtom); + + const resetAllRecoil = () => { + resetAtomSortablePinnedAppsAtom(); + resetAtomCanSaveSettingToQdnAtom(); + resetAtomSettingsQDNLastUpdatedAtom(); + resetAtomSettingsLocalLastUpdatedAtom(); + resetAtomOldPinnedAppsAtom(); + }; useEffect(() => { if (!isMobile) return; // Function to set the height of the app to the viewport height @@ -595,6 +612,20 @@ function App() { } } }; + const qortalRequestPermissonFromExtension = async (message, sender, sendResponse) => { + if (message.action === "QORTAL_REQUEST_PERMISSION" && isMainWindow) { + try { + console.log("payloadbefore", message.payload); + + await show('do you accept?'); + + sendResponse({ accepted: true }); + } catch (error) { + console.log("error", error); + sendResponse({ accepted: false }); + } + } + }; useEffect(() => { // Listen for messages from the background script @@ -667,7 +698,12 @@ function App() { console.log("isMainWindow", isMainWindow, window?.location?.href); return true; // Return true to indicate an async response is coming } + if (message.action === "QORTAL_REQUEST_PERMISSION" && isMainWindow && message?.isFromExtension) { + qortalRequestPermissonFromExtension(message, sender, sendResponse); + return true; + } if (message.action === "QORTAL_REQUEST_PERMISSION" && isMainWindow) { + return; } }; @@ -1014,6 +1050,7 @@ function App() { setConfirmUseOfLocal(false); setTxList([]); setMemberGroups([]); + resetAllRecoil() }; function roundUpToDecimals(number, decimals = 8) { diff --git a/src/atoms/global.ts b/src/atoms/global.ts index 5e212c2..03cf7c9 100644 --- a/src/atoms/global.ts +++ b/src/atoms/global.ts @@ -19,4 +19,9 @@ export const settingsQDNLastUpdatedAtom = atom({ export const settingsLocalLastUpdatedAtom = atom({ key: 'settingsLocalLastUpdatedAtom', default: 0, +}); + +export const oldPinnedAppsAtom = atom({ + key: 'oldPinnedAppsAtom', + default: [], }); \ No newline at end of file diff --git a/src/components/Apps/AppsLibrary.tsx b/src/components/Apps/AppsLibrary.tsx index c1cc3ef..4338b15 100644 --- a/src/components/Apps/AppsLibrary.tsx +++ b/src/components/Apps/AppsLibrary.tsx @@ -205,9 +205,12 @@ export const AppsLibrary = ({ availableQapps, setMode, myName, hasPublishApp, i width: "60px", }} onClick={()=> { - executeEvent("addTab", { - data: qapp - }) + // executeEvent("addTab", { + // data: qapp + // }) + executeEvent("selectedAppInfo", { + data: qapp, + }); }} > diff --git a/src/components/Apps/useQortalMessageListener.tsx b/src/components/Apps/useQortalMessageListener.tsx index c9ace3f..169d0b5 100644 --- a/src/components/Apps/useQortalMessageListener.tsx +++ b/src/components/Apps/useQortalMessageListener.tsx @@ -344,7 +344,7 @@ export const useQortalMessageListener = (frameWindow) => { if (UIQortalRequests.includes(event.data.action)) { console.log('event?.data', event?.data); sendMessageToRuntime( - { action: event.data.action, type: 'qortalRequest', payload: event.data }, + { action: event.data.action, type: 'qortalRequest', payload: event.data, isExtension: true }, event.ports[0] ); } else if ( @@ -368,7 +368,7 @@ export const useQortalMessageListener = (frameWindow) => { console.log('data after', data) if (data) { sendMessageToRuntime( - { action: event.data.action, type: 'qortalRequest', payload: data }, + { action: event.data.action, type: 'qortalRequest', payload: data, isExtension: true }, event.ports[0] ); } else { diff --git a/src/components/Save/Save.tsx b/src/components/Save/Save.tsx index 8a11f52..4f007e2 100644 --- a/src/components/Save/Save.tsx +++ b/src/components/Save/Save.tsx @@ -1,7 +1,7 @@ import React, { useContext, useMemo, useState } from 'react' import { useRecoilState } from 'recoil'; import isEqual from 'lodash/isEqual'; // Import deep comparison utility -import { canSaveSettingToQdnAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global'; +import { canSaveSettingToQdnAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global'; import { ButtonBase } from '@mui/material'; import { objectToBase64 } from '../../qdn/encryption/group-encryption'; import { MyContext } from '../../App'; @@ -17,7 +17,7 @@ export const Save = () => { const [openSnack, setOpenSnack] = useState(false); const [isLoading, setIsLoading] = useState(false) const [infoSnack, setInfoSnack] = useState(null); - const [oldPinnedApps, setOldPinnedApps] = useState(pinnedApps) + const [oldPinnedApps, setOldPinnedApps] = useRecoilState(oldPinnedAppsAtom) console.log('oldpin', {oldPinnedApps, pinnedApps}, settingsQdnLastUpdated, settingsLocalLastUpdated, settingsQdnLastUpdated < settingsLocalLastUpdated,) const { show } = useContext(MyContext); @@ -40,7 +40,7 @@ export const Save = () => { } console.log('!isEqual(oldChanges, newChanges)', !isEqual(oldChanges, newChanges)) if(settingsQdnLastUpdated === -100) return false - return !isEqual(oldChanges, newChanges) || settingsQdnLastUpdated < settingsLocalLastUpdated + return !isEqual(oldChanges, newChanges) && settingsQdnLastUpdated < settingsLocalLastUpdated }, [oldPinnedApps, pinnedApps, settingsQdnLastUpdated, settingsLocalLastUpdated]) const saveToQdn = async ()=> { diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index 8f1490a..4097e8c 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -63,6 +63,8 @@ function getLocalStorage(key) { chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { if (request) { + console.log('rquest', request) + const isFromExtension = request?.isExtension switch (request.action) { case "GET_USER_ACCOUNT": { getUserAccount() @@ -169,7 +171,7 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { case "VOTE_ON_POLL": { const data = request.payload; - voteOnPoll(data) + voteOnPoll(data, isFromExtension) .then((res) => { sendResponse(res); }) diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index 104c4e5..0b2e92a 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -139,7 +139,7 @@ const _deployAt = async ( } }; -const _voteOnPoll = async (pollName, optionIndex, optionName) => { +const _voteOnPoll = async ({pollName, optionIndex, optionName}, isFromExtension) => { const fee = await getFee("VOTE_ON_POLL"); const resPermission = await getUserPermission({ @@ -147,7 +147,7 @@ const _voteOnPoll = async (pollName, optionIndex, optionName) => { text2: `Poll: ${pollName}`, text3: `Option: ${optionName}`, fee: fee.fee, - }); + }, isFromExtension); const { accepted } = resPermission; if (accepted) { @@ -207,7 +207,22 @@ function sendToSaveFilePicker(data, sender) { }); } -async function getUserPermission(payload: any) { +async function responseFromExtension() { + return new Promise((resolve) => { + + // Send message to the content script to check focus + chrome.runtime.sendMessage({ action: "QORTAL_REQUEST_PERMISSION", payloa }, (response) => { + + if (chrome.runtime.lastError) { + resolve(false); // Error occurred, assume not focused + } else { + resolve(response); // Resolve based on the response + } + }); + }); +} + +async function getUserPermission(payload: any, isFromExtension?: boolean) { function waitForWindowReady(windowId) { return new Promise((resolve) => { const checkInterval = setInterval(() => { @@ -224,6 +239,33 @@ async function getUserPermission(payload: any) { }); } + console.log('isFromExtension', isFromExtension) + if(isFromExtension){ + + + return new Promise((resolve) => { + // Set a timeout for 1 second + const timeout = setTimeout(() => { + resolve(false); + }, 30000); + + // Send message to the content script to check focus + chrome.runtime.sendMessage( + { action: "QORTAL_REQUEST_PERMISSION", payload, isFromExtension }, + (response) => { + console.log("permission response", response); + if (response === undefined) return; + clearTimeout(timeout); // Clear the timeout if we get a response + + if (chrome.runtime.lastError) { + resolve(false); // Error occurred, assume not focused + } else { + resolve(response); // Resolve based on the response + } + } + ); + }); + } await new Promise((res) => { const popupUrl = chrome.runtime.getURL("index.html?secondary=true"); console.log("popupUrl", popupUrl); @@ -294,7 +336,7 @@ async function getUserPermission(payload: any) { return new Promise((resolve) => { // Set a timeout for 1 second const timeout = setTimeout(() => { - resolve(false); // No response within 10 second, assume not focused + resolve(false); }, 30000); // Send message to the content script to check focus @@ -909,7 +951,7 @@ export const publishMultipleQDNResources = async (data: any, sender) => { return true; }; -export const voteOnPoll = async (data) => { +export const voteOnPoll = async (data, isFromExtension) => { const requiredFields = ["pollName", "optionIndex"]; const missingFields: string[] = []; requiredFields.forEach((field) => { @@ -941,7 +983,7 @@ export const voteOnPoll = async (data) => { } try { const optionName = pollInfo.pollOptions[optionIndex].optionName; - const resVoteOnPoll = await _voteOnPoll(pollName, optionIndex, optionName); + const resVoteOnPoll = await _voteOnPoll({pollName, optionIndex, optionName}, isFromExtension); return resVoteOnPoll; } catch (error) { throw new Error(error?.message || "Failed to vote on the poll."); diff --git a/src/useQortalGetSaveSettings.tsx b/src/useQortalGetSaveSettings.tsx index da1fb37..4809b59 100644 --- a/src/useQortalGetSaveSettings.tsx +++ b/src/useQortalGetSaveSettings.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect } from 'react' import { useRecoilState, useSetRecoilState } from 'recoil'; -import { canSaveSettingToQdnAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global'; +import { canSaveSettingToQdnAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global'; import { getArbitraryEndpointReact, getBaseApiReact } from './App'; import { decryptResource } from './components/Group/Group'; import { base64ToUint8Array, uint8ArrayToObject } from './backgroundFunctions/encryption'; @@ -58,13 +58,19 @@ export const useQortalGetSaveSettings = (myName) => { const setCanSave = useSetRecoilState(canSaveSettingToQdnAtom); const setSettingsQDNLastUpdated = useSetRecoilState(settingsQDNLastUpdatedAtom); const [settingsLocalLastUpdated] = useRecoilState(settingsLocalLastUpdatedAtom); + const [oldPinnedApps, setOldPinnedApps] = useRecoilState(oldPinnedAppsAtom) + const getSavedSettings = useCallback(async (myName, settingsLocalLastUpdated)=> { try { const {hasPublishRecord, timestamp} = await getPublishRecord(myName) if(hasPublishRecord){ const settings = await getPublish(myName) + console.log('settings', settings, timestamp, settingsLocalLastUpdated ) if(settings?.sortablePinnedApps && timestamp > settingsLocalLastUpdated){ setSortablePinnedApps(settings.sortablePinnedApps) + setOldPinnedApps(settings.sortablePinnedApps) + setSettingsQDNLastUpdated(timestamp || 0) + } else if(settings?.sortablePinnedApps){ setSettingsQDNLastUpdated(timestamp || 0) } if(!settings){ From 44c6b5ad3289e9d0fb063d99695225d3d913dfaf Mon Sep 17 00:00:00 2001 From: PhilReact Date: Mon, 21 Oct 2024 07:19:34 +0300 Subject: [PATCH 34/58] fixed bugs --- src/components/Apps/SortablePinnedApps.tsx | 67 +++++++++++++++------- src/components/ContextMenuPinnedApps.tsx | 14 ++++- src/useQortalGetSaveSettings.tsx | 3 +- 3 files changed, 59 insertions(+), 25 deletions(-) diff --git a/src/components/Apps/SortablePinnedApps.tsx b/src/components/Apps/SortablePinnedApps.tsx index d758997..fc75919 100644 --- a/src/components/Apps/SortablePinnedApps.tsx +++ b/src/components/Apps/SortablePinnedApps.tsx @@ -28,7 +28,7 @@ const SortableItem = ({ id, name, app }) => { }; return ( - + { - console.log({myWebsite, myApp, availableQapps, pinnedApps}) - let pinned = [...pinnedApps] - const findMyWebsite = pinned?.find((item)=> item?.service === myWebsite?.service && item?.name === myWebsite?.name) - const findMyApp = pinned?.find((item)=> item?.service === myApp?.service && item?.name === myApp?.name) - - if(myWebsite && !findMyWebsite){ - pinned.unshift(myWebsite) - } - if(myApp && !findMyApp){ - pinned.unshift(myApp) - } - pinned = pinned.map((pin)=> { - const findIndex = availableQapps?.findIndex((item)=> item?.service === pin?.service && item?.name === pin?.name) - if(findIndex !== -1) return availableQapps[findIndex] - - return pin - }) - return pinned - }, [myApp, myWebsite, pinnedApps, availableQapps]) + const transformPinnedApps = useMemo(() => { + console.log({ myWebsite, myApp, availableQapps, pinnedApps }); + + // Clone the existing pinned apps list + let pinned = [...pinnedApps]; + + // Function to add or update `isMine` property + const addOrUpdateIsMine = (pinnedList, appToCheck) => { + if (!appToCheck) return pinnedList; + + const existingIndex = pinnedList.findIndex( + (item) => item?.service === appToCheck?.service && item?.name === appToCheck?.name + ); + + if (existingIndex !== -1) { + // If the app is already in the list, update it with `isMine: true` + pinnedList[existingIndex] = { ...pinnedList[existingIndex], isMine: true }; + } else { + // If not in the list, add it with `isMine: true` at the beginning + pinnedList.unshift({ ...appToCheck, isMine: true }); + } + + return pinnedList; + }; + + // Update or add `myWebsite` and `myApp` while preserving their positions + pinned = addOrUpdateIsMine(pinned, myWebsite); + pinned = addOrUpdateIsMine(pinned, myApp); + + // Update pinned list based on availableQapps + pinned = pinned.map((pin) => { + const findIndex = availableQapps?.findIndex( + (item) => item?.service === pin?.service && item?.name === pin?.name + ); + if (findIndex !== -1) return { + ...availableQapps[findIndex], + ...pin + } + + return pin; + }); + + return pinned; + }, [myApp, myWebsite, pinnedApps, availableQapps]); + console.log('transformPinnedApps', transformPinnedApps) // const hasSetPinned = useRef(false) // useEffect(() => { diff --git a/src/components/ContextMenuPinnedApps.tsx b/src/components/ContextMenuPinnedApps.tsx index 7075f4c..be0ae46 100644 --- a/src/components/ContextMenuPinnedApps.tsx +++ b/src/components/ContextMenuPinnedApps.tsx @@ -22,7 +22,7 @@ const CustomStyledMenu = styled(Menu)(({ theme }) => ({ }, })); -export const ContextMenuPinnedApps = ({ children, app, setEnableDrag }) => { +export const ContextMenuPinnedApps = ({ children, app, isMine }) => { const [menuPosition, setMenuPosition] = useState(null); const longPressTimeout = useRef(null); const maxHoldTimeout = useRef(null); @@ -31,6 +31,7 @@ export const ContextMenuPinnedApps = ({ children, app, setEnableDrag }) => { const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom); const handleContextMenu = (event) => { + if(isMine) return event.preventDefault(); event.stopPropagation(); preventClick.current = true; @@ -41,12 +42,14 @@ export const ContextMenuPinnedApps = ({ children, app, setEnableDrag }) => { }; const handleTouchStart = (event) => { + if(isMine) return + const { clientX, clientY } = event.touches[0]; startTouchPosition.current = { x: clientX, y: clientY }; longPressTimeout.current = setTimeout(() => { preventClick.current = true; - setEnableDrag(false); + event.stopPropagation(); setMenuPosition({ mouseX: clientX, @@ -61,6 +64,8 @@ export const ContextMenuPinnedApps = ({ children, app, setEnableDrag }) => { }; const handleTouchMove = (event) => { + if(isMine) return + const { clientX, clientY } = event.touches[0]; const { x, y } = startTouchPosition.current; @@ -74,9 +79,10 @@ export const ContextMenuPinnedApps = ({ children, app, setEnableDrag }) => { }; const handleTouchEnd = (event) => { + if(isMine) return + clearTimeout(longPressTimeout.current); clearTimeout(maxHoldTimeout.current); - setEnableDrag(true); if (preventClick.current) { event.preventDefault(); event.stopPropagation(); @@ -85,6 +91,8 @@ export const ContextMenuPinnedApps = ({ children, app, setEnableDrag }) => { }; const handleClose = (e) => { + if(isMine) return + e.preventDefault(); e.stopPropagation(); setMenuPosition(null); diff --git a/src/useQortalGetSaveSettings.tsx b/src/useQortalGetSaveSettings.tsx index 4809b59..6da3092 100644 --- a/src/useQortalGetSaveSettings.tsx +++ b/src/useQortalGetSaveSettings.tsx @@ -68,10 +68,11 @@ export const useQortalGetSaveSettings = (myName) => { console.log('settings', settings, timestamp, settingsLocalLastUpdated ) if(settings?.sortablePinnedApps && timestamp > settingsLocalLastUpdated){ setSortablePinnedApps(settings.sortablePinnedApps) - setOldPinnedApps(settings.sortablePinnedApps) + setSettingsQDNLastUpdated(timestamp || 0) } else if(settings?.sortablePinnedApps){ setSettingsQDNLastUpdated(timestamp || 0) + setOldPinnedApps(settings.sortablePinnedApps) } if(!settings){ // set -100 to indicate that it couldn't fetch the publish From 6b3a1f51ba50bf4f95e0f973d25f1f9824af62f7 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Mon, 21 Oct 2024 11:40:45 +0300 Subject: [PATCH 35/58] finished apps feature for mobile --- public/content-script.js | 4 - src/App.tsx | 293 +++++++++++++++++- src/atoms/global.ts | 10 + src/components/Apps/AppInfoSnippet.tsx | 23 +- src/components/Apps/AppRating.tsx | 16 +- src/components/Apps/AppViewerContainer.tsx | 10 - src/components/Apps/Apps.tsx | 51 ++- src/components/Apps/AppsCategory.tsx | 188 +++++++++++ src/components/Apps/AppsLibrary.tsx | 59 +++- src/components/Apps/AppsNavBar.tsx | 4 +- src/components/Apps/SortablePinnedApps.tsx | 24 +- .../Apps/useQortalMessageListener.tsx | 9 - src/components/Chat/ChatDirect.tsx | 2 - src/components/Group/Group.tsx | 93 +----- src/components/Group/GroupJoinRequests.tsx | 15 +- src/components/Mobile/MobileHeader.tsx | 52 +++- src/components/Save/Save.tsx | 18 +- src/qdn/encryption/group-encryption.ts | 1 - src/qdn/publish/pubish.ts | 26 +- src/qortalRequests.ts | 30 +- src/qortalRequests/get.ts | 133 ++++---- src/useAppFullscreen.tsx | 65 ++++ src/useQortalGetSaveSettings.tsx | 1 - 23 files changed, 762 insertions(+), 365 deletions(-) create mode 100644 src/components/Apps/AppsCategory.tsx create mode 100644 src/useAppFullscreen.tsx diff --git a/public/content-script.js b/public/content-script.js index b1a7569..7d0681e 100644 --- a/public/content-script.js +++ b/public/content-script.js @@ -576,7 +576,6 @@ const showSaveFilePicker = async (data) => { } chrome.runtime?.onMessage.addListener( function (message, sender, sendResponse) { - console.log('message', message) if (message.type === "LOGOUT") { // Notify the web page window.postMessage( @@ -812,7 +811,6 @@ if (!window.hasAddedQortalListener) { const sendMessageToRuntime = (message, eventPort) => { chrome?.runtime?.sendMessage(message, (response) => { - console.log('response', response); if (response.error) { eventPort.postMessage({ result: null, @@ -829,13 +827,11 @@ if (!window.hasAddedQortalListener) { // Check if action is included in the predefined list of UI requests if (UIQortalRequests.includes(event.data.action)) { - console.log('event?.data', event?.data); sendMessageToRuntime( { action: event.data.action, type: 'qortalRequest', payload: event.data }, event.ports[0] ); } else if (event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' || event?.data?.action === 'PUBLISH_QDN_RESOURCE' || event?.data?.action === 'ENCRYPT_DATA' || event?.data?.action === 'SAVE_FILE') { - console.log('event?.data?', event?.data) let data; try { data = await storeFilesInIndexedDB(event.data); diff --git a/src/App.tsx b/src/App.tsx index 0ebc465..ac3631c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -100,8 +100,9 @@ import { Settings } from "./components/Group/Settings"; import { MainAvatar } from "./components/MainAvatar"; import { useRetrieveDataLocalStorage } from "./useRetrieveDataLocalStorage"; import { useQortalGetSaveSettings } from "./useQortalGetSaveSettings"; -import { useResetRecoilState } from "recoil"; -import { canSaveSettingToQdnAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from "./atoms/global"; +import { useRecoilState, useResetRecoilState } from "recoil"; +import { canSaveSettingToQdnAtom, fullScreenAtom, hasSettingsChangedAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from "./atoms/global"; +import { useAppFullScreen } from "./useAppFullscreen"; type extStates = | "not-authenticated" @@ -297,10 +298,12 @@ function App() { const [txList, setTxList] = useState([]); const [memberGroups, setMemberGroups] = useState([]); const [isFocused, setIsFocused] = useState(true); - + const [hasSettingsChanged, setHasSettingsChanged] = useRecoilState(hasSettingsChangedAtom) const holdRefExtState = useRef("not-authenticated"); const isFocusedRef = useRef(true); const { isShow, onCancel, onOk, show, message } = useModal(); + const { isShow: isShowUnsavedChanges, onCancel: onCancelUnsavedChanges, onOk: onOkUnsavedChanges, show: showUnsavedChanges, message: messageUnsavedChanges } = useModal(); + const { onCancel: onCancelQortalRequest, onOk: onOkQortalRequest, @@ -308,7 +311,14 @@ function App() { isShow: isShowQortalRequest, message: messageQortalRequest, } = useModal(); - + const { + onCancel: onCancelQortalRequestExtension, + onOk: onOkQortalRequestExtension, + show: showQortalRequestExtension, + isShow: isShowQortalRequestExtension, + message: messageQortalRequestExtension, + } = useModal(); + const [openRegisterName, setOpenRegisterName] = useState(false); const registerNamePopoverRef = useRef(null); const [isLoadingRegisterName, setIsLoadingRegisterName] = useState(false); @@ -328,7 +338,24 @@ function App() { const qortalRequestCheckbox1Ref = useRef(null); useRetrieveDataLocalStorage() useQortalGetSaveSettings(userInfo?.name) + const [fullScreen, setFullScreen] = useRecoilState(fullScreenAtom); + const { toggleFullScreen } = useAppFullScreen(setFullScreen); + + useEffect(() => { + // Attach a global event listener for double-click + const handleDoubleClick = () => { + toggleFullScreen(); + }; + + // Add the event listener to the root HTML document + document.documentElement.addEventListener('dblclick', handleDoubleClick); + + // Clean up the event listener on unmount + return () => { + document.documentElement.removeEventListener('dblclick', handleDoubleClick); + }; + }, [toggleFullScreen]); //resets for recoil const resetAtomSortablePinnedAppsAtom = useResetRecoilState(sortablePinnedAppsAtom); const resetAtomCanSaveSettingToQdnAtom = useResetRecoilState(canSaveSettingToQdnAtom); @@ -588,15 +615,10 @@ function App() { const qortalRequestPermisson = async (message, sender, sendResponse) => { if (message.action === "QORTAL_REQUEST_PERMISSION" && !isMainWindow) { try { - console.log("payloadbefore", message.payload); await showQortalRequest(message?.payload); - console.log("payload", message.payload); if (message?.payload?.checkbox1) { - console.log( - "qortalRequestCheckbox1Ref.current", - qortalRequestCheckbox1Ref.current - ); + sendResponse({ accepted: true, checkbox1: qortalRequestCheckbox1Ref.current, @@ -605,7 +627,6 @@ function App() { } sendResponse({ accepted: true }); } catch (error) { - console.log("error", error); sendResponse({ accepted: false }); } finally { window.close(); @@ -615,13 +636,11 @@ function App() { const qortalRequestPermissonFromExtension = async (message, sender, sendResponse) => { if (message.action === "QORTAL_REQUEST_PERMISSION" && isMainWindow) { try { - console.log("payloadbefore", message.payload); - await show('do you accept?'); + await showQortalRequestExtension(message?.payload); sendResponse({ accepted: true }); } catch (error) { - console.log("error", error); sendResponse({ accepted: false }); } } @@ -695,7 +714,6 @@ function App() { // Call the permission request handler for "QORTAL_REQUEST_PERMISSION" qortalRequestPermisson(message, sender, sendResponse); if (message.action === "QORTAL_REQUEST_PERMISSION" && !isMainWindow) { - console.log("isMainWindow", isMainWindow, window?.location?.href); return true; // Return true to indicate an async response is coming } if (message.action === "QORTAL_REQUEST_PERMISSION" && isMainWindow && message?.isFromExtension) { @@ -994,8 +1012,11 @@ function App() { } }; - const logoutFunc = () => { + const logoutFunc = async () => { try { + if(hasSettingsChanged){ + await showUnsavedChanges({message: 'Your settings have changed. If you logout you will lose your changes. Click on the save button in the header to keep your changed settings.'}) + } chrome?.runtime?.sendMessage({ action: "logout" }, (response) => { if (response) { resetAllStates(); @@ -2762,6 +2783,246 @@ function App() { + )} + {isShowUnsavedChanges && ( + + {"Warning"} + + + {messageUnsavedChanges.message} + + + + + + + + )} + {isShowQortalRequestExtension && isMainWindow && ( + + { + onCancelQortalRequestExtension() + }} + size={50} + strokeWidth={5} + > + {({ remainingTime }) => {remainingTime}} + + + + + {messageQortalRequestExtension?.text1} + + + {messageQortalRequestExtension?.text2 && ( + <> + + + + {messageQortalRequestExtension?.text2} + + + + + )} + {messageQortalRequestExtension?.text3 && ( + <> + + + {messageQortalRequestExtension?.text3} + + + + + )} + + {messageQortalRequestExtension?.text4 && ( + + + {messageQortalRequestExtension?.text4} + + + )} + + {messageQortalRequestExtension?.html && ( +
+ )} + + + + {messageQortalRequestExtension?.highlightedText} + + + {messageQortalRequestExtension?.fee && ( + <> + + + + {'Fee: '}{messageQortalRequestExtension?.fee}{' QORT'} + + + + + )} + {messageQortalRequestExtension?.checkbox1 && ( + + { + qortalRequestCheckbox1Ref.current = e.target.checked; + }} + edge="start" + tabIndex={-1} + disableRipple + defaultChecked={messageQortalRequestExtension?.checkbox1?.value} + sx={{ + "&.Mui-checked": { + color: "white", // Customize the color when checked + }, + "& .MuiSvgIcon-root": { + color: "white", + }, + }} + /> + + + {messageQortalRequestExtension?.checkbox1?.label} + + + )} + + + + onOkQortalRequestExtension("accepted")} + > + accept + + onCancelQortalRequestExtension()} + > + decline + + + {sendPaymentError} + +
)} { - +export const AppInfoSnippet = ({ app, myName, isFromCategory }) => { const isInstalled = app?.status?.status === 'READY' return ( @@ -35,6 +31,12 @@ export const AppInfoSnippet = ({ app, myName }) => { width: "60px", }} onClick={()=> { + if(isFromCategory){ + executeEvent("selectedAppInfoCategory", { + data: app, + }); + return + } executeEvent("selectedAppInfo", { data: app, }); @@ -73,6 +75,12 @@ export const AppInfoSnippet = ({ app, myName }) => {
{ + if(isFromCategory){ + executeEvent("selectedAppInfoCategory", { + data: app, + }); + return + } executeEvent("selectedAppInfo", { data: app, }); @@ -91,6 +99,7 @@ export const AppInfoSnippet = ({ app, myName }) => { { + executeEvent("addTab", { data: app }) diff --git a/src/components/Apps/AppRating.tsx b/src/components/Apps/AppRating.tsx index ae5fbc1..a498779 100644 --- a/src/components/Apps/AppRating.tsx +++ b/src/components/Apps/AppRating.tsx @@ -25,9 +25,7 @@ export const AppRating = ({ app, myName, ratingCountPosition = "right" }) => { const [openSnack, setOpenSnack] = useState(false); const [infoSnack, setInfoSnack] = useState(null); const hasCalledRef = useRef(false); - console.log(`pollinfo-${app?.service}-${app?.name}`, value); - console.log("hasPublishedRating", hasPublishedRating); const getRating = useCallback(async (name, service) => { try { hasCalledRef.current = true; @@ -42,7 +40,6 @@ export const AppRating = ({ app, myName, ratingCountPosition = "right" }) => { }); const responseData = await response.json(); - console.log("responseData", responseData); if (responseData?.message?.includes("POLL_NO_EXISTS")) { setHasPublishedRating(false); } else if (responseData?.pollName) { @@ -67,14 +64,12 @@ export const AppRating = ({ app, myName, ratingCountPosition = "right" }) => { const initialValueVote = voteCount.find((vote) => vote.optionName.startsWith("initialValue-") ); - console.log("initialValueVote", initialValueVote); if (initialValueVote) { // Convert "initialValue-X" to just "X" and add it to the ratingVotes array const initialRating = parseInt( initialValueVote.optionName.split("-")[1], 10 ); - console.log("initialRating", initialRating); ratingVotes.push({ optionName: initialRating.toString(), voteCount: 1, @@ -91,14 +86,12 @@ export const AppRating = ({ app, myName, ratingCountPosition = "right" }) => { totalScore += rating * count; // Weighted score totalVotes += count; // Total number of votes }); - console.log("ratingVotes", ratingVotes, totalScore, totalVotes); // Calculate average rating (ensure no division by zero) const averageRating = totalVotes > 0 ? totalScore / totalVotes : 0; setValue(averageRating); } } catch (error) { - console.log("error rating", error); if (error?.message?.includes("POLL_NO_EXISTS")) { setHasPublishedRating(false); } @@ -114,7 +107,6 @@ export const AppRating = ({ app, myName, ratingCountPosition = "right" }) => { try { if (!myName) throw new Error("You need a name to rate."); if (!app?.name) return; - console.log("newValue", newValue); const fee = await getFee("ARBITRARY"); await show({ @@ -138,7 +130,6 @@ export const AppRating = ({ app, myName, ratingCountPosition = "right" }) => { }, }, (response) => { - console.log("response", response); if (response.error) { rej(response?.message); return; @@ -172,7 +163,6 @@ export const AppRating = ({ app, myName, ratingCountPosition = "right" }) => { }, }, (response) => { - console.log("response", response); if (response.error) { rej(response?.message); return; @@ -197,11 +187,7 @@ export const AppRating = ({ app, myName, ratingCountPosition = "right" }) => { setOpenSnack(true); } }; - console.log( - "vvotes", - (votesInfo?.totalVotes ?? 0) + votesInfo?.voteCounts?.length === 6 ? 1 : 0, - votesInfo - ); + return (
{ const { rootHeight } = useContext(MyContext); const frameRef = useRef(null); - const refreshAppFunc = (e) => { - console.log('getting refresh', e) - }; - // useEffect(() => { - // subscribeToEvent("refreshAPp", refreshAppFunc); - - // return () => { - // unsubscribeFromEvent("refreshApp", refreshAppFunc); - // }; - // }, []); return ( { const [availableQapps, setAvailableQapps] = useState([]); const [selectedAppInfo, setSelectedAppInfo] = useState(null); + const [selectedCategory, setSelectedCategory] = useState(null) const [tabs, setTabs] = useState([]); const [selectedTab, setSelectedTab] = useState(null); const [isNewTabWindow, setIsNewTabWindow] = useState(false); @@ -85,7 +87,6 @@ export const Apps = ({ mode, setMode, show , myName}) => { }); if (!response?.ok) return; const responseData = await response.json(); - console.log('responseData', responseData) const urlWebsites = `${getBaseApiReact()}/arbitrary/resources/search?service=WEBSITE&mode=ALL&limit=0&includestatus=true&includemetadata=true`; const responseWebsites = await fetch(urlWebsites, { @@ -125,8 +126,44 @@ export const Apps = ({ mode, setMode, show , myName}) => { }; }, []); + const selectedAppInfoCategoryFunc = (e) => { + const data = e.detail?.data; + setSelectedAppInfo(data); + setMode("appInfo-from-category"); + }; + + useEffect(() => { + subscribeToEvent("selectedAppInfoCategory", selectedAppInfoCategoryFunc); + + return () => { + unsubscribeFromEvent("selectedAppInfoCategory", selectedAppInfoCategoryFunc); + }; + }, []); + + + + const selectedCategoryFunc = (e) => { + const data = e.detail?.data; + setSelectedCategory(data); + setMode("category"); + }; + + useEffect(() => { + subscribeToEvent("selectedCategory", selectedCategoryFunc); + + return () => { + unsubscribeFromEvent("selectedCategory", selectedCategoryFunc); + }; + }, []); + + const navigateBackFunc = (e) => { - if (mode === "appInfo") { + if(mode === 'category'){ + setMode("library"); + setSelectedCategory(null) + } else if (mode === "appInfo-from-category") { + setMode("category"); + } else if (mode === "appInfo") { setMode("library"); } else if (mode === "library") { if (isNewTabWindow) { @@ -139,10 +176,8 @@ export const Apps = ({ mode, setMode, show , myName}) => { } else { const iframeId = `browser-iframe-${selectedTab?.tabId}`; const iframe = document.getElementById(iframeId); - console.log("iframe", iframe); // Go Back in the iframe's history if (iframe) { - console.log(iframe.contentWindow); if (iframe && iframe.contentWindow) { const iframeWindow = iframe.contentWindow; if (iframeWindow && iframeWindow.history) { @@ -247,6 +282,7 @@ export const Apps = ({ mode, setMode, show , myName}) => { }; }, [tabs]); + return ( { setMode={setMode} myName={myName} hasPublishApp={!!(myApp || myWebsite)} + categories={categories} /> {mode === "appInfo" && } + {mode === "appInfo-from-category" && } + {mode === "publish" && } {tabs.map((tab) => { @@ -282,7 +321,7 @@ export const Apps = ({ mode, setMode, show , myName}) => { {isNewTabWindow && mode === "viewer" && ( <> - + )} {mode !== "viewer" && !selectedTab && } diff --git a/src/components/Apps/AppsCategory.tsx b/src/components/Apps/AppsCategory.tsx new file mode 100644 index 0000000..a999c95 --- /dev/null +++ b/src/components/Apps/AppsCategory.tsx @@ -0,0 +1,188 @@ +import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import { + AppCircle, + AppCircleContainer, + AppCircleLabel, + AppLibrarySubTitle, + AppsContainer, + AppsLibraryContainer, + AppsParent, + AppsSearchContainer, + AppsSearchLeft, + AppsSearchRight, + AppsWidthLimiter, + PublishQAppCTAButton, + PublishQAppCTALeft, + PublishQAppCTAParent, + PublishQAppCTARight, + PublishQAppDotsBG, +} from "./Apps-styles"; +import { Avatar, Box, ButtonBase, InputBase, styled } from "@mui/material"; +import { Add } from "@mui/icons-material"; +import { MyContext, getBaseApiReact } from "../../App"; +import LogoSelected from "../../assets/svgs/LogoSelected.svg"; +import IconSearch from "../../assets/svgs/Search.svg"; +import IconClearInput from "../../assets/svgs/ClearInput.svg"; +import qappDevelopText from "../../assets/svgs/qappDevelopText.svg"; +import qappDots from "../../assets/svgs/qappDots.svg"; + +import { Spacer } from "../../common/Spacer"; +import { AppInfoSnippet } from "./AppInfoSnippet"; +import { Virtuoso } from "react-virtuoso"; +import { executeEvent } from "../../utils/events"; +const officialAppList = [ + "q-tube", + "q-blog", + "q-share", + "q-support", + "q-mail", + "qombo", + "q-fund", + "q-shop", +]; + +const ScrollerStyled = styled('div')({ + // Hide scrollbar for WebKit browsers (Chrome, Safari) + "::-webkit-scrollbar": { + width: "0px", + height: "0px", + }, + + // Hide scrollbar for Firefox + scrollbarWidth: "none", + + // Hide scrollbar for IE and older Edge + "-ms-overflow-style": "none", + }); + + const StyledVirtuosoContainer = styled('div')({ + position: 'relative', + width: '100%', + display: 'flex', + flexDirection: 'column', + + // Hide scrollbar for WebKit browsers (Chrome, Safari) + "::-webkit-scrollbar": { + width: "0px", + height: "0px", + }, + + // Hide scrollbar for Firefox + scrollbarWidth: "none", + + // Hide scrollbar for IE and older Edge + "-ms-overflow-style": "none", + }); + +export const AppsCategory = ({ availableQapps, myName, category, isShow }) => { + const [searchValue, setSearchValue] = useState(""); + const virtuosoRef = useRef(); + const { rootHeight } = useContext(MyContext); + + + + const categoryList = useMemo(() => { + return availableQapps.filter( + (app) => + app?.metadata?.category === category?.id + ); + }, [availableQapps, category]); + + const [debouncedValue, setDebouncedValue] = useState(""); // Debounced value + + // Debounce logic + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(searchValue); + }, 350); + + // Cleanup timeout if searchValue changes before the timeout completes + return () => { + clearTimeout(handler); + }; + }, [searchValue]); // Runs effect when searchValue changes + + // Example: Perform search or other actions based on debouncedValue + + const searchedList = useMemo(() => { + if (!debouncedValue) return categoryList + return categoryList.filter((app) => + app.name.toLowerCase().includes(debouncedValue.toLowerCase()) + ); + }, [debouncedValue, categoryList]); + + const rowRenderer = (index) => { + + let app = searchedList[index]; + return ; + }; + + + + return ( + + + + + + + setSearchValue(e.target.value)} + sx={{ ml: 1, flex: 1 }} + placeholder="Search for apps" + inputProps={{ + "aria-label": "Search for apps", + fontSize: "16px", + fontWeight: 400, + }} + /> + + + {searchValue && ( + { + setSearchValue(""); + }} + > + + + )} + + + + + + + {`Category: ${category?.name}`} + + + + + + + + + + + ); +}; diff --git a/src/components/Apps/AppsLibrary.tsx b/src/components/Apps/AppsLibrary.tsx index 4338b15..d719db8 100644 --- a/src/components/Apps/AppsLibrary.tsx +++ b/src/components/Apps/AppsLibrary.tsx @@ -74,23 +74,11 @@ const ScrollerStyled = styled('div')({ "-ms-overflow-style": "none", }); -export const AppsLibrary = ({ availableQapps, setMode, myName, hasPublishApp, isShow }) => { +export const AppsLibrary = ({ availableQapps, setMode, myName, hasPublishApp, isShow, categories={categories} }) => { const [searchValue, setSearchValue] = useState(""); const virtuosoRef = useRef(); const { rootHeight } = useContext(MyContext); - const [appStates, setAppStates] = useState({}); - const handleStateChange = (appId, newState) => { - setAppStates((prevState) => ({ - ...prevState, - [appId]: { - ...(prevState[appId] || {}), // Preserve existing state for the app - ...newState, // Merge in the new state properties - }, - })); - }; - - console.log('appStates', appStates) const officialApps = useMemo(() => { return availableQapps.filter( (app) => @@ -121,12 +109,10 @@ export const AppsLibrary = ({ availableQapps, setMode, myName, hasPublishApp, i app.name.toLowerCase().includes(debouncedValue.toLowerCase()) ); }, [debouncedValue]); - console.log("officialApps", searchedList); const rowRenderer = (index) => { let app = searchedList[index]; - console.log('appi', app) return ; }; @@ -273,6 +259,49 @@ export const AppsLibrary = ({ availableQapps, setMode, myName, hasPublishApp, i Categories + + + {categories?.map((category)=> { + return ( + { + executeEvent('selectedCategory', { + data: category + }) + }}> + + {category?.name} + + + ) + })} + )} diff --git a/src/components/Apps/AppsNavBar.tsx b/src/components/Apps/AppsNavBar.tsx index 8d87fca..155b3a3 100644 --- a/src/components/Apps/AppsNavBar.tsx +++ b/src/components/Apps/AppsNavBar.tsx @@ -41,7 +41,6 @@ export function saveToLocalStorage(key, subKey, newValue) { // Save combined data back to localStorage const serializedValue = JSON.stringify(combinedData); localStorage.setItem(key, serializedValue); - console.log(`Data saved to localStorage with key: ${key} and subKey: ${subKey}`); } catch (error) { console.error('Error saving to localStorage:', error); } @@ -57,6 +56,7 @@ export const AppsNavBar = () => { const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom); + const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom); const handleClick = (event) => { @@ -71,7 +71,6 @@ export const AppsNavBar = () => { // Scroll to the last tab whenever the tabs array changes (e.g., when a new tab is added) if (tabsRef.current) { const tabElements = tabsRef.current.querySelectorAll('.MuiTab-root'); - console.log('tabElements', tabElements) if (tabElements.length > 0) { const lastTab = tabElements[tabElements.length - 1]; lastTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'end' }); @@ -95,7 +94,6 @@ export const AppsNavBar = () => { }; }, []); - console.log('selectedTab', selectedTab) const isSelectedAppPinned = !!sortablePinnedApps?.find((item)=> item?.name === selectedTab?.name && item?.service === selectedTab?.service) return ( diff --git a/src/components/Apps/SortablePinnedApps.tsx b/src/components/Apps/SortablePinnedApps.tsx index fc75919..779bd5d 100644 --- a/src/components/Apps/SortablePinnedApps.tsx +++ b/src/components/Apps/SortablePinnedApps.tsx @@ -14,7 +14,6 @@ import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps'; const SortableItem = ({ id, name, app }) => { const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id }); - console.log('namednd', name) const style = { transform: CSS.Transform.toString(transform), transition, @@ -86,7 +85,6 @@ export const SortablePinnedApps = ({ myWebsite, myApp, availableQapps = [] }) = const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom); const transformPinnedApps = useMemo(() => { - console.log({ myWebsite, myApp, availableQapps, pinnedApps }); // Clone the existing pinned apps list let pinned = [...pinnedApps]; @@ -130,27 +128,7 @@ export const SortablePinnedApps = ({ myWebsite, myApp, availableQapps = [] }) = return pinned; }, [myApp, myWebsite, pinnedApps, availableQapps]); - console.log('transformPinnedApps', transformPinnedApps) - // const hasSetPinned = useRef(false) - // useEffect(() => { - // if (!apps || apps.length === 0) return; - - // setPinnedApps((prevPinnedApps) => { - // // Create a map of the previous pinned apps for easy lookup - // const pinnedAppsMap = new Map(prevPinnedApps.map(app => [`${app?.service}-${app?.name}`, app])); - - // // Update the pinnedApps list based on new apps - // const updatedPinnedApps = apps.map(app => { - // const id = `${app?.service}-${app?.name}`; - // // Keep the existing app from pinnedApps if it exists - // return pinnedAppsMap.get(id) || app; - // }); - - // return updatedPinnedApps; - // }); - // }, [apps]); - - console.log('dnd',{pinnedApps}) + const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { diff --git a/src/components/Apps/useQortalMessageListener.tsx b/src/components/Apps/useQortalMessageListener.tsx index 169d0b5..67b4461 100644 --- a/src/components/Apps/useQortalMessageListener.tsx +++ b/src/components/Apps/useQortalMessageListener.tsx @@ -97,7 +97,6 @@ async function handleGetFileFromIndexedDB(fileId, sendResponse) { const deleteRequest = deleteObjectStore.delete(fileId); deleteRequest.onsuccess = function () { - console.log(`File with ID ${fileId} has been removed from IndexedDB`); try { sendResponse({ result: base64String }); @@ -171,7 +170,6 @@ const UIQortalRequests = [ async function deleteQortalFilesFromIndexedDB() { try { - console.log("Opening IndexedDB for deleting files..."); const db = await openIndexedDB(); const transaction = db.transaction(["files"], "readwrite"); const objectStore = transaction.objectStore("files"); @@ -262,7 +260,6 @@ const UIQortalRequests = [ obj.fileId = fileId; delete obj.file; } - console.log(obj, obj.blob instanceof Blob, 'blob') if (obj.blob) { const fileId = "objFile_qortalfile"; @@ -315,7 +312,6 @@ const UIQortalRequests = [ export const useQortalMessageListener = (frameWindow) => { const [path, setPath] = useState('') useEffect(() => { - console.log("Listener added react"); const listener = async (event) => { event.preventDefault(); // Prevent default behavior @@ -325,7 +321,6 @@ export const useQortalMessageListener = (frameWindow) => { const sendMessageToRuntime = (message, eventPort) => { chrome?.runtime?.sendMessage(message, (response) => { - console.log('response', response); if (response.error) { eventPort.postMessage({ result: null, @@ -342,7 +337,6 @@ export const useQortalMessageListener = (frameWindow) => { // Check if action is included in the predefined list of UI requests if (UIQortalRequests.includes(event.data.action)) { - console.log('event?.data', event?.data); sendMessageToRuntime( { action: event.data.action, type: 'qortalRequest', payload: event.data, isExtension: true }, event.ports[0] @@ -353,7 +347,6 @@ export const useQortalMessageListener = (frameWindow) => { event?.data?.action === 'ENCRYPT_DATA' || event?.data?.action === 'SAVE_FILE' ) { - console.log('event?.data?', event?.data); let data; try { data = await storeFilesInIndexedDB(event.data); @@ -365,7 +358,6 @@ export const useQortalMessageListener = (frameWindow) => { }); return; } - console.log('data after', data) if (data) { sendMessageToRuntime( { action: event.data.action, type: 'qortalRequest', payload: data, isExtension: true }, @@ -396,7 +388,6 @@ export const useQortalMessageListener = (frameWindow) => { }, []); // Empty dependency array to run once when the component mounts chrome.runtime?.onMessage.addListener( function (message, sender, sendResponse) { - console.log('SHOWING UP') if(message.action === "SHOW_SAVE_FILE_PICKER"){ showSaveFilePicker(message?.data) } diff --git a/src/components/Chat/ChatDirect.tsx b/src/components/Chat/ChatDirect.tsx index 6547f91..a73e893 100644 --- a/src/components/Chat/ChatDirect.tsx +++ b/src/components/Chat/ChatDirect.tsx @@ -127,7 +127,6 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi const forceCloseWebSocket = () => { if (socketRef.current) { - console.log('Force closing the WebSocket'); clearTimeout(timeoutIdRef.current); clearTimeout(groupSocketTimeoutRef.current); socketRef.current.close(1000, 'forced'); @@ -161,7 +160,6 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi socketRef.current = new WebSocket(socketLink); socketRef.current.onopen = () => { - console.log('WebSocket connection opened'); setTimeout(pingWebSocket, 50); // Initial ping }; diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index 64e1adb..8557391 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -824,98 +824,7 @@ export const Group = ({ } }, [selectedGroup]); - // const handleNotification = async (data)=> { - // try { - // if(isFocusedRef.current){ - // throw new Error('isFocused') - // } - // const newActiveChats= data - // const oldActiveChats = await new Promise((res, rej) => { - // chrome?.runtime?.sendMessage( - // { - // action: "getChatHeads", - // }, - // (response) => { - // console.log({ response }); - // if (!response?.error) { - // res(response); - // } - // rej(response.error); - // } - // ); - // }); - - // let results = [] - // newActiveChats?.groups?.forEach(newChat => { - // let isNewer = true; - // oldActiveChats?.data?.groups?.forEach(oldChat => { - // if (newChat?.timestamp <= oldChat?.timestamp) { - // isNewer = false; - // } - // }); - // if (isNewer) { - // results.push(newChat) - // console.log('This newChat is newer than all oldChats:', newChat); - // } - // }); - - // if(results?.length > 0){ - // if (!lastGroupNotification.current || (Date.now() - lastGroupNotification.current >= 60000)) { - // console.log((Date.now() - lastGroupNotification.current >= 60000), lastGroupNotification.current) - // chrome?.runtime?.sendMessage( - // { - // action: "notification", - // payload: { - // }, - // }, - // (response) => { - // console.log({ response }); - // if (!response?.error) { - - // } - - // } - // ); - // audio.play(); - // lastGroupNotification.current = Date.now() - - // } - // } - - // } catch (error) { - // console.log('error not', error) - // if(!isFocusedRef.current){ - // chrome?.runtime?.sendMessage( - // { - // action: "notification", - // payload: { - // }, - // }, - // (response) => { - // console.log({ response }); - // if (!response?.error) { - - // } - - // } - // ); - // audio.play(); - // lastGroupNotification.current = Date.now() - // } - - // } finally { - - // chrome?.runtime?.sendMessage( - // { - // action: "setChatHeads", - // payload: { - // data, - // }, - // } - // ); - - // } - // } + const getAdmins = async (groupId) => { try { diff --git a/src/components/Group/GroupJoinRequests.tsx b/src/components/Group/GroupJoinRequests.tsx index d373435..47fcb31 100644 --- a/src/components/Group/GroupJoinRequests.tsx +++ b/src/components/Group/GroupJoinRequests.tsx @@ -48,20 +48,7 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get return true }) - // const getJoinGroupRequests = groupsAsAdmin.map(async (group)=> { - // console.log('getJoinGroupRequests', group) - // const joinRequestResponse = await requestQueueGroupJoinRequests.enqueue(()=> { - // return fetch( - // `${getBaseApiReact()}/groups/joinrequests/${group.groupId}` - // ); - // }) - - // const joinRequestData = await joinRequestResponse.json() - // return { - // group, - // data: joinRequestData - // } - // }) + await Promise.all(getAllGroupsAsAdmin) const res = await Promise.all(groupsAsAdmin.map(async (group)=> { diff --git a/src/components/Mobile/MobileHeader.tsx b/src/components/Mobile/MobileHeader.tsx index 0feeb7b..1773262 100644 --- a/src/components/Mobile/MobileHeader.tsx +++ b/src/components/Mobile/MobileHeader.tsx @@ -20,6 +20,10 @@ import { MessagingIcon } from "../../assets/Icons/MessagingIcon"; import { MessagingIcon2 } from "../../assets/Icons/MessagingIcon2"; import { HubsIcon } from "../../assets/Icons/HubsIcon"; import { Save } from "../Save/Save"; +import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen'; +import { useRecoilState } from "recoil"; +import { fullScreenAtom, hasSettingsChangedAtom } from "../../atoms/global"; +import { useAppFullScreen } from "../../useAppFullscreen"; const Header = ({ logoutFunc, @@ -33,16 +37,11 @@ const Header = ({ myName, setSelectedDirect, setNewChat - // selectedGroup, - // onHomeClick, - // onLogoutClick, - // onGroupChange, - // onWalletClick, - // onNotificationClick, }) => { const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); - + const [fullScreen, setFullScreen] = useRecoilState(fullScreenAtom); + const {exitFullScreen} = useAppFullScreen(setFullScreen) const handleClick = (event) => { setAnchorEl(event.currentTarget); }; @@ -77,10 +76,10 @@ const Header = ({ width: "75px", }} > - { setMobileViewModeKeepOpen(""); goToHome(); @@ -88,15 +87,24 @@ const Header = ({ // onClick={onHomeClick} > - - + - + + {fullScreen && ( + { + exitFullScreen() + setFullScreen(false) + }}> + + + )} + {/* Center Title */} @@ -254,6 +262,16 @@ const Header = ({ > + {fullScreen && ( + { + exitFullScreen() + setFullScreen(false) + }}> + + + )} {/* Center Title */} { const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom); const [settingsQdnLastUpdated, setSettingsQdnLastUpdated] = useRecoilState(settingsQDNLastUpdatedAtom); const [settingsLocalLastUpdated] = useRecoilState(settingsLocalLastUpdatedAtom); + const setHasSettingsChangedAtom = useSetRecoilState(hasSettingsChangedAtom); const [canSave] = useRecoilState(canSaveSettingToQdnAtom); const [openSnack, setOpenSnack] = useState(false); const [isLoading, setIsLoading] = useState(false) const [infoSnack, setInfoSnack] = useState(null); const [oldPinnedApps, setOldPinnedApps] = useRecoilState(oldPinnedAppsAtom) - console.log('oldpin', {oldPinnedApps, pinnedApps}, settingsQdnLastUpdated, settingsLocalLastUpdated, settingsQdnLastUpdated < settingsLocalLastUpdated,) + const { show } = useContext(MyContext); const hasChanged = useMemo(()=> { @@ -38,11 +39,14 @@ export const Save = () => { } }) } - console.log('!isEqual(oldChanges, newChanges)', !isEqual(oldChanges, newChanges)) if(settingsQdnLastUpdated === -100) return false return !isEqual(oldChanges, newChanges) && settingsQdnLastUpdated < settingsLocalLastUpdated }, [oldPinnedApps, pinnedApps, settingsQdnLastUpdated, settingsLocalLastUpdated]) + useEffect(()=> { + setHasSettingsChangedAtom(hasChanged) + }, [hasChanged]) + const saveToQdn = async ()=> { try { setIsLoading(true) @@ -64,7 +68,6 @@ export const Save = () => { }, }, (response) => { - console.log("response", response); if (response.error) { rej(response?.message); return; @@ -102,7 +105,6 @@ export const Save = () => { } ); }); - console.log('saved', response) if(response?.identifier){ setOldPinnedApps(pinnedApps) setSettingsQdnLastUpdated(Date.now()) @@ -114,7 +116,6 @@ export const Save = () => { setOpenSnack(true); } } - console.log('save encryptedData', encryptData) } catch (error) { setInfoSnack({ type: "error", @@ -126,7 +127,6 @@ export const Save = () => { setIsLoading(false) } } - console.log('settingsQdnLastUpdated', settingsQdnLastUpdated) return ( <> diff --git a/src/qdn/encryption/group-encryption.ts b/src/qdn/encryption/group-encryption.ts index f210182..4f61dd8 100644 --- a/src/qdn/encryption/group-encryption.ts +++ b/src/qdn/encryption/group-encryption.ts @@ -319,7 +319,6 @@ export const decodeBase64ForUIChatMessages = (messages)=> { if (decryptedKey) { // Decrypt the data using the symmetric key. const decryptedData = nacl.secretbox.open(encryptedData, nonce, decryptedKey) - console.log('decryptedData', decryptedData) // If decryption was successful, decryptedData will not be null. if (decryptedData) { return decryptedData diff --git a/src/qdn/publish/pubish.ts b/src/qdn/publish/pubish.ts index 9e27d5d..a57fd1e 100644 --- a/src/qdn/publish/pubish.ts +++ b/src/qdn/publish/pubish.ts @@ -61,25 +61,7 @@ export const publishData = async ({ tag5, feeAmount }: any) => { - console.log({ - registeredName, - file, - service, - identifier, - uploadType, - isBase64, - filename, - withFee, - title, - description, - category, - tag1, - tag2, - tag3, - tag4, - tag5, - feeAmount - }) + const validateName = async (receiverName: string) => { return await reusableGet(`/names/${receiverName}`) } @@ -171,7 +153,6 @@ export const publishData = async ({ fee = feeAmount } else if (withFee) { const res = await getArbitraryFee() - console.log('res', res) if (res.fee) { fee = res.fee } else { @@ -180,7 +161,6 @@ export const publishData = async ({ } let transactionBytes = await uploadData(registeredName, file, fee) - console.log('transactionBytes', transactionBytes) if (!transactionBytes || transactionBytes.error) { throw new Error(transactionBytes?.message || 'Error when uploading') } else if (transactionBytes.includes('Error 500 Internal Server Error')) { @@ -201,7 +181,6 @@ export const publishData = async ({ } const uploadData = async (registeredName: string, file:any, fee: number) => { - console.log('uploadData', registeredName, file, fee) let postBody = '' let urlSuffix = '' @@ -230,7 +209,6 @@ export const publishData = async ({ } let uploadDataUrl = `/arbitrary/${service}/${registeredName}${urlSuffix}` - console.log('uploadDataUrl', uploadDataUrl) if (identifier?.trim().length > 0) { uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}${urlSuffix}` } @@ -273,7 +251,6 @@ export const publishData = async ({ if (tag5 != null && tag5 != 'undefined') { uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag5) } - console.log('uploadDataUrl2', uploadDataUrl) return await reusablePost(uploadDataUrl, postBody) @@ -282,7 +259,6 @@ export const publishData = async ({ try { return await validate() } catch (error: any) { - console.log('error2', error) throw new Error(error?.message) } } \ No newline at end of file diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index 4097e8c..3174148 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -63,7 +63,6 @@ function getLocalStorage(key) { chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { if (request) { - console.log('rquest', request) const isFromExtension = request?.isExtension switch (request.action) { case "GET_USER_ACCOUNT": { @@ -106,7 +105,7 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { case "GET_LIST_ITEMS": { const data = request.payload; - getListItems(data) + getListItems(data, isFromExtension) .then((res) => { sendResponse(res); }) @@ -119,7 +118,7 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { case "ADD_LIST_ITEMS": { const data = request.payload; - addListItems(data) + addListItems(data, isFromExtension) .then((res) => { sendResponse(res); }) @@ -132,7 +131,7 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { case "DELETE_LIST_ITEM": { const data = request.payload; - deleteListItems(data) + deleteListItems(data, isFromExtension) .then((res) => { sendResponse(res); }) @@ -145,7 +144,7 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { case "PUBLISH_QDN_RESOURCE": { const data = request.payload; - publishQDNResource(data, sender) + publishQDNResource(data, sender, isFromExtension) .then((res) => { sendResponse(res); }) @@ -158,7 +157,7 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { case "PUBLISH_MULTIPLE_QDN_RESOURCES": { const data = request.payload; - publishMultipleQDNResources(data, sender) + publishMultipleQDNResources(data, sender, isFromExtension) .then((res) => { sendResponse(res); }) @@ -184,7 +183,7 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { case "CREATE_POLL": { const data = request.payload; - createPoll(data) + createPoll(data, isFromExtension) .then((res) => { sendResponse(res); }) @@ -196,8 +195,7 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { } case "SEND_CHAT_MESSAGE": { const data = request.payload; - console.log('data', data) - sendChatMessage(data) + sendChatMessage(data, isFromExtension) .then((res) => { sendResponse(res); }) @@ -210,7 +208,7 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { case "JOIN_GROUP": { const data = request.payload; - joinGroup(data) + joinGroup(data, isFromExtension) .then((res) => { sendResponse(res); }) @@ -223,7 +221,7 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { case "SAVE_FILE": { const data = request.payload; - saveFile(data, sender) + saveFile(data, sender, isFromExtension) .then((res) => { sendResponse(res); }) @@ -236,7 +234,7 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { case "DEPLOY_AT": { const data = request.payload; - deployAt(data) + deployAt(data, isFromExtension) .then((res) => { sendResponse(res); }) @@ -249,7 +247,7 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { case "GET_USER_WALLET": { const data = request.payload; - getUserWallet(data) + getUserWallet(data, isFromExtension) .then((res) => { sendResponse(res); }) @@ -262,7 +260,7 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { case "GET_WALLET_BALANCE": { const data = request.payload; - getWalletBalance(data) + getWalletBalance(data, false, isFromExtension) .then((res) => { sendResponse(res); }) @@ -276,7 +274,7 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { case "GET_USER_WALLET_INFO": { const data = request.payload; - getUserWalletInfo(data) + getUserWalletInfo(data, isFromExtension) .then((res) => { sendResponse(res); }) @@ -414,7 +412,7 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { case "SEND_COIN": { const data = request.payload; - sendCoin(data) + sendCoin(data, isFromExtension) .then((res) => { sendResponse(res); }) diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index 0b2e92a..de91590 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -38,7 +38,7 @@ const dgbFeePerByte = 0.00000010 const rvnFeePerByte = 0.00001125 -const _createPoll = async (pollName, pollDescription, options) => { +const _createPoll = async ({pollName, pollDescription, options}, isFromExtension) => { const fee = await getFee("CREATE_POLL"); const resPermission = await getUserPermission({ @@ -47,7 +47,7 @@ const _createPoll = async (pollName, pollDescription, options) => { text3: `Description: ${pollDescription}`, text4: `Options: ${options?.join(", ")}`, fee: fee.fee, - }); + }, isFromExtension); const { accepted } = resPermission; if (accepted) { @@ -82,13 +82,13 @@ const _createPoll = async (pollName, pollDescription, options) => { }; const _deployAt = async ( - name, + {name, description, tags, creationBytes, amount, assetId, - atType + atType}, isFromExtension ) => { const fee = await getFee("DEPLOY_AT"); @@ -97,7 +97,7 @@ const _deployAt = async ( text2: `Name: ${name}`, text3: `Description: ${description}`, fee: fee.fee, - }); + }, isFromExtension); const { accepted } = resPermission; @@ -172,7 +172,6 @@ const _voteOnPoll = async ({pollName, optionIndex, optionName}, isFromExtension) }); const signedBytes = Base58.encode(tx.signedBytes); const res = await processTransactionVersion2(signedBytes); - console.log('res', res) if (!res?.signature) throw new Error(res?.message || "Transaction was not able to be processed"); return res; @@ -182,13 +181,11 @@ const _voteOnPoll = async ({pollName, optionIndex, optionName}, isFromExtension) }; function getFileFromContentScript(fileId, sender) { - console.log("sender", sender); return new Promise((resolve, reject) => { chrome.tabs.sendMessage( sender.tab.id, { action: "getFileFromIndexedDB", fileId: fileId }, (response) => { - console.log("response2", response); if (response && response.result) { resolve(response.result); } else { @@ -199,7 +196,6 @@ function getFileFromContentScript(fileId, sender) { }); } function sendToSaveFilePicker(data, sender) { - console.log("sender", sender); chrome.tabs.sendMessage(sender.tab.id, { action: "SHOW_SAVE_FILE_PICKER", @@ -239,7 +235,6 @@ async function getUserPermission(payload: any, isFromExtension?: boolean) { }); } - console.log('isFromExtension', isFromExtension) if(isFromExtension){ @@ -253,7 +248,6 @@ async function getUserPermission(payload: any, isFromExtension?: boolean) { chrome.runtime.sendMessage( { action: "QORTAL_REQUEST_PERMISSION", payload, isFromExtension }, (response) => { - console.log("permission response", response); if (response === undefined) return; clearTimeout(timeout); // Clear the timeout if we get a response @@ -268,11 +262,9 @@ async function getUserPermission(payload: any, isFromExtension?: boolean) { } await new Promise((res) => { const popupUrl = chrome.runtime.getURL("index.html?secondary=true"); - console.log("popupUrl", popupUrl); chrome.windows.getAll( { populate: true, windowTypes: ["popup"] }, (windows) => { - console.log("windows", windows); // Attempt to find an existing popup window that has a tab with the correct URL const existingPopup = windows.find( (w) => @@ -340,11 +332,9 @@ async function getUserPermission(payload: any, isFromExtension?: boolean) { }, 30000); // Send message to the content script to check focus - console.log("send msg"); chrome.runtime.sendMessage( { action: "QORTAL_REQUEST_PERMISSION", payload }, (response) => { - console.log("permission response", response); if (response === undefined) return; clearTimeout(timeout); // Clear the timeout if we get a response @@ -386,7 +376,6 @@ export const encryptData = async (data, sender) => { const parsedData = JSON.parse(resKeyPair); const privateKey = parsedData.privateKey; const userPublicKey = parsedData.publicKey; - console.log('data', data) const encryptDataResponse = encryptDataGroup({ data64, @@ -441,7 +430,7 @@ export const decryptData = async (data) => { throw new Error("Unable to decrypt"); }; -export const getListItems = async (data) => { +export const getListItems = async (data, isFromExtension) => { const localNodeAvailable = await isUsingLocal() if(!localNodeAvailable) throw new Error('Please use your local node.') const requiredFields = ["list_name"]; @@ -474,7 +463,7 @@ export const getListItems = async (data) => { value: value, label: "Always allow lists to be retrieved automatically", }, - }); + }, isFromExtension); const { accepted, checkbox1 } = resPermission; acceptedVar = accepted; checkbox1Var = checkbox1; @@ -483,20 +472,17 @@ export const getListItems = async (data) => { if (acceptedVar || skip) { const url = await createEndpoint(`/lists/${data.list_name}`); - console.log("url", url); const response = await fetch(url); - console.log("response", response); if (!response.ok) throw new Error("Failed to fetch"); const list = await response.json(); - console.log("list", list); return list; } else { throw new Error("User declined to share list"); } }; -export const addListItems = async (data) => { +export const addListItems = async (data, isFromExtension) => { const localNodeAvailable = await isUsingLocal() if(!localNodeAvailable) throw new Error('Please use your local node.') const requiredFields = ["list_name", "items"]; @@ -519,12 +505,11 @@ export const addListItems = async (data) => { text1: "Do you give this application permission to", text2: `Add the following to the list ${list_name}:`, highlightedText: items.join(", "), - }); + }, isFromExtension); const { accepted } = resPermission; if (accepted) { const url = await createEndpoint(`/lists/${list_name}`); - console.log("url", url); const body = { items: items, }; @@ -537,7 +522,6 @@ export const addListItems = async (data) => { body: bodyToString, }); - console.log("response", response); if (!response.ok) throw new Error("Failed to add to list"); let res; try { @@ -551,7 +535,7 @@ export const addListItems = async (data) => { } }; -export const deleteListItems = async (data) => { +export const deleteListItems = async (data, isFromExtension) => { const localNodeAvailable = await isUsingLocal() if(!localNodeAvailable) throw new Error('Please use your local node.') const requiredFields = ["list_name", "item"]; @@ -574,12 +558,11 @@ export const deleteListItems = async (data) => { text1: "Do you give this application permission to", text2: `Remove the following from the list ${list_name}:`, highlightedText: item, - }); + }, isFromExtension); const { accepted } = resPermission; if (accepted) { const url = await createEndpoint(`/lists/${list_name}`); - console.log("url", url); const body = { items: [item], }; @@ -592,7 +575,6 @@ export const deleteListItems = async (data) => { body: bodyToString, }); - console.log("response", response); if (!response.ok) throw new Error("Failed to add to list"); let res; try { @@ -606,7 +588,7 @@ export const deleteListItems = async (data) => { } }; -export const publishQDNResource = async (data: any, sender) => { +export const publishQDNResource = async (data: any, sender, isFromExtension) => { const requiredFields = ["service"]; const missingFields: string[] = []; requiredFields.forEach((field) => { @@ -683,7 +665,7 @@ export const publishQDNResource = async (data: any, sender) => { text3: `identifier: ${identifier || null}`, highlightedText: `isEncrypted: ${!!data.encrypt}`, fee: fee.fee, - }); + }, isFromExtension); const { accepted } = resPermission; if (accepted) { if (data.fileId && !data.encrypt) { @@ -718,7 +700,7 @@ export const publishQDNResource = async (data: any, sender) => { } }; -export const publishMultipleQDNResources = async (data: any, sender) => { +export const publishMultipleQDNResources = async (data: any, sender, isFromExtension) => { const requiredFields = ["resources"]; const missingFields: string[] = []; let feeAmount = null; @@ -815,14 +797,12 @@ export const publishMultipleQDNResources = async (data: any, sender) => { `, highlightedText: `isEncrypted: ${!!data.encrypt}`, fee: fee.fee * resources.length, - }); + }, isFromExtension); const { accepted } = resPermission; - console.log("accepted", accepted); if (!accepted) { throw new Error("User declined request"); } let failedPublishesIdentifiers = []; - console.log("resources", resources); for (const resource of resources) { try { const requiredFields = ["service"]; @@ -934,7 +914,6 @@ export const publishMultipleQDNResources = async (data: any, sender) => { }); } } catch (error) { - console.log("error", error); failedPublishesIdentifiers.push({ reason: "Unknown error", identifier: resource.identifier, @@ -990,7 +969,7 @@ export const voteOnPoll = async (data, isFromExtension) => { } }; -export const createPoll = async (data) => { +export const createPoll = async (data, isFromExtension) => { const requiredFields = [ "pollName", "pollDescription", @@ -1014,10 +993,12 @@ export const createPoll = async (data) => { const pollOwnerAddress = data.pollOwnerAddress; try { const resCreatePoll = await _createPoll( - pollName, + { + pollName, pollDescription, - pollOptions, - pollOwnerAddress + options: pollOptions, + }, + isFromExtension ); return resCreatePoll; } catch (error) { @@ -1025,7 +1006,7 @@ export const createPoll = async (data) => { } }; -export const sendChatMessage = async (data) => { +export const sendChatMessage = async (data, isFromExtension) => { const message = data.message; const recipient = data.destinationAddress; const groupId = data.groupId; @@ -1034,7 +1015,7 @@ export const sendChatMessage = async (data) => { text1: "Do you give this application permission to send this chat message?", text2: `To: ${isRecipient ? recipient : `group ${groupId}`}`, text3: `${message?.slice(0, 25)}${message?.length > 25 ? "..." : ""}`, - }); + }, isFromExtension); const { accepted } = resPermission; if (accepted) { @@ -1058,11 +1039,7 @@ export const sendChatMessage = async (data) => { repliedTo: "", version: 3, }; - try { - JSON.stringify(messageObject); - } catch (error) { - console.log("my error", error); - } + const stringifyMessageObject = JSON.stringify(messageObject); const balance = await getBalanceInfo(); @@ -1088,7 +1065,6 @@ export const sendChatMessage = async (data) => { // Otherwise, treat it as plain text res = await response.text(); } - console.log("res", res); if (res?.error === 102) { key = ""; hasPublicKey = false; @@ -1198,7 +1174,7 @@ export const sendChatMessage = async (data) => { } }; -export const joinGroup = async (data) => { +export const joinGroup = async (data, isFromExtension) => { const requiredFields = ["groupId"]; const missingFields: string[] = []; requiredFields.forEach((field) => { @@ -1228,7 +1204,7 @@ export const joinGroup = async (data) => { text1: "Confirm joining the group:", highlightedText: `${groupInfo.groupName}`, fee: fee.fee, - }); + }, isFromExtension); const { accepted } = resPermission; if (accepted) { @@ -1249,9 +1225,8 @@ export const joinGroup = async (data) => { } }; -export const saveFile = async (data, sender) => { +export const saveFile = async (data, sender, isFromExtension) => { try { - console.log('save file', data) const requiredFields = ["filename", "fileId"]; const missingFields: string[] = []; requiredFields.forEach((field) => { @@ -1270,7 +1245,7 @@ export const saveFile = async (data, sender) => { const resPermission = await getUserPermission({ text1: "Would you like to download:", highlightedText: `${filename}`, - }); + }, isFromExtension); const { accepted } = resPermission; if (accepted) { @@ -1314,7 +1289,7 @@ export const saveFile = async (data, sender) => { } }; -export const deployAt = async (data) => { +export const deployAt = async (data, isFromExtension) => { const requiredFields = [ "name", "description", @@ -1337,13 +1312,16 @@ export const deployAt = async (data) => { } try { const resDeployAt = await _deployAt( - data.name, - data.description, - data.tags, - data.creationBytes, - data.amount, - data.assetId, - data.type + { + name: data.name, + description: data.description, + tags: data.tags, + creationBytes: data.creationBytes, + amount: data.amount, + assetId: data.assetId, + atType: data.type + }, + isFromExtension ); return resDeployAt; } catch (error) { @@ -1351,7 +1329,7 @@ export const deployAt = async (data) => { } }; -export const getUserWallet = async (data) => { +export const getUserWallet = async (data, isFromExtension) => { const requiredFields = ["coin"]; const missingFields: string[] = []; requiredFields.forEach((field) => { @@ -1367,7 +1345,7 @@ export const getUserWallet = async (data) => { const resPermission = await getUserPermission({ text1: "Do you give this application permission to get your wallet information?", - }); + }, isFromExtension); const { accepted } = resPermission; if (accepted) { @@ -1437,7 +1415,7 @@ export const getUserWallet = async (data) => { } }; -export const getWalletBalance = async (data, bypassPermission?: boolean) => { +export const getWalletBalance = async (data, bypassPermission?: boolean, isFromExtension) => { const requiredFields = ["coin"]; const missingFields: string[] = []; requiredFields.forEach((field) => { @@ -1456,7 +1434,7 @@ export const getWalletBalance = async (data, bypassPermission?: boolean) => { resPermission = await getUserPermission({ text1: "Do you give this application permission to fetch your", highlightedText: `${data.coin} balance`, - }); + }, isFromExtension); } else { resPermission = { accepted: false @@ -1558,7 +1536,6 @@ const getUserWalletFunc = async (coin) => { const address = wallet.address0; const resKeyPair = await getKeyPair(); const parsedData = JSON.parse(resKeyPair); - console.log('coin', coin) switch (coin) { case "QORT": userWallet["address"] = address; @@ -1592,7 +1569,7 @@ const getUserWalletFunc = async (coin) => { return userWallet; }; -export const getUserWalletInfo = async (data) => { +export const getUserWalletInfo = async (data, isFromExtension) => { const requiredFields = ["coin"]; const missingFields: string[] = []; requiredFields.forEach((field) => { @@ -1607,14 +1584,12 @@ export const getUserWalletInfo = async (data) => { } const resPermission = await getUserPermission({ text1: "Do you give this application permission to retrieve your wallet information", - }); + }, isFromExtension); const { accepted } = resPermission; if (accepted) { let coin = data.coin; let walletKeys = await getUserWalletFunc(coin); - console.log('walletKeys', walletKeys) - console.log('walletKeys["publickey"]', walletKeys["publickey"]) const _url = await createEndpoint( `/crosschain/` + data.coin.toLowerCase() + `/addressinfos` ); @@ -2076,7 +2051,7 @@ export const getTxActivitySummary = async (data) => { } }; -export const sendCoin = async (data) => { +export const sendCoin = async (data, isFromExtension) => { const requiredFields = ['coin', 'destinationAddress', 'amount'] const missingFields: string[] = [] requiredFields.forEach((field) => { @@ -2105,9 +2080,7 @@ export const sendCoin = async (data) => { const recipient = data.destinationAddress const url = await createEndpoint(`/addresses/balance/${address}`); - console.log("url", url); const response = await fetch(url); - console.log("response", response); if (!response.ok) throw new Error("Failed to fetch"); let walletBalance; try { @@ -2141,7 +2114,7 @@ export const sendCoin = async (data) => { text1: "Do you give this application permission to send coins?", text2: `To: ${recipient}`, highlightedText: `${amount} ${checkCoin}`, - }); + }, isFromExtension); const { accepted } = resPermission; if (accepted) { @@ -2174,7 +2147,7 @@ export const sendCoin = async (data) => { text2: `To: ${recipient}`, highlightedText: `${amount} ${checkCoin}`, fee: fee - }); + }, isFromExtension); const { accepted } = resPermission; if (accepted) { @@ -2231,7 +2204,7 @@ export const sendCoin = async (data) => { text2: `To: ${recipient}`, highlightedText: `${amount} ${checkCoin}`, fee: fee - }); + }, isFromExtension); const { accepted } = resPermission; if (accepted) { @@ -2288,7 +2261,7 @@ export const sendCoin = async (data) => { text2: `To: ${recipient}`, highlightedText: `${amount} ${checkCoin}`, fee: fee - }); + }, isFromExtension); const { accepted } = resPermission; if (accepted) { @@ -2343,7 +2316,7 @@ export const sendCoin = async (data) => { text2: `To: ${recipient}`, highlightedText: `${amount} ${checkCoin}`, fee: fee - }); + }, isFromExtension); const { accepted } = resPermission; if (accepted) { @@ -2401,7 +2374,7 @@ export const sendCoin = async (data) => { text2: `To: ${recipient}`, highlightedText: `${amount} ${checkCoin}`, fee: fee - }); + }, isFromExtension); const { accepted } = resPermission; if (accepted) { @@ -2455,7 +2428,7 @@ export const sendCoin = async (data) => { text2: `To: ${recipient}`, highlightedText: `${amount} ${checkCoin}`, fee: fee - }); + }, isFromExtension); const { accepted } = resPermission; if (accepted) { diff --git a/src/useAppFullscreen.tsx b/src/useAppFullscreen.tsx new file mode 100644 index 0000000..9887a41 --- /dev/null +++ b/src/useAppFullscreen.tsx @@ -0,0 +1,65 @@ +import { useCallback, useEffect } from 'react'; + +export const useAppFullScreen = (setFullScreen) => { + const enterFullScreen = useCallback(() => { + const element = document.documentElement; // Target the entire HTML document + if (element.requestFullscreen) { + element.requestFullscreen(); + } else if (element.mozRequestFullScreen) { // Firefox + element.mozRequestFullScreen(); + } else if (element.webkitRequestFullscreen) { // Chrome, Safari and Opera + element.webkitRequestFullscreen(); + } else if (element.msRequestFullscreen) { // IE/Edge + element.msRequestFullscreen(); + } + }, []); + + const exitFullScreen = useCallback(() => { + if (document.fullscreenElement) { + document.exitFullscreen(); + } else if (document.mozFullScreenElement) { + document.mozCancelFullScreen(); + } else if (document.webkitFullscreenElement) { + document.webkitExitFullscreen(); + } else if (document.msFullscreenElement) { + document.msExitFullscreen(); + } + }, []); + + const toggleFullScreen = useCallback(() => { + if (document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement) { + exitFullScreen(); + setFullScreen(false) + } else { + enterFullScreen(); + setFullScreen(true) + } + }, [enterFullScreen, exitFullScreen]); + + // Listen for changes to fullscreen state + useEffect(() => { + const handleFullScreenChange = () => { + if (document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement) { + + } else { + setFullScreen(false); + } + }; + + document.addEventListener('fullscreenchange', handleFullScreenChange); + document.addEventListener('webkitfullscreenchange', handleFullScreenChange); // Safari + document.addEventListener('mozfullscreenchange', handleFullScreenChange); // Firefox + document.addEventListener('MSFullscreenChange', handleFullScreenChange); // IE/Edge + + return () => { + document.removeEventListener('fullscreenchange', handleFullScreenChange); + document.removeEventListener('webkitfullscreenchange', handleFullScreenChange); + document.removeEventListener('mozfullscreenchange', handleFullScreenChange); + document.removeEventListener('MSFullscreenChange', handleFullScreenChange); + }; + }, []); + + return { enterFullScreen, exitFullScreen, toggleFullScreen }; +}; + + diff --git a/src/useQortalGetSaveSettings.tsx b/src/useQortalGetSaveSettings.tsx index 6da3092..0223f12 100644 --- a/src/useQortalGetSaveSettings.tsx +++ b/src/useQortalGetSaveSettings.tsx @@ -65,7 +65,6 @@ export const useQortalGetSaveSettings = (myName) => { const {hasPublishRecord, timestamp} = await getPublishRecord(myName) if(hasPublishRecord){ const settings = await getPublish(myName) - console.log('settings', settings, timestamp, settingsLocalLastUpdated ) if(settings?.sortablePinnedApps && timestamp > settingsLocalLastUpdated){ setSortablePinnedApps(settings.sortablePinnedApps) From bec51a07bb179fe9beef13794f48a6dfcb9d852e Mon Sep 17 00:00:00 2001 From: PhilReact Date: Tue, 22 Oct 2024 20:12:54 +0300 Subject: [PATCH 36/58] finished desktop view --- public/appsBg.svg | 9 + src/App.tsx | 19 +- src/assets/svgs/AppIcon.svg | 11 + src/assets/svgs/qappLibraryText.svg | 4 + src/background.ts | 2 +- src/components/Apps/AppInfo.tsx | 56 ++- src/components/Apps/AppPublish.tsx | 12 +- src/components/Apps/AppViewer.tsx | 4 +- src/components/Apps/AppViewerContainer.tsx | 4 +- src/components/Apps/AppsCategoryDesktop.tsx | 223 +++++++++ src/components/Apps/AppsDesktop-styles.tsx | 24 + src/components/Apps/AppsDesktop.tsx | 429 +++++++++++++++++ src/components/Apps/AppsHomeDesktop.tsx | 39 ++ src/components/Apps/AppsLibraryDesktop.tsx | 411 +++++++++++++++++ src/components/Apps/AppsNavBarDesktop.tsx | 347 ++++++++++++++ src/components/Apps/SortablePinnedApps.tsx | 11 +- src/components/Desktop/DesktopFooter.tsx | 143 ++++-- src/components/Group/Group.tsx | 481 +++----------------- src/components/Save/Save.tsx | 18 +- src/index.css | 6 +- src/useAppFullscreen.tsx | 2 + 21 files changed, 1747 insertions(+), 508 deletions(-) create mode 100644 public/appsBg.svg create mode 100644 src/assets/svgs/AppIcon.svg create mode 100644 src/assets/svgs/qappLibraryText.svg create mode 100644 src/components/Apps/AppsCategoryDesktop.tsx create mode 100644 src/components/Apps/AppsDesktop-styles.tsx create mode 100644 src/components/Apps/AppsDesktop.tsx create mode 100644 src/components/Apps/AppsHomeDesktop.tsx create mode 100644 src/components/Apps/AppsLibraryDesktop.tsx create mode 100644 src/components/Apps/AppsNavBarDesktop.tsx diff --git a/public/appsBg.svg b/public/appsBg.svg new file mode 100644 index 0000000..9775d89 --- /dev/null +++ b/public/appsBg.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/App.tsx b/src/App.tsx index ac3631c..b459070 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -146,7 +146,7 @@ const defaultValues: MyContextInterface = { message: "", }, }; -export let isMobile = true; +export let isMobile = false; const isMobileDevice = () => { const userAgent = navigator.userAgent || navigator.vendor || window.opera; @@ -240,7 +240,7 @@ export const getBaseApiReact = (customApi?: string) => { // }; export const getArbitraryEndpointReact = () => { if (globalApiKey) { - return `/arbitrary/resources/search`; + return `/arbitrary/resources/searchsimple`; } else { return `/arbitrary/resources/searchsimple`; } @@ -259,6 +259,8 @@ export const getBaseApiReactSocket = (customApi?: string) => { export const isMainWindow = window?.location?.href?.includes("?main=true"); function App() { const [extState, setExtstate] = useState("not-authenticated"); + const [desktopViewMode, setDesktopViewMode] = useState('home') + const [backupjson, setBackupjson] = useState(null); const [rawWallet, setRawWallet] = useState(null); const [ltcBalanceLoading, setLtcBalanceLoading] = useState(false); @@ -1554,12 +1556,13 @@ function App() { - {/* {extState === 'group' && ( - - )} */} - + {extState === "not-authenticated" && ( <> @@ -1780,8 +1783,10 @@ function App() { isMain={isMain} isOpenDrawerProfile={isOpenDrawerProfile} setIsOpenDrawerProfile={setIsOpenDrawerProfile} + desktopViewMode={desktopViewMode} + setDesktopViewMode={setDesktopViewMode} /> - {!isMobile && renderProfile()} + {(!isMobile && desktopViewMode !== 'apps') && renderProfile()} + + + + + + + + + + diff --git a/src/assets/svgs/qappLibraryText.svg b/src/assets/svgs/qappLibraryText.svg new file mode 100644 index 0000000..297c466 --- /dev/null +++ b/src/assets/svgs/qappLibraryText.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/background.ts b/src/background.ts index 8d3fdf0..e2d7b2b 100644 --- a/src/background.ts +++ b/src/background.ts @@ -117,7 +117,7 @@ const getApiKeyFromStorage = async () => { const getArbitraryEndpoint = async () => { const apiKey = await getApiKeyFromStorage(); // Retrieve apiKey asynchronously if (apiKey) { - return `/arbitrary/resources/search`; + return `/arbitrary/resources/searchsimple`; } else { return `/arbitrary/resources/searchsimple`; } diff --git a/src/components/Apps/AppInfo.tsx b/src/components/Apps/AppInfo.tsx index 615965b..3a3c687 100644 --- a/src/components/Apps/AppInfo.tsx +++ b/src/components/Apps/AppInfo.tsx @@ -22,7 +22,7 @@ import { } from "./Apps-styles"; import { Avatar, Box, ButtonBase, InputBase } from "@mui/material"; import { Add } from "@mui/icons-material"; -import { getBaseApiReact } from "../../App"; +import { getBaseApiReact, isMobile } from "../../App"; import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import { Spacer } from "../../common/Spacer"; @@ -32,7 +32,21 @@ import { AppRating } from "./AppRating"; export const AppInfo = ({ app, myName }) => { const isInstalled = app?.status?.status === "READY"; return ( - + + + + + {!isMobile && } { - - - - - - - - - Category: - - - {app?.metadata?.categoryName || "none"} - - - - - - About this Q-App - + + + + + + + + Category: + + + {app?.metadata?.categoryName || "none"} + + + + + About this Q-App - {app?.metadata?.description || "No description"} + {app?.metadata?.description || "No description"} + ); }; diff --git a/src/components/Apps/AppPublish.tsx b/src/components/Apps/AppPublish.tsx index 117aff2..22534bd 100644 --- a/src/components/Apps/AppPublish.tsx +++ b/src/components/Apps/AppPublish.tsx @@ -39,7 +39,7 @@ import { Option as BaseOption, optionClasses } from "@mui/base/Option"; import { styled } from "@mui/system"; import UnfoldMoreRoundedIcon from "@mui/icons-material/UnfoldMoreRounded"; import { Add } from "@mui/icons-material"; -import { MyContext, getBaseApiReact } from "../../App"; +import { MyContext, getBaseApiReact, isMobile } from "../../App"; import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import { Spacer } from "../../common/Spacer"; @@ -257,8 +257,14 @@ export const AppPublish = ({ names, categories }) => { } }; return ( - - + + Create Apps! diff --git a/src/components/Apps/AppViewer.tsx b/src/components/Apps/AppViewer.tsx index ba371ae..8fa14bb 100644 --- a/src/components/Apps/AppViewer.tsx +++ b/src/components/Apps/AppViewer.tsx @@ -16,7 +16,7 @@ import { } from "./Apps-styles"; import { Avatar, Box, ButtonBase, InputBase } from "@mui/material"; import { Add } from "@mui/icons-material"; -import { MyContext, getBaseApiReact } from "../../App"; +import { MyContext, getBaseApiReact, isMobile } from "../../App"; import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import { Spacer } from "../../common/Spacer"; @@ -61,7 +61,7 @@ export const AppViewer = ({ app }) => { return ( + + ); -}; +}); diff --git a/src/components/Apps/AppViewerContainer.tsx b/src/components/Apps/AppViewerContainer.tsx index df748ed..51bc0ff 100644 --- a/src/components/Apps/AppViewerContainer.tsx +++ b/src/components/Apps/AppViewerContainer.tsx @@ -1,26 +1,24 @@ -import React, { useContext, useEffect, useRef } from 'react' -import { AppViewer } from './AppViewer' +import React, { useContext, } from 'react'; +import { AppViewer } from './AppViewer'; import Frame from 'react-frame-component'; import { MyContext, isMobile } from '../../App'; -import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events'; -const AppViewerContainer = ({app, isSelected, hide}) => { - const { rootHeight } = useContext(MyContext); - const frameRef = useRef(null); +const AppViewerContainer = React.forwardRef(({ app, isSelected, hide }, ref) => { + const { rootHeight } = useContext(MyContext); + - - return ( - - {/* Inject styles directly into the iframe */} - } style={{ - height: !isMobile ? '100vh' : `calc(${rootHeight} - 60px - 45px )`, - border: 'none', - width: '100%', - overflow: 'hidden', - display: (!isSelected || hide) && 'none' - }} > - ) -} + } + style={{ + display: (!isSelected || hide) && 'none', + height: !isMobile ? '100vh' : `calc(${rootHeight} - 60px - 45px)`, + border: 'none', + width: '100%', + overflow: 'hidden', + }} + > + + + ); +}); -export default AppViewerContainer \ No newline at end of file +export default AppViewerContainer; diff --git a/src/components/Apps/Apps.tsx b/src/components/Apps/Apps.tsx index d0163ef..b444c22 100644 --- a/src/components/Apps/Apps.tsx +++ b/src/components/Apps/Apps.tsx @@ -1,20 +1,17 @@ -import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { AppsHome } from "./AppsHome"; import { Spacer } from "../../common/Spacer"; -import { MyContext, getBaseApiReact } from "../../App"; +import { getBaseApiReact } from "../../App"; import { AppInfo } from "./AppInfo"; import { executeEvent, subscribeToEvent, unsubscribeFromEvent, } from "../../utils/events"; -import { AppsNavBar } from "./AppsNavBar"; import { AppsParent } from "./Apps-styles"; -import { AppViewer } from "./AppViewer"; import AppViewerContainer from "./AppViewerContainer"; import ShortUniqueId from "short-unique-id"; import { AppPublish } from "./AppPublish"; -import { useRecoilState } from "recoil"; import { AppsCategory } from "./AppsCategory"; import { AppsLibrary } from "./AppsLibrary"; @@ -28,6 +25,7 @@ export const Apps = ({ mode, setMode, show , myName}) => { const [selectedTab, setSelectedTab] = useState(null); const [isNewTabWindow, setIsNewTabWindow] = useState(false); const [categories, setCategories] = useState([]) + const iframeRefs = useRef({}); const myApp = useMemo(()=> { @@ -158,33 +156,26 @@ export const Apps = ({ mode, setMode, show , myName}) => { const navigateBackFunc = (e) => { - if(mode === 'category'){ - setMode("library"); - setSelectedCategory(null) - } else if (mode === "appInfo-from-category") { - setMode("category"); - } else if (mode === "appInfo") { - setMode("library"); - } else if (mode === "library") { - if (isNewTabWindow) { - setMode("viewer"); - } else { - setMode("home"); - } - } else if(mode === 'publish'){ - setMode('library') - } else { - const iframeId = `browser-iframe-${selectedTab?.tabId}`; - const iframe = document.getElementById(iframeId); - // Go Back in the iframe's history - if (iframe) { - if (iframe && iframe.contentWindow) { - const iframeWindow = iframe.contentWindow; - if (iframeWindow && iframeWindow.history) { - iframeWindow.history.back(); - } + if (['category', 'appInfo-from-category', 'appInfo', 'library', 'publish'].includes(mode)) { + // Handle the various modes as needed + if (mode === 'category') { + setMode('library'); + setSelectedCategory(null); + } else if (mode === 'appInfo-from-category') { + setMode('category'); + } else if (mode === 'appInfo') { + setMode('library'); + } else if (mode === 'library') { + if (isNewTabWindow) { + setMode('viewer'); + } else { + setMode('home'); } + } else if (mode === 'publish') { + setMode('library'); } + } else if(selectedTab?.tabId) { + executeEvent(`navigateBackApp-${selectedTab?.tabId}`, {}) } }; @@ -309,11 +300,16 @@ export const Apps = ({ mode, setMode, show , myName}) => { {mode === "publish" && !selectedTab && } {tabs.map((tab) => { + if (!iframeRefs.current[tab.tabId]) { + iframeRefs.current[tab.tabId] = React.createRef(); + } return ( ); })} diff --git a/src/components/Apps/AppsDesktop.tsx b/src/components/Apps/AppsDesktop.tsx index 64bba6b..7065782 100644 --- a/src/components/Apps/AppsDesktop.tsx +++ b/src/components/Apps/AppsDesktop.tsx @@ -8,15 +8,10 @@ import { subscribeToEvent, unsubscribeFromEvent, } from "../../utils/events"; -import { AppsNavBar } from "./AppsNavBar"; import { AppsParent } from "./Apps-styles"; -import { AppViewer } from "./AppViewer"; import AppViewerContainer from "./AppViewerContainer"; import ShortUniqueId from "short-unique-id"; import { AppPublish } from "./AppPublish"; -import { useRecoilState } from "recoil"; -import { AppsCategory } from "./AppsCategory"; -import { AppsLibrary } from "./AppsLibrary"; import { AppsLibraryDesktop } from "./AppsLibraryDesktop"; import { AppsCategoryDesktop } from "./AppsCategoryDesktop"; import { AppsNavBarDesktop } from "./AppsNavBarDesktop"; @@ -36,8 +31,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop const [selectedTab, setSelectedTab] = useState(null); const [isNewTabWindow, setIsNewTabWindow] = useState(false); const [categories, setCategories] = useState([]) - - + const iframeRefs = useRef({}); const myApp = useMemo(()=> { return availableQapps.find((app)=> app.name === myName && app.service === 'APP') @@ -164,37 +158,35 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop }; }, []); + + + + const navigateBackFunc = (e) => { - if(mode === 'category'){ - setMode("library"); - setSelectedCategory(null) - } else if (mode === "appInfo-from-category") { - setMode("category"); - } else if (mode === "appInfo") { - setMode("library"); - } else if (mode === "library") { - if (isNewTabWindow) { - setMode("viewer"); - } else { - setMode("home"); - } - } else if(mode === 'publish'){ - setMode('library') - } else { - const iframeId = `browser-iframe-${selectedTab?.tabId}`; - const iframe = document.getElementById(iframeId); - // Go Back in the iframe's history - if (iframe) { - if (iframe && iframe.contentWindow) { - const iframeWindow = iframe.contentWindow; - if (iframeWindow && iframeWindow.history) { - iframeWindow.history.back(); - } + if (['category', 'appInfo-from-category', 'appInfo', 'library', 'publish'].includes(mode)) { + // Handle the various modes as needed + if (mode === 'category') { + setMode('library'); + setSelectedCategory(null); + } else if (mode === 'appInfo-from-category') { + setMode('category'); + } else if (mode === 'appInfo') { + setMode('library'); + } else if (mode === 'library') { + if (isNewTabWindow) { + setMode('viewer'); + } else { + setMode('home'); } + } else if (mode === 'publish') { + setMode('library'); } + } else if(selectedTab?.tabId) { + executeEvent(`navigateBackApp-${selectedTab?.tabId}`, {}) } }; + useEffect(() => { subscribeToEvent("navigateBack", navigateBackFunc); @@ -217,6 +209,8 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop setIsNewTabWindow(false); }; + + useEffect(() => { subscribeToEvent("addTab", addTabFunc); @@ -224,7 +218,6 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop unsubscribeFromEvent("addTab", addTabFunc); }; }, [tabs]); - const setSelectedTabFunc = (e) => { const data = e.detail?.data; @@ -240,6 +233,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop }, 100); setIsNewTabWindow(false); }; + useEffect(() => { subscribeToEvent("setSelectedTab", setSelectedTabFunc); @@ -364,7 +358,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop {mode !== 'home' && ( - + )} @@ -398,13 +392,17 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop {mode === "appInfo-from-category" && !selectedTab && } {mode === "publish" && !selectedTab && } - {tabs.map((tab) => { + if (!iframeRefs.current[tab.tabId]) { + iframeRefs.current[tab.tabId] = React.createRef(); + } return ( ); })} diff --git a/src/components/Apps/AppsNavBar.tsx b/src/components/Apps/AppsNavBar.tsx index 064d3d3..e387a08 100644 --- a/src/components/Apps/AppsNavBar.tsx +++ b/src/components/Apps/AppsNavBar.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { AppsNavBarLeft, AppsNavBarParent, @@ -26,6 +26,7 @@ import PushPinIcon from "@mui/icons-material/PushPin"; import RefreshIcon from "@mui/icons-material/Refresh"; import { useRecoilState, useSetRecoilState } from "recoil"; import { + navigationControllerAtom, settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom, } from "../../atoms/global"; @@ -71,6 +72,13 @@ export const AppsNavBar = () => { const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState( sortablePinnedAppsAtom ); + const [navigationController, setNavigationController] = useRecoilState(navigationControllerAtom) + + const isDisableBackButton = useMemo(()=> { + if(selectedTab && navigationController[selectedTab?.tabId]?.hasBack) return false + if(selectedTab && !navigationController[selectedTab?.tabId]?.hasBack) return true + return false + }, [navigationController, selectedTab]) const setSettingsLocalLastUpdated = useSetRecoilState( settingsLocalLastUpdatedAtom @@ -103,7 +111,7 @@ export const AppsNavBar = () => { const { tabs, selectedTab, isNewTabWindow } = e.detail?.data; setTabs([...tabs]); - setSelectedTab(!selectedTab ? nulll : { ...selectedTab }); + setSelectedTab(!selectedTab ? null : { ...selectedTab }); setIsNewTabWindow(isNewTabWindow); }; @@ -123,8 +131,13 @@ export const AppsNavBar = () => { { - executeEvent("navigateBack", {}); + onClick={() => { + executeEvent("navigateBack", selectedTab?.tabId); + }} + disabled={isDisableBackButton} + sx={{ + opacity: !isDisableBackButton ? 1 : 0.1, + cursor: !isDisableBackButton ? 'pointer': 'default' }} > diff --git a/src/components/Apps/AppsNavBarDesktop.tsx b/src/components/Apps/AppsNavBarDesktop.tsx index 409013e..253cb98 100644 --- a/src/components/Apps/AppsNavBarDesktop.tsx +++ b/src/components/Apps/AppsNavBarDesktop.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { AppsNavBarLeft, AppsNavBarParent, @@ -26,6 +26,7 @@ import PushPinIcon from "@mui/icons-material/PushPin"; import RefreshIcon from "@mui/icons-material/Refresh"; import { useRecoilState, useSetRecoilState } from "recoil"; import { + navigationControllerAtom, settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom, } from "../../atoms/global"; @@ -64,6 +65,8 @@ export function saveToLocalStorage(key, subKey, newValue) { export const AppsNavBarDesktop = () => { const [tabs, setTabs] = useState([]); const [selectedTab, setSelectedTab] = useState(null); + const [navigationController, setNavigationController] = useRecoilState(navigationControllerAtom) + const [isNewTabWindow, setIsNewTabWindow] = useState(false); const tabsRef = useRef(null); const [anchorEl, setAnchorEl] = useState(null); @@ -72,6 +75,7 @@ export const AppsNavBarDesktop = () => { sortablePinnedAppsAtom ); + const setSettingsLocalLastUpdated = useSetRecoilState( settingsLocalLastUpdatedAtom ); @@ -99,11 +103,22 @@ export const AppsNavBarDesktop = () => { } }, [tabs.length]); // Dependency on the number of tabs + + + const isDisableBackButton = useMemo(()=> { + if(selectedTab && navigationController[selectedTab?.tabId]?.hasBack) return false + if(selectedTab && !navigationController[selectedTab?.tabId]?.hasBack) return true + return false + }, [navigationController, selectedTab]) + + + + const setTabsToNav = (e) => { const { tabs, selectedTab, isNewTabWindow } = e.detail?.data; setTabs([...tabs]); - setSelectedTab(!selectedTab ? nulll : { ...selectedTab }); + setSelectedTab(!selectedTab ? null : { ...selectedTab }); setIsNewTabWindow(isNewTabWindow); }; @@ -115,6 +130,8 @@ export const AppsNavBarDesktop = () => { }; }, []); + + const isSelectedAppPinned = !!sortablePinnedApps?.find( (item) => item?.name === selectedTab?.name && item?.service === selectedTab?.service @@ -138,7 +155,12 @@ export const AppsNavBarDesktop = () => { > { - executeEvent("navigateBack", {}); + executeEvent("navigateBack", selectedTab?.tabId); + }} + disabled={isDisableBackButton} + sx={{ + opacity: !isDisableBackButton ? 1 : 0.1, + cursor: !isDisableBackButton ? 'pointer': 'default' }} > diff --git a/src/components/Apps/useQortalMessageListener.tsx b/src/components/Apps/useQortalMessageListener.tsx index cc3dc2a..11e1680 100644 --- a/src/components/Apps/useQortalMessageListener.tsx +++ b/src/components/Apps/useQortalMessageListener.tsx @@ -1,5 +1,8 @@ -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import FileSaver from 'file-saver'; +import { executeEvent } from '../../utils/events'; +import { useSetRecoilState } from 'recoil'; +import { navigationControllerAtom } from '../../atoms/global'; class Semaphore { constructor(count) { this.count = count @@ -313,13 +316,54 @@ const UIQortalRequests = [ return obj; // Updated object with references to stored files } -export const useQortalMessageListener = (frameWindow) => { +export const useQortalMessageListener = (frameWindow, iframeRef, tabId) => { const [path, setPath] = useState('') + const [history, setHistory] = useState({ + customQDNHistoryPaths: [], +currentIndex: -1, +isDOMContentLoaded: false + }) + const setHasSettingsChangedAtom = useSetRecoilState(navigationControllerAtom); + + + useEffect(()=> { + if(tabId && !isNaN(history?.currentIndex)){ + setHasSettingsChangedAtom((prev)=> { + return { + ...prev, + [tabId]: { + hasBack: history?.currentIndex > 0, + } + } + }) + } + }, [history?.currentIndex, tabId]) + + + const changeCurrentIndex = useCallback((value)=> { + setHistory((prev)=> { + return { + ...prev, + currentIndex: value + } + }) + }, []) + + const resetHistory = useCallback(()=> { + setHistory({ + customQDNHistoryPaths: [], + currentIndex: -1, + isManualNavigation: true, + isDOMContentLoaded: false + }) + }, []) + useEffect(() => { const listener = async (event) => { - event.preventDefault(); // Prevent default behavior - event.stopImmediatePropagation(); // Stop other listeners from firing + console.log('eventreactt', event) + // event.preventDefault(); // Prevent default behavior + // event.stopImmediatePropagation(); // Stop other listeners from firing if (event?.data?.requestedHandler !== 'UI') return; @@ -377,6 +421,39 @@ export const useQortalMessageListener = (frameWindow) => { event?.data?.action === 'QDN_RESOURCE_DISPLAYED'){ const pathUrl = event?.data?.path != null ? (event?.data?.path.startsWith('/') ? '' : '/') + event?.data?.path : null setPath(pathUrl) + } else if(event?.data?.action === 'NAVIGATION_HISTORY'){ + if(event?.data?.payload?.isDOMContentLoaded){ + setHistory((prev)=> { + const copyPrev = {...prev} + if((copyPrev?.customQDNHistoryPaths || []).at(-1) === (event?.data?.payload?.customQDNHistoryPaths || []).at(-1)) { + console.log('customQDNHistoryPaths.length', prev?.customQDNHistoryPaths.length) + return { + ...prev, + currentIndex: prev.customQDNHistoryPaths.length - 1 === -1 ? 0 : prev.customQDNHistoryPaths.length - 1 + } + } + const copyHistory = {...prev} + const paths = [...(copyHistory?.customQDNHistoryPaths || []), ...(event?.data?.payload?.customQDNHistoryPaths || [])] + console.log('paths', paths) + return { + ...prev, + customQDNHistoryPaths: paths, + currentIndex: paths.length - 1 + } + }) + } else { + setHistory(event?.data?.payload) + + } + } else if(event?.data?.action === 'SET_TAB'){ + executeEvent("addTab", { + data: event?.data?.payload + }) + iframeRef.current.contentWindow.postMessage( + { action: 'SET_TAB_SUCCESS', requestedHandler: 'UI',payload: { + name: event?.data?.payload?.name + } }, '*' + ); } }; @@ -402,6 +479,6 @@ export const useQortalMessageListener = (frameWindow) => { } }); - return {path} + return {path, history, resetHistory, changeCurrentIndex} }; From 970d8da4eafd90fe1f263597d675c7f52b3554e5 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Sun, 27 Oct 2024 15:56:26 +0200 Subject: [PATCH 58/58] fix permission checkbox --- src/App.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index ab48b87..5c5445e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -609,7 +609,7 @@ function App() { try { await showQortalRequest(message?.payload); - if (message?.payload?.checkbox1) { + if (qortalRequestCheckbox1Ref.current) { sendResponse({ accepted: true, @@ -631,6 +631,14 @@ function App() { await showQortalRequestExtension(message?.payload); + if (qortalRequestCheckbox1Ref.current) { + + sendResponse({ + accepted: true, + checkbox1: qortalRequestCheckbox1Ref.current, + }); + return; + } sendResponse({ accepted: true }); } catch (error) { sendResponse({ accepted: false });