diff --git a/qortal-ui-core/language/us.json b/qortal-ui-core/language/us.json index dafef222..e9fae851 100644 --- a/qortal-ui-core/language/us.json +++ b/qortal-ui-core/language/us.json @@ -638,7 +638,13 @@ "bchange35": "Do you give this application permission to send coins?", "bchange36": "Do you want to publish instant to QDN without computing proof-of-work?", "bchange37": "Enter Fullscreen", - "bchange38": "Exit Fullscreen" + "bchange38": "Exit Fullscreen", + "bchange39": "Always allow lists to be retrieved automatically", + "bchange40": "List", + "bchange41": "Do you give this application permission to access this list?", + "bchange42": "Items", + "bchange43": "Do you give this application permission to add to this list?", + "bchange44": "Do you give this application permission to delete from this list?" }, "datapage": { "dchange1": "Data Management", diff --git a/qortal-ui-core/src/components/app-view.js b/qortal-ui-core/src/components/app-view.js index db343f78..6c570082 100644 --- a/qortal-ui-core/src/components/app-view.js +++ b/qortal-ui-core/src/components/app-view.js @@ -1528,8 +1528,16 @@ class AppView extends connect(store)(LitElement) { - - + + + + + + + + + + diff --git a/qortal-ui-core/src/components/login-view/login-view.js b/qortal-ui-core/src/components/login-view/login-view.js index 52cb406c..c29be5ab 100644 --- a/qortal-ui-core/src/components/login-view/login-view.js +++ b/qortal-ui-core/src/components/login-view/login-view.js @@ -15,14 +15,17 @@ import './login-section.js' import '../qort-theme-toggle.js' import settings from '../../functional-components/settings-page.js' -import { addAutoLoadImageChat, removeAutoLoadImageChat, addChatLastSeen } from '../../redux/app/app-actions.js' +import { addAutoLoadImageChat, removeAutoLoadImageChat, addChatLastSeen, allowQAPPAutoAuth, removeQAPPAutoAuth, removeQAPPAutoLists, allowQAPPAutoLists } from '../../redux/app/app-actions.js' window.reduxStore = store window.reduxAction = { addAutoLoadImageChat: addAutoLoadImageChat, removeAutoLoadImageChat: removeAutoLoadImageChat, - addChatLastSeen: addChatLastSeen - + addChatLastSeen: addChatLastSeen, + allowQAPPAutoAuth: allowQAPPAutoAuth, + removeQAPPAutoAuth: removeQAPPAutoAuth, + allowQAPPAutoLists: allowQAPPAutoLists, + removeQAPPAutoLists: removeQAPPAutoLists } const animationDuration = 0.7 // Seconds diff --git a/qortal-ui-core/src/components/settings-view/security-view.js b/qortal-ui-core/src/components/settings-view/security-view.js index a09b5fbe..aaee8212 100644 --- a/qortal-ui-core/src/components/settings-view/security-view.js +++ b/qortal-ui-core/src/components/settings-view/security-view.js @@ -1,8 +1,10 @@ import { LitElement, html, css } from 'lit' import { connect } from 'pwa-helpers' import { store } from '../../store.js' +import { allowQAPPAutoAuth, removeQAPPAutoAuth, removeQAPPAutoLists, allowQAPPAutoLists } from '../../redux/app/app-actions.js' import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate' +import '@material/mwc-checkbox' import '@material/mwc-textfield' import '@material/mwc-icon' import '@vaadin/password-field/vaadin-password-field.js' @@ -27,6 +29,10 @@ class SecurityView extends connect(store)(LitElement) { --lumo-body-text-color: var(--black); --lumo-secondary-text-color: var(--sectxt); --lumo-contrast-60pct: var(--vdicon); + --mdc-checkbox-unchecked-color: var(--black); + --mdc-theme-on-surface: var(--black); + --mdc-checkbox-disabled-color: var(--black); + --mdc-checkbox-ink-color: var(--black); } .center-box { @@ -37,6 +43,16 @@ class SecurityView extends connect(store)(LitElement) { text-align: center; } + .checkbox-row { + position: relative; + display: flex; + align-items: center; + align-content: center; + font-family: Montserrat, sans-serif; + font-weight: 600; + color: var(--black); + } + .q-button { display: inline-flex; flex-direction: column; @@ -92,6 +108,19 @@ class SecurityView extends connect(store)(LitElement) {
this.checkForDownload()} class="q-button"> ${translate("settings.download")}
+
+
+ + this.checkForAuth(e)} ?checked=${store.getState().app.qAPPAutoAuth}> +
+
+ + this.checkForLists(e)} ?checked=${store.getState().app.qAPPAutoLists}> +
` } @@ -99,6 +128,21 @@ class SecurityView extends connect(store)(LitElement) { stateChanged(state) { } + checkForAuth(e) { + if (e.target.checked) { + store.dispatch(removeQAPPAutoAuth(false)) + } else { + store.dispatch(allowQAPPAutoAuth(true)) + } + } + checkForLists(e) { + if (e.target.checked) { + store.dispatch(removeQAPPAutoLists(false)) + } else { + store.dispatch(allowQAPPAutoLists(true)) + } + } + checkForDownload() { const checkPass = this.shadowRoot.getElementById('downloadBackupPassword').value if (checkPass === '') { diff --git a/qortal-ui-core/src/redux/app/actions/app-core.js b/qortal-ui-core/src/redux/app/actions/app-core.js index 894f2c2e..35767dae 100644 --- a/qortal-ui-core/src/redux/app/actions/app-core.js +++ b/qortal-ui-core/src/redux/app/actions/app-core.js @@ -1,5 +1,5 @@ // Core App Actions here... -import { UPDATE_BLOCK_INFO, UPDATE_NODE_STATUS, UPDATE_NODE_INFO, CHAT_HEADS, ACCOUNT_INFO, COPY_MENU_SWITCH, PASTE_MENU_SWITCH, FRAME_PASTE_MENU_SWITCH, ADD_AUTO_LOAD_IMAGES_CHAT, REMOVE_AUTO_LOAD_IMAGES_CHAT, SET_CHAT_LAST_SEEN, ADD_CHAT_LAST_SEEN } from '../app-action-types.js' +import { UPDATE_BLOCK_INFO, UPDATE_NODE_STATUS, UPDATE_NODE_INFO, CHAT_HEADS, ACCOUNT_INFO, COPY_MENU_SWITCH, PASTE_MENU_SWITCH, FRAME_PASTE_MENU_SWITCH, ADD_AUTO_LOAD_IMAGES_CHAT, REMOVE_AUTO_LOAD_IMAGES_CHAT, ALLOW_QAPP_AUTO_AUTH, REMOVE_QAPP_AUTO_AUTH, SET_CHAT_LAST_SEEN, ADD_CHAT_LAST_SEEN, ALLOW_QAPP_AUTO_LISTS, REMOVE_QAPP_AUTO_LISTS } from '../app-action-types.js' export const doUpdateBlockInfo = (blockObj) => { return (dispatch, getState) => { @@ -120,6 +120,33 @@ export const removeAutoLoadImageChat = (payload) => { } } +export const allowQAPPAutoAuth = (payload) => { + return { + type: ALLOW_QAPP_AUTO_AUTH, + payload + } +} + +export const removeQAPPAutoAuth = (payload) => { + return { + type: REMOVE_QAPP_AUTO_AUTH, + payload + } +} +export const allowQAPPAutoLists = (payload) => { + return { + type: ALLOW_QAPP_AUTO_LISTS, + payload + } +} + +export const removeQAPPAutoLists = (payload) => { + return { + type: REMOVE_QAPP_AUTO_LISTS, + payload + } +} + export const setChatLastSeen = (payload) => { return { type: SET_CHAT_LAST_SEEN, diff --git a/qortal-ui-core/src/redux/app/app-action-types.js b/qortal-ui-core/src/redux/app/app-action-types.js index 518d4cd7..5ace372c 100644 --- a/qortal-ui-core/src/redux/app/app-action-types.js +++ b/qortal-ui-core/src/redux/app/app-action-types.js @@ -22,5 +22,9 @@ export const PASTE_MENU_SWITCH = 'PASTE_MENU_SWITCH' export const FRAME_PASTE_MENU_SWITCH = 'FRAME_PASTE_MENU_SWITCH' export const ADD_AUTO_LOAD_IMAGES_CHAT = 'ADD_AUTO_LOAD_IMAGES_CHAT' export const REMOVE_AUTO_LOAD_IMAGES_CHAT = 'REMOVE_AUTO_LOAD_IMAGES_CHAT' +export const ALLOW_QAPP_AUTO_AUTH = 'ALLOW_QAPP_AUTO_AUTH' +export const REMOVE_QAPP_AUTO_AUTH = 'REMOVE_QAPP_AUTO_AUTH' +export const ALLOW_QAPP_AUTO_LISTS = 'ALLOW_QAPP_AUTO_LISTS' +export const REMOVE_QAPP_AUTO_LISTS = 'REMOVE_QAPP_AUTO_LISTS' export const SET_CHAT_LAST_SEEN = 'SET_CHAT_LAST_SEEN' export const ADD_CHAT_LAST_SEEN = 'ADD_CHAT_LAST_SEEN' diff --git a/qortal-ui-core/src/redux/app/app-reducer.js b/qortal-ui-core/src/redux/app/app-reducer.js index 68a48675..0855ecbf 100644 --- a/qortal-ui-core/src/redux/app/app-reducer.js +++ b/qortal-ui-core/src/redux/app/app-reducer.js @@ -1,6 +1,6 @@ // Loading state, login state, isNavDrawOpen state etc. None of this needs to be saved to localstorage. import { loadStateFromLocalStorage, saveStateToLocalStorage } from '../../localStorageHelpers.js' -import { LOG_IN, LOG_OUT, NETWORK_CONNECTION_STATUS, INIT_WORKERS, ADD_PLUGIN_URL, ADD_PLUGIN, ADD_NEW_PLUGIN_URL, NAVIGATE, SELECT_ADDRESS, ACCOUNT_INFO, CHAT_HEADS, UPDATE_BLOCK_INFO, UPDATE_NODE_STATUS, UPDATE_NODE_INFO, LOAD_NODE_CONFIG, SET_NODE, ADD_NODE, PAGE_URL, COPY_MENU_SWITCH, PASTE_MENU_SWITCH, FRAME_PASTE_MENU_SWITCH, ADD_AUTO_LOAD_IMAGES_CHAT, REMOVE_AUTO_LOAD_IMAGES_CHAT, SET_CHAT_LAST_SEEN, ADD_CHAT_LAST_SEEN } from './app-action-types.js' +import { LOG_IN, LOG_OUT, NETWORK_CONNECTION_STATUS, INIT_WORKERS, ADD_PLUGIN_URL, ADD_PLUGIN, ADD_NEW_PLUGIN_URL, NAVIGATE, SELECT_ADDRESS, ACCOUNT_INFO, CHAT_HEADS, UPDATE_BLOCK_INFO, UPDATE_NODE_STATUS, UPDATE_NODE_INFO, LOAD_NODE_CONFIG, SET_NODE, ADD_NODE, PAGE_URL, COPY_MENU_SWITCH, PASTE_MENU_SWITCH, FRAME_PASTE_MENU_SWITCH, ADD_AUTO_LOAD_IMAGES_CHAT, REMOVE_AUTO_LOAD_IMAGES_CHAT, ALLOW_QAPP_AUTO_AUTH, REMOVE_QAPP_AUTO_AUTH, SET_CHAT_LAST_SEEN, ADD_CHAT_LAST_SEEN, ALLOW_QAPP_AUTO_LISTS, REMOVE_QAPP_AUTO_LISTS } from './app-action-types.js' import { initWorkersReducer } from './reducers/init-workers.js' import { loginReducer } from './reducers/login-reducer.js' import { setNode, addNode } from './reducers/manage-node.js' @@ -50,6 +50,8 @@ const INITIAL_STATE = { elementId: '' }, autoLoadImageChats: loadStateFromLocalStorage('autoLoadImageChats') || [], + qAPPAutoAuth: loadStateFromLocalStorage('qAPPAutoAuth') || false, + qAPPAutoLists: loadStateFromLocalStorage('qAPPAutoLists') || false, chatLastSeen: [] } @@ -175,6 +177,38 @@ export default (state = INITIAL_STATE, action) => { autoLoadImageChats: updatedState } } + + case ALLOW_QAPP_AUTO_AUTH: { + saveStateToLocalStorage("qAPPAutoAuth", true) + return { + ...state, + qAPPAutoAuth: action.payload + } + } + + case REMOVE_QAPP_AUTO_AUTH: { + saveStateToLocalStorage("qAPPAutoAuth", false) + return { + ...state, + qAPPAutoAuth: action.payload + } + } + case ALLOW_QAPP_AUTO_LISTS: { + saveStateToLocalStorage("qAPPAutoLists", true) + return { + ...state, + qAPPAutoLists: action.payload + } + } + + case REMOVE_QAPP_AUTO_LISTS: { + saveStateToLocalStorage("qAPPAutoLists", false) + return { + ...state, + qAPPAutoLists: action.payload + } + } + case SET_CHAT_LAST_SEEN: { return { ...state, diff --git a/qortal-ui-plugins/build-config.js b/qortal-ui-plugins/build-config.js index e1bfac58..1c90aae0 100644 --- a/qortal-ui-plugins/build-config.js +++ b/qortal-ui-plugins/build-config.js @@ -144,6 +144,10 @@ const generateForPlugins = () => { in: 'plugins/core/puzzles/puzzles.src.js', out: 'plugins/core/puzzles/puzzles.js', }, + { + in: 'plugins/core/q-app/q-apps.src.js', + out: 'plugins/core/q-app/q-apps.js', + }, ].map((file) => { return generateRollupConfig( path.join(__dirname, file.in), diff --git a/qortal-ui-plugins/plugins/core/components/ChatGifs/ChatGifs.js b/qortal-ui-plugins/plugins/core/components/ChatGifs/ChatGifs.js index 63361262..b018f9c0 100644 --- a/qortal-ui-plugins/plugins/core/components/ChatGifs/ChatGifs.js +++ b/qortal-ui-plugins/plugins/core/components/ChatGifs/ChatGifs.js @@ -520,7 +520,7 @@ setOpenGifModal: { attribute: false } service: 'GIF_REPOSITORY', identifier: this.newCollectionName, parentEpml, - metaData: `title=${this.newCollectionName}`, + title: this.newCollectionName, uploadType: 'zip', selectedAddress: this.selectedAddress, worker: this.webWorkerImage, diff --git a/qortal-ui-plugins/plugins/core/components/ChatPage.js b/qortal-ui-plugins/plugins/core/components/ChatPage.js index 86df3694..3e405f4f 100644 --- a/qortal-ui-plugins/plugins/core/components/ChatPage.js +++ b/qortal-ui-plugins/plugins/core/components/ChatPage.js @@ -3426,7 +3426,7 @@ class ChatPage extends LitElement { }], isImageDeleted: false, repliedTo: '', - version: 2 + version: 3 }; const stringifyMessageObject = JSON.stringify(messageObject); this.sendMessage(stringifyMessageObject, typeMessage); @@ -3447,7 +3447,7 @@ class ChatPage extends LitElement { filePath: outSideMsg.filePath }], repliedTo: '', - version: 2 + version: 3 }; const stringifyMessageObject = JSON.stringify(messageObject); this.sendMessage(stringifyMessageObject, typeMessage); @@ -3509,7 +3509,7 @@ class ChatPage extends LitElement { }], isAttachmentDeleted: false, repliedTo: '', - version: 2 + version: 3 }; const stringifyMessageObject = JSON.stringify(messageObject); this.sendMessage(stringifyMessageObject, typeMessage); diff --git a/qortal-ui-plugins/plugins/core/components/ChatWelcomePage.js b/qortal-ui-plugins/plugins/core/components/ChatWelcomePage.js index eb95cb18..58bc2ce5 100644 --- a/qortal-ui-plugins/plugins/core/components/ChatWelcomePage.js +++ b/qortal-ui-plugins/plugins/core/components/ChatWelcomePage.js @@ -412,7 +412,7 @@ class ChatWelcomePage extends LitElement { messageText, images: [''], repliedTo: '', - version: 1 + version: 3 }; const stringifyMessageObject = JSON.stringify(messageObject); let chatResponse = await parentEpml.request('chat', { diff --git a/qortal-ui-plugins/plugins/core/components/qdn-action-types.js b/qortal-ui-plugins/plugins/core/components/qdn-action-types.js new file mode 100644 index 00000000..1eb816a6 --- /dev/null +++ b/qortal-ui-plugins/plugins/core/components/qdn-action-types.js @@ -0,0 +1,38 @@ +// GET_USER_ACCOUNT action +export const GET_USER_ACCOUNT = 'GET_USER_ACCOUNT'; + +// LINK_TO_QDN_RESOURCE action +export const LINK_TO_QDN_RESOURCE = 'LINK_TO_QDN_RESOURCE'; + +// QDN_RESOURCE_DISPLAYED action +export const QDN_RESOURCE_DISPLAYED = 'QDN_RESOURCE_DISPLAYED'; + +// PUBLISH_QDN_RESOURCE action +export const PUBLISH_QDN_RESOURCE = 'PUBLISH_QDN_RESOURCE'; + +// SEND_CHAT_MESSAGE action +export const SEND_CHAT_MESSAGE = 'SEND_CHAT_MESSAGE'; + +// JOIN_GROUP action +export const JOIN_GROUP = 'JOIN_GROUP'; + +// DEPLOY_AT action +export const DEPLOY_AT = 'DEPLOY_AT'; + +// GET_WALLET_BALANCE action +export const GET_WALLET_BALANCE = 'GET_WALLET_BALANCE'; + +// SEND_COIN action +export const SEND_COIN = 'SEND_COIN'; + +// PUBLISH_MULTIPLE_QDN_RESOURCES +export const PUBLISH_MULTIPLE_QDN_RESOURCES = 'PUBLISH_MULTIPLE_QDN_RESOURCES' + +// GET_LIST_ITEMS +export const GET_LIST_ITEMS = 'GET_LIST_ITEMS' + +// ADD_LIST_ITEMS +export const ADD_LIST_ITEMS = 'ADD_LIST_ITEMS' + +// DELETE_LIST_ITEM +export const DELETE_LIST_ITEM = 'DELETE_LIST_ITEM' \ No newline at end of file diff --git a/qortal-ui-plugins/plugins/core/main.src.js b/qortal-ui-plugins/plugins/core/main.src.js index 68de819a..62dd79e7 100644 --- a/qortal-ui-plugins/plugins/core/main.src.js +++ b/qortal-ui-plugins/plugins/core/main.src.js @@ -97,6 +97,15 @@ parentEpml.ready().then(() => { menus: [], parent: false, }, + { + url: 'qapps', + domain: 'core', + page: 'q-app/index.html', + title: 'Q-Apps', + icon: 'vaadin:desktop', + menus: [], + parent: false, + }, { url: 'data-management', domain: 'core', diff --git a/qortal-ui-plugins/plugins/core/q-app/index.html b/qortal-ui-plugins/plugins/core/q-app/index.html new file mode 100644 index 00000000..8898264a --- /dev/null +++ b/qortal-ui-plugins/plugins/core/q-app/index.html @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + diff --git a/qortal-ui-plugins/plugins/core/q-app/q-apps.src.js b/qortal-ui-plugins/plugins/core/q-app/q-apps.src.js new file mode 100644 index 00000000..b50c038c --- /dev/null +++ b/qortal-ui-plugins/plugins/core/q-app/q-apps.src.js @@ -0,0 +1,1233 @@ +import { LitElement, html, css } from 'lit' +import { render } from 'lit/html.js' +import { Epml } from '../../../epml.js' +import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate' +import { columnBodyRenderer, gridRowDetailsRenderer } from '@vaadin/grid/lit.js' + +registerTranslateConfig({ + loader: lang => fetch(`/language/${lang}.json`).then(res => res.json()) +}) + +import '@material/mwc-icon' +import '@material/mwc-button' +import '@material/mwc-tab-bar' +import '@material/mwc-textfield' +import '@polymer/paper-dialog/paper-dialog.js' +import '@vaadin/button' +import '@vaadin/grid' +import '@vaadin/icon' +import '@vaadin/icons' +import '@vaadin/text-field' + +const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }) + +class QApps extends LitElement { + static get properties() { + return { + service: { type: String }, + identifier: { type: String }, + loading: { type: Boolean }, + resources: { type: Array }, + pageRes: { type: Array }, + followedNames: { type: Array }, + blockedNames: { type: Array }, + relayMode: { type: Boolean }, + btnDisabled: { type: Boolean }, + selectedAddress: { type: Object }, + searchName: { type: String }, + searchResources: { type: Array }, + followedResources: { type: Array }, + blockedResources: { type: Array }, + textStatus: { type: String }, + textProgress: { type: String }, + theme: { type: String, reflect: true } + } + } + + static get styles() { + return css` + * { + --mdc-theme-primary: rgb(3, 169, 244); + --mdc-button-disabled-fill-color: rgba(3, 169, 244, 0.5); + + --paper-input-container-focus-color: var(--mdc-theme-primary); + --lumo-primary-text-color: rgb(0, 167, 245); + --lumo-primary-color-50pct: rgba(0, 167, 245, 0.5); + --lumo-primary-color-10pct: rgba(0, 167, 245, 0.1); + --lumo-primary-color: hsl(199, 100%, 48%); + --lumo-base-color: var(--white); + --lumo-body-text-color: var(--black); + --lumo-secondary-text-color: var(--sectxt); + --lumo-contrast-60pct: var(--vdicon); + --_lumo-grid-border-color: var(--border); + --_lumo-grid-secondary-border-color: var(--border2); + } + + #tabs-1 { + --mdc-tab-height: 50px; + } + + #tabs-1-content { + height: 100%; + padding-bottom: 10px; + } + + mwc-tab-bar { + --mdc-text-transform: none; + --mdc-tab-color-default: var(--black); + --mdc-tab-text-label-color-default: var(--black); + } + + #pages { + display: flex; + flex-wrap: wrap; + padding: 10px 5px 5px 5px; + margin: 0px 20px 20px 20px; + } + + #pages > button { + user-select: none; + padding: 5px; + margin: 0 5px; + border-radius: 10%; + border: 0; + background: transparent; + font: inherit; + outline: none; + cursor: pointer; + color: var(--black); + } + + #pages > button:not([disabled]):hover, + #pages > button:focus { + color: #ccc; + background-color: #eee; + } + + #pages > button[selected] { + font-weight: bold; + color: var(--white); + background-color: #ccc; + } + + #pages > button[disabled] { + opacity: 0.5; + cursor: default; + } + + #apps-list-page { + background: var(--white); + padding: 12px 24px; + } + + #search { + display: flex; + width: 50%; + align-items: center; + } + + .divCard { + border: 1px solid var(--border); + padding: 1em; + box-shadow: 0 .3px 1px 0 rgba(0,0,0,0.14), 0 1px 1px -1px rgba(0,0,0,0.12), 0 1px 2px 0 rgba(0,0,0,0.20); + margin-bottom: 2em; + } + + paper-dialog.progress { + width: auto; + max-width: 50vw; + height: auto; + max-height: 30vh; + background-color: var(--white); + color: var(--black); + border: 1px solid var(--black); + border-radius: 15px; + text-align:center; + padding: 15px; + line-height: 1.6; + overflow-y: auto; + } + + h2 { + margin:0; + } + + h2, h3, h4, h5 { + color: var(--black); + font-weight: 400; + } + + a.visitSite { + color: var(--black); + text-decoration: none; + } + + [hidden] { + display: hidden !important; + visibility: none !important; + } + + .details { + display: flex; + font-size: 18px; + } + + span { + font-size: 14px; + word-break: break-all; + } + + select { + padding: 13px 20px; + width: 100%; + font-size: 14px; + color: #555; + font-weight: 400; + } + + .title { + font-weight:600; + font-size:12px; + line-height: 32px; + opacity: 0.66; + } + + .resourceTitle { + font-size:15px; + line-height: 32px; + } + + .resourceDescription { + font-size:11px; + padding-bottom: 5px; + } + + .resourceCategoryTags { + font-size:11px; + padding-bottom: 10px; + } + + .resourceRegisteredName { + font-size:15px; + line-height: 32px; + } + + .resourceStatus, .resourceStatus span { + font-size:11px; + } + + .itemList { + padding:0; + } + + .relay-mode-notice { + margin:auto; + text-align:center; + word-break:normal; + font-size:14px; + line-height:20px; + color: var(--relaynodetxt); + } + + img { + border-radius: 25%; + max-width: 65px; + height: 100%; + max-height: 65px; + } + + .green { + --mdc-theme-primary: #198754; + } + + .lds-roller { + display: inline-block; + position: relative; + width: 80px; + height: 80px; + } + + .lds-roller div { + animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; + transform-origin: 40px 40px; + } + + .lds-roller div:after { + content: " "; + display: block; + position: absolute; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--black); + margin: -4px 0 0 -4px; + } + + .lds-roller div:nth-child(1) { + animation-delay: -0.036s; + } + + .lds-roller div:nth-child(1):after { + top: 63px; + left: 63px; + } + + .lds-roller div:nth-child(2) { + animation-delay: -0.072s; + } + + .lds-roller div:nth-child(2):after { + top: 68px; + left: 56px; + } + + .lds-roller div:nth-child(3) { + animation-delay: -0.108s; + } + + .lds-roller div:nth-child(3):after { + top: 71px; + left: 48px; + } + + .lds-roller div:nth-child(4) { + animation-delay: -0.144s; + } + + .lds-roller div:nth-child(4):after { + top: 72px; + left: 40px; + } + + .lds-roller div:nth-child(5) { + animation-delay: -0.18s; + } + + .lds-roller div:nth-child(5):after { + top: 71px; + left: 32px; + } + + .lds-roller div:nth-child(6) { + animation-delay: -0.216s; + } + + .lds-roller div:nth-child(6):after { + top: 68px; + left: 24px; + } + + .lds-roller div:nth-child(7) { + animation-delay: -0.252s; + } + + .lds-roller div:nth-child(7):after { + top: 63px; + left: 17px; + } + + .lds-roller div:nth-child(8) { + animation-delay: -0.288s; + } + + .lds-roller div:nth-child(8):after { + top: 56px; + left: 12px; + } + + @keyframes lds-roller { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + + ` + } + + constructor() { + super() + this.service = "APP" + this.identifier = null + this.selectedAddress = {} + this.resources = [] + this.pageRes = [] + this.followedNames = [] + this.blockedNames = [] + this.relayMode = null + this.isLoading = false + this.btnDisabled = false + this.searchName = '' + this.searchResources = [] + this.followedResources = [] + this.blockedResources = [] + this.textStatus = '' + this.textProgress = '' + this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light' + } + + render() { + return html` +
+ + + + + +
+
+
+

${translate("appspage.schange1")}

+

${this.renderPublishButton()}

+
+
+

${translate("appspage.schange4")}

+
+ + { + render(html`${this.renderAvatar(data.item)}`, root) + }}> + + { + render(html`${this.renderInfo(data.item)}`, root) + }}> + + { + render(html`${this.renderPublishedBy(data.item)}`, root) + }}> + + { + render(html`${this.renderDownload(data.item)}`, root) + }}> + + { + render(html`${this.renderFollowUnfollowButton(data.item)}`, root); + }}> + + { + render(html`${this.renderBlockUnblockButton(data.item)}`, root); + }}> + +
+
+
+

${translate("appspage.schange9")}

+ + { + render(html`${this.renderAvatar(data.item)}`, root) + }}> + + { + render(html`${this.renderInfo(data.item)}`, root) + }}> + + { + render(html`${this.renderPublishedBy(data.item)}`, root) + }}> + + { + render(html`${this.renderDownload(data.item)}`, root) + }}> + + { + render(html`${this.renderFollowUnfollowButton(data.item)}`, root); + }}> + + { + render(html`${this.renderBlockUnblockButton(data.item)}`, root); + }}> + + +
+ ${this.pageRes == null ? html` + Loading... + ` : ''} + ${this.isEmptyArray(this.pageRes) ? html` + ${translate("appspage.schange10")} + ` : ''} +
+ ${this.renderRelayModeText()}
+
+
+
+

${translate("appspage.schange11")}

+

${this.renderPublishButton()}

+
+
+

${translate("appspage.schange12")}

+ + { + render(html`${this.renderAvatar(data.item)}`, root) + }}> + + { + render(html`${this.renderInfo(data.item)}`, root) + }}> + + { + render(html`${this.renderPublishedBy(data.item)}`, root) + }}> + + { + render(html`${this.renderDownload(data.item)}`, root) + }}> + + { + render(html`${this.renderFollowUnfollowButton(data.item)}`, root); + }}> + + { + render(html`${this.renderBlockUnblockButton(data.item)}`, root); + }}> + + + ${this.followedResources == null ? html` + Loading... + ` : ''} + ${this.isEmptyArray(this.followedResources) ? html` + ${translate("appspage.schange13")} + ` : ''} +
+ ${this.renderRelayModeText()} +
+
+
+

${translate("appspage.schange14")}

+

${this.renderPublishButton()}

+
+
+

${translate("appspage.schange15")}

+ + { + render(html`${this.renderAvatar(data.item)}`, root) + }}> + + { + render(html`${this.renderInfo(data.item)}`, root) + }}> + + { + render(html`${this.renderPublishedBy(data.item)}`, root) + }}> + + { + render(html`${this.renderDownload(data.item)}`, root) + }}> + + { + render(html`${this.renderFollowUnfollowButton(data.item)}`, root); + }}> + + { + render(html`${this.renderBlockUnblockButton(data.item)}`, root); + }}> + + + ${this.blockedResources == null ? html` + Loading... + ` : ''} + ${this.isEmptyArray(this.blockedResources) ? html` + ${translate("appspage.schange16")} + ` : ''} +
+ ${this.renderRelayModeText()} +
+
+
+ + +
+

${translate("appspage.schange41")}

+

${this.textProgress}

+
+ ` + } + + firstUpdated() { + + this.changeTheme() + this.changeLanguage() + this.showapps() + + setTimeout(() => { + this.displayTabContent('browse') + }, 0) + + const getFollowedNames = async () => { + let followedNames = await parentEpml.request('apiCall', { + url: `/lists/followedNames?apiKey=${this.getApiKey()}` + }) + + this.followedNames = followedNames + setTimeout(getFollowedNames, 60000) + } + + const getBlockedNames = async () => { + let blockedNames = await parentEpml.request('apiCall', { + url: `/lists/blockedNames?apiKey=${this.getApiKey()}` + }) + this.blockedNames = blockedNames + setTimeout(getBlockedNames, 60000) + } + + const getRelayMode = async () => { + let relayMode = await parentEpml.request('apiCall', { + url: `/arbitrary/relaymode?apiKey=${this.getApiKey()}` + }) + + this.relayMode = relayMode; + setTimeout(getRelayMode, 600000) + } + + window.addEventListener("contextmenu", (event) => { + event.preventDefault(); + this._textMenu(event) + }); + + window.addEventListener("click", () => { + parentEpml.request('closeCopyTextMenu', null) + }); + + window.onkeyup = (e) => { + if (e.keyCode === 27) { + 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) + }) + + 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 => { + if (!configLoaded) { + setTimeout(getFollowedNames, 1) + setTimeout(getBlockedNames, 1) + setTimeout(getRelayMode, 1) + setTimeout(this.getFollowedNamesResource, 1) + setTimeout(this.getBlockedNamesResource, 1) + setInterval(this.getArbitraryResources, 600000) + configLoaded = true + } + this.config = JSON.parse(c) + }) + parentEpml.subscribe('copy_menu_switch', async value => { + if (value === 'false' && window.getSelection().toString().length !== 0) { + this.clearSelection() + } + }) + }) + parentEpml.imReady() + } + + 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) + } + } + + renderCatText() { + return html`${translate("appspage.schange26")}` + } + + displayTabContent(tab) { + const tabBrowseContent = this.shadowRoot.getElementById('tab-browse-content') + const tabFollowedContent = this.shadowRoot.getElementById('tab-followed-content') + const tabBlockedContent = this.shadowRoot.getElementById('tab-blocked-content') + tabBrowseContent.style.display = (tab === 'browse') ? 'block' : 'none' + tabFollowedContent.style.display = (tab === 'followed') ? 'block' : 'none' + tabBlockedContent.style.display = (tab === 'blocked') ? 'block' : 'none' + } + + searchListener(e) { + if (e.key === 'Enter') { + this.doSearch(e) + } + } + + async getResourcesGrid() { + this.resourcesGrid = this.shadowRoot.querySelector(`#resourcesGrid`) + this.pagesControl = this.shadowRoot.querySelector('#pages') + this.pages = undefined + } + + getArbitraryResources = async () => { + const resources = await parentEpml.request('apiCall', { + url: `/arbitrary/resources?service=${this.service}&default=true&limit=0&reverse=false&includestatus=false&includemetadata=false` + }) + this.resources = resources + } + + getFollowedNamesResource = async () => { + const followedRes = await parentEpml.request('apiCall', { + url: `/arbitrary/resources?service=${this.service}&default=true&limit=0&reverse=false&includestatus=true&includemetadata=true&namefilter=followedNames` + }) + this.followedResources = followedRes + } + + getFollowedNamesRefresh = async () => { + let followedNames = await parentEpml.request('apiCall', { + url: `/lists/followedNames?apiKey=${this.getApiKey()}` + }) + this.followedNames = followedNames + } + + getBlockedNamesResource = async () => { + const blockedRes = await parentEpml.request('apiCall', { + url: `/arbitrary/resources?service=${this.service}&default=true&limit=0&reverse=false&includestatus=true&includemetadata=true&namefilter=blockedNames` + }) + this.blockedResources = blockedRes + } + + getBlockedNamesRefresh = async () => { + let blockedNames = await parentEpml.request('apiCall', { + url: `/lists/blockedNames?apiKey=${this.getApiKey()}` + }) + this.blockedNames = blockedNames + } + + async getData(offset) { + const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] + const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port + let jsonOffsetUrl = `${nodeUrl}/arbitrary/resources?service=APP&default=true&limit=20&offset=${offset}&reverse=false&includestatus=true&includemetadata=true` + + const jsonOffsetRes = await fetch(jsonOffsetUrl) + const jsonOffsetData = await jsonOffsetRes.json() + + this.pageRes = jsonOffsetData + } + + async updateItemsFromPage(page) { + if (page === undefined) { + return + } + + if (!this.pages) { + this.pages = Array.apply(null, { length: Math.ceil(this.resources.length / 20) }).map((item, index) => { + return index + 1 + }) + + let offset = 0 + + const prevBtn = document.createElement('button') + prevBtn.textContent = '<' + prevBtn.addEventListener('click', () => { + if (parseInt(this.pagesControl.querySelector('[selected]').textContent) > 1) { + offset = (parseInt(this.pagesControl.querySelector('[selected]').textContent) - 2) * 20 + } else { + offset = 0 + } + this.getData(offset); + const selectedPage = parseInt(this.pagesControl.querySelector('[selected]').textContent) + this.updateItemsFromPage(selectedPage - 1) + }) + this.pagesControl.appendChild(prevBtn) + + this.pages.forEach((pageNumber) => { + const pageBtn = document.createElement('button') + pageBtn.textContent = pageNumber + let offset = 0; + pageBtn.addEventListener('click', (e) => { + if (parseInt(e.target.textContent) > 1) { + offset = (parseInt(e.target.textContent) - 1) * 20 + } else { + offset = 0 + } + this.getData(offset); + this.updateItemsFromPage(parseInt(e.target.textContent)) + }) + if (pageNumber === page) { + pageBtn.setAttribute('selected', true) + } + this.pagesControl.appendChild(pageBtn) + }) + + const nextBtn = window.document.createElement('button') + nextBtn.textContent = '>' + nextBtn.addEventListener('click', () => { + if (parseInt(this.pagesControl.querySelector('[selected]').textContent) >= 1) { + offset = ((parseInt(this.pagesControl.querySelector('[selected]').textContent) + 1) * 20) - 20 + } else { + offset = 0 + } + this.getData(offset); + const selectedPage = parseInt(this.pagesControl.querySelector('[selected]').textContent) + this.updateItemsFromPage(selectedPage + 1) + }) + this.pagesControl.appendChild(nextBtn) + } + + const buttons = Array.from(this.pagesControl.children) + buttons.forEach((btn, index) => { + if (parseInt(btn.textContent) === page) { + btn.setAttribute('selected', true) + } else { + btn.removeAttribute('selected') + } + if (index === 0) { + if (page === 1) { + btn.setAttribute('disabled', '') + } else { + btn.removeAttribute('disabled') + } + } + if (index === buttons.length - 1) { + if (page === this.pages.length) { + btn.setAttribute('disabled', '') + } else { + btn.removeAttribute('disabled') + } + } + }) + } + + async showapps() { + await this.getData(0) + await this.getArbitraryResources() + await this.getResourcesGrid() + await this.updateItemsFromPage(1, true) + } + + doSearch(e) { + this.searchResult() + } + + async searchResult() { + let searchName = this.shadowRoot.getElementById('searchName').value + if (searchName.length === 0) { + let err1string = get("appspage.schange34") + parentEpml.request('showSnackBar', `${err1string}`) + } else { + let searchResources = await parentEpml.request('apiCall', { + url: `/arbitrary/resources/search?service=${this.service}&query=${searchName}&default=true&limit=5&reverse=false&includestatus=true&includemetadata=true` + }) + if (this.isEmptyArray(searchResources)) { + let err2string = get("appspage.schange17") + parentEpml.request('showSnackBar', `${err2string}`) + } else { + this.searchResources = searchResources + } + } + } + + renderAvatar(appObj) { + let name = appObj.name + const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] + const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port + const url = `${nodeUrl}/arbitrary/THUMBNAIL/${name}/qortal_avatar?async=true&apiKey=${this.getApiKey()}` + return html`` + } + + renderRelayModeText() { + if (this.relayMode === true) { + return html`
${translate("appspage.schange18")} "relayModeEnabled": false ${translate("appspage.schange19")} settings.json
` + } + else if (this.relayMode === false) { + return html`
${translate("appspage.schange20")} "relayModeEnabled": true ${translate("appspage.schange19")} settings.json
` + } + return html`` + } + + renderPublishButton() { + if (this.followedNames == null || !Array.isArray(this.followedNames)) { + return html`` + } + return html` this.publishApp()}>add${translate("appspage.schange21")}` + } + + renderDownload(downObj) { + if (downObj.status.description === "Published but not yet downloaded" || downObj.status.status === "MISSING_DATA") { + return html` this.downloadApp(downObj)}>` + } else if (downObj.status.description === "Ready" || downObj.status.status === "DOWNLOADED") { + return html`` + } else { + return html`` + } + } + + async downloadApp(downObj) { + this.showChunks(downObj) + await parentEpml.request('apiCall', { + url: `/arbitrary/resource/status/APP/${downObj.name}?build=true&apiKey=${this.getApiKey()}` + }) + } + + showChunks(downObj) { + const checkStatus = async () => { + const service = this.service + const name = downObj.name + + const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] + const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port + const url = `${nodeUrl}/arbitrary/resource/status/${service}/${name}?build=true&apiKey=${this.getApiKey()}` + + this.textStatus = 'Loading...' + + this.btnDisabled = true + + this.shadowRoot.getElementById('downloadProgressDialog').open() + + let timerDownload + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + + const status = await response.json() + + console.log("status:", status.id) + + if (status.id === "UNSUPPORTED") { + this.btnDisabled = false + this.textProgress = '' + clearTimeout(timerDownload) + this.textStatus = status.description + } else if (status.id === "BLOCKED") { + this.btnDisabled = false + this.textProgress = '' + this.textStatus = name + " is blocked so content cannot be served" + clearTimeout(timerDownload) + timerDownload = setTimeout(checkStatus, 5000) + } else if (status.id === "READY") { + this.btnDisabled = false + clearTimeout(timerDownload) + this.textStatus = '' + this.textProgress = '' + this.shadowRoot.getElementById('downloadProgressDialog').close() + this.getData(0) + this.updateComplete.then(() => this.requestUpdate()) + } else if (status.id === "BUILDING") { + this.btnDisabled = true + this.textProgress = '' + this.textStatus = status.description + clearTimeout(timerDownload) + timerDownload = setTimeout(checkStatus, 1000) + } else if (status.id === "BUILD_FAILED") { + this.btnDisabled = false + this.textProgress = '' + clearTimeout(timerDownload) + this.textStatus = status.description + } else if (status.id === "NOT_STARTED") { + this.btnDisabled = false + this.textProgress = '' + this.textStatus = status.description + clearTimeout(timerDownload) + timerDownload = setTimeout(checkStatus, 1000) + } else if (status.id === "DOWNLOADING") { + this.btnDisabled = true + this.textStatus = status.description + let progressString = get("appspage.schange42") + this.textProgress = progressString + ": " + status.localChunkCount + " / " + status.totalChunkCount + clearTimeout(timerDownload) + timerDownload = setTimeout(checkStatus, 1000) + } else if (status.id === "MISSING_DATA") { + this.btnDisabled = true + this.textProgress = '' + this.textStatus = status.description + clearTimeout(timerDownload) + timerDownload = setTimeout(checkStatus, 5000) + } else if (status.id === "DOWNLOADED") { + this.btnDisabled = true + this.textProgress = '' + this.textStatus = status.description + clearTimeout(timerDownload) + timerDownload = setTimeout(checkStatus, 1000) + } + } + checkStatus() + } + + publishApp() { + window.location.href = `../qdn/publish/index.html?service=${this.service}&identifier=${this.identifier}&uploadType=zip&category=app&showName=true&showService=false&showIdentifier=false&showMetadata=true` + } + + async followName(appObj) { + let name = appObj.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) { + this.followedNames = this.followedNames.filter(item => item != name) + this.followedNames.push(name) + this.getFollowedNamesRefresh() + this.getFollowedNamesResource() + this.getArbitraryResources() + this.updateComplete.then(() => this.requestUpdate()) + } else { + let err3string = get("appspage.schange22") + parentEpml.request('showSnackBar', `${err3string}`) + } + return ret + } + + async unfollowName(appObj) { + let name = appObj.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) { + this.followedNames = this.followedNames.filter(item => item != name) + this.getFollowedNamesRefresh() + this.getFollowedNamesResource() + this.getArbitraryResources() + this.updateComplete.then(() => this.requestUpdate()) + } else { + let err4string = get("appspage.schange23") + parentEpml.request('showSnackBar', `${err4string}`) + } + return ret + } + + async blockName(appObj) { + let name = appObj.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) { + this.blockedNames = this.blockedNames.filter(item => item != name) + this.blockedNames.push(name) + this.getBlockedNamesRefresh() + this.getBlockedNamesResource() + this.getArbitraryResources() + this.updateComplete.then(() => this.requestUpdate()) + } else { + let err5string = get("appspage.schange24") + parentEpml.request('showSnackBar', `${err5string}`) + } + return ret + } + + async unblockName(appObj) { + let name = appObj.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) { + this.blockedNames = this.blockedNames.filter(item => item != name) + this.getBlockedNamesRefresh() + this.getBlockedNamesResource() + this.getArbitraryResources() + this.updateComplete.then(() => this.requestUpdate()) + } else { + let err6string = get("appspage.schange25") + parentEpml.request('showSnackBar', `${err6string}`) + } + return ret + } + + renderInfo(appObj) { + let name = appObj.name + let title = name + let description = "" + let categoryName = this.renderCatText() + let tags = ""; + let sizeReadable = "" + + if (appObj.metadata != null) { + title = appObj.metadata.title; + description = appObj.metadata.description; + categoryName = appObj.metadata.categoryName; + if (appObj.metadata.tags != null && appObj.metadata.tags.length > 0) { + tags = "Tags: " + appObj.metadata.tags.join(", ") + } + } + + if (appObj.size != null) { + sizeReadable = this.bytesToSize(appObj.size); + } + + return html` +
+ ${title} +
+
+ ${description} +
+
+ ${categoryName}  + ${tags.length > 0 ? " | " : ""} +  ${tags}  + ${sizeReadable.length > 0 ? " | " : ""} +  ${translate("appspage.schange27")}: ${sizeReadable} +
+ ` + } + + renderPublishedBy(appObj) { + return html`
${appObj.name}
+
${translate("appspage.schange28")}: ${appObj.status.title}
` + } + + renderSize(appObj) { + if (appObj.size === null) { + return html`` + } + let sizeReadable = this.bytesToSize(appObj.size) + return html`${sizeReadable}` + } + + renderFollowUnfollowButton(appObj) { + let name = appObj.name + + if (this.followedNames == null || !Array.isArray(this.followedNames)) { + return html`` + } + + if (this.followedNames.indexOf(name) === -1) { + return html` this.followName(appObj)}>add_to_queue ${translate("appspage.schange29")}` + } else { + return html` this.unfollowName(appObj)}>remove_from_queue ${translate("appspage.schange30")}` + } + } + + renderBlockUnblockButton(appObj) { + let name = appObj.name + + if (this.blockedNames == null || !Array.isArray(this.blockedNames)) { + return html`` + } + + if (this.blockedNames.indexOf(name) === -1) { + return html` this.blockName(appObj)}>block ${translate("appspage.schange31")}` + } else { + return html` this.unblockName(appObj)}>radio_button_unchecked ${translate("appspage.schange32")}` + } + } + + bytesToSize(bytes) { + var sizes = ['bytes', 'KB', 'MB', 'GB', 'TB'] + if (bytes == 0) return '0 bytes' + var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))) + return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i] + } + + _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() + } + + isEmptyArray(arr) { + if (!arr) { return true } + return arr.length === 0 + } +} + +window.customElements.define('q-apps', QApps) 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 283e07f3..455cc1ac 100644 --- a/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js +++ b/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js @@ -1,40 +1,66 @@ -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' - -const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }) +import '@material/mwc-button'; +import '@material/mwc-icon'; +import '@material/mwc-checkbox' +import WebWorker from 'web-worker:./computePowWorkerFile.src.js'; +import WebWorkerChat from 'web-worker:./computePowWorker.src.js'; +import { publishData } from '../../../utils/publish-image.js'; +import { Loader } from '../../../utils/loader.js'; +import { QORT_DECIMALS } from 'qortal-ui-crypto/api/constants'; +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 }, - followedNames: { type: Array }, - blockedNames: { type: Array }, - theme: { type: String, reflect: true } - } - } + static get properties() { + return { + selectedAddress: { type: Object }, + url: { type: String }, + name: { type: String }, + service: { type: String }, + identifier: { type: String }, + path: { type: String }, + preview: { type: String }, + displayUrl: { type: String }, + followedNames: { type: Array }, + blockedNames: { type: Array }, + theme: { type: String, reflect: true }, + btcFeePerByte: { type: Number }, + ltcFeePerByte: { type: Number }, + dogeFeePerByte: { type: Number }, + dgbFeePerByte: { type: Number }, + rvnFeePerByte: { type: Number }, + arrrWalletAddress: { type: String }, + theme: { type: String, reflect: true } + } + } - static get observers() { - return ['_kmxKeyUp(amount)'] - } + static get observers() { + return ['_kmxKeyUp(amount)'] + } - static get styles() { - return css` + static get styles() { + return css` * { --mdc-theme-primary: rgb(3, 169, 244); --mdc-theme-secondary: var(--mdc-theme-primary); --paper-input-container-focus-color: var(--mdc-theme-primary); + --mdc-checkbox-unchecked-color: var(--black); + --mdc-theme-on-surface: var(--black); + --mdc-checkbox-disabled-color: var(--black); + --mdc-checkbox-ink-color: var(--black); } #websitesWrapper paper-button { @@ -76,7 +102,7 @@ class WebBrowser extends LitElement { background-color: var(--white); } - input[type=text] { + input[type='text'] { margin: 0; padding: 2px 0 0 20px; border: 0; @@ -92,415 +118,2930 @@ class WebBrowser extends LitElement { .float-right { float: right; } - - ` - } + `; + } - constructor() { - super() - this.url = 'about:blank' + constructor() { + super(); + this.url = 'about:blank'; + this.myAddress = window.parent.reduxStore.getState().app.selectedAddress; + this._publicKey = { key: '', hasPubKey: false }; + 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.preview = urlParams.get('preview'); + 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 urlParams = new URLSearchParams(window.location.search); - this.name = urlParams.get('name'); - this.service = urlParams.get('service'); - // FUTURE: add support for identifiers - this.identifier = null; - this.followedNames = [] - this.blockedNames = [] - this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light' + const getFollowedNames = async () => { + let followedNames = await parentEpml.request('apiCall', { + url: `/lists/followedNames?apiKey=${this.getApiKey()}`, + }); - const getFollowedNames = async () => { + this.followedNames = followedNames; + setTimeout( + getFollowedNames, + this.config.user.nodeSettings.pingInterval + ); + }; - let followedNames = await parentEpml.request('apiCall', { - url: `/lists/followedNames?apiKey=${this.getApiKey()}` - }) + const getBlockedNames = async () => { + let blockedNames = await parentEpml.request('apiCall', { + url: `/lists/blockedNames?apiKey=${this.getApiKey()}`, + }); - this.followedNames = followedNames - setTimeout(getFollowedNames, this.config.user.nodeSettings.pingInterval) - } + this.blockedNames = blockedNames; + setTimeout( + getBlockedNames, + this.config.user.nodeSettings.pingInterval + ); + }; - const getBlockedNames = async () => { + 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 - let blockedNames = await parentEpml.request('apiCall', { - url: `/lists/blockedNames?apiKey=${this.getApiKey()}` - }) + if (this.preview != null && this.preview.length > 0) { + // In preview mode we access the preview URL path directly + this.url = `${nodeUrl}${this.preview}&theme=${this.theme}` + } + else { + // Normal mode + this.url = `${nodeUrl}/render/${this.service}/${this.name}${this.path != null ? this.path : '' + }?theme=${this.theme}&identifier=${this.identifier != null ? this.identifier : '' + }` + } + } - this.blockedNames = blockedNames - setTimeout(getBlockedNames, this.config.user.nodeSettings.pingInterval) - } + this.selectedAddress = {} + this.btcFeePerByte = 100 + this.ltcFeePerByte = 30 + this.dogeFeePerByte = 1000 + this.dgbFeePerByte = 10 + this.rvnFeePerByte = 1125 + this.arrrWalletAddress = '' - 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}?theme=${this.theme}`; - } + let configLoaded = false; - 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() - } - }) - } + parentEpml.ready().then(() => { + parentEpml.subscribe('selected_address', async (selectedAddress) => { + selectedAddress = JSON.parse(selectedAddress) + if (!selectedAddress || Object.entries(selectedAddress).length === 0) return - let configLoaded = false + this.selectedAddress = selectedAddress + this.btcWallet = window.parent.reduxStore.getState().app.selectedAddress.btcWallet + this.ltcWallet = window.parent.reduxStore.getState().app.selectedAddress.ltcWallet + this.dogeWallet = window.parent.reduxStore.getState().app.selectedAddress.dogeWallet + this.dgbWallet = window.parent.reduxStore.getState().app.selectedAddress.dgbWallet + this.rvnWallet = window.parent.reduxStore.getState().app.selectedAddress.rvnWallet + this.arrrWallet = window.parent.reduxStore.getState().app.selectedAddress.arrrWallet + }) + parentEpml.subscribe('config', (c) => { + this.config = JSON.parse(c); + if (!configLoaded) { + render(); + 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(); + } + }) + }) + } - 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 => { + 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.renderFullScreen()} + this.delete()} title="${translate('browserpage.bchange4')} ${this.service} ${this.name} ${translate('browserpage.bchange5')}" class="address-bar-button float-right">delete + ${this.renderBlockUnblockButton()} + ${this.renderFollowUnfollowButton()} +
+
+ +
+
+
+ `; + } - if (value === 'false' && window.getSelection().toString().length !== 0) { + renderFullScreen() { + if (window.innerHeight == screen.height) { + return html` + this.exitFullScreen()} + title="${translate('browserpage.bchange38')}" + class="address-bar-button float-right" + > + fullscreen_exit + + ` + } else { + return html` + this.goFullScreen()} + title="${translate('browserpage.bchange37')}" + class="address-bar-button float-right" + > + fullscreen + + ` + } + } - this.clearSelection() - } - }) - }) - } + goFullScreen() { + var elem = this.shadowRoot.getElementById('websitesWrapper') - 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()} + if (elem.requestFullscreen) { + elem.requestFullscreen() + } else if (elem.mozRequestFullScreen) { + elem.mozRequestFullScreen() + } else if (elem.webkitRequestFullscreen) { + elem.webkitRequestFullscreen() + } else if (elem.msRequestFullscreen) { + elem.msRequestFullscreen() + } + + this.renderFullScreen() + } + + exitFullScreen() { + if(document.exitFullscreen) { + document.exitFullscreen() + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen() + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen() + } else if (document.msExitFullscreen) { + document.msExitFullscreen() + } + + this.renderFullScreen() + } + + async unitJoinFee() { + const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] + const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port + const url = `${nodeUrl}/transactions/unitfee?txType=JOIN_GROUP` + const response = await fetch(url) + if (!response.ok) { + throw new Error('Error when fetching join fee'); + } + + const data = await response.json() + const joinFee = (Number(data) / 1e8).toFixed(8) + return joinFee + } + + async deployAtFee() { + const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] + const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port + const url = `${nodeUrl}/transactions/unitfee?txType=DEPLOY_AT` + const response = await fetch(url) + if (!response.ok) { + throw new Error('Error when fetching join fee'); + } + + const data = await response.json() + const joinFee = (Number(data) / 1e8).toFixed(8) + return joinFee + } + async sendQortFee() { + const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] + const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port + const url = `${nodeUrl}/transactions/unitfee?txType=PAYMENT` + const response = await fetch(url) + if (!response.ok) { + throw new Error('Error when fetching join fee'); + } + + const data = await response.json() + const qortFee = (Number(data) / 1e8).toFixed(8) + return qortFee + } + + async _joinGroup(groupId, groupName) { + const joinFeeInput = await this.unitJoinFee() + const getLastRef = async () => { + let myRef = await parentEpml.request('apiCall', { + type: 'api', + url: `/addresses/lastreference/${this.selectedAddress.address}` + }) + return myRef + }; + + const validateReceiver = async () => { + let lastRef = await getLastRef(); + let myTransaction = await makeTransactionRequest(lastRef) + const res = getTxnRequestResponse(myTransaction) + return res + } + + const makeTransactionRequest = async (lastRef) => { + let groupdialog1 = get("transactions.groupdialog1") + let groupdialog2 = get("transactions.groupdialog2") + let myTxnrequest = await parentEpml.request('transaction', { + type: 31, + nonce: this.selectedAddress.nonce, + params: { + fee: joinFeeInput, + registrantAddress: this.selectedAddress.address, + rGroupName: groupName, + rGroupId: groupId, + lastReference: lastRef, + groupdialog1: groupdialog1, + groupdialog2: groupdialog2 + }, + apiVersion: 2 + }) + return myTxnrequest + } + + const getTxnRequestResponse = (txnResponse) => { + if (txnResponse.success === false && txnResponse.message) { + throw new Error(txnResponse.message) + } else if (txnResponse.success === true && !txnResponse.data.error) { + return txnResponse.data + } else if (txnResponse.data && txnResponse.data.message) { + throw new Error(txnResponse.data.message) + } else { + throw new Error('Server error. Could not perform action.') + } + } + const groupRes = await validateReceiver() + return groupRes + + } + + async _deployAt(name, description, tags, creationBytes, amount, assetId, fee, atType) { + const deployAtFee = await this.deployAtFee() + const getLastRef = async () => { + let myRef = await parentEpml.request('apiCall', { + type: 'api', + url: `/addresses/lastreference/${this.selectedAddress.address}` + }) + return myRef + }; + + const validateReceiver = async () => { + let lastRef = await getLastRef(); + let myTransaction = await makeTransactionRequest(lastRef) + const res = getTxnRequestResponse(myTransaction) + return res + } + + const makeTransactionRequest = async (lastRef) => { + let groupdialog1 = get("transactions.groupdialog1") + let groupdialog2 = get("transactions.groupdialog2") + let myTxnrequest = await parentEpml.request('transaction', { + type: 16, + nonce: this.selectedAddress.nonce, + params: { + fee: fee || deployAtFee, + rName: name, + rDescription: description, + rTags: tags, + rAmount: amount, + rAssetId: assetId, + rCreationBytes: creationBytes, + atType: atType, + lastReference: lastRef, + atDeployDialog1: groupdialog1, + atDeployDialog2: groupdialog2 + }, + apiVersion: 2 + }) + return myTxnrequest + } + + const getTxnRequestResponse = (txnResponse) => { + if (txnResponse.success === false && txnResponse.message) { + throw new Error(txnResponse.message) + } else if (txnResponse.success === true && !txnResponse.data.error) { + return txnResponse.data + } else if (txnResponse.data && txnResponse.data.message) { + throw new Error(txnResponse.data.message) + } else { + throw new Error('Server error. Could not perform action.') + } + } + const groupRes = await validateReceiver() + return groupRes + + } + + firstUpdated() { + this.changeTheme(); + this.changeLanguage(); + + this.btcWallet = window.parent.reduxStore.getState().app.selectedAddress.btcWallet + this.ltcWallet = window.parent.reduxStore.getState().app.selectedAddress.ltcWallet + this.dogeWallet = window.parent.reduxStore.getState().app.selectedAddress.dogeWallet + this.dgbWallet = window.parent.reduxStore.getState().app.selectedAddress.dgbWallet + this.rvnWallet = window.parent.reduxStore.getState().app.selectedAddress.rvnWallet + this.arrrWallet = window.parent.reduxStore.getState().app.selectedAddress.arrrWallet + + 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.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; + + switch (data.action) { + case actions.GET_USER_ACCOUNT: { + + let skip = false; + if (window.parent.reduxStore.getState().app.qAPPAutoAuth) { + skip = true; + } + let res1; + if (!skip) { + res1 = await showModalAndWait( + actions.GET_USER_ACCOUNT, + { + service: this.service, + name: this.name + } + ); + }; + if ((res1 && res1.action === 'accept') || skip) { + let account = {}; + account['address'] = this.selectedAddress.address; + account['publicKey'] = + this.selectedAddress.base58PublicKey; + response = JSON.stringify(account); + break; + } else { + const data = {}; + const errorMsg = "User declined to share account details" + data['error'] = errorMsg; + response = JSON.stringify(data); + break; + } + } + case actions.GET_LIST_ITEMS: { + const requiredFields = ['list_name']; + const missingFields = []; + + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}` + let data = {}; + data['error'] = errorMsg; + response = JSON.stringify(data); + break + } + let skip = false; + if (window.parent.reduxStore.getState().app.qAPPAutoLists) { + skip = true; + } + let res1; + if (!skip) { + res1 = await showModalAndWait( + actions.GET_LIST_ITEMS, + { + list_name: data.list_name + } + ); + }; + + + if (res1 && res1.action === 'accept' || skip) { + + try { + const list = await parentEpml.request('apiCall', { + type: 'api', + url: `/lists/${data.list_name}?apiKey=${this.getApiKey()}`, + }); + response = JSON.stringify(list); + + } catch (error) { + const data = {}; + const errorMsg = "Error in retrieving list" + data['error'] = errorMsg; + response = JSON.stringify(data); + } finally { + break; + } + + } else { + const data = {}; + const errorMsg = "User declined to share list" + data['error'] = errorMsg; + response = JSON.stringify(data); + break; + } + }; + case actions.ADD_LIST_ITEMS: { + const requiredFields = ['list_name', 'items']; + const missingFields = []; + + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}` + let data = {}; + data['error'] = errorMsg; + response = JSON.stringify(data); + break + } + const items = data.items + const list_name = data.list_name + const res = await showModalAndWait( + actions.ADD_LIST_ITEMS, + { + list_name: list_name, + items: items + } + ); + + if (res && res.action === 'accept') { + + try { + const body = { + items: items, + }; + + const bodyToString = JSON.stringify(body); + const data = await parentEpml.request('apiCall', { + type: 'api', + method: 'POST', + url: `/lists/${list_name}?apiKey=${this.getApiKey()}`, + body: bodyToString, + headers: { + 'Content-Type': 'application/json', + }, + }); + response = data + } catch (error) { + const data = {}; + const errorMsg = "Error in adding to list" + data['error'] = errorMsg; + response = JSON.stringify(data); + } finally { + break; + } + + } else { + const data = {}; + const errorMsg = "User declined add to list" + data['error'] = errorMsg; + response = JSON.stringify(data); + break; + } + }; + case actions.DELETE_LIST_ITEM: { + const requiredFields = ['list_name', 'item']; + const missingFields = []; + + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}` + let data = {}; + data['error'] = errorMsg; + response = JSON.stringify(data); + break + } + const item = data.item + const list_name = data.list_name + const res = await showModalAndWait( + actions.DELETE_LIST_ITEM, + { + list_name: list_name, + item: item + } + ); + + if (res && res.action === 'accept') { + + try { + const body = { + items: [item], + }; + + const bodyToString = JSON.stringify(body); + + const data = await parentEpml.request('apiCall', { + type: 'api', + method: 'DELETE', + url: `/lists/${list_name}?apiKey=${this.getApiKey()}`, + body: bodyToString, + headers: { + 'Content-Type': 'application/json', + }, + }); + response = data + } catch (error) { + const data = {}; + const errorMsg = "Error in adding to list" + data['error'] = errorMsg; + response = JSON.stringify(data); + } finally { + break; + } + + } else { + const data = {}; + const errorMsg = "User declined add to list" + data['error'] = errorMsg; + response = JSON.stringify(data); + break; + } + }; + + + case actions.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. + if (this.preview != null && this.preview.length > 0) { + this.displayUrl = translate("appspage.schange40"); + return; + } + + 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: { + const requiredFields = ['service', 'name', 'data64']; + const missingFields = []; + + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}` + let data = {}; + data['error'] = errorMsg; + response = JSON.stringify(data); + break + } + // 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; + const filename = data.filename; + const title = data.title; + const description = data.description; + const category = data.category; + const tag1 = data.tag1; + const tag2 = data.tag2; + const tag3 = data.tag3; + const tag4 = data.tag4; + const tag5 = data.tag5; + if (data.identifier == null) { + identifier = 'default'; + } + const res2 = await showModalAndWait( + actions.PUBLISH_QDN_RESOURCE, + { + name, + identifier, + service + } + ); + if (res2.action === 'accept') { + const worker = new WebWorker(); + try { + this.loader.show(); + const resPublish = await publishData({ + registeredName: encodeURIComponent(name), + file: data64, + service: service, + identifier: encodeURIComponent(identifier), + parentEpml, + uploadType: 'file', + selectedAddress: this.selectedAddress, + worker: worker, + isBase64: true, + filename: filename, + title, + description, + category, + tag1, + tag2, + tag3, + tag4, + tag5, + apiVersion: 2, + withFee: res2.userData.isWithFee === true ? true: false + }); + + response = JSON.stringify(resPublish); + worker.terminate(); + } catch (error) { + worker.terminate(); + const obj = {}; + const errorMsg = error.message || 'Upload failed'; + obj['error'] = errorMsg; + response = JSON.stringify(obj); + console.error(error); + break; + } 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 actions.PUBLISH_MULTIPLE_QDN_RESOURCES: { + const requiredFields = ['resources']; + const missingFields = []; + + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}` + let data = {}; + data['error'] = errorMsg; + response = JSON.stringify(data); + break + } + const resources = data.resources + if (!Array.isArray(resources)) { + let data = {}; + data['error'] = "Invalid data" + response = JSON.stringify(data); + break + } + if (resources.length === 0) { + let data = {}; + data['error'] = "No resources to publish" + response = JSON.stringify(data); + break + } + const res2 = await showModalAndWait( + actions.PUBLISH_MULTIPLE_QDN_RESOURCES, + { + resources, + } + ); + + if (res2.action === 'reject') { + response = '{"error": "User declined request"}'; + break + + } + const resourcesMap = resources.map(async (resource) => { + const requiredFields = ['service', 'name', 'data64']; + const missingFields = []; + + requiredFields.forEach((field) => { + if (!resource[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}` + throw new Error(errorMsg) + } + + const service = resource.service; + const name = resource.name; + let identifier = resource.identifier; + const data64 = resource.data64; + const filename = resource.filename; + const title = resource.title; + const description = resource.description; + const category = resource.category; + const tag1 = resource.tag1; + const tag2 = resource.tag2; + const tag3 = resource.tag3; + const tag4 = resource.tag4; + const tag5 = resource.tag5; + if (resource.identifier == null) { + identifier = 'default'; + } + + const worker = new WebWorker(); + try { + + const resPublish = await publishData({ + registeredName: encodeURIComponent(name), + file: data64, + service: service, + identifier: encodeURIComponent(identifier), + parentEpml, + uploadType: 'file', + selectedAddress: this.selectedAddress, + worker: worker, + isBase64: true, + filename: filename, + title, + description, + category, + tag1, + tag2, + tag3, + tag4, + tag5, + apiVersion: 2, + withFee: res2.userData.isWithFee === true ? true : false + }); + + worker.terminate(); + return resPublish + } catch (error) { + worker.terminate(); + const errorMsg = error.message || 'Upload failed'; + throw new Error(errorMsg) + } + + + }) + + try { + this.loader.show(); + const results = await Promise.all(resourcesMap); + response = JSON.stringify(results); + this.loader.hide(); + break + // handle successful results + } catch (error) { + const obj = {}; + const errorMsg = error.message || 'Upload failed'; + obj['error'] = errorMsg; + response = JSON.stringify(obj); + this.loader.hide(); + break; + } + + // 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 actions.SEND_CHAT_MESSAGE: { + const message = data.message; + const recipient = data.destinationAddress; + const groupId = data.groupId; + const isRecipient = groupId ? false : true + const sendMessage = async (messageText, chatReference) => { + + let _reference = new Uint8Array(64); + window.crypto.getRandomValues(_reference); + let reference = window.parent.Base58.encode(_reference); + const sendMessageRequest = async () => { + let chatResponse + + if(isRecipient){ + chatResponse = await parentEpml.request('chat', { + type: 18, + nonce: this.selectedAddress.nonce, + params: { + timestamp: Date.now(), + recipient: recipient, + recipientPublicKey: this._publicKey.key, + hasChatReference: 0, + chatReference: chatReference, + message: messageText, + lastReference: reference, + proofOfWorkNonce: 0, + isEncrypted: 1, + isText: 1 + } + }); + + + } + + if(!isRecipient){ + chatResponse = await parentEpml.request('chat', { + type: 181, + nonce: this.selectedAddress.nonce, + params: { + timestamp: Date.now(), + groupID: Number(groupId), + hasReceipient: 0, + hasChatReference: 0, + chatReference: chatReference, + message: messageText, + lastReference: reference, + proofOfWorkNonce: 0, + isEncrypted: 0, + isText: 1 + } + }); + + + } + + const msgResponse = await _computePow(chatResponse) + return msgResponse; + }; + + const _computePow = async (chatBytes) => { + const difficulty = 8; + const path = window.parent.location.origin + '/memory-pow/memory-pow.wasm.full' + const worker = new WebWorkerChat(); + let nonce = null; + let chatBytesArray = null; + + await new Promise((res) => { + worker.postMessage({ chatBytes, path, difficulty }); + worker.onmessage = e => { + chatBytesArray = e.data.chatBytesArray; + nonce = e.data.nonce; + res(); + } + }); + + let _response = await parentEpml.request('sign_chat', { + nonce: this.selectedAddress.nonce, + chatBytesArray: chatBytesArray, + chatNonce: nonce, + apiVersion: 2 + }); + + const chatResponse = getSendChatResponse(_response); + return chatResponse; + }; + + const getSendChatResponse = (res) => { + if (res.signature) { + return res + } else if (res.error) { + throw new Error(res.message); + } else { + throw new Error('ERROR: Could not send message'); + } + }; + + const chatResponse = await sendMessageRequest(); + return chatResponse; + } + + const result = await showModalAndWait( + actions.SEND_CHAT_MESSAGE + ); + if (result.action === "accept") { + let hasPublicKey = true; + + if(isRecipient){ + const res = await parentEpml.request('apiCall', { + type: 'api', + url: `/addresses/publickey/${recipient}` + }); + + if (res.error === 102) { + this._publicKey.key = '' + this._publicKey.hasPubKey = false + hasPublicKey = false; + } else if (res !== false) { + this._publicKey.key = res + this._publicKey.hasPubKey = true + } else { + this._publicKey.key = '' + this._publicKey.hasPubKey = false + hasPublicKey = false; + } + } + + + if (!hasPublicKey && isRecipient) { + response = '{"error": "Cannot send an encrypted message to this user since they do not have their publickey on chain."}'; + break + } + + + + const tiptapJson = { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: message, + }, + + ], + }, + ], + } + + const messageObject = { + messageText: tiptapJson, + images: [''], + repliedTo: '', + version: 3 + }; + + const stringifyMessageObject = JSON.stringify(messageObject); + // if (this.balance < 4) { + // this.myTrimmedMeassage = '' + // this.myTrimmedMeassage = stringifyMessageObject + // this.shadowRoot.getElementById('confirmDialog').open() + // } else { + // this.sendMessage(stringifyMessageObject, typeMessage); + // } + try { + this.loader.show(); + const msgResponse = await sendMessage(stringifyMessageObject); + response = msgResponse; + } catch (error) { + console.error(error); + if(error.message){ + let data = {}; + data['error'] = error.message; + response = JSON.stringify(data); + break + } + response = '{"error": "Request could not be fulfilled"}'; + } finally { + this.loader.hide(); + + } + + } else { + response = '{"error": "User declined request"}'; + } + // this.loader.show(); + // 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 requiredFields = ['groupId']; + const missingFields = []; + + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}` + let data = {}; + data['error'] = errorMsg; + response = JSON.stringify(data); + break + } + const groupId = data.groupId; + + + let groupInfo = null + try { + groupInfo = await parentEpml.request("apiCall", { + type: "api", + url: `/groups/${groupId}`, + }); + } catch (error) { + const errorMsg = (error && error.message) || 'Group not found'; + let obj = {}; + obj['error'] = errorMsg; + response = JSON.stringify(obj); + break + } + + if (!groupInfo || groupInfo.error) { + const errorMsg = (groupInfo && groupInfo.message) || 'Group not found'; + let obj = {}; + obj['error'] = errorMsg; + response = JSON.stringify(obj); + break + } + + try { + this.loader.show(); + const resJoinGroup = await this._joinGroup(groupId, groupInfo.groupName) + response = JSON.stringify(resJoinGroup); + } catch (error) { + const obj = {}; + const errorMsg = error.message || 'Failed to join the group.'; + obj['error'] = errorMsg; + response = JSON.stringify(obj); + } finally { + this.loader.hide(); + } + + // 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': { + // const requiredFields = ['name', 'description', 'tags', 'creationBytes', 'amount', 'assetId', 'type']; + // const missingFields = []; + + // requiredFields.forEach((field) => { + // if (!data[field]) { + // missingFields.push(field); + // } + // }); + + // if (missingFields.length > 0) { + // const missingFieldsString = missingFields.join(', '); + // const errorMsg = `Missing fields: ${missingFieldsString}` + // let data = {}; + // data['error'] = errorMsg; + // response = JSON.stringify(data); + // break + // } + + + // try { + // this.loader.show(); + // const fee = data.fee || undefined + // const resJoinGroup = await this._deployAt(data.name, data.description, data.tags, data.creationBytes, data.amount, data.assetId, fee, data.type) + // response = JSON.stringify(resJoinGroup); + // } catch (error) { + // const obj = {}; + // const errorMsg = error.message || 'Failed to join the group.'; + // obj['error'] = errorMsg; + // response = JSON.stringify(obj); + // } finally { + // this.loader.hide(); + // } + // break; + // } + + + case actions.GET_WALLET_BALANCE: { + const requiredFields = ['coin']; + const missingFields = []; + + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}` + let data = {}; + data['error'] = errorMsg; + response = JSON.stringify(data); + break + } + // Params: data.coin (QORT / BTC / 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"}` + 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()}`, + }) + response = QORTBalance + + + } catch (error) { + console.error(error); + const data = {}; + const errorMsg = error.message || get("browserpage.bchange21"); + data['error'] = errorMsg; + response = JSON.stringify(data); + + } 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(); + // const res = await parentEpml.request('apiCall', { + // url: _url, + // method: 'POST', + // body: _body, + // }) + // if (isNaN(Number(res))) { + // const data = {}; + // const errorMsg = error.message || get("browserpage.bchange21"); + // data['error'] = errorMsg; + // response = JSON.stringify(data); + // return; + // } else { + // response = (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 actions.SEND_COIN: { + const requiredFields = ['coin', 'destinationAddress', 'amount'] + const missingFields = [] + + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field) + } + }) + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', ') + const errorMsg = `Missing fields: ${missingFieldsString}` + showErrorAndWait("MISSING_FIELDS", errorMsg) + let data = {} + data['error'] = errorMsg + response = JSON.stringify(data) + break + } + + let checkCoin = data.coin + + if (checkCoin === "QORT") { + // Params: data.coin, data.destinationAddress, data.amount, data.fee + // TODO: prompt user to send. If they confirm, call `POST /crosschain/:coin/send`, or for QORT, broadcast a PAYMENT transaction + // then set the response string from the core to the `response` variable (defined above) + // If they decline, send back JSON that includes an `error` key, such as `{"error": "User declined request"}` + const amount = Number(data.amount) + const recipient = data.destinationAddress + const coin = data.coin + + const walletBalance = await parentEpml.request('apiCall', { + url: `/addresses/balance/${this.myAddress.address}`, + }) + + if (isNaN(Number(walletBalance))) { + let errorMsg = "Failed to Fetch QORT Balance. Try again!" + let failedMsg = get("walletpage.wchange33") + " QORT " + get("general.balance") + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("FAILED_FETCH", failedMsg, pleaseMsg) + let obj = {} + obj['error'] = errorMsg + response = JSON.stringify(obj) + break + } + + const myRef = await parentEpml.request("apiCall", { + type: "api", + url: `/addresses/lastreference/${this.myAddress.address}`, + }) + + const transformDecimals = (Number(walletBalance) * QORT_DECIMALS).toFixed(0) + const walletBalanceDecimals = Number(transformDecimals) + const amountDecimals = Number(amount) * QORT_DECIMALS + const balance = (Number(transformDecimals) / 1e8).toFixed(8) + const fee = await this.sendQortFee() + + if (amountDecimals + (fee * QORT_DECIMALS) > walletBalanceDecimals) { + let errorMsg = "Insufficient Funds!" + let failedMsg = get("walletpage.wchange26") + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("INSSUFFICIENT_FUNDS", failedMsg, pleaseMsg) + let obj = {} + obj['error'] = errorMsg + response = JSON.stringify(obj) + break + } + + if (amount <= 0) { + let errorMsg = "Invalid Amount!" + showErrorAndWait("INVALID_AMOUNT", errorMsg) + let obj = {} + obj['error'] = errorMsg + response = JSON.stringify(obj) + break + } + + if (recipient.length === 0) { + let errorMsg = "Receiver cannot be empty!" + showErrorAndWait("NO_RECEIVER", errorMsg) + let obj = {} + obj['error'] = errorMsg + response = JSON.stringify(obj) + break + } + + const processPayment = await showModalAndWait( + actions.SEND_COIN, + { + amount, + recipient, + coin, + balance + } + ) + + if (processPayment.action === 'reject') { + let errorMsg = "User declined request" + let myMsg1 = get("transactions.declined") + let myMsg2 = get("walletpage.wchange44") + showErrorAndWait("DECLINED_REQUEST", myMsg1, myMsg2) + response = '{"error": "User declined request"}' + break + } + + const validateName = async (receiverName) => { + let myRes + let myNameRes = await parentEpml.request('apiCall', { + type: 'api', + url: `/names/${receiverName}`, + }) + + if (myNameRes.error === 401) { + myRes = false + } else { + myRes = myNameRes + } + return myRes + } + + const validateAddress = async (receiverAddress) => { + let myAddress = await window.parent.validateAddress(receiverAddress) + return myAddress + } + + const validateReceiver = async (recipient) => { + let lastRef = myRef + let isAddress + + try { + isAddress = await validateAddress(recipient) + } catch (err) { + isAddress = false + } + + if (isAddress) { + let myTransaction = await makeTransactionRequest(recipient, lastRef) + const res = getTxnRequestResponse(myTransaction) + return res + } else { + let myNameRes = await validateName(recipient) + if (myNameRes !== false) { + let myNameAddress = myNameRes.owner + let myTransaction = await makeTransactionRequest(myNameAddress, lastRef) + const res = getTxnRequestResponse(myTransaction) + return res + } else { + let errorMsg = get("walletpage.wchange29") + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("INVALID_RECEIVER", errorMsg, pleaseMsg) + throw new Error(errorMsg) + } + } + } + + const getName = async (recipient) => { + try { + const getNames = await parentEpml.request("apiCall", { + type: "api", + url: `/names/address/${recipient}` + }) + + if (getNames.length > 0) { + return getNames[0].name + } else { + return '' + } + } catch (error) { + return "" + } + } + + this.loader.show() + + const makeTransactionRequest = async (receiver, lastRef) => { + let myReceiver = receiver + let mylastRef = lastRef + let dialogamount = get("transactions.amount") + let dialogAddress = get("login.address") + let dialogName = get("login.name") + let dialogto = get("transactions.to") + let recipientName = await getName(myReceiver) + let myTxnrequest = await parentEpml.request('transaction', { + type: 2, + nonce: this.myAddress.nonce, + params: { + recipient: myReceiver, + recipientName: recipientName, + amount: amount, + lastReference: mylastRef, + fee: fee, + dialogamount: dialogamount, + dialogto: dialogto, + dialogAddress, + dialogName + }, + apiVersion: 2 + }) + return myTxnrequest + } + + const getTxnRequestResponse = (txnResponse) => { + if (txnResponse.success === false && txnResponse.message) { + this.loader.hide() + throw new Error(txnResponse.message) + } else if (txnResponse.success === true && !txnResponse.data.error) { + this.loader.hide() + return txnResponse.data + } else { + this.loader.hide() + throw new Error('Error: could not send coin') + } + + } + + try { + const result = await validateReceiver(recipient) + response = result + } catch (error) { + console.error(error) + response = '{"error": "Request could not be fulfilled"}' + } finally { + this.loader.hide() + } + break + } else if (checkCoin === "BTC") { + this.loader.show() + const amount = Number(data.amount) + const recipient = data.destinationAddress + const coin = data.coin + const xprv58 = this.btcWallet.derivedMasterPrivateKey + + const btcWalletBalance = await parentEpml.request('apiCall', { + url: `/crosschain/btc/walletbalance?apiKey=${this.getApiKey()}`, + method: 'POST', + body: `${this.btcWallet.derivedMasterPublicKey}` + }) + + if (isNaN(Number(btcWalletBalance))) { + this.loader.hide() + let errorMsg = "Failed to Fetch BTC Balance. Try again!" + let failedMsg = get("walletpage.wchange33") + " BTC " + get("general.balance") + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("FAILED_FETCH", failedMsg, pleaseMsg) + let obj = {} + obj['error'] = errorMsg + response = JSON.stringify(obj) + break + } + + const btcWalletBalanceDecimals = Number(btcWalletBalance) + const btcAmountDecimals = Number(amount) * QORT_DECIMALS + const balance = (Number(btcWalletBalance) / 1e8).toFixed(8) + const fee = 50000 + + if (btcAmountDecimals + fee > btcWalletBalanceDecimals) { + this.loader.hide() + let errorMsg = "Insufficient Funds!" + let failedMsg = get("walletpage.wchange26") + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("INSSUFFICIENT_FUNDS", failedMsg, pleaseMsg) + let obj = {} + obj['error'] = errorMsg + response = JSON.stringify(obj) + break + } + + this.loader.hide() + + const processPayment = await showModalAndWait( + actions.SEND_COIN, + { + amount, + recipient, + coin, + balance + } + ) + + if (processPayment.action === 'reject') { + let errorMsg = "User declined request" + let myMsg1 = get("transactions.declined") + let myMsg2 = get("walletpage.wchange44") + showErrorAndWait("DECLINED_REQUEST", myMsg1, myMsg2) + response = '{"error": "User declined request"}' + break + } + + this.loader.show() + + const makeRequest = async () => { + const opts = { + xprv58: xprv58, + receivingAddress: recipient, + bitcoinAmount: amount, + feePerByte: (this.btcFeePerByte / 1e8).toFixed(8) + } + const response = await parentEpml.request('sendBtc', opts) + return response + } + + const manageResponse = (response) => { + if (response.length === 64) { + this.loader.hide() + let successMsg = get("walletpage.wchange30") + let patientMsg = get("walletpage.wchange43") + showErrorAndWait("TRANSACTION_SUCCESS", successMsg, patientMsg) + } else if (response === false) { + this.loader.hide() + let errorMsg = get("walletpage.wchange31") + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("TRANSACTION_FAILED", errorMsg, pleaseMsg) + } else { + this.loader.hide() + let errorMsg = response.message + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("TRANSACTION_FAILED", errorMsg, pleaseMsg) + throw new Error(response) + } + } + + try { + const res = await makeRequest() + manageResponse(res) + } catch (error) { + console.error(error) + response = '{"error": "Request could not be fulfilled"}' + } finally { + this.loader.hide() + } + break + } else if (checkCoin === "LTC") { + this.loader.show() + const amount = Number(data.amount) + const recipient = data.destinationAddress + const coin = data.coin + const xprv58 = this.ltcWallet.derivedMasterPrivateKey + + const ltcWalletBalance = await parentEpml.request('apiCall', { + url: `/crosschain/ltc/walletbalance?apiKey=${this.getApiKey()}`, + method: 'POST', + body: `${this.ltcWallet.derivedMasterPublicKey}` + }) + + if (isNaN(Number(ltcWalletBalance))) { + this.loader.hide() + let errorMsg = "Failed to Fetch LTC Balance. Try again!" + let failedMsg = get("walletpage.wchange33") + " LTC " + get("general.balance") + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("FAILED_FETCH", failedMsg, pleaseMsg) + let obj = {} + obj['error'] = errorMsg + response = JSON.stringify(obj) + break + } + + const ltcWalletBalanceDecimals = Number(ltcWalletBalance) + const ltcAmountDecimals = Number(amount) * QORT_DECIMALS + const balance = (Number(ltcWalletBalance) / 1e8).toFixed(8) + const fee = 30000 + + if (ltcAmountDecimals + fee > ltcWalletBalanceDecimals) { + this.loader.hide() + let errorMsg = "Insufficient Funds!" + let failedMsg = get("walletpage.wchange26") + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("INSSUFFICIENT_FUNDS", failedMsg, pleaseMsg) + let obj = {} + obj['error'] = errorMsg + response = JSON.stringify(obj) + break + } + + this.loader.hide() + + const processPayment = await showModalAndWait( + actions.SEND_COIN, + { + amount, + recipient, + coin, + balance + } + ) + + if (processPayment.action === 'reject') { + let errorMsg = "User declined request" + let myMsg1 = get("transactions.declined") + let myMsg2 = get("walletpage.wchange44") + showErrorAndWait("DECLINED_REQUEST", myMsg1, myMsg2) + response = '{"error": "User declined request"}' + break + } + + this.loader.show() + + const makeRequest = async () => { + const opts = { + xprv58: xprv58, + receivingAddress: recipient, + litecoinAmount: amount, + feePerByte: (this.ltcFeePerByte / 1e8).toFixed(8) + } + const response = await parentEpml.request('sendLtc', opts) + return response + } + + const manageResponse = (response) => { + if (response.length === 64) { + this.loader.hide() + let successMsg = get("walletpage.wchange30") + let patientMsg = get("walletpage.wchange43") + showErrorAndWait("TRANSACTION_SUCCESS", successMsg, patientMsg) + } else if (response === false) { + this.loader.hide() + let errorMsg = get("walletpage.wchange31") + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("TRANSACTION_FAILED", errorMsg, pleaseMsg) + } else { + this.loader.hide() + let errorMsg = response.message + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("TRANSACTION_FAILED", errorMsg, pleaseMsg) + throw new Error(response) + } + } + + try { + const res = await makeRequest() + manageResponse(res) + } catch (error) { + console.error(error) + response = '{"error": "Request could not be fulfilled"}' + } finally { + this.loader.hide() + } + break + } else if (checkCoin === "DOGE") { + this.loader.show() + const amount = Number(data.amount) + const recipient = data.destinationAddress + const coin = data.coin + const xprv58 = this.dogeWallet.derivedMasterPrivateKey + + const dogeWalletBalance = await parentEpml.request('apiCall', { + url: `/crosschain/doge/walletbalance?apiKey=${this.getApiKey()}`, + method: 'POST', + body: `${this.dogeWallet.derivedMasterPublicKey}` + }) + + if (isNaN(Number(dogeWalletBalance))) { + this.loader.hide() + let errorMsg = "Failed to Fetch DOGE Balance. Try again!" + let failedMsg = get("walletpage.wchange33") + " DOGE " + get("general.balance") + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("FAILED_FETCH", failedMsg, pleaseMsg) + let obj = {} + obj['error'] = errorMsg + response = JSON.stringify(obj) + break + } + + const dogeWalletBalanceDecimals = Number(dogeWalletBalance) + const dogeAmountDecimals = Number(amount) * QORT_DECIMALS + const balance = (Number(dogeWalletBalance) / 1e8).toFixed(8) + const fee = 5000000 + + if (dogeAmountDecimals + fee > dogeWalletBalanceDecimals) { + this.loader.hide() + let errorMsg = "Insufficient Funds!" + let failedMsg = get("walletpage.wchange26") + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("INSSUFFICIENT_FUNDS", failedMsg, pleaseMsg) + let obj = {} + obj['error'] = errorMsg + response = JSON.stringify(obj) + break + } + + this.loader.hide() + + const processPayment = await showModalAndWait( + actions.SEND_COIN, + { + amount, + recipient, + coin, + balance + } + ) + + if (processPayment.action === 'reject') { + let errorMsg = "User declined request" + let myMsg1 = get("transactions.declined") + let myMsg2 = get("walletpage.wchange44") + showErrorAndWait("DECLINED_REQUEST", myMsg1, myMsg2) + response = '{"error": "User declined request"}' + break + } + + this.loader.show() + + const makeRequest = async () => { + const opts = { + xprv58: xprv58, + receivingAddress: recipient, + dogecoinAmount: amount, + feePerByte: (this.dogeFeePerByte / 1e8).toFixed(8) + } + const response = await parentEpml.request('sendDoge', opts) + return response + } + + const manageResponse = (response) => { + if (response.length === 64) { + this.loader.hide() + let successMsg = get("walletpage.wchange30") + let patientMsg = get("walletpage.wchange43") + showErrorAndWait("TRANSACTION_SUCCESS", successMsg, patientMsg) + } else if (response === false) { + this.loader.hide() + let errorMsg = get("walletpage.wchange31") + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("TRANSACTION_FAILED", errorMsg, pleaseMsg) + } else { + this.loader.hide() + let errorMsg = response.message + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("TRANSACTION_FAILED", errorMsg, pleaseMsg) + throw new Error(response) + } + } + + try { + const res = await makeRequest() + manageResponse(res) + } catch (error) { + console.error(error) + response = '{"error": "Request could not be fulfilled"}' + } finally { + this.loader.hide() + } + break + } else if (checkCoin === "DGB") { + this.loader.show() + const amount = Number(data.amount) + const recipient = data.destinationAddress + const coin = data.coin + const xprv58 = this.dgbWallet.derivedMasterPrivateKey + + const dgbWalletBalance = await parentEpml.request('apiCall', { + url: `/crosschain/dgb/walletbalance?apiKey=${this.getApiKey()}`, + method: 'POST', + body: `${this.dgbWallet.derivedMasterPublicKey}` + }) + + if (isNaN(Number(dgbWalletBalance))) { + this.loader.hide() + let errorMsg = "Failed to Fetch DGB Balance. Try again!" + let failedMsg = get("walletpage.wchange33") + " DGB " + get("general.balance") + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("FAILED_FETCH", failedMsg, pleaseMsg) + let obj = {} + obj['error'] = errorMsg + response = JSON.stringify(obj) + break + } + + const dgbWalletBalanceDecimals = Number(dgbWalletBalance) + const dgbAmountDecimals = Number(amount) * QORT_DECIMALS + const balance = (Number(dgbWalletBalance) / 1e8).toFixed(8) + const fee = 5000 + + if (dgbAmountDecimals + fee > dgbWalletBalanceDecimals) { + this.loader.hide() + let errorMsg = "Insufficient Funds!" + let failedMsg = get("walletpage.wchange26") + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("INSSUFFICIENT_FUNDS", failedMsg, pleaseMsg) + let obj = {} + obj['error'] = errorMsg + response = JSON.stringify(obj) + break + } + + this.loader.hide() + + const processPayment = await showModalAndWait( + actions.SEND_COIN, + { + amount, + recipient, + coin, + balance + } + ) + + if (processPayment.action === 'reject') { + let errorMsg = "User declined request" + let myMsg1 = get("transactions.declined") + let myMsg2 = get("walletpage.wchange44") + showErrorAndWait("DECLINED_REQUEST", myMsg1, myMsg2) + response = '{"error": "User declined request"}' + break + } + + this.loader.show() + + const makeRequest = async () => { + const opts = { + xprv58: xprv58, + receivingAddress: recipient, + digibyteAmount: amount, + feePerByte: (this.dgbFeePerByte / 1e8).toFixed(8) + } + const response = await parentEpml.request('sendDgb', opts) + return response + } + + const manageResponse = (response) => { + if (response.length === 64) { + this.loader.hide() + let successMsg = get("walletpage.wchange30") + let patientMsg = get("walletpage.wchange43") + showErrorAndWait("TRANSACTION_SUCCESS", successMsg, patientMsg) + } else if (response === false) { + this.loader.hide() + let errorMsg = get("walletpage.wchange31") + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("TRANSACTION_FAILED", errorMsg, pleaseMsg) + } else { + this.loader.hide() + let errorMsg = response.message + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("TRANSACTION_FAILED", errorMsg, pleaseMsg) + throw new Error(response) + } + } + + try { + const res = await makeRequest() + manageResponse(res) + } catch (error) { + console.error(error) + response = '{"error": "Request could not be fulfilled"}' + } finally { + this.loader.hide() + } + break + } else if (checkCoin === "RVN") { + this.loader.show() + const amount = Number(data.amount) + const recipient = data.destinationAddress + const coin = data.coin + const xprv58 = this.rvnWallet.derivedMasterPrivateKey + + const rvnWalletBalance = await parentEpml.request('apiCall', { + url: `/crosschain/rvn/walletbalance?apiKey=${this.getApiKey()}`, + method: 'POST', + body: `${this.rvnWallet.derivedMasterPublicKey}` + }) + + if (isNaN(Number(rvnWalletBalance))) { + this.loader.hide() + let errorMsg = "Failed to Fetch RVN Balance. Try again!" + let failedMsg = get("walletpage.wchange33") + " RVN " + get("general.balance") + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("FAILED_FETCH", failedMsg, pleaseMsg) + let obj = {} + obj['error'] = errorMsg + response = JSON.stringify(obj) + break + } + + const rvnWalletBalanceDecimals = Number(rvnWalletBalance) + const rvnAmountDecimals = Number(amount) * QORT_DECIMALS + const balance = (Number(rvnWalletBalance) / 1e8).toFixed(8) + const fee = 562500 + + if (rvnAmountDecimals + fee > rvnWalletBalanceDecimals) { + this.loader.hide() + let errorMsg = "Insufficient Funds!" + let failedMsg = get("walletpage.wchange26") + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("INSSUFFICIENT_FUNDS", failedMsg, pleaseMsg) + let obj = {} + obj['error'] = errorMsg + response = JSON.stringify(obj) + break + } + + this.loader.hide() + + const processPayment = await showModalAndWait( + actions.SEND_COIN, + { + amount, + recipient, + coin, + balance + } + ) + + if (processPayment.action === 'reject') { + let errorMsg = "User declined request" + let myMsg1 = get("transactions.declined") + let myMsg2 = get("walletpage.wchange44") + showErrorAndWait("DECLINED_REQUEST", myMsg1, myMsg2) + response = '{"error": "User declined request"}' + break + } + + this.loader.show() + + const makeRequest = async () => { + const opts = { + xprv58: xprv58, + receivingAddress: recipient, + ravencoinAmount: amount, + feePerByte: (this.rvnFeePerByte / 1e8).toFixed(8) + } + const response = await parentEpml.request('sendRvn', opts) + return response + } + + const manageResponse = (response) => { + if (response.length === 64) { + this.loader.hide() + let successMsg = get("walletpage.wchange30") + let patientMsg = get("walletpage.wchange43") + showErrorAndWait("TRANSACTION_SUCCESS", successMsg, patientMsg) + } else if (response === false) { + this.loader.hide() + let errorMsg = get("walletpage.wchange31") + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("TRANSACTION_FAILED", errorMsg, pleaseMsg) + } else { + this.loader.hide() + let errorMsg = response.message + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("TRANSACTION_FAILED", errorMsg, pleaseMsg) + throw new Error(response) + } + } + + try { + const res = await makeRequest() + manageResponse(res) + } catch (error) { + console.error(error) + response = '{"error": "Request could not be fulfilled"}' + } finally { + this.loader.hide() + } + break + } else if (checkCoin === "ARRR") { + this.loader.show() + const amount = Number(data.amount) + const recipient = data.destinationAddress + const coin = data.coin + const memo = data.memo + const seed58 = this.arrrWallet.seed58 + + const arrrWalletBalance = await parentEpml.request('apiCall', { + url: `/crosschain/arrr/walletbalance?apiKey=${this.getApiKey()}`, + method: 'POST', + body: `${this.arrrWallet.seed58}` + }) + + if (isNaN(Number(arrrWalletBalance))) { + this.loader.hide() + let errorMsg = "Failed to Fetch ARRR Balance. Try again!" + let failedMsg = get("walletpage.wchange33") + " ARRR " + get("general.balance") + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("FAILED_FETCH", failedMsg, pleaseMsg) + let obj = {} + obj['error'] = errorMsg + response = JSON.stringify(obj) + break + } + + const arrrWalletBalanceDecimals = Number(arrrWalletBalance) + const arrrAmountDecimals = Number(amount) * QORT_DECIMALS + const balance = (Number(arrrWalletBalance) / 1e8).toFixed(8) + const fee = 10000 + + if (arrrAmountDecimals + fee > arrrWalletBalanceDecimals) { + this.loader.hide() + let errorMsg = "Insufficient Funds!" + let failedMsg = get("walletpage.wchange26") + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("INSSUFFICIENT_FUNDS", failedMsg, pleaseMsg) + let obj = {} + obj['error'] = errorMsg + response = JSON.stringify(obj) + break + } + + this.loader.hide() + + const processPayment = await showModalAndWait( + actions.SEND_COIN, + { + amount, + recipient, + coin, + balance + } + ) + + if (processPayment.action === 'reject') { + let errorMsg = "User declined request" + let myMsg1 = get("transactions.declined") + let myMsg2 = get("walletpage.wchange44") + showErrorAndWait("DECLINED_REQUEST", myMsg1, myMsg2) + response = '{"error": "User declined request"}' + break + } + + this.loader.show() + + const makeRequest = async () => { + const opts = { + entropy58: seed58, + receivingAddress: recipient, + arrrAmount: amount, + memo: memo + } + const response = await parentEpml.request('sendArrr', opts) + return response + } + + const manageResponse = (response) => { + if (response.length === 64) { + this.loader.hide() + let successMsg = get("walletpage.wchange30") + let patientMsg = get("walletpage.wchange43") + showErrorAndWait("TRANSACTION_SUCCESS", successMsg, patientMsg) + } else if (response === false) { + this.loader.hide() + let errorMsg = get("walletpage.wchange31") + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("TRANSACTION_FAILED", errorMsg, pleaseMsg) + } else { + this.loader.hide() + let errorMsg = response.message + let pleaseMsg = get("walletpage.wchange44") + showErrorAndWait("TRANSACTION_FAILED", errorMsg, pleaseMsg) + throw new Error(response) + } + } + + try { + const res = await makeRequest() + manageResponse(res) + } catch (error) { + console.error(error) + response = '{"error": "Request could not be fulfilled"}' + } finally { + this.loader.hide() + } + 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 : '' + }&time=${new Date().getMilliseconds()}`; + } + + goBackToList() { + if (this.service == "APP") { + this.exitFullScreen() + window.location = '../../q-app/index.html'; + } + else { // Default to websites list + this.exitFullScreen() + 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 || this.identifier.length == 0) ? 'default' : this.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(); + } +} + +window.customElements.define('web-browser', WebBrowser); + +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 = ` +