diff --git a/index.html b/index.html index e24c53d..2dab011 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@
- +Member ${nameOrAddress} has requested that you regenerate the group's secret key. Group: ${groupName}
` + }) + return true +} + +async function getChatHeads(){ + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key = `chatheads-${address}` + const res = await chrome.storage.local.get([key]); +if (res?.[key]) { + const parsedData = JSON.parse(res[key]) + return parsedData; +} else { + throw new Error("No Chatheads saved"); +} +} + +async function getChatHeadsDirect(){ + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key = `chatheads-direct-${address}` + const res = await chrome.storage.local.get([key]); +if (res?.[key]) { + const parsedData = JSON.parse(res[key]) + return parsedData; +} else { + throw new Error("No Chatheads saved"); +} +} chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request) { switch (request.action) { @@ -708,20 +2323,21 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { }); break; case "getWalletInfo": - - getKeyPair().then(() => { - chrome.storage.local.get(["walletInfo"], (result) => { - if (chrome.runtime.lastError) { - sendResponse({ error: chrome.runtime.lastError.message }); - } else if (result.walletInfo) { - sendResponse({ walletInfo: result.walletInfo }); - } else { - sendResponse({ error: "No wallet info found" }); - } + getKeyPair() + .then(() => { + chrome.storage.local.get(["walletInfo"], (result) => { + if (chrome.runtime.lastError) { + sendResponse({ error: chrome.runtime.lastError.message }); + } else if (result.walletInfo) { + sendResponse({ walletInfo: result.walletInfo }); + } else { + sendResponse({ error: "No wallet info found" }); + } + }); + }) + .catch((error) => { + sendResponse({ error: error.message }); }); - }).catch((error) => { - sendResponse({ error: error.message }); - }) break; case "validApi": @@ -744,6 +2360,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { case "userInfo": getUserInfo() .then((name) => { + sendResponse(name); }) .catch((error) => { @@ -751,20 +2368,23 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { console.error(error.message); }); break; - case "decryptWallet": { - const { password, wallet } = request.payload; + case "decryptWallet": + { + const { password, wallet } = request.payload; - decryptWallet({ - password, wallet, walletVersion - }) - .then((hasDecrypted) => { - sendResponse(hasDecrypted); + decryptWallet({ + password, + wallet, + walletVersion, }) - .catch((error) => { - sendResponse({ error: error?.message }); - console.error(error.message); - }); - } + .then((hasDecrypted) => { + sendResponse(hasDecrypted); + }) + .catch((error) => { + sendResponse({ error: error?.message }); + console.error(error.message); + }); + } break; case "balance": @@ -776,23 +2396,26 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { console.error(error.message); }); break; - case "ltcBalance": { - getLTCBalance() - .then((balance) => { - sendResponse(balance); - }) - .catch((error) => { - console.error(error.message); - }); - - - } - break; + case "ltcBalance": + { + getLTCBalance() + .then((balance) => { + sendResponse(balance); + }) + .catch((error) => { + console.error(error.message); + }); + } + break; case "sendCoin": { const { receiver, password, amount } = request.payload; sendCoin({ receiver, password, amount }) - .then(() => { + .then(({res}) => { + if(!res?.success){ + sendResponse({ error: res?.data?.message }); + return + } sendResponse(true); }) .catch((error) => { @@ -802,11 +2425,202 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { } break; + case "inviteToGroup": + { + const { groupId, qortalAddress, inviteTime } = request.payload; + inviteToGroup({ groupId, qortalAddress, inviteTime }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + break; + case "saveTempPublish": + { + const { data, key } = request.payload; + saveTempPublish({ data, key }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + return true + } + break; + case "getTempPublish": + { + + getTempPublish() + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + + case "createGroup": + { + const { groupName, groupDescription, groupType, groupApprovalThreshold, minBlock, maxBlock } = request.payload; + createGroup({ groupName, groupDescription, groupType, groupApprovalThreshold, minBlock, maxBlock }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "cancelInvitationToGroup": + { + const { groupId, qortalAddress } = request.payload; + cancelInvitationToGroup({ groupId, qortalAddress }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "leaveGroup": + { + const { groupId } = request.payload; + leaveGroup({ groupId }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "joinGroup": + { + const { groupId } = request.payload; + joinGroup({ groupId }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + + case "kickFromGroup": + { + const { groupId, qortalAddress, rBanReason } = request.payload; + kickFromGroup({ groupId, qortalAddress, rBanReason }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "banFromGroup": + { + const { groupId, qortalAddress, rBanReason, rBanTime } = request.payload; + banFromGroup({ groupId, qortalAddress, rBanReason, rBanTime }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "cancelBan": + { + const { groupId, qortalAddress } = request.payload; + cancelBan({ groupId, qortalAddress}) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "registerName": + { + const { name } = request.payload; + registerName({ name}) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "makeAdmin": + { + const { groupId, qortalAddress } = request.payload; + makeAdmin({ groupId, qortalAddress}) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "removeAdmin": + { + const { groupId, qortalAddress } = request.payload; + removeAdmin({ groupId, qortalAddress}) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + } + + break; + case "oauth": { - const { nodeBaseUrl, senderAddress, senderPublicKey, timestamp } = request.payload; + const { nodeBaseUrl, senderAddress, senderPublicKey, timestamp } = + request.payload; - listenForChatMessage({ nodeBaseUrl, senderAddress, senderPublicKey, timestamp }) + listenForChatMessage({ + nodeBaseUrl, + senderAddress, + senderPublicKey, + timestamp, + }) .then(({ secretCode }) => { sendResponse(secretCode); }) @@ -817,6 +2631,149 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { break; } + case "setChatHeads": { + const { data} = + request.payload; + + setChatHeads({ + data + }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + + break; + } + case "getChatHeads": { + + getChatHeads() + .then((res) => { + + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + + break; + } + case "notification": { + const notificationId = 'chat_notification_' + Date.now(); // Create a unique ID + + const { } = + request.payload; + chrome.notifications.create(notificationId, { + type: 'basic', + iconUrl: 'qort.png', // Add an appropriate icon for chat notifications + title: 'New Group Message!', + message: 'You have received a new message from one of your groups', + priority: 2, // Use the maximum priority to ensure it's noticeable + // buttons: [ + // { title: 'Go to group' } + // ] + }); + // Set a timeout to clear the notification after 'timeout' milliseconds + setTimeout(() => { + chrome.notifications.clear(notificationId); + }, 3000); + sendResponse(true) + break; + } + case "addTimestampEnterChat": { + + const { groupId, timestamp } = + request.payload; + addTimestampEnterChat({groupId, timestamp}) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + break; + } + + case "setApiKey": { + const { payload } = request; + + + // Save the apiKey in chrome.storage.local for persistence + chrome.storage.local.set({ apiKey: payload }, () => { + + sendResponse(true) + }); + return true + break; + } + case "getApiKey": { + getApiKeyFromStorage() + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + return true + break; + } + case "notifyAdminRegenerateSecretKey": { + const { groupName, adminAddress } = + request.payload; + notifyAdminRegenerateSecretKey({groupName, adminAddress}) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + break; + } + + + case "addGroupNotificationTimestamp": { + + const { groupId, timestamp } = + request.payload; + addTimestampGroupAnnouncement({groupId, timestamp, seenTimestamp: true}) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + break; + } + case "getTimestampEnterChat": { + getTimestampEnterChat() + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + break; + } + case "getGroupNotificationTimestamp": { + getTimestampGroupAnnouncement() + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + console.error(error.message); + }); + break; + } case "authentication": { getSaveWallet() @@ -824,7 +2781,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { sendResponse(true); }) .catch((error) => { - const popupUrl = chrome.runtime.getURL("index.html"); + const popupUrl = chrome.runtime.getURL("index.html?secondary=true"); chrome.windows.getAll( { populate: true, windowTypes: ["popup"] }, @@ -860,12 +2817,14 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { (primaryDisplay.bounds.height - windowHeight) / 2; chrome.windows.create({ - url: chrome.runtime.getURL("index.html"), + url: chrome.runtime.getURL("index.html?secondary=true"), type: "popup", width: windowWidth, height: windowHeight, left: leftPosition, top: 0, + } , () => { + removeDuplicateWindow(popupUrl) }); }); } @@ -913,7 +2872,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { error: "User has not authenticated, try again.", }); clearInterval(intervalId); // Stop checking due to timeout - console.log("Timeout exceeded"); + // Handle timeout situation if needed } }; @@ -924,94 +2883,14 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { }); } break; - case "buyOrder": { - const { qortalAtAddress, hostname } = request.payload; - getTradeInfo(qortalAtAddress) - .then((crosschainAtInfo) => { - const popupUrl = chrome.runtime.getURL("index.html"); - - chrome.windows.getAll( - { populate: true, windowTypes: ["popup"] }, - (windows) => { - // Attempt to find an existing popup window that has a tab with the correct URL - const existingPopup = windows.find( - (w) => - 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", - }); - } 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: chrome.runtime.getURL("index.html"), - type: "popup", - width: windowWidth, - height: windowHeight, - left: leftPosition, - top: 0, - }); - }); - } - - const interactionId = Date.now().toString(); // Simple example; consider a better unique ID - - setTimeout(() => { - chrome.runtime.sendMessage({ - action: "SET_COUNTDOWN", - payload: request.timeout ? 0.9 * request.timeout : 20, - }); - chrome.runtime.sendMessage({ - action: "UPDATE_STATE_REQUEST_BUY_ORDER", - payload: { - hostname, - crosschainAtInfo, - interactionId, - }, - }); - }, 500); - - // Store sendResponse callback with the interaction ID - pendingResponses.set(interactionId, sendResponse); - } - ); - - - }) - .catch((error) => { - console.error(error.message); - }); - } - - break; - case "connection": { - const { hostname } = request.payload; - connection(hostname) - .then((isConnected) => { - if (Object.keys(isConnected)?.length > 0 && isConnected[hostname]) { - sendResponse(true); - } else { - const popupUrl = chrome.runtime.getURL("index.html"); + case "buyOrder": + { + + const { qortalAtAddress, hostname } = request.payload; + getTradeInfo(qortalAtAddress) + .then((crosschainAtInfo) => { + + const popupUrl = chrome.runtime.getURL("index.html?secondary=true") chrome.windows.getAll( { populate: true, windowTypes: ["popup"] }, @@ -1047,12 +2926,14 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { (primaryDisplay.bounds.height - windowHeight) / 2; chrome.windows.create({ - url: chrome.runtime.getURL("index.html"), + url: chrome.runtime.getURL("index.html?secondary=true"), type: "popup", width: windowWidth, height: windowHeight, left: leftPosition, top: 0, + }, () => { + removeDuplicateWindow(popupUrl) }); }); } @@ -1065,9 +2946,10 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { payload: request.timeout ? 0.9 * request.timeout : 20, }); chrome.runtime.sendMessage({ - action: "UPDATE_STATE_REQUEST_CONNECTION", + action: "UPDATE_STATE_REQUEST_BUY_ORDER", payload: { hostname, + crosschainAtInfo, interactionId, }, }); @@ -1077,18 +2959,109 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { pendingResponses.set(interactionId, sendResponse); } ); - } - }) - .catch((error) => { - console.error(error.message); - }); - } + }) + .catch((error) => { + console.error(error.message); + }); + } + + break; + case "connection": + { + + const { hostname } = request.payload; + + connection(hostname) + .then((isConnected) => { + if ( + Object.keys(isConnected)?.length > 0 && + isConnected[hostname] + ) { + sendResponse(true); + } else { + const popupUrl = chrome.runtime.getURL("index.html?secondary=true"); + chrome.windows.getAll( + { populate: true, windowTypes: ["popup"] }, + (windows) => { + // Attempt to find an existing popup window that has a tab with the correct URL + const existingPopup = windows.find( + (w) => + 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", + }); + } 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, + }, () => { + removeDuplicateWindow(popupUrl) + }); + }); + } + + const interactionId = Date.now().toString(); // Simple example; consider a better unique ID + + setTimeout(() => { + chrome.runtime.sendMessage({ + action: "SET_COUNTDOWN", + payload: request.timeout ? 0.9 * request.timeout : 20, + }); + chrome.runtime.sendMessage({ + action: "UPDATE_STATE_REQUEST_CONNECTION", + payload: { + hostname, + interactionId, + }, + }); + }, 500); + + // Store sendResponse callback with the interaction ID + pendingResponses.set(interactionId, sendResponse); + } + ); + } + }) + .catch((error) => { + console.error(error.message); + }); + } break; case "sendQort": { const { amount, hostname, address, description } = request.payload; - const popupUrl = chrome.runtime.getURL("index.html"); + const popupUrl = chrome.runtime.getURL("index.html?secondary=true"); chrome.windows.getAll( { populate: true, windowTypes: ["popup"] }, @@ -1122,12 +3095,14 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { (primaryDisplay.bounds.height - windowHeight) / 2; chrome.windows.create({ - url: chrome.runtime.getURL("index.html"), + url: chrome.runtime.getURL("index.html?secondary=true"), type: "popup", width: windowWidth, height: windowHeight, left: leftPosition, top: 0, + }, () => { + removeDuplicateWindow(popupUrl) }); }); } @@ -1212,49 +3187,307 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { sendResponse({ error: error.message }); originalSendResponse({ error: error.message }); }); - } break; - case "buyOrderConfirmation": { - const { crosschainAtInfo, isDecline } = request.payload; - const interactionId2 = request.payload.interactionId; - // Retrieve the stored sendResponse callback - const originalSendResponse = pendingResponses.get(interactionId2); + case "buyOrderConfirmation": + { + const { crosschainAtInfo, isDecline } = request.payload; + const interactionId2 = request.payload.interactionId; + // Retrieve the stored sendResponse callback + const originalSendResponse = pendingResponses.get(interactionId2); - if (originalSendResponse) { - if (isDecline) { - originalSendResponse({ error: "User has declined" }); - sendResponse(false); - pendingResponses.delete(interactionId2); - return; - } - createBuyOrderTx({ crosschainAtInfo }) - .then((res) => { - sendResponse(true); - originalSendResponse(res); + if (originalSendResponse) { + if (isDecline) { + originalSendResponse({ error: "User has declined" }); + sendResponse(false); pendingResponses.delete(interactionId2); - }) - .catch((error) => { - console.error(error.message); - sendResponse({ error: error.message }); - // originalSendResponse({ error: error.message }); - }); - + return; + } + createBuyOrderTx({ crosschainAtInfo }) + .then((res) => { + sendResponse(true); + originalSendResponse(res); + pendingResponses.delete(interactionId2); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + // originalSendResponse({ error: error.message }); + }); + } } + + break; + case "encryptAndPublishSymmetricKeyGroupChat": { + const { groupId, previousData, previousNumber } = request.payload; + + encryptAndPublishSymmetricKeyGroupChat({ + groupId, + previousData, + previousNumber, + }) + .then(({data, numberOfMembers}) => { + sendResponse(data); + + if(!previousData){ + // first secret key of the group + sendChatGroup({ + groupId, + typeMessage: undefined, + chatReference: undefined, + messageText: PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY, + }) + .then(() => {}) + .catch((error) => { + console.error('1',error.message); + }); + return + } + sendChatNotification(data, groupId, previousData, numberOfMembers) + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "publishGroupEncryptedResource": { + const { encryptedData, identifier } = request.payload; + + publishGroupEncryptedResource({ + encryptedData, identifier + }) + .then((data) => { + sendResponse(data); + + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + return true + break; + } + case "handleActiveGroupDataFromSocket": { + const { groups, directs } = request.payload; + handleActiveGroupDataFromSocket({ + groups, directs + }) + .then((data) => { + sendResponse(true); + + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "getThreadActivity": { + checkThreads(true) + .then((data) => { + + sendResponse(data); + + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; } + + case "updateThreadActivity": { + const { threadId, qortalName, groupId, thread } = request.payload; + + updateThreadActivity({threadId, qortalName, groupId, thread}) + .then(() => { + sendResponse(true); + + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); break; + } + case "decryptGroupEncryption": { + const { data } = request.payload; + + decryptGroupEncryption({ data }) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "encryptSingle": { + const { data, secretKeyObject } = request.payload; + + encryptSingle({ data64: data, secretKeyObject: secretKeyObject }) + .then((res) => { + + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "decryptSingle": { + const { data, secretKeyObject, skipDecodeBase64 } = request.payload; + + decryptSingleFunc({ messages: data, secretKeyObject, skipDecodeBase64 }) + .then((res) => { + + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "decryptSingleForPublishes": { + const { data, secretKeyObject, skipDecodeBase64 } = request.payload; + + decryptSingleForPublishes({ messages: data, secretKeyObject, skipDecodeBase64 }) + .then((res) => { + + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + + case "decryptDirect": { + const { data, involvingAddress } = request.payload; + + decryptDirectFunc({ messages: data, involvingAddress }) + .then((res) => { + + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + + case "sendChatGroup": { + const { + groupId, + typeMessage = undefined, + chatReference = undefined, + messageText, + } = request.payload; + + sendChatGroup({ groupId, typeMessage, chatReference, messageText }) + .then((res) => { + + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "sendChatDirect": { + const { + directTo, + typeMessage = undefined, + chatReference = undefined, + messageText, + } = request.payload; + + sendChatDirect({ directTo, chatReference, messageText, typeMessage }) + .then((res) => { + + sendResponse(res); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + + break; + } + case "setupGroupWebsocket": { + + checkNewMessages() + checkThreads() + + + // if(socket){ + // if(groups){ + // console.log('hasgroups1') + // chrome.runtime.sendMessage({ + // action: "SET_GROUPS", + // payload: groups, + // }); + // } + // if(directs){ + // console.log('hasgroups1') + // chrome.runtime.sendMessage({ + // action: "SET_DIRECTS", + // payload: directs, + // }); + // } + // sendResponse(true) + // return + // } + // setTimeout(() => { + // // initWebsocketMessageGroup() + // listenForNewGroupAnnouncements() + // listenForThreadUpdates() + // }, 200); + sendResponse(true) + + break; + } + case "logout": { - chrome.storage.local.remove(["keyPair", "walletInfo"], () => { + try { + const logoutFunc = async()=> { + forceCloseWebSocket() + if(interval){ + // for announcement notification + clearInterval(interval) + } + + const wallet = await getSaveWallet(); + const address = wallet.address0; + const key1 = `tempPublish-${address}` + + chrome.storage.local.remove(["keyPair", "walletInfo", "apiKey", "active-groups-directs", key1], () => { if (chrome.runtime.lastError) { // Handle error console.error(chrome.runtime.lastError.message); } else { chrome.tabs.query({}, function (tabs) { - tabs.forEach(tab => { + tabs.forEach((tab) => { chrome.tabs.sendMessage(tab.id, { type: "LOGOUT" }); }); }); @@ -1262,6 +3495,12 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { sendResponse(true); } }); + } + logoutFunc() + } catch (error) { + + } + } break; @@ -1270,16 +3509,42 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { return true; }); +// Function to save window position and size +const saveWindowBounds = (windowId) => { + chrome.windows.get(windowId, (window) => { + const { top, left, width, height } = window; + chrome.storage.local.set({ + windowBounds: { top, left, width, height } + }, () => { + console.log('Window bounds saved:', { top, left, width, height }); + }); + }); +}; + +// Function to restore window position and size +const restoreWindowBounds = (callback) => { + chrome.storage.local.get('windowBounds', (data) => { + if (data.windowBounds) { + callback(data.windowBounds); + } else { + callback(null); // No saved bounds, use default size/position + } + }); +}; + chrome.action.onClicked.addListener((tab) => { - const popupUrl = chrome.runtime.getURL("index.html"); + const popupUrl = chrome.runtime.getURL("index.html?main=true"); chrome.windows.getAll( { populate: true, windowTypes: ["popup"] }, (windows) => { // Attempt to find an existing popup window that has a tab with the correct URL const existingPopup = windows.find( (w) => - w.tabs && - w.tabs.some((tab) => tab.url && tab.url.startsWith(popupUrl)) + { + + return w.tabs && + w.tabs.some((tab) => tab.url && tab.url.startsWith(popupUrl)); + } ); if (existingPopup) { // If the popup exists but is minimized or not focused, focus it @@ -1288,27 +3553,40 @@ chrome.action.onClicked.addListener((tab) => { state: "normal", }); } 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 + // No existing popup found, restore the saved bounds or create a new one + restoreWindowBounds((savedBounds) => { + 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 screenHeight = primaryDisplay.bounds.height; - // Calculate left position for the window to appear on the right of the screen - const leftPosition = screenWidth - windowWidth; + // Create a new window that uses the saved bounds if available + chrome.windows.create({ + url: chrome.runtime.getURL("index.html?main=true"), + type: "popup", + width: savedBounds ? savedBounds.width : screenWidth, + height: savedBounds ? savedBounds.height : screenHeight, + left: savedBounds ? savedBounds.left : 0, + top: savedBounds ? savedBounds.top : 0, + }, (newWindow) => { + - // Calculate top position for the window, adjust as desired - const topPosition = (primaryDisplay.bounds.height - windowHeight) / 2; + // Listen for changes in the window's size or position and save them + chrome.windows.onBoundsChanged.addListener((window) => { + if (window.id === newWindow.id) { + saveWindowBounds(newWindow.id); + } + }); - chrome.windows.create({ - url: chrome.runtime.getURL("index.html"), - type: "popup", - width: windowWidth, - height: windowHeight, - left: leftPosition, - top: 0, + // Save the final window bounds when the window is closed + chrome.windows.onRemoved.addListener((windowId) => { + if (windowId === newWindow.id) { + + saveWindowBounds(windowId); // Save the position/size before it’s closed + } + }); + }); }); }); } @@ -1317,11 +3595,8 @@ chrome.action.onClicked.addListener((tab) => { setTimeout(() => { chrome.runtime.sendMessage({ - action: "UPDATE_STATE_REQUEST_CONNECTION", - payload: { - hostname, - interactionId, - }, + action: "INITIATE_MAIN", + payload: {}, }); }, 500); @@ -1330,3 +3605,240 @@ chrome.action.onClicked.addListener((tab) => { } ); }); + +const checkGroupList = async() => { + try { + + const wallet = await getSaveWallet(); + const address = wallet.address0; + const url = await createEndpoint(`/chat/active/${address}`); + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const data = await response.json(); + + const filteredGroups = data.groups?.filter(item => item?.groupId !== 0) || []; + const sortedGroups = filteredGroups.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); + const sortedDirects = (data?.direct || []).filter(item => + item?.name !== 'extension-proxy' && item?.address !== 'QSMMGSgysEuqDCuLw3S4cHrQkBrh3vP3VH' + ).sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); + + handleActiveGroupDataFromSocket({ + groups: sortedGroups, + directs: sortedDirects + }) + } catch (error) { + console.error(error) + } finally { + } +} + +const checkActiveChatsForNotifications = async ()=> { + try { + + + const popupUrl = chrome.runtime.getURL("index.html?main=true"); + + chrome.windows.getAll( + { populate: true, windowTypes: ["popup"] }, + (windows) => { + // Attempt to find an existing popup window that has a tab with the correct URL + const existingPopup = windows.find( + (w) => { + + return w.tabs && w.tabs.some((tab) => tab.url && tab.url.startsWith(popupUrl)); + } + ); + + if (existingPopup) { + + } else { + checkGroupList() + + } + + + } + ); + } catch (error) { + + } +} +chrome.notifications.onClicked.addListener( (notificationId) => { + + const popupUrl = chrome.runtime.getURL("index.html?main=true"); + const isDirect = notificationId.includes('_type=direct_'); + const isGroup = notificationId.includes('_type=group_'); + const isGroupAnnouncement = notificationId.includes('_type=group-announcement_'); + const isNewThreadPost = notificationId.includes('_type=thread-post_'); + + let isExisting = false + chrome.windows.getAll( + { populate: true, windowTypes: ["popup"] }, + async (windows) => { + // Attempt to find an existing popup window that has a tab with the correct URL + const existingPopup = windows.find( + (w) => { + + return w.tabs && w.tabs.some((tab) => tab.url && tab.url.startsWith(popupUrl)); + } + ); + + if (existingPopup) { + // If the popup exists but is minimized or not focused, focus it + chrome.windows.update(existingPopup.id, { + focused: true, + state: "normal", + }); + isExisting = true + } else { + // No existing popup found, restore saved bounds or create a new one + restoreWindowBounds((savedBounds) => { + 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 screenHeight = primaryDisplay.bounds.height; + + // Create a new window that takes up the full screen or uses saved bounds + chrome.windows.create({ + url: chrome.runtime.getURL("index.html?main=true"), + type: "popup", + width: savedBounds ? savedBounds.width : screenWidth, + height: savedBounds ? savedBounds.height : screenHeight, + left: savedBounds ? savedBounds.left : 0, + top: savedBounds ? savedBounds.top : 0, + }, (newWindow) => { + + + // Listen for changes in the window's size or position and save them + chrome.windows.onBoundsChanged.addListener((window) => { + if (window.id === newWindow.id) { + saveWindowBounds(newWindow.id); + } + }); + + // Save the final window bounds when the window is closed + chrome.windows.onRemoved.addListener((windowId) => { + if (windowId === newWindow.id) { + + saveWindowBounds(windowId); // Save the position/size before it’s closed + } + }); + }); + }); + }); + } + const activeData = await getStoredData('active-groups-directs') || { groups: [], directs: [] }; + setTimeout(() => { + + + chrome.runtime.sendMessage({ + action: "SET_GROUPS", + payload: activeData?.groups || [], + }); + chrome.runtime.sendMessage({ + action: "SET_DIRECTS", + payload: activeData?.directs || [], + }); + }, isExisting ? 100 : 1000); + const interactionId = Date.now().toString(); // Simple example; consider a better unique ID + + setTimeout(() => { + chrome.runtime.sendMessage({ + action: "INITIATE_MAIN", + payload: {}, + }); + + // Handle different types of notifications + if (isDirect) { + const fromValue = notificationId.split('_from=')[1]; + chrome.runtime.sendMessage({ + action: "NOTIFICATION_OPEN_DIRECT", + payload: { from: fromValue }, + }); + } else if (isGroup) { + const fromValue = notificationId.split('_from=')[1]; + chrome.runtime.sendMessage({ + action: "NOTIFICATION_OPEN_GROUP", + payload: { from: fromValue }, + }); + } else if (isGroupAnnouncement) { + const fromValue = notificationId.split('_from=')[1]; + chrome.runtime.sendMessage({ + action: "NOTIFICATION_OPEN_ANNOUNCEMENT_GROUP", + payload: { from: fromValue }, + }); + } else if (isNewThreadPost) { + const dataValue = notificationId.split('_data=')[1]; + const dataParsed = JSON.parse(dataValue); + + chrome.runtime.sendMessage({ + action: "NOTIFICATION_OPEN_THREAD_NEW_POST", + payload: { data: dataParsed }, + }); + } + }, isExisting ? 400 : 3000); + + // Store sendResponse callback with the interaction ID + pendingResponses.set(interactionId, sendResponse); + } + ); +}); + +// Reconnect when service worker wakes up +chrome.runtime.onStartup.addListener(() => { + console.log("Service worker started up, reconnecting WebSocket..."); + // initWebsocketMessageGroup(); + // listenForNewGroupAnnouncements() + // listenForThreadUpdates() +}); + +chrome.runtime.onInstalled.addListener((details) => { + if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) { + console.log('Extension Installed'); + // Perform tasks that should only happen on extension installation + // Example: Initialize WebSocket, set default settings, etc. + } else if (details.reason === chrome.runtime.OnInstalledReason.UPDATE) { + console.log('Extension Updated'); + // Handle the update logic here (e.g., migrate settings) + } else if (details.reason === chrome.runtime.OnInstalledReason.CHROME_UPDATE) { + console.log('Chrome updated'); + // Optional: Handle Chrome-specific updates if necessary + } + + // Initialize WebSocket and other required listeners + // initWebsocketMessageGroup(); + // listenForNewGroupAnnouncements(); + // listenForThreadUpdates(); +}); + +// Check if the alarm already exists before creating it +chrome.alarms.get("checkForNotifications", (existingAlarm) => { + if (!existingAlarm) { + // If the alarm does not exist, create it + chrome.alarms.create("checkForNotifications", { periodInMinutes: 4 }); + } +}); + +chrome.alarms.onAlarm.addListener(async (alarm) => { + try { + + if (alarm.name === "checkForNotifications") { + // initWebsocketMessageGroup(address); + const wallet = await getSaveWallet(); + const address = wallet.address0; + if(!address) return + checkActiveChatsForNotifications() + checkNewMessages() + checkThreads() + + } + } catch (error) { + + } +}); + diff --git a/src/backgroundFunctions/encryption.ts b/src/backgroundFunctions/encryption.ts new file mode 100644 index 0000000..d641644 --- /dev/null +++ b/src/backgroundFunctions/encryption.ts @@ -0,0 +1,208 @@ +import { getBaseApi } from "../background"; +import { createSymmetricKeyAndNonce, decryptGroupData, encryptDataGroup, objectToBase64 } from "../qdn/encryption/group-encryption"; +import { publishData } from "../qdn/publish/pubish"; + +const apiEndpoints = [ + "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", +]; + +async function findUsableApi() { + for (const endpoint of apiEndpoints) { + try { + const response = await fetch(`${endpoint}/admin/status`); + if (!response.ok) throw new Error("Failed to fetch"); + + const data = await response.json(); + if (data.isSynchronizing === false && data.syncPercent === 100) { + console.log(`Usable API found: ${endpoint}`); + return endpoint; + } else { + console.log(`API not ready: ${endpoint}`); + } + } catch (error) { + console.error(`Error checking API ${endpoint}:`, error); + } + } + + throw new Error("No usable API found"); + } + + +async function getSaveWallet() { + const res = await chrome.storage.local.get(["walletInfo"]); + if (res?.walletInfo) { + return res.walletInfo; + } else { + throw new Error("No wallet saved"); + } + } +async function getNameInfo() { + const wallet = await getSaveWallet(); + const address = wallet.address0; + const validApi = await getBaseApi() + const response = await fetch(validApi + "/names/address/" + address); + const nameData = await response.json(); + if (nameData?.length > 0) { + return nameData[0].name; + } else { + return ""; + } + } +async function getKeyPair() { + const res = await chrome.storage.local.get(["keyPair"]); + if (res?.keyPair) { + return res.keyPair; + } else { + throw new Error("Wallet not authenticated"); + } + } +const getPublicKeys = async (groupNumber: number) => { + const validApi = await getBaseApi() + const response = await fetch(`${validApi}/groups/members/${groupNumber}?limit=0`); + const groupData = await response.json(); + + let members: any = []; + if (groupData && Array.isArray(groupData?.members)) { + for (const member of groupData.members) { + if (member.member) { + const resAddress = await fetch(`${validApi}/addresses/${member.member}`); + const resData = await resAddress.json(); + const publicKey = resData.publicKey; + members.push(publicKey) + } + } + } + + return members + } + + + +export const encryptAndPublishSymmetricKeyGroupChat = async ({groupId, previousData}: { + groupId: number, + previousData: Object, +}) => { + try { + + let highestKey = 0 + if(previousData){ + highestKey = Math.max(...Object.keys((previousData || {})).filter(item=> !isNaN(+item)).map(Number)); + + } + + const resKeyPair = await getKeyPair() + const parsedData = JSON.parse(resKeyPair) + const privateKey = parsedData.privateKey + const userPublicKey = parsedData.publicKey + const groupmemberPublicKeys = await getPublicKeys(groupId) + const symmetricKey = createSymmetricKeyAndNonce() + const nextNumber = highestKey + 1 + const objectToSave = { + ...previousData, + [nextNumber]: symmetricKey + } + + const symmetricKeyAndNonceBase64 = await objectToBase64(objectToSave) + + const encryptedData = encryptDataGroup({ + data64: symmetricKeyAndNonceBase64, + publicKeys: groupmemberPublicKeys, + privateKey, + userPublicKey + }) + if(encryptedData){ + const registeredName = await getNameInfo() + const data = await publishData({ + registeredName, file: encryptedData, service: 'DOCUMENT_PRIVATE', identifier: `symmetric-qchat-group-${groupId}`, uploadType: 'file', isBase64: true, withFee: true + }) + return { + data, + numberOfMembers: groupmemberPublicKeys.length + } + + } else { + throw new Error('Cannot encrypt content') + } + } catch (error: any) { + throw new Error(error.message); + } +} +export const publishGroupEncryptedResource = async ({encryptedData, identifier}) => { + try { + + if(encryptedData && identifier){ + const registeredName = await getNameInfo() + if(!registeredName) throw new Error('You need a name to publish') + const data = await publishData({ + registeredName, file: encryptedData, service: 'DOCUMENT', identifier, uploadType: 'file', isBase64: true, withFee: true + }) + return data + + } else { + throw new Error('Cannot encrypt content') + } + } catch (error: any) { + throw new Error(error.message); + } +} + +export function uint8ArrayToBase64(uint8Array: any) { + const length = uint8Array.length + let binaryString = '' + const chunkSize = 1024 * 1024; // Process 1MB at a time + for (let i = 0; i < length; i += chunkSize) { + const chunkEnd = Math.min(i + chunkSize, length) + const chunk = uint8Array.subarray(i, chunkEnd) + + // @ts-ignore + binaryString += Array.from(chunk, byte => String.fromCharCode(byte)).join('') + } + return btoa(binaryString) +} + +export function base64ToUint8Array(base64: string) { + const binaryString = atob(base64) + const len = binaryString.length + const bytes = new Uint8Array(len) + + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i) + } + + return bytes + } + +export const decryptGroupEncryption = async ({data}: { + data: string +}) => { + try { + const resKeyPair = await getKeyPair() + const parsedData = JSON.parse(resKeyPair) + const privateKey = parsedData.privateKey + const encryptedData = decryptGroupData( + data, + privateKey, + ) + return { + data: uint8ArrayToBase64(encryptedData.decryptedData), + count: encryptedData.count + } + } catch (error: any) { + throw new Error(error.message); + } +} + +export function uint8ArrayToObject(uint8Array: any) { + // Decode the byte array using TextDecoder + const decoder = new TextDecoder() + const jsonString = decoder.decode(uint8Array) + // Convert the JSON string back into an object + return JSON.parse(jsonString) +} \ No newline at end of file diff --git a/src/common/CustomLoader.tsx b/src/common/CustomLoader.tsx new file mode 100644 index 0000000..5998f4b --- /dev/null +++ b/src/common/CustomLoader.tsx @@ -0,0 +1,7 @@ +import React from 'react' +import './customloader.css' +export const CustomLoader = () => { + return ( +Groups list
+{children}+ case 'heading-2': + return ( +
+ {children}
+
+ )
+ case 'code-line':
+ return + {children} +
+ ) + } +} + + +export const renderLeaf = ({ attributes, children, leaf }: RenderLeafProps) => { + let el = children + + if (leaf.bold) { + el = {el} + } + + if (leaf.italic) { + el = {el} + } + + if (leaf.underline) { + el = {el} + } + + if (leaf.link) { + el = ( + + {el} + + ) + } + + return {el} +} + +interface ReadOnlySlateProps { + content: any + mode?: string +} +const ReadOnlySlate: React.FCBan list
+Invitees list
+Join request list
+Member list
+Invite list
+---------- Forwarded message ---------
From: Alex
Date: Mon, Jun 9 2014 9:32 PM
Subject: Batteries
To: Jessica
From:.*?<\/p>/, `
From: ${newFrom}
`); +// htmlString = htmlString.replace(/Date:.*?<\/p>/, `
Date: ${formattedDate}
`); +// htmlString = htmlString.replace(/To:.*?<\/p>/, `
To: ${newTo}
`); + +// return htmlString; +// } + +const originalHtml = `---------- Forwarded message ---------
From: Alex
Subject: Batteries
To: Jessica
From:.*?<\/p>/, `
From: ${newFrom}
`); + htmlString = htmlString.replace(/Subject:.*?<\/p>/, `
Subject: ${newSubject}
`); + htmlString = htmlString.replace(/To:.*?<\/p>/, `
To: ${newTo}
`); + + return htmlString; +} \ No newline at end of file diff --git a/src/utils/qortalLink/index.ts b/src/utils/qortalLink/index.ts new file mode 100644 index 0000000..bc2f132 --- /dev/null +++ b/src/utils/qortalLink/index.ts @@ -0,0 +1,12 @@ +export function convertQortalLinks(inputHtml: string) { + // Regular expression to match 'qortal://...' URLs. + // This will stop at the first whitespace, comma, or HTML tag + var regex = /(qortal:\/\/[^\s,<]+)/g; + + // Replace matches in inputHtml with formatted anchor tag + var outputHtml = inputHtml.replace(regex, function (match) { + return `${match}`; + }); + + return outputHtml; +} \ No newline at end of file diff --git a/src/utils/queue/queue.ts b/src/utils/queue/queue.ts new file mode 100644 index 0000000..fcbd544 --- /dev/null +++ b/src/utils/queue/queue.ts @@ -0,0 +1,56 @@ +export class RequestQueueWithPromise { + constructor(maxConcurrent = 5) { + this.queue = []; + this.maxConcurrent = maxConcurrent; + this.currentlyProcessing = 0; + this.isPaused = false; // Flag to track whether the queue is paused + } + + // Add a request to the queue and return a promise + enqueue(request) { + return new Promise((resolve, reject) => { + // Push the request and its resolve and reject callbacks to the queue + this.queue.push({ request, resolve, reject }); + this.process(); + }); + } + + // Process requests in the queue + async process() { + // Process requests only if the queue is not paused + if (this.isPaused) return; + + while (this.queue.length > 0 && this.currentlyProcessing < this.maxConcurrent) { + this.currentlyProcessing++; + + const { request, resolve, reject } = this.queue.shift(); + + try { + const response = await request(); + resolve(response); + } catch (error) { + reject(error); + } finally { + this.currentlyProcessing--; + await this.process(); + } + } + } + + // Pause the queue processing + pause() { + this.isPaused = true; + } + + // Resume the queue processing + resume() { + this.isPaused = false; + this.process(); // Continue processing when resumed + } + + // Clear pending requests in the queue + clear() { + this.queue.length = 0; + } + } + \ No newline at end of file diff --git a/src/utils/time.ts b/src/utils/time.ts new file mode 100644 index 0000000..77a0daf --- /dev/null +++ b/src/utils/time.ts @@ -0,0 +1,38 @@ +import moment from "moment" + +export function formatTimestamp(timestamp: number): string { + const now = moment() + const timestampMoment = moment(timestamp) + const elapsedTime = now.diff(timestampMoment, 'minutes') + + if (elapsedTime < 1) { + return 'Just now' + } else if (elapsedTime < 60) { + return `${elapsedTime}m ago` + } else if (elapsedTime < 1440) { + return `${Math.floor(elapsedTime / 60)}h ago` + } else { + return timestampMoment.format('MMM D') + } + } + export function formatTimestampForum(timestamp: number): string { + const now = moment(); + const timestampMoment = moment(timestamp); + const elapsedTime = now.diff(timestampMoment, 'minutes'); + + if (elapsedTime < 1) { + return `Just now - ${timestampMoment.format('h:mm A')}`; + } else if (elapsedTime < 60) { + return `${elapsedTime}m ago - ${timestampMoment.format('h:mm A')}`; + } else if (elapsedTime < 1440) { + return `${Math.floor(elapsedTime / 60)}h ago - ${timestampMoment.format('h:mm A')}`; + } else { + return timestampMoment.format('MMM D, YYYY - h:mm A'); + } +} + + export const formatDate = (unixTimestamp: number): string => { + const date = moment(unixTimestamp, 'x').fromNow() + + return date + } \ No newline at end of file