diff --git a/qortal-ui-core/language/us.json b/qortal-ui-core/language/us.json index 008bee19..30320079 100644 --- a/qortal-ui-core/language/us.json +++ b/qortal-ui-core/language/us.json @@ -493,7 +493,12 @@ "bchange13": "Error occurred when trying to block this registered name. Please try again!", "bchange14": "Error occurred when trying to unblock this registered name. Please try again!", "bchange15": "Can't delete data from followed names. Please unfollow first.", - "bchange16": "Error occurred when trying to delete this resource. Please try again!" + "bchange16": "Error occurred when trying to delete this resource. Please try again!", + "bchange17": "User declined to share account details", + "bchange18": "Do you give this application permission to get your user address?", + "bchange19": "Do you give this application permission to publish to QDN?", + "bchange20": "Do you give this application permission to get your wallet balance?", + "bchange21": "Fetch Wallet Failed. Please try again!" }, "datapage": { "dchange1": "Data Management", diff --git a/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js b/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js index 7142e714..2f58042b 100644 --- a/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js +++ b/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js @@ -1,42 +1,46 @@ -import { LitElement, html, css } from 'lit' -import { render } from 'lit/html.js' -import { Epml } from '../../../../epml' -import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate' +import { LitElement, html, css } from 'lit'; +import { render } from 'lit/html.js'; +import { Epml } from '../../../../epml'; +import { + use, + get, + translate, + translateUnsafeHTML, + registerTranslateConfig, +} from 'lit-translate'; import * as actions from '../../components/qdn-action-types'; registerTranslateConfig({ - loader: lang => fetch(`/language/${lang}.json`).then(res => res.json()) -}) + loader: (lang) => fetch(`/language/${lang}.json`).then((res) => res.json()), +}); -import '@material/mwc-button' -import '@material/mwc-icon' +import '@material/mwc-button'; +import '@material/mwc-icon'; import WebWorker from 'web-worker:./computePowWorkerFile.src.js'; -import {publishData} from '../../../utils/publish-image.js'; +import { publishData } from '../../../utils/publish-image.js'; import { Loader } from '../../../utils/loader.js'; -const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }) - - +const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }); class WebBrowser extends LitElement { - static get properties() { - return { - url: { type: String }, - name: { type: String }, - service: { type: String }, - identifier: { type: String }, - path: { type: String }, - displayUrl: {type: String }, - followedNames: { type: Array }, - blockedNames: { type: Array }, - theme: { type: String, reflect: true } - } - } - - static get observers() { - return ['_kmxKeyUp(amount)'] - } - - static get styles() { - return css` + static get properties() { + return { + url: { type: String }, + name: { type: String }, + service: { type: String }, + identifier: { type: String }, + path: { type: String }, + displayUrl: { type: String }, + followedNames: { type: Array }, + blockedNames: { type: Array }, + theme: { type: String, reflect: true }, + }; + } + + static get observers() { + return ['_kmxKeyUp(amount)']; + } + + static get styles() { + return css` * { --mdc-theme-primary: rgb(3, 169, 244); --mdc-theme-secondary: var(--mdc-theme-primary); @@ -82,7 +86,7 @@ class WebBrowser extends LitElement { background-color: var(--white); } - input[type=text] { + input[type='text'] { margin: 0; padding: 2px 0 0 20px; border: 0; @@ -98,697 +102,945 @@ class WebBrowser extends LitElement { .float-right { float: right; } - - ` - } - - constructor() { - super() - this.url = 'about:blank' - - const urlParams = new URLSearchParams(window.location.search); - this.name = urlParams.get('name'); - this.service = urlParams.get('service'); - this.identifier = urlParams.get('identifier') != null ? urlParams.get('identifier') : null; - this.path = urlParams.get('path') != null ? ((urlParams.get('path').startsWith("/") ? "" : "/") + urlParams.get('path')) : ""; - this.followedNames = [] - this.blockedNames = [] - this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light' - this.loader = new Loader(); - // Build initial display URL - let displayUrl = "qortal://" + this.service + "/" + this.name; - if (this.identifier != null && data.identifier != "" && this.identifier != "default") displayUrl = displayUrl.concat("/" + this.identifier); - if (this.path != null && this.path != "/") displayUrl = displayUrl.concat(this.path); - this.displayUrl = displayUrl; - - const getFollowedNames = async () => { - - let followedNames = await parentEpml.request('apiCall', { - url: `/lists/followedNames?apiKey=${this.getApiKey()}` - }) - - this.followedNames = followedNames - setTimeout(getFollowedNames, this.config.user.nodeSettings.pingInterval) - } - - const getBlockedNames = async () => { - - let blockedNames = await parentEpml.request('apiCall', { - url: `/lists/blockedNames?apiKey=${this.getApiKey()}` - }) - - this.blockedNames = blockedNames - setTimeout(getBlockedNames, this.config.user.nodeSettings.pingInterval) - } - - const render = () => { - const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] - const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port - this.url = `${nodeUrl}/render/${this.service}/${this.name}${this.path != null ? this.path : ""}?theme=${this.theme}&identifier=${this.identifier != null ? this.identifier : ""}`; - } - - const authorizeAndRender = () => { - parentEpml.request('apiCall', { - url: `/render/authorize/${this.name}?apiKey=${this.getApiKey()}`, - method: "POST" - }).then(res => { - if (res.error) { - // Authorization problem - API key incorrect? - } - else { - render() - } - }) - } - - let configLoaded = false - - parentEpml.ready().then(() => { - parentEpml.subscribe('selected_address', async selectedAddress => { - this.selectedAddress = {} - selectedAddress = JSON.parse(selectedAddress) - if (!selectedAddress || Object.entries(selectedAddress).length === 0) return - this.selectedAddress = selectedAddress - }) - parentEpml.subscribe('config', c => { - this.config = JSON.parse(c) - if (!configLoaded) { - authorizeAndRender() - setTimeout(getFollowedNames, 1) - setTimeout(getBlockedNames, 1) - configLoaded = true - } - }) - parentEpml.subscribe('copy_menu_switch', async value => { - - if (value === 'false' && window.getSelection().toString().length !== 0) { - - this.clearSelection() - } - }) - }) - } - - render() { - return html` -
-
-
- this.goBack()} title="${translate("general.back")}" class="address-bar-button">arrow_back_ios - this.goForward()} title="${translate("browserpage.bchange1")}" class="address-bar-button">arrow_forward_ios - this.refresh()} title="${translate("browserpage.bchange2")}" class="address-bar-button">refresh - this.goBackToList()} title="${translate("browserpage.bchange3")}" class="address-bar-button">home - - this.delete()} title="${translate("browserpage.bchange4")} ${this.service} ${this.name} ${translate("browserpage.bchange5")}" class="address-bar-button float-right">delete - ${this.renderBlockUnblockButton()} - ${this.renderFollowUnfollowButton()} -
-
- -
-
-
- ` - } - - firstUpdated() { - - this.changeTheme() - this.changeLanguage() - - window.addEventListener('contextmenu', (event) => { - event.preventDefault() - this._textMenu(event) - }) - - window.addEventListener('click', () => { - parentEpml.request('closeCopyTextMenu', null) - }) - - window.addEventListener('storage', () => { - const checkLanguage = localStorage.getItem('qortalLanguage') - const checkTheme = localStorage.getItem('qortalTheme') - - use(checkLanguage) - - if (checkTheme === 'dark') { - this.theme = 'dark' - } else { - this.theme = 'light' - } - document.querySelector('html').setAttribute('theme', this.theme) - }) + `; + } + + constructor() { + super(); + this.url = 'about:blank'; + + const urlParams = new URLSearchParams(window.location.search); + this.name = urlParams.get('name'); + this.service = urlParams.get('service'); + this.identifier = + urlParams.get('identifier') != null + ? urlParams.get('identifier') + : null; + this.path = + urlParams.get('path') != null + ? (urlParams.get('path').startsWith('/') ? '' : '/') + + urlParams.get('path') + : ''; + this.followedNames = []; + this.blockedNames = []; + this.theme = localStorage.getItem('qortalTheme') + ? localStorage.getItem('qortalTheme') + : 'light'; + this.loader = new Loader(); + // Build initial display URL + let displayUrl = 'qortal://' + this.service + '/' + this.name; + if ( + this.identifier != null && + data.identifier != '' && + this.identifier != 'default' + ) + displayUrl = displayUrl.concat('/' + this.identifier); + if (this.path != null && this.path != '/') + displayUrl = displayUrl.concat(this.path); + this.displayUrl = displayUrl; + + const getFollowedNames = async () => { + let followedNames = await parentEpml.request('apiCall', { + url: `/lists/followedNames?apiKey=${this.getApiKey()}`, + }); + + this.followedNames = followedNames; + setTimeout( + getFollowedNames, + this.config.user.nodeSettings.pingInterval + ); + }; + + const getBlockedNames = async () => { + let blockedNames = await parentEpml.request('apiCall', { + url: `/lists/blockedNames?apiKey=${this.getApiKey()}`, + }); + + this.blockedNames = blockedNames; + setTimeout( + getBlockedNames, + this.config.user.nodeSettings.pingInterval + ); + }; + + const render = () => { + const myNode = + window.parent.reduxStore.getState().app.nodeConfig.knownNodes[ + window.parent.reduxStore.getState().app.nodeConfig.node + ]; + const nodeUrl = + myNode.protocol + '://' + myNode.domain + ':' + myNode.port; + this.url = `${nodeUrl}/render/${this.service}/${this.name}${ + this.path != null ? this.path : '' + }?theme=${this.theme}&identifier=${ + this.identifier != null ? this.identifier : '' + }`; + }; + + const authorizeAndRender = () => { + parentEpml + .request('apiCall', { + url: `/render/authorize/${ + this.name + }?apiKey=${this.getApiKey()}`, + method: 'POST', + }) + .then((res) => { + if (res.error) { + // Authorization problem - API key incorrect? + } else { + render(); + } + }); + }; + + let configLoaded = false; + + parentEpml.ready().then(() => { + parentEpml.subscribe( + 'selected_address', + async (selectedAddress) => { + this.selectedAddress = {}; + selectedAddress = JSON.parse(selectedAddress); + if ( + !selectedAddress || + Object.entries(selectedAddress).length === 0 + ) + return; + this.selectedAddress = selectedAddress; + } + ); + parentEpml.subscribe('config', (c) => { + this.config = JSON.parse(c); + if (!configLoaded) { + authorizeAndRender(); + setTimeout(getFollowedNames, 1); + setTimeout(getBlockedNames, 1); + configLoaded = true; + } + }); + parentEpml.subscribe('copy_menu_switch', async (value) => { + if ( + value === 'false' && + window.getSelection().toString().length !== 0 + ) { + this.clearSelection(); + } + }); + }); + } + + render() { + console.log(2, 'browser page here'); + return html` +
+
+
+ this.goBack()} title="${translate( + 'general.back' + )}" class="address-bar-button">arrow_back_ios + this.goForward()} title="${translate( + 'browserpage.bchange1' + )}" class="address-bar-button">arrow_forward_ios + this.refresh()} title="${translate( + 'browserpage.bchange2' + )}" class="address-bar-button">refresh + this.goBackToList()} title="${translate( + 'browserpage.bchange3' + )}" class="address-bar-button">home + + this.delete()} title="${translate( + 'browserpage.bchange4' + )} ${this.service} ${this.name} ${translate( + 'browserpage.bchange5' + )}" class="address-bar-button float-right">delete + ${this.renderBlockUnblockButton()} + ${this.renderFollowUnfollowButton()} +
+
+ +
+
+
+ `; + } + + firstUpdated() { + this.changeTheme(); + this.changeLanguage(); + + window.addEventListener('contextmenu', (event) => { + event.preventDefault(); + this._textMenu(event); + }); + + window.addEventListener('click', () => { + parentEpml.request('closeCopyTextMenu', null); + }); + + window.addEventListener('storage', () => { + const checkLanguage = localStorage.getItem('qortalLanguage'); + const checkTheme = localStorage.getItem('qortalTheme'); + + use(checkLanguage); + + if (checkTheme === 'dark') { + this.theme = 'dark'; + } else { + this.theme = 'light'; + } + document.querySelector('html').setAttribute('theme', this.theme); + }); - window.onkeyup = (e) => { - if (e.keyCode === 27) { - parentEpml.request('closeCopyTextMenu', null) - } - } + window.onkeyup = (e) => { + if (e.keyCode === 27) { + parentEpml.request('closeCopyTextMenu', null); + } + }; + + window.addEventListener('message', async (event) => { + if ( + event == null || + event.data == null || + event.data.length == 0 || + event.data.action == null + ) { + return; + } - window.addEventListener("message", async (event) => { - if (event == null || event.data == null || event.data.length == 0 || event.data.action == null) { - return; - } + let response = '{"error": "Request could not be fulfilled"}'; + let data = event.data; + console.log('UI received event: ' + JSON.stringify(data)); + + switch (data.action) { + case 'GET_USER_ACCOUNT': + case actions.GET_USER_ACCOUNT: + const res1 = await showModalAndWait( + actions.GET_USER_ACCOUNT + ); + if (res1.action === 'accept') { + let account = {}; + account['address'] = this.selectedAddress.address; + account['publicKey'] = + this.selectedAddress.base58PublicKey; + response = JSON.stringify(account); + break; + } else { + const data = {}; + const errorMsg = get('browserpage.bchange17'); + data['error'] = errorMsg; + response = JSON.stringify(data); + return; + } + case 'LINK_TO_QDN_RESOURCE': + case actions.QDN_RESOURCE_DISPLAYED: + // Links are handled by the core, but the UI also listens for these actions in order to update the address bar. + // Note: don't update this.url here, as we don't want to force reload the iframe each time. + let url = 'qortal://' + data.service + '/' + data.name; + this.path = + data.path != null + ? (data.path.startsWith('/') ? '' : '/') + data.path + : null; + if ( + data.identifier != null && + data.identifier != '' && + data.identifier != 'default' + ) + url = url.concat('/' + data.identifier); + if (this.path != null && this.path != '/') + url = url.concat(this.path); + this.name = data.name; + this.service = data.service; + this.identifier = data.identifier; + this.displayUrl = url; + return; + + case actions.PUBLISH_QDN_RESOURCE: + // Use "default" if user hasn't specified an identifer + const service = data.service; + const name = data.name; + let identifier = data.identifier; + const data64 = data.data64; + + if (!service || !name || !data64) { + return; + } + if (data.identifier == null) { + identifier = 'default'; + } + const res2 = await showModalAndWait( + actions.PUBLISH_QDN_RESOURCE + ); + if (res2.action === 'accept') { + const worker = new WebWorker(); + try { + this.loader.show(); + const resPublish = await publishData({ + registeredName: name, + file: data64, + service: service, + identifier: identifier, + parentEpml, + uploadType: 'file', + selectedAddress: this.selectedAddress, + worker: worker, + isBase64: true, + }); + let data = {}; + data['data'] = resPublish; + response = JSON.stringify(data); + worker.terminate(); + } catch (error) { + worker.terminate(); + const data = {}; + const errorMsg = error.message || 'Upload failed'; + data['error'] = errorMsg; + response = JSON.stringify(data); + console.error(error); + return; + } finally { + this.loader.hide(); + } + } else if (res2.action === 'reject') { + response = '{"error": "User declined request"}'; + } + // Params: data.service, data.name, data.identifier, data.data64, + // TODO: prompt user for publish. If they confirm, call `POST /arbitrary/{service}/{name}/{identifier}/base64` and sign+process 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"}` + break; + + case 'SEND_CHAT_MESSAGE': + // Params: data.groupId, data.destinationAddress, data.message + // TODO: prompt user to send chat message. If they confirm, sign+process a CHAT 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"}` + break; + + case actions.JOIN_GROUP: + const groupId = data.groupId; + + if (!groupId) { + return; + } + + // Params: data.groupId + // TODO: prompt user to join group. If they confirm, sign+process a JOIN_GROUP 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"}` + break; + + case 'DEPLOY_AT': + // Params: data.creationBytes, data.name, data.description, data.type, data.tags, data.amount, data.assetId, data.fee + // TODO: prompt user to deploy an AT. If they confirm, sign+process a DEPLOY_AT 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"}` + break; + + case 'GET_WALLET_BALANCE': + // Params: data.coin (QORT / LTC / DOGE / DGB / C / ARRR) + // TODO: prompt user to share wallet balance. If they confirm, call `GET /crosschain/:coin/walletbalance`, or for QORT, call `GET /addresses/balance/:address` + // 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"}` + console.log('case passed here'); + console.log(data.coin, "data coin here"); + const res3 = await showModalAndWait( + actions.GET_WALLET_BALANCE + ); + if (res3.action === 'accept') { + let coin = data.coin; + if (coin === "QORT") { + let qortAddress = window.parent.reduxStore.getState().app.selectedAddress.address + try { + this.loader.show(); + const QORTBalance = await parentEpml.request('apiCall', { + url: `/addresses/balance/${qortAddress}?apiKey=${this.getApiKey()}`, + }) + console.log({QORTBalance}) + return QORTBalance; + } catch (error) { + console.error(error); + const data = {}; + const errorMsg = error.message || get("browserpage.bchange21"); + data['error'] = errorMsg; + response = JSON.stringify(data); + return; + } finally { + this.loader.hide(); + } + } else { + let _url = `` + let _body = null + + switch (coin) { + case 'LTC': + _url = `/crosschain/ltc/walletbalance?apiKey=${this.getApiKey()}` + _body = window.parent.reduxStore.getState().app.selectedAddress.ltcWallet.derivedMasterPublicKey + break + case 'DOGE': + _url = `/crosschain/doge/walletbalance?apiKey=${this.getApiKey()}` + _body = window.parent.reduxStore.getState().app.selectedAddress.dogeWallet.derivedMasterPublicKey + break + case 'DGB': + _url = `/crosschain/dgb/walletbalance?apiKey=${this.getApiKey()}` + _body = window.parent.reduxStore.getState().app.selectedAddress.dgbWallet.derivedMasterPublicKey + break + case 'RVN': + _url = `/crosschain/rvn/walletbalance?apiKey=${this.getApiKey()}` + _body = window.parent.reduxStore.getState().app.selectedAddress.rvnWallet.derivedMasterPublicKey + break + case 'ARRR': + _url = `/crosschain/arrr/walletbalance?apiKey=${this.getApiKey()}` + _body = window.parent.reduxStore.getState().app.selectedAddress.arrrWallet.seed58 + break + default: + break + } + try { + this.loader.show() + await parentEpml.request('apiCall', { + url: _url, + method: 'POST', + body: _body, + }).then((res) => { + if (isNaN(Number(res))) { + throw new Error(get("browserpage.bchange21")); + } else { + console.log((Number(res) / 1e8).toFixed(8), "other wallet balance here"); + return (Number(res) / 1e8).toFixed(8) + } + }) + } catch (error) { + console.error(error); + const data = {}; + const errorMsg = error.message || get("browserpage.bchange21"); + data['error'] = errorMsg; + response = JSON.stringify(data); + return; + } finally { + this.loader.hide() + } + } + } else if (res3.action === 'reject') { + response = '{"error": "User declined request"}'; + } + break; + + case 'SEND_COIN': + // 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"}` + break; + + default: + console.log('Unhandled message: ' + JSON.stringify(data)); + return; + } - let response = "{\"error\": \"Request could not be fulfilled\"}"; - let data = event.data; - console.log("UI received event: " + JSON.stringify(data)); - - switch (data.action) { - case "GET_USER_ACCOUNT": - // For now, we will return this without prompting the user, but we may need to add a prompt later - let account = {}; - account["address"] = this.selectedAddress.address; - account["publicKey"] = this.selectedAddress.base58PublicKey; - response = JSON.stringify(account); - break; - - case "LINK_TO_QDN_RESOURCE": - case actions.QDN_RESOURCE_DISPLAYED: - // Links are handled by the core, but the UI also listens for these actions in order to update the address bar. - // Note: don't update this.url here, as we don't want to force reload the iframe each time. - let url = "qortal://" + data.service + "/" + data.name; - this.path = data.path != null ? ((data.path.startsWith("/") ? "" : "/") + data.path) : null; - if (data.identifier != null && data.identifier != "" && data.identifier != "default") url = url.concat("/" + data.identifier); - if (this.path != null && this.path != "/") url = url.concat(this.path); - this.name = data.name; - this.service = data.service; - this.identifier = data.identifier; - this.displayUrl = url; - return; - - case actions.PUBLISH_QDN_RESOURCE: - // Use "default" if user hasn't specified an identifer - const service = data.service - const name = data.name - let identifier = data.identifier - const data64 = data.data64 - if(!service || !name || !data64){ - return - } - if (data.identifier == null) { - identifier = "default"; - } - - console.log('hello2') - const result = await showModalAndWait(actions.PUBLISH_QDN_RESOURCE); - console.log({result}) - if (result.action === 'accept') { - const worker = new WebWorker(); - console.log({worker}) - try { - this.loader.show(); - const resPublish = await publishData({ - registeredName: name, - file: data64, - service: service, - identifier: identifier, - parentEpml, - uploadType: 'file', - selectedAddress: this.selectedAddress, - worker: worker, - isBase64: true, - }); - let data = {}; - data["data"] = resPublish; - response = JSON.stringify(data); - worker.terminate(); - } catch (error) { - worker.terminate(); - const data = {} - const errorMsg = error.message || 'Upload failed' - data["error"] = errorMsg - response = JSON.stringify(data); - - return - } finally { - this.loader.hide(); - } - - console.log('User accepted:', result.userData); - } else if (result.action === 'reject') { - console.log('User rejected'); - response = "{\"error\": \"User declined request\"}" - } - // Params: data.service, data.name, data.identifier, data.data64, - // TODO: prompt user for publish. If they confirm, call `POST /arbitrary/{service}/{name}/{identifier}/base64` and sign+process 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"}` - break; - - case "SEND_CHAT_MESSAGE": - // Params: data.groupId, data.destinationAddress, data.message - // TODO: prompt user to send chat message. If they confirm, sign+process a CHAT 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"}` - break; - - case actions.JOIN_GROUP: - const groupId = data.groupId - - if(!groupId){ - return - } - - - // Params: data.groupId - // TODO: prompt user to join group. If they confirm, sign+process a JOIN_GROUP 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"}` - break; - - case "DEPLOY_AT": - // Params: data.creationBytes, data.name, data.description, data.type, data.tags, data.amount, data.assetId, data.fee - // TODO: prompt user to deploy an AT. If they confirm, sign+process a DEPLOY_AT 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"}` - break; - - case "GET_WALLET_BALANCE": - // Params: data.coin (QORT / LTC / DOGE / DGB / RVN / ARRR) - // TODO: prompt user to share wallet balance. If they confirm, call `GET /crosschain/:coin/walletbalance`, or for QORT, call `GET /addresses/balance/:address` - // 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"}` - break; - - case "SEND_COIN": - // 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"}` - break; - - default: - console.log("Unhandled message: " + JSON.stringify(data)); - return; - } + // Parse response + let responseObj; + try { + responseObj = JSON.parse(response); + } catch (e) { + // Not all responses will be JSON + responseObj = response; + } + // Respond to app + if (responseObj.error != null) { + event.ports[0].postMessage({ + result: null, + error: responseObj, + }); + } else { + event.ports[0].postMessage({ + result: responseObj, + error: null, + }); + } + }); + } + + changeTheme() { + const checkTheme = localStorage.getItem('qortalTheme'); + if (checkTheme === 'dark') { + this.theme = 'dark'; + } else { + this.theme = 'light'; + } + document.querySelector('html').setAttribute('theme', this.theme); + } + + changeLanguage() { + const checkLanguage = localStorage.getItem('qortalLanguage'); + + if (checkLanguage === null || checkLanguage.length === 0) { + localStorage.setItem('qortalLanguage', 'us'); + use('us'); + } else { + use(checkLanguage); + } + } + + renderFollowUnfollowButton() { + // Only show the follow/unfollow button if we have permission to modify the list on this node + if (this.followedNames == null || !Array.isArray(this.followedNames)) { + return html``; + } + + if (this.followedNames.indexOf(this.name) === -1) { + // render follow button + return html` this.follow()} + title="${translate('browserpage.bchange7')} ${this.name}" + class="address-bar-button float-right" + >add_to_queue`; + } else { + // render unfollow button + return html` this.unfollow()} + title="${translate('browserpage.bchange8')} ${this.name}" + class="address-bar-button float-right" + >remove_from_queue`; + } + } + + renderBlockUnblockButton() { + // Only show the block/unblock button if we have permission to modify the list on this node + if (this.blockedNames == null || !Array.isArray(this.blockedNames)) { + return html``; + } + + if (this.blockedNames.indexOf(this.name) === -1) { + // render block button + return html` this.block()} + title="${translate('browserpage.bchange9')} ${this.name}" + class="address-bar-button float-right" + >block`; + } else { + // render unblock button + return html` this.unblock()} + title="${translate('browserpage.bchange10')} ${this.name}" + class="address-bar-button float-right" + >radio_button_unchecked`; + } + } + + // Navigation + + goBack() { + window.history.back(); + } + + goForward() { + window.history.forward(); + } + + refresh() { + const myNode = + window.parent.reduxStore.getState().app.nodeConfig.knownNodes[ + window.parent.reduxStore.getState().app.nodeConfig.node + ]; + const nodeUrl = + myNode.protocol + '://' + myNode.domain + ':' + myNode.port; + this.url = `${nodeUrl}/render/${this.service}/${this.name}${ + this.path != null ? this.path : '' + }?theme=${this.theme}&identifier=${ + this.identifier != null ? this.identifier : '' + }`; + } + + goBackToList() { + window.location = '../index.html'; + } + + follow() { + this.followName(this.name); + } + + unfollow() { + this.unfollowName(this.name); + } + + block() { + this.blockName(this.name); + } + + unblock() { + this.unblockName(this.name); + } + + delete() { + this.deleteCurrentResource(); + } + + async followName(name) { + let items = [name]; + let namesJsonString = JSON.stringify({ items: items }); + + let ret = await parentEpml.request('apiCall', { + url: `/lists/followedNames?apiKey=${this.getApiKey()}`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: `${namesJsonString}`, + }); + + if (ret === true) { + // Successfully followed - add to local list + // Remove it first by filtering the list - doing it this way ensures the UI updates + // immediately, as apposed to only adding if it doesn't already exist + this.followedNames = this.followedNames.filter( + (item) => item != name + ); + this.followedNames.push(name); + } else { + let err1string = get('browserpage.bchange11'); + parentEpml.request('showSnackBar', `${err1string}`); + } + + return ret; + } + + async unfollowName(name) { + let items = [name]; + let namesJsonString = JSON.stringify({ items: items }); + + let ret = await parentEpml.request('apiCall', { + url: `/lists/followedNames?apiKey=${this.getApiKey()}`, + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: `${namesJsonString}`, + }); + + if (ret === true) { + // Successfully unfollowed - remove from local list + this.followedNames = this.followedNames.filter( + (item) => item != name + ); + } else { + let err2string = get('browserpage.bchange12'); + parentEpml.request('showSnackBar', `${err2string}`); + } + + return ret; + } + + async blockName(name) { + let items = [name]; + let namesJsonString = JSON.stringify({ items: items }); + + let ret = await parentEpml.request('apiCall', { + url: `/lists/blockedNames?apiKey=${this.getApiKey()}`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: `${namesJsonString}`, + }); + + if (ret === true) { + // Successfully blocked - add to local list + // Remove it first by filtering the list - doing it this way ensures the UI updates + // immediately, as apposed to only adding if it doesn't already exist + this.blockedNames = this.blockedNames.filter( + (item) => item != name + ); + this.blockedNames.push(name); + } else { + let err3string = get('browserpage.bchange13'); + parentEpml.request('showSnackBar', `${err3string}`); + } + + return ret; + } + + async unblockName(name) { + let items = [name]; + let namesJsonString = JSON.stringify({ items: items }); + + let ret = await parentEpml.request('apiCall', { + url: `/lists/blockedNames?apiKey=${this.getApiKey()}`, + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: `${namesJsonString}`, + }); + + if (ret === true) { + // Successfully unblocked - remove from local list + this.blockedNames = this.blockedNames.filter( + (item) => item != name + ); + } else { + let err4string = get('browserpage.bchange14'); + parentEpml.request('showSnackBar', `${err4string}`); + } + + return ret; + } + + async deleteCurrentResource() { + if (this.followedNames.indexOf(this.name) != -1) { + // Following name - so deleting won't work + let err5string = get('browserpage.bchange15'); + parentEpml.request('showSnackBar', `${err5string}`); + return; + } + + let identifier = + this.identifier == null ? 'default' : resource.identifier; + + let ret = await parentEpml.request('apiCall', { + url: `/arbitrary/resource/${this.service}/${ + this.name + }/${identifier}?apiKey=${this.getApiKey()}`, + method: 'DELETE', + }); + + if (ret === true) { + this.goBackToList(); + } else { + let err6string = get('browserpage.bchange16'); + parentEpml.request('showSnackBar', `${err6string}`); + } + + return ret; + } + + _textMenu(event) { + const getSelectedText = () => { + var text = ''; + if (typeof window.getSelection != 'undefined') { + text = window.getSelection().toString(); + } else if ( + typeof this.shadowRoot.selection != 'undefined' && + this.shadowRoot.selection.type == 'Text' + ) { + text = this.shadowRoot.selection.createRange().text; + } + return text; + }; + + const checkSelectedTextAndShowMenu = () => { + let selectedText = getSelectedText(); + if (selectedText && typeof selectedText === 'string') { + let _eve = { + pageX: event.pageX, + pageY: event.pageY, + clientX: event.clientX, + clientY: event.clientY, + }; + let textMenuObject = { + selectedText: selectedText, + eventObject: _eve, + isFrame: true, + }; + parentEpml.request('openCopyTextMenu', textMenuObject); + } + }; + checkSelectedTextAndShowMenu(); + } + + getApiKey() { + const myNode = + window.parent.reduxStore.getState().app.nodeConfig.knownNodes[ + window.parent.reduxStore.getState().app.nodeConfig.node + ]; + let apiKey = myNode.apiKey; + return apiKey; + } + + clearSelection() { + window.getSelection().removeAllRanges(); + window.parent.getSelection().removeAllRanges(); + } +} - // Parse response - let responseObj; - try { - responseObj = JSON.parse(response); - } catch (e) { - // Not all responses will be JSON - responseObj = response; - } +window.customElements.define('web-browser', WebBrowser); - // Respond to app - if (responseObj.error != null) { - event.ports[0].postMessage({ - result: null, - error: responseObj - }); +async function showModalAndWait(type, data) { + // Create a new Promise that resolves with user data and an action when the user clicks a button + return new Promise((resolve) => { + // Create the modal and add it to the DOM + const modal = document.createElement('div'); + modal.id = "backdrop" + modal.classList.add("backdrop"); + modal.innerHTML = + ` + `; + document.body.appendChild(modal); + + // Add click event listeners to the buttons + const okButton = modal.querySelector('#ok-button'); + okButton.addEventListener('click', () => { + const userData = {}; + if (modal.parentNode === document.body) { + document.body.removeChild(modal); } - else { - event.ports[0].postMessage({ - result: responseObj, - error: null - }); + resolve({ action: 'accept', userData }); + }); + const backdropClick = document.getElementById('backdrop'); + backdropClick.addEventListener('click', () => { + if (modal.parentNode === document.body) { + document.body.removeChild(modal); } + resolve({ action: 'reject' }); + }); + const cancelButton = modal.querySelector('#cancel-button'); + cancelButton.addEventListener('click', () => { + if (modal.parentNode === document.body) { + document.body.removeChild(modal); + } + resolve({ action: 'reject' }); + }); + }); +} - }); - } - - changeTheme() { - const checkTheme = localStorage.getItem('qortalTheme') - if (checkTheme === 'dark') { - this.theme = 'dark'; - } else { - this.theme = 'light'; - } - document.querySelector('html').setAttribute('theme', this.theme); - } - - changeLanguage() { - const checkLanguage = localStorage.getItem('qortalLanguage') - - if (checkLanguage === null || checkLanguage.length === 0) { - localStorage.setItem('qortalLanguage', 'us') - use('us') - } else { - use(checkLanguage) - } - } - - renderFollowUnfollowButton() { - // Only show the follow/unfollow button if we have permission to modify the list on this node - if (this.followedNames == null || !Array.isArray(this.followedNames)) { - return html`` - } - - if (this.followedNames.indexOf(this.name) === -1) { - // render follow button - return html` this.follow()} title="${translate("browserpage.bchange7")} ${this.name}" class="address-bar-button float-right">add_to_queue` - } - else { - // render unfollow button - return html` this.unfollow()} title="${translate("browserpage.bchange8")} ${this.name}" class="address-bar-button float-right">remove_from_queue` - } - } - - renderBlockUnblockButton() { - // Only show the block/unblock button if we have permission to modify the list on this node - if (this.blockedNames == null || !Array.isArray(this.blockedNames)) { - return html`` - } - - if (this.blockedNames.indexOf(this.name) === -1) { - // render block button - return html` this.block()} title="${translate("browserpage.bchange9")} ${this.name}" class="address-bar-button float-right">block` - } - else { - // render unblock button - return html` this.unblock()} title="${translate("browserpage.bchange10")} ${this.name}" class="address-bar-button float-right">radio_button_unchecked` - } - } - - - // Navigation - - goBack() { - window.history.back(); - } - - goForward() { - window.history.forward(); - } - - refresh() { - const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] - const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port - this.url = `${nodeUrl}/render/${this.service}/${this.name}${this.path != null ? this.path : ""}?theme=${this.theme}&identifier=${this.identifier != null ? this.identifier : ""}`; - } - - goBackToList() { - window.location = "../index.html"; - } - - follow() { - this.followName(this.name); - } - - unfollow() { - this.unfollowName(this.name); - } - - block() { - this.blockName(this.name); - } +// Add the styles for the modal +const styles = ` +.backdrop { +position: fixed; +top: 0; +left: 0; +width: 100%; +height: 100%; +background: rgb(186 186 186 / 26%); +overflow: hidden; +animation: backdrop_blur cubic-bezier(0.22, 1, 0.36, 1) 1s forwards; +z-index: 1000000; +} - unblock() { - this.unblockName(this.name); +@keyframes backdrop_blur { +0% { + backdrop-filter: blur(0px); + background: transparent; } - - delete() { - this.deleteCurrentResource(); +100% { + backdrop-filter: blur(5px); + background: rgb(186 186 186 / 26%); } +} +@keyframes modal_transition { +0% { + visibility: hidden; + opacity: 0; +} +100% { + visibility: visible; + opacity: 1; +} +} - async followName(name) { - let items = [ - name - ] - let namesJsonString = JSON.stringify({ "items": items }) - - let ret = await parentEpml.request('apiCall', { - url: `/lists/followedNames?apiKey=${this.getApiKey()}`, - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: `${namesJsonString}` - }) - - if (ret === true) { - // Successfully followed - add to local list - // Remove it first by filtering the list - doing it this way ensures the UI updates - // immediately, as apposed to only adding if it doesn't already exist - this.followedNames = this.followedNames.filter(item => item != name); - this.followedNames.push(name) - } - else { - let err1string = get("browserpage.bchange11") - parentEpml.request('showSnackBar', `${err1string}`) - } - - return ret - } - - async unfollowName(name) { - let items = [ - name - ] - let namesJsonString = JSON.stringify({ "items": items }) - - let ret = await parentEpml.request('apiCall', { - url: `/lists/followedNames?apiKey=${this.getApiKey()}`, - method: 'DELETE', - headers: { - 'Content-Type': 'application/json' - }, - body: `${namesJsonString}` - }) - - if (ret === true) { - // Successfully unfollowed - remove from local list - this.followedNames = this.followedNames.filter(item => item != name); - } - else { - let err2string = get("browserpage.bchange12") - parentEpml.request('showSnackBar', `${err2string}`) - } - - return ret - } +.modal { +position: relative; +display: flex; +justify-content: center; +align-items: center; +width: 100%; +height: 100%; +animation: 1s cubic-bezier(0.22, 1, 0.36, 1) 0s 1 normal forwards running modal_transition; +z-index: 1000001; +} - async blockName(name) { - let items = [ - name - ] - let namesJsonString = JSON.stringify({ "items": items }) - - let ret = await parentEpml.request('apiCall', { - url: `/lists/blockedNames?apiKey=${this.getApiKey()}`, - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: `${namesJsonString}` - }) - - if (ret === true) { - // Successfully blocked - add to local list - // Remove it first by filtering the list - doing it this way ensures the UI updates - // immediately, as apposed to only adding if it doesn't already exist - this.blockedNames = this.blockedNames.filter(item => item != name); - this.blockedNames.push(name) - } - else { - let err3string = get("browserpage.bchange13") - parentEpml.request('showSnackBar', `${err3string}`) - } - - return ret - } +@keyframes modal_transition { +0% { + visibility: hidden; + opacity: 0; +} +100% { + visibility: visible; + opacity: 1; +} +} - async unblockName(name) { - let items = [ - name - ] - let namesJsonString = JSON.stringify({ "items": items }) - - let ret = await parentEpml.request('apiCall', { - url: `/lists/blockedNames?apiKey=${this.getApiKey()}`, - method: 'DELETE', - headers: { - 'Content-Type': 'application/json' - }, - body: `${namesJsonString}` - }) - - if (ret === true) { - // Successfully unblocked - remove from local list - this.blockedNames = this.blockedNames.filter(item => item != name); - } - else { - let err4string = get("browserpage.bchange14") - parentEpml.request('showSnackBar', `${err4string}`) - } - - return ret - } +.modal-content { +background-color: #fff; +border-radius: 10px; +padding: 20px; +box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); +max-width: 80%; +min-width: 300px; +display: flex; +flex-direction: column; +justify-content: space-between; +} - async deleteCurrentResource() { - if (this.followedNames.indexOf(this.name) != -1) { - // Following name - so deleting won't work - let err5string = get("browserpage.bchange15") - parentEpml.request('showSnackBar', `${err5string}`) - return; - } - - let identifier = this.identifier == null ? "default" : resource.identifier; - - let ret = await parentEpml.request('apiCall', { - url: `/arbitrary/resource/${this.service}/${this.name}/${identifier}?apiKey=${this.getApiKey()}`, - method: 'DELETE' - }) - - if (ret === true) { - this.goBackToList(); - } - else { - let err6string = get("browserpage.bchange16") - parentEpml.request('showSnackBar', `${err6string}`) - } - - return ret - } +.modal-body { +padding: 25px; +} - _textMenu(event) { - const getSelectedText = () => { - var text = '' - if (typeof window.getSelection != 'undefined') { - text = window.getSelection().toString() - } else if (typeof this.shadowRoot.selection != 'undefined' && this.shadowRoot.selection.type == 'Text') { - text = this.shadowRoot.selection.createRange().text - } - return text - } - - const checkSelectedTextAndShowMenu = () => { - let selectedText = getSelectedText() - if (selectedText && typeof selectedText === 'string') { - let _eve = { pageX: event.pageX, pageY: event.pageY, clientX: event.clientX, clientY: event.clientY } - let textMenuObject = { selectedText: selectedText, eventObject: _eve, isFrame: true } - parentEpml.request('openCopyTextMenu', textMenuObject) - } - } - checkSelectedTextAndShowMenu() - } +.modal-paragraph { +font-family: Roboto, sans-serif; +font-size: 18px; +letter-spacing: 0.3px; +font-weight: 300; +color: black; +margin: 0; +} - getApiKey() { - const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]; - let apiKey = myNode.apiKey; - return apiKey; - } +.modal-buttons { +display: flex; +justify-content: space-between; +margin-top: 20px; +} - clearSelection() { - window.getSelection().removeAllRanges() - window.parent.getSelection().removeAllRanges() - } +.modal-buttons button { +background-color: #4caf50; +border: none; +color: #fff; +padding: 10px 20px; +border-radius: 5px; +cursor: pointer; +transition: background-color 0.2s; } -window.customElements.define('web-browser', WebBrowser) +.modal-buttons button:hover { +background-color: #3e8e41; +} +#cancel-button { +background-color: #f44336; +} -async function showModalAndWait(type, data) { - // Create a new Promise that resolves with user data and an action when the user clicks a button - return new Promise((resolve) => { - // Create the modal and add it to the DOM - const modal = document.createElement('div'); - modal.innerHTML = ` - - `; - document.body.appendChild(modal); - - // Add click event listeners to the buttons - const okButton = modal.querySelector('#ok-button'); - okButton.addEventListener('click', () => { - const userData = { - - }; - document.body.removeChild(modal); - resolve({ action: 'accept', userData }); - }); - const cancelButton = modal.querySelector('#cancel-button'); - cancelButton.addEventListener('click', () => { - document.body.removeChild(modal); - resolve({ action: 'reject' }); - }); - }); - } - - // Add the styles for the modal -const styles = ` -.modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.6); - z-index: 1000000; - display: flex; - justify-content: center; - align-items: center; - } - - .modal-content { - background-color: #fff; - border-radius: 10px; - padding: 20px; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); - max-width: 80%; - min-width: 300px; - min-height: 200px; - display: flex; - flex-direction: column; - justify-content: space-between; - } - .modal-body { - - } - - .modal-buttons { - display: flex; - justify-content: space-between; - margin-top: 20px; - } - - .modal-buttons button { - background-color: #4caf50; - border: none; - color: #fff; - padding: 10px 20px; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.2s; - } - - .modal-buttons button:hover { - background-color: #3e8e41; - } - - #cancel-button { - background-color: #f44336; - } - - #cancel-button:hover { - background-color: #d32f2f; - } +#cancel-button:hover { +background-color: #d32f2f; +} `; const styleSheet = new CSSStyleSheet(); styleSheet.replaceSync(styles); -document.adoptedStyleSheets = [styleSheet]; \ No newline at end of file +document.adoptedStyleSheets = [styleSheet];