+
@@ -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`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
${translate('profile.profile6')}
+
+ ${Object.keys(this.wallets).map((key) => {
+ return html`
+
+
+
+ {
+ this.wallets = {
+ ...this.wallets,
+ [key]: e.target.value,
+ };
+ }}
+ />
+
+
+ this.fillAddress(key)}
+ style="color:var(--black);cursor:pointer"
+ >upload_2
+
+
+
+
+ `;
+ })}
+
+
+ ${Object.keys(this.customData).map((key) => {
+ return html`
+
+
+
+
+ ${key}
+
+
+
+ this.updateCustomData(key,this.customData[key])}
+ style="color:var(--black);cursor:pointer"
+ >edit
+
+ this.removeCustomData(key)}
+ style="color:var(--black);cursor:pointer"
+ >remove
+
+
+ `;
+ })}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ this.newCustomDataKey = e.target.value
+ }}
+ />
+
+
+
+
${translate('profile.profile10')}
+
+ ${Object.keys(this.newCustomDataField).map((key) => {
+ return html`
+
+
+
+
+ {
+ this.newCustomDataField = {
+ ...this.newCustomDataField,
+ [key]: e.target.value,
+ };
+ }}
+ />
+
+
+ this.removeField(key)}
+ style="color:var(--black);cursor:pointer"
+ >remove
+
+
+ `;
+ })}
+
+
+
+ {
+ this.newFieldName = e.target.value
+
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+}
+
+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')}
+
+
+
+
{
+ store.dispatch(
+ setNewTab({
+ url: `group-management`,
+ id: this.uid.rnd(),
+ myPlugObj: {
+ url: 'name-registration',
+ domain: 'core',
+ page: 'name-registration/index.html',
+ title: 'Name Registration',
+ icon: 'vaadin:user-check',
+ mwcicon: 'manage_accounts',
+ pluginNumber:
+ 'plugin-qCmtXAQmtu',
+ menus: [],
+ parent: false,
+ },
+ openExisting: true,
+ })
+ );
+ const popover =
+ this.shadowRoot.querySelector(
+ 'popover-component'
+ );
+ if (popover) {
+ popover.closePopover();
+ }
+ }}"
+ >
+ ${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.avatarFullImage()}
+ ${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')}
+
+
+
+
{
+ store.dispatch(
+ setNewTab({
+ url: `group-management`,
+ id: this.uid.rnd(),
+ myPlugObj: {
+ url: 'name-registration',
+ domain: 'core',
+ page: 'name-registration/index.html',
+ title: 'Name Registration',
+ icon: 'vaadin:user-check',
+ mwcicon: 'manage_accounts',
+ pluginNumber:
+ 'plugin-qCmtXAQmtu',
+ menus: [],
+ parent: false,
+ },
+ openExisting: true,
+ })
+ );
+ const popover =
+ this.shadowRoot.querySelector(
+ 'popover-component'
+ );
+ if (popover) {
+ popover.closePopover();
+ }
+ }}"
+ >
+ ${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")}
`
: ""}
-
`
}
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) {