diff --git a/blog-test.json b/blog-test.json index 35323b11..04dd800a 100644 --- a/blog-test.json +++ b/blog-test.json @@ -7,24 +7,16 @@ "version": 1, "updated": 1696646223261, "title": "Q-Blog Post creations", - "description": "blablabla", + "description": "Get your friends Q-Blog posts on your feed", "search": { "query": "-post-", "identifier": "q-blog-", "service": "BLOG_POST", "exactmatchnames": true }, - "click": "qortal://APP/Q-Blog/$${resource.name}$$/$${customParams.blogId}$$/$${customParams.shortIdentifier}$$", + "click": "qortal://APP/Q-Blog/$${resource.name}$$/blog/$${resource.identifier}$$", "display": { "title": "$${rawdata.title}$$" - }, - "customParams": { - "blogId": "**methods.getBlogId(resource)**", - "shortIdentifier": "**methods.getShortId(resource)**" - }, - "methods": { - "getShortId": "return resource.identifier.split('-post-')[1];", - "getBlogId": "const arr = resource.identifier.split('-post-'); const id = arr[0]; return id.startsWith('q-blog-') ? id.substring(7) : id;" } } ] diff --git a/core/language/us.json b/core/language/us.json index cccd6b1a..9778380d 100644 --- a/core/language/us.json +++ b/core/language/us.json @@ -729,7 +729,10 @@ "bchange46": "Do you give this application permission to save the following file", "bchange47": "Instant publish - requires", "bchange48": "Do you give this application permission to send you notifications", - "bchange49": "Do you give this application permission to get your wallet information?" + "bchange49": "Do you grant this application permission to access the following private information from your profile?", + "bchange50": "This app has requested a change to your public profile. Property: ", + "bchange51": "To submit the changes don't forget to click on 'Update profile'", + "bchange52": "Do you give this application permission to get your wallet information?" }, "datapage": { "dchange1": "Data Management", @@ -1214,5 +1217,28 @@ "saving2": "Nothing to save", "saving3": "Save unsaved changes", "saving4": "Undo changes" + }, + "profile": { + "profile1": "You do not have a name", + "profile2": "Go to name registration", + "profile3": "Update profile", + "profile4": "Tagline", + "profile5": "Bio", + "profile6": "Wallet Addresses", + "profile7": "Fill from UI", + "profile8": "Add custom property", + "profile9": "Property name", + "profile10": "Fields", + "profile11": "Add new field", + "profile12": "Field name", + "profile13": "Fill out", + "profile14": "Activity", + "profile15": "No value", + "profile16": "This name has no profile", + "profile17": "Unable to fetch profile", + "profile18": "Open profile", + "profile19": "Cannot fetch profile", + "profile20": "Register name", + "profile21": "Insert address from UI" } } \ No newline at end of file diff --git a/core/src/components/WebWorkerFile.js b/core/src/components/WebWorkerFile.js new file mode 100644 index 00000000..3b8c2b87 --- /dev/null +++ b/core/src/components/WebWorkerFile.js @@ -0,0 +1,5 @@ +import WebWorker from 'web-worker:./computePowWorkerFile.js'; + +// You can add any initialization or configuration for the Web Worker here + +export default WebWorker; \ No newline at end of file diff --git a/core/src/components/app-view.js b/core/src/components/app-view.js index dba95054..452c5588 100644 --- a/core/src/components/app-view.js +++ b/core/src/components/app-view.js @@ -1,12 +1,12 @@ -import {css, html, LitElement} from 'lit' -import {connect} from 'pwa-helpers' -import {store} from '../store.js' -import {Epml} from '../epml.js' -import {addTradeBotRoutes} from '../tradebot/addTradeBotRoutes.js' -import {get, translate} from 'lit-translate' +import { css, html, LitElement } from 'lit' +import { connect } from 'pwa-helpers' +import { store } from '../store.js' +import { Epml } from '../epml.js' +import { addTradeBotRoutes } from '../tradebot/addTradeBotRoutes.js' +import { get, translate } from 'lit-translate' import localForage from 'localforage' -import {decryptData, encryptData} from '../lockScreen.js' -import {setChatLastSeen} from '../redux/app/app-actions.js' +import { decryptData, encryptData } from '../lockScreen.js' +import { setChatLastSeen } from '../redux/app/app-actions.js' import isElectron from 'is-electron' import '@material/mwc-button' import '@material/mwc-icon' @@ -41,6 +41,7 @@ import './notification-view/notification-bell-general.js' import './friends-view/friends-side-panel-parent.js' import './friends-view/save-settings-qdn.js' import './friends-view/core-sync-status.js' +import './friends-view/profile.js' import './controllers/coin-balances-controller.js' const chatLastSeen = localForage.createInstance({ @@ -564,7 +565,8 @@ class AppView extends connect(store)(LitElement) { -
+
+ @@ -705,11 +707,11 @@ class AppView extends connect(store)(LitElement) { var drawerTog = this.shadowRoot.getElementById("mb") var drawerOut = this.shadowRoot.getElementById("appsidebar") - drawerTog.addEventListener('mouseover', function() { + drawerTog.addEventListener('mouseover', function () { drawerTog.click() }) - drawerOut.addEventListener('mouseleave', function() { + drawerOut.addEventListener('mouseleave', function () { drawerTog.click() }) diff --git a/core/src/components/computePowWorkerFile.js b/core/src/components/computePowWorkerFile.js new file mode 100644 index 00000000..d9f5f662 --- /dev/null +++ b/core/src/components/computePowWorkerFile.js @@ -0,0 +1,92 @@ +import { Sha256 } from 'asmcrypto.js' + + + +function sbrk(size, heap){ + let brk = 512 * 1024 // stack top + let old = brk + brk += size + + if (brk > heap.length) + throw new Error('heap exhausted') + + return old +} + + + + +self.addEventListener('message', async e => { + const response = await computePow(e.data.convertedBytes, e.data.path) + postMessage(response) + +}) + + +const memory = new WebAssembly.Memory({ initial: 256, maximum: 256 }) +const heap = new Uint8Array(memory.buffer) + + + +const computePow = async (convertedBytes, path) => { + + + let response = null + + await new Promise((resolve, reject)=> { + + const _convertedBytesArray = Object.keys(convertedBytes).map( + function (key) { + return convertedBytes[key] + } +) +const convertedBytesArray = new Uint8Array(_convertedBytesArray) +const convertedBytesHash = new Sha256() + .process(convertedBytesArray) + .finish().result +const hashPtr = sbrk(32, heap) +const hashAry = new Uint8Array( + memory.buffer, + hashPtr, + 32 +) + +hashAry.set(convertedBytesHash) +const difficulty = 14 +const workBufferLength = 8 * 1024 * 1024 +const workBufferPtr = sbrk( + workBufferLength, + heap +) + + const importObject = { + env: { + memory: memory + }, + }; + + function loadWebAssembly(filename, imports) { + return fetch(filename) + .then(response => response.arrayBuffer()) + .then(buffer => WebAssembly.compile(buffer)) + .then(module => { + return new WebAssembly.Instance(module, importObject); + }); +} + + +loadWebAssembly(path) + .then(wasmModule => { + response = { + nonce : wasmModule.exports.compute2(hashPtr, workBufferPtr, workBufferLength, difficulty), + + } + resolve() + + }); + + + }) + + return response +} \ No newline at end of file diff --git a/core/src/components/friends-view/avatar.js b/core/src/components/friends-view/avatar.js new file mode 100644 index 00000000..12f7090b --- /dev/null +++ b/core/src/components/friends-view/avatar.js @@ -0,0 +1,310 @@ +import { LitElement, html, css } from 'lit'; +import { get, translate } from 'lit-translate'; +import axios from 'axios'; +import '@material/mwc-menu'; +import '@material/mwc-list/mwc-list-item.js'; +import { RequestQueueWithPromise } from '../../../../plugins/plugins/utils/queue'; +import '../../../../plugins/plugins/core/components/TimeAgo'; +import { connect } from 'pwa-helpers'; +import { store } from '../../store'; +import { setNewTab } from '../../redux/app/app-actions'; +import ShortUniqueId from 'short-unique-id'; + +const requestQueue = new RequestQueueWithPromise(3); +const requestQueueRawData = new RequestQueueWithPromise(3); +const requestQueueStatus = new RequestQueueWithPromise(3); + +export class AvatarComponent extends connect(store)(LitElement) { + static get properties() { + return { + resource: { type: Object }, + isReady: { type: Boolean }, + status: { type: Object }, + name: { type: String }, + }; + } + + static get styles() { + return css` + * { + --mdc-theme-text-primary-on-background: var(--black); + box-sizing: border-box; + } + :host { + width: 100%; + box-sizing: border-box; + } + img { + width: 100%; + max-height: 30vh; + border-radius: 5px; + cursor: pointer; + position: relative; + } + .smallLoading, + .smallLoading:after { + border-radius: 50%; + width: 2px; + height: 2px; + } + + .defaultSize { + width: 100%; + height: 160px; + } + .parent-feed-item { + position: relative; + display: flex; + background-color: var(--chat-bubble-bg); + flex-grow: 0; + flex-direction: column; + align-items: flex-start; + justify-content: center; + border-radius: 5px; + padding: 12px 15px 4px 15px; + min-width: 150px; + width: 100%; + box-sizing: border-box; + cursor: pointer; + font-size: 16px; + } + .avatar { + width: 36px; + height: 36px; + border-radius: 50%; + overflow: hidden; + display: flex; + align-items: center; + } + .avatarApp { + width: 30px; + height: 30px; + border-radius: 50%; + overflow: hidden; + display: flex; + align-items: center; + } + .feed-item-name { + user-select: none; + color: #03a9f4; + margin-bottom: 5px; + } + + .app-name { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + } + + mwc-menu { + position: absolute; + } + `; + } + + constructor() { + super(); + this.resource = { + identifier: '', + name: '', + service: '', + }; + this.status = { + status: '', + }; + this.isReady = false; + this.nodeUrl = this.getNodeUrl(); + this.myNode = this.getMyNode(); + this.isFetching = false; + this.uid = new ShortUniqueId(); + } + getNodeUrl() { + const myNode = + window.parent.reduxStore.getState().app.nodeConfig.knownNodes[ + window.parent.reduxStore.getState().app.nodeConfig.node + ]; + + const nodeUrl = + myNode.protocol + '://' + myNode.domain + ':' + myNode.port; + return nodeUrl; + } + getMyNode() { + const myNode = + window.parent.reduxStore.getState().app.nodeConfig.knownNodes[ + window.parent.reduxStore.getState().app.nodeConfig.node + ]; + + return myNode; + } + + getApiKey() { + const myNode = + window.parent.reduxStore.getState().app.nodeConfig.knownNodes[ + window.parent.reduxStore.getState().app.nodeConfig.node + ]; + let apiKey = myNode.apiKey; + return apiKey; + } + + async fetchResource() { + try { + if (this.isFetching) return; + this.isFetching = true; + await axios.get( + `${this.nodeUrl}/arbitrary/resource/properties/${this.resource.service}/${this.resource.name}/${this.resource.identifier}?apiKey=${this.myNode.apiKey}` + ); + this.isFetching = false; + } catch (error) { + this.isFetching = false; + } + } + + async fetchVideoUrl() { + this.fetchResource(); + } + + async getRawData() { + const url = `${this.nodeUrl}/arbitrary/${this.resource.service}/${this.resource.name}/${this.resource.identifier}?apiKey=${this.myNode.apiKey}`; + return await requestQueueRawData.enqueue(() => { + return axios.get(url); + }); + // const response2 = await fetch(url, { + // method: 'GET', + // headers: { + // 'Content-Type': 'application/json' + // } + // }) + + // const responseData2 = await response2.json() + // return responseData2 + } + + updateDisplayWithPlaceholders(display, resource, rawdata) { + const pattern = /\$\$\{([a-zA-Z0-9_\.]+)\}\$\$/g; + + for (const key in display) { + const value = display[key]; + + display[key] = value.replace(pattern, (match, p1) => { + if (p1.startsWith('rawdata.')) { + const dataKey = p1.split('.')[1]; + if (rawdata[dataKey] === undefined) { + console.error('rawdata key not found:', dataKey); + } + return rawdata[dataKey] || match; + } else if (p1.startsWith('resource.')) { + const resourceKey = p1.split('.')[1]; + if (resource[resourceKey] === undefined) { + console.error('resource key not found:', resourceKey); + } + return resource[resourceKey] || match; + } + return match; + }); + } + } + + async fetchStatus() { + let isCalling = false; + let percentLoaded = 0; + let timer = 24; + const response = await requestQueueStatus.enqueue(() => { + return axios.get( + `${this.nodeUrl}/arbitrary/resource/status/${this.resource.service}/${this.resource.name}/${this.resource.identifier}?apiKey=${this.myNode.apiKey}` + ); + }); + if (response && response.data && response.data.status === 'READY') { + this.status = response.data; + + return; + } + const intervalId = setInterval(async () => { + if (isCalling) return; + isCalling = true; + + const data = await requestQueue.enqueue(() => { + return axios.get( + `${this.nodeUrl}/arbitrary/resource/status/${this.resource.service}/${this.resource.name}/${this.resource.identifier}?apiKey=${this.myNode.apiKey}` + ); + }); + const res = data.data; + + isCalling = false; + if (res.localChunkCount) { + if (res.percentLoaded) { + if ( + res.percentLoaded === percentLoaded && + res.percentLoaded !== 100 + ) { + timer = timer - 5; + } else { + timer = 24; + } + if (timer < 0) { + clearInterval(intervalId); + } + percentLoaded = res.percentLoaded; + } + + this.status = res; + if (this.status.status === 'DOWNLOADED') { + this.fetchResource(); + } + } + + // check if progress is 100% and clear interval if true + if (res.status === 'READY') { + clearInterval(intervalId); + this.status = res; + this.isReady = true; + } + }, 5000); // 1 second interval + } + + async _fetchImage() { + try { + this.fetchVideoUrl(); + this.fetchStatus(); + } catch (error) { + /* empty */ + } + } + + firstUpdated() { + this._fetchImage(); + } + + render() { + console.log('hello', this.name, this.resource, this.status); + return html` +
+ ${this.status.status !== 'READY' + ? html` + account_circle + ` + : ''} + ${this.status.status === 'READY' + ? html` +
+ +
+ ` + : ''} +
+ `; + } +} + +customElements.define('avatar-component', AvatarComponent); diff --git a/core/src/components/friends-view/friends-feed.js b/core/src/components/friends-view/friends-feed.js index f184de30..232e32e6 100644 --- a/core/src/components/friends-view/friends-feed.js +++ b/core/src/components/friends-view/friends-feed.js @@ -134,7 +134,7 @@ class FriendsFeed extends connect(store)(LitElement) { this.endpointOffsets = Array(this.endpoints.length).fill(0); return } - const baseurl = `${this.nodeUrl}/arbitrary/resources/search?reverse=true&exactmatchnames=true&${names}` + const baseurl = `${this.nodeUrl}/arbitrary/resources/search?reverse=true&mode=ALL&exactmatchnames=true&${names}` let formEndpoints = [] schemas.forEach((schema)=> { const feedData = schema.feed[0] @@ -350,7 +350,7 @@ this.getFeedOnInterval() // Merge new data with old data immutably this.feed = [...enhancedNewData, ...this.feed]; - + this.feed = this.removeDuplicates(this.feed) this.feed.sort((a, b) => new Date(b.created) - new Date(a.created)); // Sort by timestamp, most recent first this.feed = this.trimDataToLimit(this.feed, maxResultsInMemory); // Trim to the maximum allowed in memory this.feedToRender = this.feed.slice(0, 20); @@ -365,6 +365,17 @@ this.getFeedOnInterval() } + removeDuplicates(array) { + const seenIds = new Set(); + return array.filter(item => { + if (!seenIds.has(item.identifier)) { + seenIds.add(item.identifier); + return true; + } + return false; + }); + } + async loadAndMergeData() { let allData = this.feed @@ -373,6 +384,7 @@ this.getFeedOnInterval() allData = this.mergeData(newData, allData); allData.sort((a, b) => new Date(b.created) - new Date(a.created)); // Sort by timestamp, most recent first allData = this.trimDataToLimit(allData, maxResultsInMemory); // Trim to the maximum allowed in memory + allData = this.removeDuplicates(allData) this.feed = [...allData] this.feedToRender = this.feed.slice(0,20) this.hasInitialFetch = true diff --git a/core/src/components/friends-view/profile-modal-update.js b/core/src/components/friends-view/profile-modal-update.js new file mode 100644 index 00000000..714467d6 --- /dev/null +++ b/core/src/components/friends-view/profile-modal-update.js @@ -0,0 +1,725 @@ +import { LitElement, html, css } from 'lit'; +import { render } from 'lit/html.js'; +import { + use, + get, + translate, + translateUnsafeHTML, + registerTranslateConfig, +} from 'lit-translate'; +import '@material/mwc-button'; +import '@material/mwc-icon'; +import '@vaadin/tooltip'; +import '@material/mwc-dialog'; +import '@material/mwc-checkbox'; +import { connect } from 'pwa-helpers'; +import { store } from '../../store'; +import '@polymer/paper-spinner/paper-spinner-lite.js'; +import { parentEpml } from '../show-plugin'; + +class ProfileModalUpdate extends connect(store)(LitElement) { + static get properties() { + return { + isOpen: { type: Boolean }, + setIsOpen: { attribute: false }, + isLoading: { type: Boolean }, + onSubmit: { attribute: false }, + editContent: { type: Object }, + onClose: { attribute: false }, + tagline: { type: String }, + bio: { type: String }, + wallets: { type: Array }, + hasFetchedArrr: { type: Boolean }, + isOpenCustomDataModal: { type: Boolean }, + customData: { type: Object }, + newCustomDataField: {type: Object}, + newFieldName: {type: String}, + qortalRequestCustomData: {type: Object}, + newCustomDataKey: {type: String}, + isSaving: {type: Boolean} + }; + } + + constructor() { + super(); + this.isOpen = false; + this.isLoading = false; + this.nodeUrl = this.getNodeUrl(); + this.myNode = this.getMyNode(); + this.tagline = ''; + this.bio = ''; + this.walletList = ['btc', 'ltc', 'doge', 'dgb', 'rvn', 'arrr']; + let wallets = {}; + this.walletList.forEach((item) => { + wallets[item] = ''; + }); + this.wallets = wallets; + this.walletsUi = new Map(); + let coinProp = { + wallet: null, + }; + + this.walletList.forEach((c, i) => { + this.walletsUi.set(c, { ...coinProp }); + }); + this.walletsUi.get('btc').wallet = + window.parent.reduxStore.getState().app.selectedAddress.btcWallet; + this.walletsUi.get('ltc').wallet = + window.parent.reduxStore.getState().app.selectedAddress.ltcWallet; + this.walletsUi.get('doge').wallet = + window.parent.reduxStore.getState().app.selectedAddress.dogeWallet; + this.walletsUi.get('dgb').wallet = + window.parent.reduxStore.getState().app.selectedAddress.dgbWallet; + this.walletsUi.get('rvn').wallet = + window.parent.reduxStore.getState().app.selectedAddress.rvnWallet; + this.hasFetchedArrr = false; + this.isOpenCustomDataModal = false; + this.customData = {}; + this.newCustomDataKey = "" + this.newCustomDataField = {}; + this.newFieldName = ''; + this.isSaving = false + + } + + static get styles() { + return css` + * { + --mdc-theme-primary: rgb(3, 169, 244); + --mdc-theme-secondary: var(--mdc-theme-primary); + --mdc-theme-surface: var(--white); + --mdc-dialog-content-ink-color: var(--black); + --mdc-dialog-min-width: 400px; + --mdc-dialog-max-width: 1024px; + box-sizing: border-box; + } + .input { + width: 90%; + outline: 0; + border-width: 0 0 2px; + border-color: var(--mdc-theme-primary); + background-color: transparent; + padding: 10px; + font-family: Roboto, sans-serif; + font-size: 15px; + color: var(--chat-bubble-msg-color); + box-sizing: border-box; + } + .input::selection { + background-color: var(--mdc-theme-primary); + color: white; + } + + .input::placeholder { + opacity: 0.6; + color: var(--black); + } + + .modal-button { + font-family: Roboto, sans-serif; + font-size: 16px; + color: var(--mdc-theme-primary); + background-color: transparent; + padding: 8px 10px; + border-radius: 5px; + border: none; + transition: all 0.3s ease-in-out; + } + + .modal-button-red { + font-family: Roboto, sans-serif; + font-size: 16px; + color: #f44336; + background-color: transparent; + padding: 8px 10px; + border-radius: 5px; + border: none; + transition: all 0.3s ease-in-out; + } + + .modal-button-red:hover { + cursor: pointer; + background-color: #f4433663; + } + + .modal-button:hover { + cursor: pointer; + background-color: #03a8f475; + } + .checkbox-row { + position: relative; + display: flex; + align-items: center; + align-content: center; + font-family: Montserrat, sans-serif; + font-weight: 600; + color: var(--black); + } + .modal-overlay { + display: block; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba( + 0, + 0, + 0, + 0.5 + ); /* Semi-transparent backdrop */ + z-index: 1000; + } + + .modal-content { + position: fixed; + top: 50vh; + left: 50vw; + transform: translate(-50%, -50%); + background-color: var(--mdc-theme-surface); + width: 80vw; + max-width: 600px; + padding: 20px; + box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px; + z-index: 1001; + border-radius: 5px; + display: flex; + flex-direction: column; + } + + .modal-overlay.hidden { + display: none; + } + .avatar { + width: 36px; + height: 36px; + display: flex; + align-items: center; + } + + .app-name { + display: flex; + gap: 20px; + align-items: center; + width: 100%; + cursor: pointer; + padding: 5px; + border-radius: 5px; + margin-bottom: 10px; + } + .inner-content { + display: flex; + flex-direction: column; + max-height: 75vh; + flex-grow: 1; + overflow: auto; + } + + .inner-content::-webkit-scrollbar-track { + background-color: whitesmoke; + border-radius: 7px; + } + + .inner-content::-webkit-scrollbar { + width: 12px; + border-radius: 7px; + background-color: whitesmoke; + } + + .inner-content::-webkit-scrollbar-thumb { + background-color: rgb(180, 176, 176); + border-radius: 7px; + transition: all 0.3s ease-in-out; + } + `; + } + + async updated(changedProperties) { + if ( + changedProperties && + changedProperties.has('editContent') && + this.editContent + ) { + const {bio, tagline, wallets, customData} = this.editContent + this.bio = bio ?? ''; + this.tagline = tagline ?? ''; + let formWallets = {...this.wallets} + if(wallets && Object.keys(wallets).length){ + Object.keys(formWallets).forEach((key)=> { + if(wallets[key]){ + formWallets[key] = wallets[key] + } + }) + } + this.wallets = formWallets + + this.customData = {...customData} + this.requestUpdate(); + } + if ( + changedProperties && + changedProperties.has('qortalRequestCustomData') && + this.qortalRequestCustomData + ) { + this.isOpenCustomDataModal = true + this.newCustomDataField = {...this.qortalRequestCustomData.payload.customData} + this.newCustomDataKey = this.qortalRequestCustomData.property + this.requestUpdate(); + } + + + } + + async firstUpdated() { + try { + await this.fetchWalletAddress('arrr'); + } catch (error) { + console.log({ error }); + } finally { + } + } + + async fetchWalletAddress(coin) { + switch (coin) { + case 'arrr': + const arrrWalletName = `${coin}Wallet`; + + let res = await parentEpml.request('apiCall', { + url: `/crosschain/${coin}/walletaddress?apiKey=${this.myNode.apiKey}`, + method: 'POST', + body: `${ + window.parent.reduxStore.getState().app.selectedAddress[ + arrrWalletName + ].seed58 + }`, + }); + if (res != null && res.error != 1201 && res.length === 78) { + this.arrrWalletAddress = res; + this.hasFetchedArrr = true; + } + break; + + default: + // Not used for other coins yet + break; + } + } + + async getSelectedWalletAddress(wallet) { + switch (wallet) { + case 'arrr': + if(!this.arrrWalletAddress){ + try { + await this.fetchWalletAddress('arrr'); + } catch (error) { + console.log({error}) + } + } + // Use address returned by core API + return this.arrrWalletAddress; + + default: + // Use locally derived address + return this.walletsUi.get(wallet).wallet.address; + } + } + + getNodeUrl() { + const myNode = + store.getState().app.nodeConfig.knownNodes[ + window.parent.reduxStore.getState().app.nodeConfig.node + ]; + + const nodeUrl = + myNode.protocol + '://' + myNode.domain + ':' + myNode.port; + return nodeUrl; + } + getMyNode() { + const myNode = + store.getState().app.nodeConfig.knownNodes[ + window.parent.reduxStore.getState().app.nodeConfig.node + ]; + + return myNode; + } + + clearFields() { + this.bio = ''; + this.tagline = ''; + } + + fillAddress(coin) { + const address = this.getSelectedWalletAddress(coin); + if (address) { + this.wallets = { + ...this.wallets, + [coin]: address, + }; + } + } + + async saveProfile() { + try { + const data = { + version: 1, + tagline: this.tagline, + bio: this.bio, + wallets: this.wallets, + customData: this.customData + }; + this.isSaving = true + await this.onSubmit(data); + this.setIsOpen(false); + this.clearFields(); + this.onClose('success'); + } catch (error) {} finally { + this.isSaving = false + } + } + + removeField(key){ + const copyObj = {...this.newCustomDataField} + delete copyObj[key] + this.newCustomDataField = copyObj + } + + addField(){ + const copyObj = {...this.newCustomDataField} + copyObj[this.newFieldName] = '' + this.newCustomDataField = copyObj + this.newFieldName = "" + } + + addCustomData(){ + const copyObj = {...this.customData} + copyObj[this.newCustomDataKey] = this.newCustomDataField + this.customData = copyObj + this.newCustomDataKey = "" + this.newCustomDataField = {}; + this.newFieldName = '' + this.isOpenCustomDataModal = false; + } + + updateCustomData(key, data){ + this.isOpenCustomDataModal = true + this.newCustomDataField = data + this.newCustomDataKey = key + + } + removeCustomData(key){ + const copyObj = {...this.customData} + delete copyObj[key] + this.customData = copyObj + } + + render() { + return html` + + + + + `; + } +} + +customElements.define('profile-modal-update', ProfileModalUpdate); diff --git a/core/src/components/friends-view/profile.js b/core/src/components/friends-view/profile.js new file mode 100644 index 00000000..8d574ab1 --- /dev/null +++ b/core/src/components/friends-view/profile.js @@ -0,0 +1,1023 @@ +import { LitElement, html, css } from 'lit'; +import '@material/mwc-icon'; +import './friends-side-panel.js'; +import { connect } from 'pwa-helpers'; +import { store } from '../../store.js'; +import WebWorker2 from '../WebWorkerFile.js'; +import '@polymer/paper-spinner/paper-spinner-lite.js'; +import '@vaadin/tooltip'; +import { get, translate } from 'lit-translate'; +import ShortUniqueId from 'short-unique-id'; +import '@polymer/paper-dialog/paper-dialog.js'; + +import { + decryptGroupData, + encryptDataGroup, + objectToBase64, + uint8ArrayToBase64, + uint8ArrayToObject, +} from '../../../../plugins/plugins/core/components/qdn-action-encryption.js'; +import { publishData } from '../../../../plugins/plugins/utils/publish-image.js'; +import { parentEpml } from '../show-plugin.js'; +import '../notification-view/popover.js'; +import './avatar.js'; +import { setNewTab, setProfileData } from '../../redux/app/app-actions.js'; +import './profile-modal-update.js'; +import { modalHelper } from '../../../../plugins/plugins/utils/publish-modal.js'; + +class ProfileQdn extends connect(store)(LitElement) { + static get properties() { + return { + isOpen: { type: Boolean }, + syncPercentage: { type: Number }, + settingsRawData: { type: Object }, + valuesToBeSavedOnQdn: { type: Object }, + resourceExists: { type: Boolean }, + isSaving: { type: Boolean }, + fee: { type: Object }, + name: { type: String }, + isOpenProfileModalUpdate: { type: Boolean }, + editContent: { type: Object }, + profileData: { type: Object }, + imageUrl: { type: String }, + dialogOpenedProfile: {type: Boolean}, + profileDataVisiting: {type: Object}, + nameVisiting: {type: String}, + hasName: {type: Boolean}, + resourceExistsVisiting: {type:Boolean}, + error: {type: String} + }; + } + + constructor() { + super(); + this.isOpen = false; + this.getProfile = this.getProfile.bind(this); + this._handleQortalRequestSetData = + this._handleQortalRequestSetData.bind(this); + this._handleOpenVisiting = this._handleOpenVisiting.bind(this) + this.setValues = this.setValues.bind(this); + this.saveToQdn = this.saveToQdn.bind(this); + this.syncPercentage = 0; + this.hasRetrievedResource = false; + this.hasAttemptedToFetchResource = false; + this.resourceExists = undefined; + this.settingsRawData = null; + this.nodeUrl = this.getNodeUrl(); + this.myNode = this.getMyNode(); + this.valuesToBeSavedOnQdn = {}; + this.isSaving = false; + this.fee = null; + this.name = undefined; + this.uid = new ShortUniqueId(); + this.isOpenProfileModalUpdate = false; + this.editContent = null; + this.profileData = null; + this.qortalRequestCustomData = null; + this.imageUrl = ''; + this.dialogOpenedProfile = false + this.profileDataVisiting = null; + this.nameVisiting = "" + this.hasName = false + this.resourceExistsVisiting = undefined + this.error = "" + } + static styles = css` + + * { + + --mdc-theme-primary: rgb(3, 169, 244); + --mdc-theme-secondary: var(--mdc-theme-primary); + --mdc-theme-surface: var(--white); + --mdc-dialog-content-ink-color: var(--black); + box-sizing: border-box; + } + + + .header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + border-bottom: 1px solid #e0e0e0; + } + + .content { + padding: 16px; + } + .close { + visibility: hidden; + position: fixed; + z-index: -100; + right: -1000px; + } + + .parent-side-panel { + transform: translateX(100%); /* start from outside the right edge */ + transition: transform 0.3s ease-in-out; + } + .parent-side-panel.open { + transform: translateX(0); /* slide in to its original position */ + } + .notActive { + opacity: 0.5; + cursor: default; + color: var(--black); + } + .active { + opacity: 1; + cursor: pointer; + color: green; + } + .accept-button { + font-family: Roboto, sans-serif; + letter-spacing: 0.3px; + font-weight: 300; + padding: 8px 5px; + border-radius: 3px; + text-align: center; + color: var(--mdc-theme-primary); + transition: all 0.3s ease-in-out; + display: flex; + align-items: center; + gap: 10px; + font-size: 18px; + } + + .accept-button:hover { + cursor: pointer; + background-color: #03a8f485; + } + + .undo-button { + font-family: Roboto, sans-serif; + letter-spacing: 0.3px; + font-weight: 300; + padding: 8px 5px; + border-radius: 3px; + text-align: center; + color: #f44336; + transition: all 0.3s ease-in-out; + display: flex; + align-items: center; + gap: 10px; + font-size: 18px; + } + + .undo-button:hover { + cursor: pointer; + background-color: #f4433663; + } + .full-info-wrapper { + width: 100%; + min-width: 600px; + max-width: 600px; + text-align: center; + background: var(--white); + border: 1px solid var(--black); + border-radius: 15px; + padding: 25px; + box-shadow: 0px 10px 15px rgba(0, 0, 0, 0.1); + display: block !important; + } + + .full-info-logo { + width: 120px; + height: 120px; + background: var(--white); + border: 1px solid var(--black); + border-radius: 50%; + position: relative; + top: -110px; + left: 210px; + } + + .data-info{ + margin-top: 10px; + margin-right: 25px; + display:flex; + flex-direction: column; + align-items: flex-start; + min-height: 55vh; + max-height: 55vh; + overflow: auto; + } + + .data-info::-webkit-scrollbar-track { + background: #a1a1a1; + } + + .data-info::-webkit-scrollbar-thumb { + background-color: #6a6c75; + border-radius: 6px; + border: 3px solid #a1a1a1; + } + .data-info > * { + flex-shrink: 0; +} + .decline { + --mdc-theme-primary: var(--mdc-theme-error) + } + + .warning { + --mdc-theme-primary: #f0ad4e; + } + + .green { + --mdc-theme-primary: #198754; + } + + .buttons { + display: inline; + float: right; + margin-bottom: 5px; + } + + .paybutton { + display: inline; + float: left; + margin-bottom: 5px; + } + .round-fullinfo { + position: relative; + width: 120px; + height: 120px; + border-radius: 50%; + right: 25px; + top: -1px; + } + + h2 { + margin: 10px 0; + } + + h3 { + margin-top: -80px; + color: #03a9f4; + font-size: 18px; + } + + h4 { + margin: 5px 0; + } + + p { + margin-top: 5px; + line-height: 1.2; + font-size: 16px; + color: var(--black); + text-align: start; + overflow: hidden; + word-break: break-word; + } + `; + + getNodeUrl() { + const myNode = + window.parent.reduxStore.getState().app.nodeConfig.knownNodes[ + window.parent.reduxStore.getState().app.nodeConfig.node + ]; + + const nodeUrl = + myNode.protocol + '://' + myNode.domain + ':' + myNode.port; + return nodeUrl; + } + getMyNode() { + const myNode = + window.parent.reduxStore.getState().app.nodeConfig.knownNodes[ + window.parent.reduxStore.getState().app.nodeConfig.node + ]; + + return myNode; + } + + async getRawData(dataItem) { + const url = `${this.nodeUrl}/arbitrary/${dataItem.service}/${dataItem.name}/${dataItem.identifier}`; + const res = await fetch(url); + const data = await res.json(); + if (data.error) throw new Error('Cannot retrieve your data from qdn'); + return data; + } + + async getMyFollowedNames() { + let myFollowedNames = []; + try { + myFollowedNames = await parentEpml.request('apiCall', { + url: `/lists/followedNames?apiKey=${this.myNode.apiKey}`, + }); + } catch (error) {} + + return myFollowedNames; + } + + async followNames(names) { + let items = names; + let namesJsonString = JSON.stringify({ items: items }); + + let ret = await parentEpml.request('apiCall', { + url: `/lists/followedNames?apiKey=${this.myNode.apiKey}`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: `${namesJsonString}`, + }); + + return ret; + } + + + async setValues(response, resource) { + if (response) { + let data = { ...response }; + let customData = {}; + for (const key of Object.keys(data.customData || {})) { + if (key.includes('-private')) { + try { + const decryptedData = decryptGroupData( + data.customData[key] + ); + if (decryptedData && !decryptedData.error) { + const decryptedDataToBase64 = + uint8ArrayToObject(decryptedData); + if ( + decryptedDataToBase64 && + !decryptedDataToBase64.error + ) { + customData[key] = decryptedDataToBase64; + } + } + } catch (error) { + console.log({ error }); + } + } else { + customData[key] = data.customData[key]; + } + } + this.profileData = { + ...response, + customData, + }; + + store.dispatch(setProfileData(this.profileData)); + } + } + + async getVisitingProfile(name) { + try { + this.isLoadingVisitingProfile = true + this.nameVisiting = name + const url = `${this.nodeUrl}/arbitrary/resources/search?service=DOCUMENT&identifier=qortal_profile&name=${name}&prefix=true&exactmatchnames=true&excludeblocked=true&limit=20`; + const res = await fetch(url); + let data = ''; + try { + data = await res.json(); + if (Array.isArray(data)) { + data = data.filter( + (item) => item.identifier === 'qortal_profile' + ); + + if (data.length > 0) { + this.resourceExistsVisiting = true; + const dataItem = data[0]; + try { + const response = await this.getRawData(dataItem); + if (response.wallets) { + this.profileDataVisiting = response + // this.setValues(response, dataItem); + } else { + // this.error = 'Cannot get saved user settings'; + } + } catch (error) { + console.log({ error }); + // this.error = 'Cannot get saved user settings'; + } + } else { + this.resourceExistsVisiting = false; + } + } else { + // this.error = 'Unable to perform query'; + } + } catch (error) { + console.log({ error }); + data = { + error: 'No resource found', + }; + } + + } catch (error) { + console.log({ error }); + } finally { + this.isLoadingVisitingProfile = false + + } + } + + async getProfile() { + try { + this.error = '' + const arbFee = await this.getArbitraryFee(); + this.fee = arbFee; + this.hasAttemptedToFetchResource = true; + let resource; + + let nameObject + try { + nameObject = store.getState().app.accountInfo.names[0]; + + } catch (error) { + + } + if (!nameObject) { + this.name = null; + this.error = 'no name' + throw new Error('no name'); + } + this.hasName = true + const name = nameObject.name; + this.name = name; + const url = `${this.nodeUrl}/arbitrary/resources/search?service=DOCUMENT&identifier=qortal_profile&name=${name}&prefix=true&exactmatchnames=true&excludeblocked=true&limit=20`; + const res = await fetch(url); + let data = ''; + try { + data = await res.json(); + if (Array.isArray(data)) { + data = data.filter( + (item) => item.identifier === 'qortal_profile' + ); + + if (data.length > 0) { + this.resourceExists = true; + const dataItem = data[0]; + try { + const response = await this.getRawData(dataItem); + if (response.wallets) { + this.setValues(response, dataItem); + } else { + this.error = 'Cannot get saved user settings'; + } + } catch (error) { + this.error = 'Cannot get saved user settings'; + } + } else { + this.resourceExists = false; + } + } else { + this.error = 'Unable to perform query'; + } + } catch (error) { + data = { + error: 'No resource found', + }; + } + + if (resource) { + this.hasRetrievedResource = true; + } + } catch (error) { + console.log({ error }); + } + } + + stateChanged(state) { + if ( + state.app.nodeStatus && + state.app.nodeStatus.syncPercent !== this.syncPercentage + ) { + this.syncPercentage = state.app.nodeStatus.syncPercent; + + if ( + !this.hasAttemptedToFetchResource && + state.app.nodeStatus.syncPercent === 100 + ) { + this.getProfile(); + } + } + if ( + state.app.accountInfo && + state.app.accountInfo.names.length && + state.app.nodeStatus && + state.app.nodeStatus.syncPercent === 100 && this.hasName === false && this.hasAttemptedToFetchResource && state.app.accountInfo && state.app.accountInfo.names && state.app.accountInfo.names.length > 0 + ) { + this.getProfile(); + } + } + + async getArbitraryFee() { + const timestamp = Date.now(); + const url = `${this.nodeUrl}/transactions/unitfee?txType=ARBITRARY×tamp=${timestamp}`; + const response = await fetch(url); + if (!response.ok) { + throw new Error('Error when fetching arbitrary fee'); + } + const data = await response.json(); + const arbitraryFee = (Number(data) / 1e8).toFixed(8); + return { + timestamp, + fee: Number(data), + feeToShow: arbitraryFee, + }; + } + + async saveToQdn(data) { + try { + this.isSaving = true; + if (this.resourceExists === true && this.error) + throw new Error('Unable to save'); + + const nameObject = store.getState().app.accountInfo.names[0]; + if (!nameObject) throw new Error('no name'); + + const arbitraryFeeData = await modalHelper.getArbitraryFee(); + const res = await modalHelper.showModalAndWaitPublish({ + feeAmount: arbitraryFeeData.feeToShow, + }); + if (res.action !== 'accept') + throw new Error('User declined publish'); + const name = nameObject.name; + const identifer = 'qortal_profile'; + const filename = 'qortal_profile.json'; + const selectedAddress = store.getState().app.selectedAddress; + const getArbitraryFee = await this.getArbitraryFee(); + const feeAmount = getArbitraryFee.fee; + + let newObject = structuredClone(data) + + for (const key of Object.keys(newObject.customData || {})) { + if (key.includes('-private')) { + const dataKey = newObject.customData[key] + let isBase64 = false + try { + const decodedString = atob(dataKey); + isBase64 = decodedString.includes('qortalGroupEncryptedData') + } catch (e) { + console.log(e) + } + if(isBase64){ + newObject['customData'][key] = newObject.customData[key]; + } else { + const toBase64 = await objectToBase64( + newObject.customData[key] + ); + const encryptedData = encryptDataGroup({ + data64: toBase64, + publicKeys: [], + }); + newObject['customData'][key] = encryptedData; + } + + } else { + newObject['customData'][key] = newObject.customData[key]; + } + } + const newObjectToBase64 = await objectToBase64(newObject); + + const worker = new WebWorker2(); + try { + const resPublish = await publishData({ + registeredName: encodeURIComponent(name), + file: newObjectToBase64, + service: 'DOCUMENT', + identifier: encodeURIComponent(identifer), + parentEpml: parentEpml, + uploadType: 'file', + selectedAddress: selectedAddress, + worker: worker, + isBase64: true, + filename: filename, + apiVersion: 2, + withFee: true, + feeAmount: feeAmount, + }); + + this.resourceExists = true; + this.profileData = data; + store.dispatch(setProfileData(data)); + + + worker.terminate(); + } catch (error) { + worker.terminate(); + } + } catch (error) { + console.log({ error }); + throw new Error(error.message); + } finally { + this.isSaving = false; + } + } + + sendBackEvent(detail) { + let iframes; + + const mainApp = document.getElementById('main-app'); + if (mainApp && mainApp.shadowRoot) { + const appView = mainApp.shadowRoot.querySelector('app-view'); + if (appView && appView.shadowRoot) { + const showPlugin = + appView.shadowRoot.querySelector('show-plugin'); + if (showPlugin && showPlugin.shadowRoot) { + iframes = showPlugin.shadowRoot.querySelectorAll('iframe'); + } + } + } + + iframes.forEach((iframe) => { + const iframeWindow = iframe.contentWindow; + const customEvent = new CustomEvent( + 'qortal-request-set-profile-data-response', + { + detail: detail, + } + ); + + iframeWindow.dispatchEvent(customEvent); + }); + } + + async _handleQortalRequestSetData(event) { + const detail = event.detail; + + try { + if (!detail.property || !detail.payload) + throw new Error('not saved'); + if ( + !this.profileData && + (this.resourceExists || this.resourceExists === undefined) + ) + throw new Error("unable to fetch the user's profile data"); + this.isOpenProfileModalUpdate = true; + this.editContent = { + ...(this.profileData || {}), + }; + if (detail.payload.customData) { + this.qortalRequestCustomData = detail; + } + + // Wait for response event + const response = await new Promise((resolve, reject) => { + function handleResponseEvent(event) { + // Handle the data from the event, if any + const responseData = event.detail; + // Clean up by removing the event listener once we've received the response + window.removeEventListener( + 'send-back-event', + handleResponseEvent + ); + + if (responseData.response === 'saved') { + resolve(responseData); + } else { + reject(new Error(responseData.error)); + } + } + + // Set up an event listener to wait for the response + window.addEventListener('send-back-event', handleResponseEvent); + }); + + this.sendBackEvent({ + response: response.response, + uniqueId: detail.uniqueId, + }); + } catch (error) { + this.sendBackEvent({ + response: 'error', + uniqueId: detail.uniqueId, + }); + } + } + + _handleOpenVisiting(event){ + try { + const name = event.detail; + this.getVisitingProfile(name) + this.dialogOpenedProfile = true + } catch (error) { + + } + } + + connectedCallback() { + super.connectedCallback(); + window.addEventListener( + 'qortal-request-set-profile-data', + this._handleQortalRequestSetData + ); + window.addEventListener( + 'open-visiting-profile', + this._handleOpenVisiting + ); + } + + disconnectedCallback() { + window.removeEventListener( + 'qortal-request-set-profile-data', + this._handleQortalRequestSetData + ); + window.removeEventListener( + 'open-visiting-profile', + this._handleOpenVisiting + ); + super.disconnectedCallback(); + } + + onClose(isSuccess) { + this.isOpenProfileModalUpdate = false; + this.editContent = null; + if (this.qortalRequestCustomData) { + // Create and dispatch custom event + const customEvent = new CustomEvent('send-back-event', { + detail: { + response: isSuccess ? 'saved' : 'not saved', + }, + }); + window.dispatchEvent(customEvent); + + this.qortalRequestCustomData = null; + } + } + + avatarFullImage() { + this.imageUrl = `${this.nodeUrl}/arbitrary/THUMBNAIL/${this.nameVisiting}/qortal_avatar?async=true&apiKey=${this.myNode.apiKey}`; + + return html``; + } + + openUserInfo() { + + const infoDialog = document.getElementById('main-app').shadowRoot.querySelector('app-view').shadowRoot.querySelector('user-info-view') + infoDialog.openUserInfo(this.nameVisiting) + + } + + openEdit(){ + this.isOpenProfileModalUpdate = !this.isOpenProfileModalUpdate; + } + + onCloseVisitingProfile(){ + this.profileDataVisiting = null; + this.nameVisiting = "" + this.imageUrl = ''; + this.resourceExistsVisiting = undefined + } + + updated(changedProperties){ + if ( + changedProperties && + changedProperties.has('dialogOpenedProfile') && + this.dialogOpenedProfile === false + ) { + + const prevVal = changedProperties.get('dialogOpenedProfile') + if(prevVal === true) this.onCloseVisitingProfile() + } + + } + render() { + return html` + ${this.isSaving || + (!this.error && this.resourceExists === undefined) + ? html` + + ` + : !this.name + ? html` + { + const target = this.shadowRoot.getElementById( + 'popover-notification' + ); + const popover = + this.shadowRoot.querySelector( + 'popover-component' + ); + if (popover) { + popover.openPopover(target); + } + }} + style="user-select:none;cursor:pointer" + >account_circle + + + +
+

+ ${translate('profile.profile1')} +

+
+
+
+ ${translate('profile.profile2')} +
+
+
+ ` + : this.error + ? html` +
+ +
+ + + ` + : html` +
{ + if (this.resourceExists && this.profileData) { + this.editContent = this.profileData; + } else if ( + this.resourceExists && + !this.profileData + ) { + return; + } + if(this.profileData){ + this.profileDataVisiting = this.profileData + this.nameVisiting = this.name + this.dialogOpenedProfile = true + } else { + this.isOpenProfileModalUpdate = + !this.isOpenProfileModalUpdate; + } + + }} + > + +
+ `} + + { + this.isOpenProfileModalUpdate = val; + }} + .onSubmit=${this.saveToQdn} + .editContent=${this.editContent} + .onClose=${(val) => this.onClose(val)} + .qortalRequestCustomData=${this.qortalRequestCustomData} + > + + + + + ${this.dialogOpenedProfile ? html` + + +

${this.nameVisiting}

+ + +
+ ${this.isLoadingVisitingProfile ? html` +
+ +
+ ` : this.resourceExistsVisiting === false ? html` +
+

${translate('profile.profile16')}

+
+ ` : this.profileDataVisiting === null ? html` +
+

${translate('profile.profile17')}

+
+ ` : html` +

${translate('profile.profile4')}

+

${this.profileDataVisiting.tagline || translate('profile.profile15')}

+

${translate('profile.profile5')}

+

${this.profileDataVisiting.bio || translate('profile.profile15')}

+

${translate('profile.profile6')}

+ ${Object.keys(this.profileDataVisiting.wallets).map((key, i)=> { + return html ` +

+ ${key}: ${this.profileDataVisiting.wallets[key] || translate('profile.profile15')} +

+ ` + })} + `} + +
+ + +
+ + this.openUserInfo()} + >${translate('profile.profile14')} + + + ${this.nameVisiting === this.name ? html` + this.openEdit()}>${translate("profile.profile3")} + ` : ''} + + { + this.dialogOpenedProfile = false + }} + >${translate('general.close')} + +
+
+ + + ` : ''} + + + + + + + + + + + + `; + } +} + +customElements.define('profile-qdn', ProfileQdn); diff --git a/core/src/components/friends-view/save-settings-qdn.js b/core/src/components/friends-view/save-settings-qdn.js index 37b64f54..f339e0b0 100644 --- a/core/src/components/friends-view/save-settings-qdn.js +++ b/core/src/components/friends-view/save-settings-qdn.js @@ -1,14 +1,17 @@ import {css, html, LitElement} from 'lit'; import '@material/mwc-icon'; import './friends-side-panel.js'; -import {connect} from 'pwa-helpers'; -import {store} from '../../store.js'; -import WebWorker from 'web-worker:./computePowWorkerFile.src.js'; +import { connect } from 'pwa-helpers'; +import { store } from '../../store.js'; +import WebWorker from '../WebWorkerFile.js'; import '@polymer/paper-spinner/paper-spinner-lite.js'; import '@vaadin/tooltip'; -import {translate} from 'lit-translate'; +import { get, translate } from 'lit-translate'; +import ShortUniqueId from 'short-unique-id'; + import { decryptGroupData, + encryptDataGroup, objectToBase64, uint8ArrayToObject, @@ -16,6 +19,7 @@ import { import {publishData} from '../../../../plugins/plugins/utils/publish-image.js'; import {parentEpml} from '../show-plugin.js'; import '../notification-view/popover.js'; +import { setNewTab } from '../../redux/app/app-actions.js'; class SaveSettingsQdn extends connect(store)(LitElement) { static get properties() { @@ -27,6 +31,9 @@ class SaveSettingsQdn extends connect(store)(LitElement) { resourceExists: { type: Boolean }, isSaving: { type: Boolean }, fee: { type: Object }, + hasName: {type: Boolean}, + error: {type: String}, + name: {type: String} }; } @@ -47,6 +54,11 @@ class SaveSettingsQdn extends connect(store)(LitElement) { this.valuesToBeSavedOnQdn = {}; this.isSaving = false; this.fee = null; + this.hasName = false; + this.error = ""; + this.uid = new ShortUniqueId(); + this.name = undefined; + } static styles = css` :host { @@ -309,13 +321,26 @@ class SaveSettingsQdn extends connect(store)(LitElement) { async getGeneralSettingsQdn() { try { + this.error = "" const arbFee = await this.getArbitraryFee(); this.fee = arbFee; this.hasAttemptedToFetchResource = true; let resource; - const nameObject = store.getState().app.accountInfo.names[0]; - if (!nameObject) throw new Error('no name'); + let nameObject + try { + nameObject = store.getState().app.accountInfo.names[0]; + + } catch (error) { + + } + if (!nameObject) { + this.name = null; + this.error = 'no name' + throw new Error('no name'); + } const name = nameObject.name; + this.name = name; + this.hasName = true this.error = ''; const url = `${this.nodeUrl}/arbitrary/resources/search?service=DOCUMENT_PRIVATE&identifier=qortal_general_settings&name=${name}&prefix=true&exactmatchnames=true&excludeblocked=true&limit=20`; const res = await fetch(url); @@ -363,8 +388,6 @@ class SaveSettingsQdn extends connect(store)(LitElement) { stateChanged(state) { if ( - state.app.accountInfo && - state.app.accountInfo.names.length && state.app.nodeStatus && state.app.nodeStatus.syncPercent !== this.syncPercentage ) { @@ -377,6 +400,15 @@ class SaveSettingsQdn extends connect(store)(LitElement) { this.getGeneralSettingsQdn(); } } + + if ( + state.app.accountInfo && + state.app.accountInfo.names.length && + state.app.nodeStatus && + state.app.nodeStatus.syncPercent === 100 && this.hasName === false && this.hasAttemptedToFetchResource && state.app.accountInfo && state.app.accountInfo.names && state.app.accountInfo.names.length > 0 + ) { + this.getGeneralSettingsQdn(); + } } async getArbitraryFee() { @@ -529,7 +561,78 @@ class SaveSettingsQdn extends connect(store)(LitElement) { style="display: block; margin: 0 auto;" > ` - : html` + : !this.name ? html` + { + const target = this.shadowRoot.getElementById( + 'popover-notification' + ); + const popover = + this.shadowRoot.querySelector( + 'popover-component' + ); + if (popover) { + popover.openPopover(target); + } + }} + style="user-select:none;cursor:pointer" + >save + + + +
+

+ ${translate('profile.profile1')} +

+
+
+
+ ${translate('profile.profile2')} +
+
+
+ ` : html` store.getState().app.loggedIn) @@ -19,6 +20,7 @@ export const chatHeadsStateStream = new EpmlStream(CHAT_HEADS_STREAM_NAME, () => export const nodeConfigStream = new EpmlStream(NODE_CONFIG_STREAM_NAME, () => store.getState().app.nodeConfig) export const chatLastSeenStream = new EpmlStream(CHAT_LAST_SEEN, () => store.getState().app.chatLastSeen) export const sideEffectActionStream = new EpmlStream(SIDE_EFFECT_ACTION, () => store.getState().app.sideEffectAction) +export const profileDataActionStream = new EpmlStream(SIDE_EFFECT_ACTION, () => store.getState().app.profileData) export const coinBalancesActionStream = new EpmlStream(COIN_BALANCES_ACTION, () => store.getState().app.coinBalances) @@ -65,6 +67,9 @@ store.subscribe(() => { if (oldState.app.sideEffectAction !== state.app.sideEffectAction) { sideEffectActionStream.emit(state.app.sideEffectAction) } + if(oldState.app.profileDataActionStream !== state.app.profileDataActionStream){ + profileDataActionStream.emit(state.app.profileData) + } if (oldState.app.coinBalances !== state.app.coinBalances) { coinBalancesActionStream.emit(state.app.coinBalances) } diff --git a/core/src/redux/app/actions/app-core.js b/core/src/redux/app/actions/app-core.js index 77f99a87..694e8858 100644 --- a/core/src/redux/app/actions/app-core.js +++ b/core/src/redux/app/actions/app-core.js @@ -19,7 +19,8 @@ import { SET_TAB_NOTIFICATIONS, UPDATE_BLOCK_INFO, UPDATE_NODE_INFO, - UPDATE_NODE_STATUS + UPDATE_NODE_STATUS, + SET_PROFILE_DATA } from '../app-action-types.js' export const doUpdateBlockInfo = (blockObj) => { @@ -179,6 +180,12 @@ export const setSideEffectAction = (payload)=> { payload } } +export const setProfileData = (payload)=> { + return { + type: SET_PROFILE_DATA, + payload + } +} export const setCoinBalances = (payload)=> { return { diff --git a/core/src/redux/app/app-action-types.js b/core/src/redux/app/app-action-types.js index 97165152..81c0793b 100644 --- a/core/src/redux/app/app-action-types.js +++ b/core/src/redux/app/app-action-types.js @@ -33,4 +33,5 @@ export const SET_TAB_NOTIFICATIONS = 'SET_TAB_NOTIFICATIONS' export const IS_OPEN_DEV_DIALOG = 'IS_OPEN_DEV_DIALOG' export const SET_NEW_NOTIFICATION = 'SET_NEW_NOTIFICATION' export const SET_SIDE_EFFECT= 'SET_SIDE_EFFECT' +export const SET_PROFILE_DATA = 'SET_PROFILE_DATA' export const SET_COIN_BALANCES= 'SET_COIN_BALANCES' diff --git a/core/src/redux/app/app-reducer.js b/core/src/redux/app/app-reducer.js index 8de46a5f..ba1419ca 100644 --- a/core/src/redux/app/app-reducer.js +++ b/core/src/redux/app/app-reducer.js @@ -35,7 +35,8 @@ import { SET_TAB_NOTIFICATIONS, UPDATE_BLOCK_INFO, UPDATE_NODE_INFO, - UPDATE_NODE_STATUS + UPDATE_NODE_STATUS, + SET_PROFILE_DATA } from './app-action-types.js' import {initWorkersReducer} from './reducers/init-workers.js' import {loginReducer} from './reducers/login-reducer.js' @@ -90,6 +91,7 @@ const INITIAL_STATE = { isOpenDevDialog: false, newNotification: null, sideEffectAction: null, + profileData: null, coinBalances: {} } @@ -331,6 +333,12 @@ export default (state = INITIAL_STATE, action) => { sideEffectAction: action.payload } } + case SET_PROFILE_DATA: { + return { + ...state, + profileData: action.payload + } + } case SET_COIN_BALANCES: { const copyBalances = {...state.coinBalances} copyBalances[action.payload.type] = { diff --git a/plugins/plugins/core/components/ChatGroupsModal.js b/plugins/plugins/core/components/ChatGroupsModal.js index efd29dce..0770db0d 100644 --- a/plugins/plugins/core/components/ChatGroupsModal.js +++ b/plugins/plugins/core/components/ChatGroupsModal.js @@ -65,7 +65,6 @@ export class ChatGroupsModal extends LitElement { render() { - console.log('hello') return html`

${translate("chatpage.cchange42")}

- this.sendMessage(this.myTrimmedMeassage)} dialog-confirm>${translate("transpage.tchange3")} + { + this.sendMessage(this.myMessageUnder4Qort) + + }} dialog-confirm>${translate("transpage.tchange3")}
` : ""} -
+
+ ${this.userName ? html` +
{ + setTimeout(() => { + this.openProfile() + }, 250); + this.setOpenUserInfo(false) + + + }}> + ${translate("profile.profile18")} +
+ ` : ''} +
` } diff --git a/plugins/plugins/core/components/qdn-action-types.js b/plugins/plugins/core/components/qdn-action-types.js index 271bb05c..83afa057 100644 --- a/plugins/plugins/core/components/qdn-action-types.js +++ b/plugins/plugins/core/components/qdn-action-types.js @@ -70,5 +70,11 @@ export const VOTE_ON_POLL= 'VOTE_ON_POLL' //CREATE_POLL export const CREATE_POLL= 'CREATE_POLL' +//GET_PROFILE_DATA +export const GET_PROFILE_DATA = 'GET_PROFILE_DATA' + + +// SET_PROFILE_DATA +export const SET_PROFILE_DATA= 'SET_PROFILE_DATA' //GET_DAY_SUMMARY export const GET_DAY_SUMMARY = 'GET_DAY_SUMMARY' diff --git a/plugins/plugins/core/qdn/browser/browser.src.js b/plugins/plugins/core/qdn/browser/browser.src.js index e1cac571..8f7d07e2 100644 --- a/plugins/plugins/core/qdn/browser/browser.src.js +++ b/plugins/plugins/core/qdn/browser/browser.src.js @@ -481,11 +481,10 @@ class WebBrowser extends LitElement { const arbitraryFee = (Number(data) / 1e8).toFixed(8) return { timestamp, - fee : Number(data), + fee: Number(data), feeToShow: arbitraryFee } } - 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 @@ -660,9 +659,9 @@ class WebBrowser extends LitElement { } const makeTransactionRequest = async (lastRef) => { - let votedialog1 = get("transactions.votedialog1") - let votedialog2 = get("transactions.votedialog2") - let feeDialog = get("walletpage.wchange12") + let votedialog1 = get("transactions.votedialog1") + let votedialog2 = get("transactions.votedialog2") + let feeDialog = get("walletpage.wchange12") let myTxnrequest = await parentEpml.request('transaction', { type: 9, @@ -1622,7 +1621,7 @@ class WebBrowser extends LitElement { } case actions.SEND_LOCAL_NOTIFICATION: { - const {title, url, icon, message} = data + const { title, url, icon, message } = data try { const id = `appNotificationList-${this.selectedAddress.address}` const checkData = localStorage.getItem(id) ? JSON.parse(localStorage.getItem(id)) : null @@ -1641,7 +1640,7 @@ class WebBrowser extends LitElement { this.updateLastNotification(id, this.name) break } else { - throw new Error(`duration until another notification can be sent: ${interval - timeDifference}`) + throw new Error(`invalid data`) } } else if(!lastNotification){ parentEpml.request('showNotification', { @@ -2064,6 +2063,199 @@ class WebBrowser extends LitElement { break } + case 'GET_PROFILE_DATA': { + const defaultProperties = ['tagline', 'bio', 'wallets'] + const requiredFields = ['property']; + const missingFields = []; + + requiredFields.forEach((field) => { + if (!data[field] && data[field] !== 0) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}` + let data = {}; + data['error'] = errorMsg; + response = JSON.stringify(data); + break + } + + + try { + const profileData = window.parent.reduxStore.getState().app.profileData + if (!profileData) { + throw new Error('User does not have a profile') + } + const property = data.property + const propertyIndex = defaultProperties.indexOf(property) + if (propertyIndex !== -1) { + const requestedData = profileData[property] + if (requestedData) { + response = JSON.stringify(requestedData); + break + } else { + throw new Error('Cannot find requested data') + } + } + + if (property.includes('-private')) { + const resPrivateProperty = await showModalAndWait( + actions.GET_PROFILE_DATA, { + property + } + ); + + if (resPrivateProperty.action === 'accept') { + + const requestedData = profileData.customData[property] + if (requestedData) { + response = JSON.stringify(requestedData); + break + } else { + throw new Error('Cannot find requested data') + } + } else { + throw new Error('User denied permission for private property') + } + } else { + const requestedData = profileData.customData[property] + if (requestedData) { + response = JSON.stringify(requestedData); + break + } else { + throw new Error('Cannot find requested data') + } + } + + } 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 'SET_PROFILE_DATA': { + const requiredFields = ['property', 'data']; + const missingFields = []; + + requiredFields.forEach((field) => { + if (!data[field] && data[field] !== 0) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}` + let data = {}; + data['error'] = errorMsg; + response = JSON.stringify(data); + break + } + + + try { + const property = data.property + const payload = data.data + const uniqueId = this.uid.rnd() + const fee = await this.getArbitraryFee() + const resSetPrivateProperty = await showModalAndWait( + actions.SET_PROFILE_DATA, { + property, + fee: fee.feeToShow + } + ); + + + if (resSetPrivateProperty.action !== 'accept') throw new Error('User declined permission') + + //dispatch event and wait until I get a response to continue + + // Create and dispatch custom event + const customEvent = new CustomEvent('qortal-request-set-profile-data', { + detail: { + property, + payload, + uniqueId + } + }); + window.parent.dispatchEvent(customEvent); + + // Wait for response event + const res = await new Promise((resolve, reject) => { + function handleResponseEvent(event) { + // Handle the data from the event, if any + const responseData = event.detail; + if(responseData && responseData.uniqueId !== uniqueId) return + // Clean up by removing the event listener once we've received the response + window.removeEventListener('qortal-request-set-profile-data-response', handleResponseEvent); + + if (responseData.response === 'saved') { + resolve(responseData); + } else { + reject(new Error('not saved')); + } + } + + // Set up an event listener to wait for the response + window.addEventListener('qortal-request-set-profile-data-response', handleResponseEvent); + }); + if(!res.response) throw new Error('Failed to set property') + response = JSON.stringify(res.response); + + } catch (error) { + const obj = {}; + const errorMsg = error.message || 'Failed to set property.'; + obj['error'] = errorMsg; + response = JSON.stringify(obj); + } finally { + this.loader.hide(); + } + break; + } + + case 'OPEN_PROFILE': { + const requiredFields = ['name']; + const missingFields = []; + + requiredFields.forEach((field) => { + if (!data[field] && data[field] !== 0) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}` + let data = {}; + data['error'] = errorMsg; + response = JSON.stringify(data); + break + } + + + try { + const customEvent = new CustomEvent('open-visiting-profile', { + detail: data.name + }); + window.parent.dispatchEvent(customEvent); + response = JSON.stringify(true); + } catch (error) { + const obj = {}; + const errorMsg = error.message || 'Failed to open profile'; + obj['error'] = errorMsg; + response = JSON.stringify(obj); + } + break; + } + + case actions.GET_USER_WALLET: { const requiredFields = ['coin']; const missingFields = []; @@ -3561,7 +3753,7 @@ async function showModalAndWait(type, data) { ${type === actions.GET_USER_WALLET ? ` ` : ''} ${type === actions.GET_WALLET_BALANCE ? ` @@ -3593,6 +3785,21 @@ async function showModalAndWait(type, data) {
` : ''} + ${type === actions.GET_PROFILE_DATA ? ` + + ` : ''} + ${type === actions.SET_PROFILE_DATA ? ` + + ` : ''} ${type === actions.NOTIFICATIONS_PERMISSION ? `