diff --git a/blog-test.json b/blog-test.json new file mode 100644 index 00000000..35323b11 --- /dev/null +++ b/blog-test.json @@ -0,0 +1,31 @@ +{ + "name": "Q-Blog", + "defaultFeedIndex": 0, + "feed": [ + { + "id": "post-creation", + "version": 1, + "updated": 1696646223261, + "title": "Q-Blog Post creations", + "description": "blablabla", + "search": { + "query": "-post-", + "identifier": "q-blog-", + "service": "BLOG_POST", + "exactmatchnames": true + }, + "click": "qortal://APP/Q-Blog/$${resource.name}$$/$${customParams.blogId}$$/$${customParams.shortIdentifier}$$", + "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 71499c52..0c4b866d 100644 --- a/core/language/us.json +++ b/core/language/us.json @@ -1182,6 +1182,36 @@ "notifications": { "notify1": "Confirming transaction", "notify2": "Transaction confirmed", - "explanation": "Your transaction is getting confirmed. To track its progress, click on the bell icon." + "explanation": "Your transaction is getting confirmed. To track its progress, click on the bell icon.", + "status1": "Fully synced", + "status2": "Not synced", + "notify3": "No notifications", + "notify4": "Tx notifications" + }, + "friends": { + "friend1": "Add name", + "friend2": "Add friend", + "friend3": "Adding a friend allows you to connect easily with that person. Be sure to also follow that user to support the hosting of their published resources.", + "friend4": "Notes", + "friend5": "Follow name", + "friend6": "Alias", + "friend7": "Add an alias to better remember your friend (Optional)", + "friend8": "Send Q-Chat", + "friend9": "Send Q-Mail", + "friend10": "Edit friend", + "friend11": "Following", + "friend12": "Friends", + "friend13": "Feed", + "friend14": "Remove friend", + "friend15": "Feed settings", + "friend16": "Select the Q-Apps you want updates from, especially those related to your friends.", + "friends17": "Friends", + "friends18": "No items in your feed" + }, + "save": { + "saving1": "Unable to fetch saved settings", + "saving2": "Nothing to save", + "saving3": "Save unsaved changes", + "saving4": "Undo changes" } } \ No newline at end of file diff --git a/core/src/components/app-view.js b/core/src/components/app-view.js index ba13820d..1bf6a5fa 100644 --- a/core/src/components/app-view.js +++ b/core/src/components/app-view.js @@ -43,8 +43,9 @@ import '../functional-components/side-menu-item.js' import './start-minting.js' import './notification-view/notification-bell.js' 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' const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }) class AppView extends connect(store)(LitElement) { @@ -583,8 +584,10 @@ class AppView extends connect(store)(LitElement) {
+ +
@@ -671,6 +674,8 @@ class AppView extends connect(store)(LitElement) {
+
+ ` } diff --git a/core/src/components/friends-view/ChatSideNavHeads.js b/core/src/components/friends-view/ChatSideNavHeads.js new file mode 100644 index 00000000..2bae1d7a --- /dev/null +++ b/core/src/components/friends-view/ChatSideNavHeads.js @@ -0,0 +1,224 @@ +import { LitElement, html, css } from 'lit' +import { render } from 'lit/html.js' +import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate' +import '@material/mwc-icon' +import '@vaadin/tooltip'; + +import './friend-item-actions' + +class ChatSideNavHeads extends LitElement { + static get properties() { + return { + selectedAddress: { type: Object }, + config: { type: Object }, + chatInfo: { type: Object }, + iconName: { type: String }, + activeChatHeadUrl: { type: String }, + isImageLoaded: { type: Boolean }, + setActiveChatHeadUrl: {attribute: false}, + openEditFriend: {attribute: false}, + closeSidePanel: {attribute: false, type: Object} + } + } + + static get styles() { + return css` + :host { + width: 100%; + } + ul { + list-style-type: none; + } + li { + padding: 10px 2px 10px 5px; + cursor: pointer; + width: 100%; + display: flex; + box-sizing: border-box; + font-size: 14px; + transition: 0.2s background-color; + } + + li:hover { + background-color: var(--lightChatHeadHover); + } + + .active { + background: var(--menuactive); + border-left: 4px solid #3498db; + } + + .img-icon { + font-size:40px; + color: var(--chat-group); + } + + .status { + color: #92959e; + } + + .clearfix { + display: flex; + align-items: center; + } + + .clearfix:after { + visibility: hidden; + display: block; + font-size: 0; + content: " "; + clear: both; + height: 0; + } + ` + } + + constructor() { + super() + this.selectedAddress = {} + this.config = { + user: { + node: { + + } + } + } + this.chatInfo = {} + this.iconName = '' + this.activeChatHeadUrl = '' + this.isImageLoaded = false + this.imageFetches = 0 + } + + createImage(imageUrl) { + const imageHTMLRes = new Image(); + imageHTMLRes.src = imageUrl; + imageHTMLRes.style= "width:30px; height:30px; float: left; border-radius:50%; font-size:14px"; + imageHTMLRes.onclick= () => { + this.openDialogImage = true; + } + imageHTMLRes.onload = () => { + this.isImageLoaded = true; + } + imageHTMLRes.onerror = () => { + if (this.imageFetches < 4) { + setTimeout(() => { + this.imageFetches = this.imageFetches + 1; + imageHTMLRes.src = imageUrl; + }, 500); + } else { + this.isImageLoaded = false + } + }; + return imageHTMLRes; + } + + render() { + let avatarImg = "" + if (this.chatInfo.name) { + const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]; + const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port; + const avatarUrl = `${nodeUrl}/arbitrary/THUMBNAIL/${this.chatInfo.name}/qortal_avatar?async=true&apiKey=${myNode.apiKey}`; + avatarImg = this.createImage(avatarUrl) + } + + return html` +
  • { + const target = e.target + const popover = + this.shadowRoot.querySelector('friend-item-actions'); + if (popover) { + popover.openPopover(target); + } + }} class="clearfix" id=${`friend-item-parent-${this.chatInfo.name}`}> +
    + ${this.isImageLoaded ? html`${avatarImg}` : html``} + ${!this.isImageLoaded && !this.chatInfo.name && !this.chatInfo.groupName + ? html`account_circle` + : html``} + ${!this.isImageLoaded && this.chatInfo.name + ? html`
    + ${this.chatInfo.name.charAt(0)} +
    ` + : ""} + ${!this.isImageLoaded && this.chatInfo.groupName + ? html`
    + ${this.chatInfo.groupName.charAt(0)} +
    ` + : ""} +
    +
    + + ${this.chatInfo.groupName + ? this.chatInfo.groupName + : this.chatInfo.name !== undefined + ? (this.chatInfo.alias || this.chatInfo.name) + : this.chatInfo.address.substr(0, 15)} + +
    +
    + +
    +
    + ${this.chatInfo.willFollow ? html` + connect_without_contact + + + ` : ''} +
    +
  • + { + this.openEditFriend(this.chatInfo) + }} + name=${this.chatInfo.name} + .closeSidePanel=${this.closeSidePanel} + > + ` + } + + + + shouldUpdate(changedProperties) { + if(changedProperties.has('activeChatHeadUrl')){ + return true + } + if(changedProperties.has('chatInfo')){ + return true + } + if(changedProperties.has('isImageLoaded')){ + return true + } + + return false + } + + getUrl(chatUrl) { + this.setActiveChatHeadUrl(chatUrl) + } + + +} + +window.customElements.define('chat-side-nav-heads', ChatSideNavHeads) diff --git a/core/src/components/friends-view/add-friends-modal.js b/core/src/components/friends-view/add-friends-modal.js new file mode 100644 index 00000000..f89e36de --- /dev/null +++ b/core/src/components/friends-view/add-friends-modal.js @@ -0,0 +1,496 @@ +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-dialog'; +import '@material/mwc-checkbox'; +import { connect } from 'pwa-helpers'; +import { store } from '../../store'; +import '@polymer/paper-spinner/paper-spinner-lite.js' + +class AddFriendsModal extends connect(store)(LitElement) { + static get properties() { + return { + isOpen: { type: Boolean }, + setIsOpen: { attribute: false }, + isLoading: { type: Boolean }, + userSelected: { type: Object }, + alias: { type: String }, + willFollow: { type: Boolean }, + notes: { type: String }, + onSubmit: { attribute: false }, + editContent: { type: Object }, + onClose: { attribute: false }, + mySelectedFeeds: { type: Array }, + availableFeeedSchemas: {type: Array}, + isLoadingSchemas: {type: Boolean} + }; + } + + constructor() { + super(); + this.isOpen = false; + this.isLoading = false; + this.alias = ''; + this.willFollow = true; + this.notes = ''; + this.nodeUrl = this.getNodeUrl(); + this.myNode = this.getMyNode(); + this.mySelectedFeeds = []; + this.availableFeeedSchemas = []; + this.isLoadingSchemas= 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; + } + `; + } + + firstUpdated() {} + + 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.alias = ''; + this.willFollow = true; + this.notes = ''; + } + + addFriend() { + this.onSubmit({ + name: this.userSelected.name, + alias: this.alias, + notes: this.notes, + willFollow: this.willFollow, + mySelectedFeeds: this.mySelectedFeeds + + }); + this.clearFields(); + this.onClose(); + } + + removeFriend() { + this.onSubmit( + { + name: this.userSelected.name, + alias: this.alias, + notes: this.notes, + willFollow: this.willFollow, + mySelectedFeeds: this.mySelectedFeeds + }, + true + ); + this.clearFields(); + this.onClose(); + } + + async updated(changedProperties) { + if ( + changedProperties && + changedProperties.has('editContent') && + this.editContent + ) { + this.userSelected = { + name: this.editContent.name ?? '', + }; + this.notes = this.editContent.notes ?? ''; + this.willFollow = this.editContent.willFollow ?? true; + this.alias = this.editContent.alias ?? ''; + this.requestUpdate() + } + if ( + changedProperties && + changedProperties.has('isOpen') && this.isOpen + ) { + this.getAvailableFeedSchemas() + } + + } + + async getAvailableFeedSchemas() { + try { + this.isLoadingSchemas= true + const url = `${this.nodeUrl}/arbitrary/resources/search?service=DOCUMENT&identifier=ui_schema_feed&prefix=true`; + const res = await fetch(url); + const data = await res.json(); + if (data.error === 401) { + this.availableFeeedSchemas = []; + } else { + const result = data.filter( + (item) => item.identifier === 'ui_schema_feed' + ); + + this.availableFeeedSchemas = result; + } + this.userFoundModalOpen = true; + } catch (error) {} finally { + this.isLoadingSchemas= false + } + } + + render() { + return html` + + `; + } +} + +customElements.define('add-friends-modal', AddFriendsModal); diff --git a/core/src/components/friends-view/computePowWorkerFile.src.js b/core/src/components/friends-view/computePowWorkerFile.src.js new file mode 100644 index 00000000..d9f5f662 --- /dev/null +++ b/core/src/components/friends-view/computePowWorkerFile.src.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/core-sync-status.js b/core/src/components/friends-view/core-sync-status.js new file mode 100644 index 00000000..8ec00f68 --- /dev/null +++ b/core/src/components/friends-view/core-sync-status.js @@ -0,0 +1,78 @@ +import { LitElement, html, css } from 'lit'; +import '@material/mwc-icon'; +import { store } from '../../store'; +import { connect } from 'pwa-helpers'; +import '@vaadin/tooltip'; +import { get } from 'lit-translate'; +class CoreSyncStatus extends connect(store)(LitElement) { + static get properties() { + return { + nodeStatus: {type: Object} + }; + } + + + constructor() { + super(); + this.nodeStatus = { + isMintingPossible:false, +isSynchronizing:true, +syncPercent:undefined, +numberOfConnections:undefined, +height:undefined, + } + } + static styles = css` + .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 */ + + } + `; + +stateChanged(state) { + this.nodeStatus = state.app.nodeStatus +} + + render() { + return html` + lightbulb + + + + + `; + } + + +} + +customElements.define('core-sync-status', CoreSyncStatus); diff --git a/core/src/components/friends-view/feed-item.js b/core/src/components/friends-view/feed-item.js new file mode 100644 index 00000000..3e590d4d --- /dev/null +++ b/core/src/components/friends-view/feed-item.js @@ -0,0 +1,516 @@ +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 FeedItem extends connect(store)(LitElement) { + static get properties() { + return { + resource: { type: Object }, + isReady: { type: Boolean}, + status: {type: Object}, + feedItem: {type: Object}, + appName: {type: String}, + link: {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; + } + + .smallLoading { + border-width: 0.8em; + border-style: solid; + border-color: rgba(3, 169, 244, 0.2) rgba(3, 169, 244, 0.2) + rgba(3, 169, 244, 0.2) rgb(3, 169, 244); + font-size: 30px; + position: relative; + text-indent: -9999em; + transform: translateZ(0px); + animation: 1.1s linear 0s infinite normal none running loadingAnimation; + } + + .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; + } + + + + + @-webkit-keyframes loadingAnimation { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } + } + + @keyframes loadingAnimation { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } + } + `; + } + + constructor() { + super(); + this.resource = { + identifier: "", + name: "", + service: "" + } + this.status = { + status: '' + } + this.isReady = false + this.nodeUrl = this.getNodeUrl() + this.myNode = this.getMyNode() + this.hasCalledWhenDownloaded = false + this.isFetching = false + this.uid = new ShortUniqueId() + + this.observer = new IntersectionObserver(entries => { + for (const entry of entries) { + if (entry.isIntersecting && this.status.status !== 'READY') { + this._fetchImage(); + // Stop observing after the image has started loading + this.observer.unobserve(this); + } + } + }); + this.feedItem = null + } + 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'){ + const rawData = await this.getRawData() + const object = { + ...this.resource.schema.display + } + this.updateDisplayWithPlaceholders(object, {},rawData.data) + this.feedItem = object + 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) { + timer = 24 + isCalling = true + this.status = { + ...res, + status: 'REFETCHING' + } + + setTimeout(() => { + isCalling = false + this.fetchResource() + }, 25000) + return + } + 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') { + const rawData = await this.getRawData() + const object = { + ...this.resource.schema.display + } + this.updateDisplayWithPlaceholders(object, {},rawData.data) + this.feedItem = object + clearInterval(intervalId) + this.status = res + this.isReady = true + } + }, 5000) // 1 second interval + } + + async _fetchImage() { + try { + this.fetchVideoUrl() + this.fetchStatus() + } catch (error) { /* empty */ } + } + + firstUpdated(){ + this.observer.observe(this); + + } + + + + async goToFeedLink(){ + try { + let newQuery = this.link + if (newQuery.endsWith('/')) { + newQuery = newQuery.slice(0, -1) + } + const res = await this.extractComponents(newQuery) + if (!res) return + const { service, name, identifier, path } = res + let query = `?service=${service}` + if (name) { + query = query + `&name=${name}` + } + if (identifier) { + query = query + `&identifier=${identifier}` + } + if (path) { + query = query + `&path=${path}` + } + + store.dispatch(setNewTab({ + url: `qdn/browser/index.html${query}`, + id: this.uid.rnd(), + myPlugObj: { + "url": "myapp", + "domain": "core", + "page": `qdn/browser/index.html${query}`, + "title": name, + "icon": 'vaadin:external-browser', + "mwcicon": 'open_in_browser', + "menus": [], + "parent": false + }, + openExisting: true + })) + } catch (error) { + console.log({error}) + } + } + + + + async extractComponents(url) { + if (!url.startsWith("qortal://")) { + return null + } + + url = url.replace(/^(qortal\:\/\/)/, "") + if (url.includes("/")) { + let parts = url.split("/") + const service = parts[0].toUpperCase() + parts.shift() + const name = parts[0] + parts.shift() + let identifier + + if (parts.length > 0) { + identifier = parts[0] // Do not shift yet + // Check if a resource exists with this service, name and identifier combination + const myNode = store.getState().app.nodeConfig.knownNodes[store.getState().app.nodeConfig.node] + const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port + const url = `${nodeUrl}/arbitrary/resource/status/${service}/${name}/${identifier}?apiKey=${myNode.apiKey}}` + + const res = await fetch(url); + const data = await res.json(); + if (data.totalChunkCount > 0) { + // Identifier exists, so don't include it in the path + parts.shift() + } + else { + identifier = null + } + } + + const path = parts.join("/") + + const components = {} + components["service"] = service + components["name"] = name + components["identifier"] = identifier + components["path"] = path + return components + } + return null +} + + + + + + + + + render() { + let avatarImg + const avatarUrl = `${this.nodeUrl}/arbitrary/THUMBNAIL/${this.resource.name}/qortal_avatar?async=true&apiKey=${this.myNode.apiKey}`; + avatarImg = html``; + let avatarImgApp + const avatarUrl2 = `${this.nodeUrl}/arbitrary/THUMBNAIL/${this.appName}/qortal_avatar?async=true&apiKey=${this.myNode.apiKey}`; + avatarImgApp = html``; + return html` +
    + ${ + this.status.status !== 'READY' + ? html` +
    +
    +

    ${`${Math.round(this.status.percentLoaded || 0 + ).toFixed(0)}% `}${translate('chatpage.cchange94')}

    +
    + ` + : '' + } + ${this.status.status === 'READY' && this.feedItem ? html` +
    +
    +
    + ${avatarImg}
    ${this.resource.name} +
    +
    +

    ${this.feedItem.title}

    +
    +
    +
    + ${avatarImgApp} +
    + +
    +
    + ` : ''} + +
    + + ` + + + } +} + +customElements.define('feed-item', FeedItem); diff --git a/core/src/components/friends-view/friend-item-actions.js b/core/src/components/friends-view/friend-item-actions.js new file mode 100644 index 00000000..975fddc7 --- /dev/null +++ b/core/src/components/friends-view/friend-item-actions.js @@ -0,0 +1,237 @@ +// popover-component.js +import { LitElement, html, css } from 'lit'; +import { createPopper } from '@popperjs/core'; +import '@material/mwc-icon'; +import { use, get, translate } from 'lit-translate'; +import { store } from '../../store'; +import { connect } from 'pwa-helpers'; +import { setNewTab, setSideEffectAction } from '../../redux/app/app-actions'; +import ShortUniqueId from 'short-unique-id'; + +export class FriendItemActions extends connect(store)(LitElement) { + static styles = css` + :host { + display: none; + position: absolute; + background-color: var(--white); + border: 1px solid #ddd; + padding: 8px; + z-index: 10; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + color: var(--black); + max-width: 250px; + } + + .close-icon { + cursor: pointer; + float: right; + margin-left: 10px; + color: var(--black); + } + + .send-message-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 + } + + .send-message-button:hover { + cursor: pointer; + background-color: #03a8f485; + } + .action-parent { + display: flex; + flex-direction: column; + width: 100%; + } + + div[tabindex='0']:focus { + outline: none; + } + `; + + static get properties() { + return { + for: { type: String, reflect: true }, + message: { type: String }, + openEditFriend: { attribute: false }, + name: { type: String }, + closeSidePanel: {attribute: false, type: Object} + }; + } + + constructor() { + super(); + this.message = ''; + this.nodeUrl = this.getNodeUrl(); + this.uid = new ShortUniqueId(); + this.getUserAddress = this.getUserAddress.bind(this) + } + 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; + } + + firstUpdated() { + // We'll defer the popper attachment to the openPopover() method to ensure target availability + } + + attachToTarget(target) { + if (!this.popperInstance && target) { + this.popperInstance = createPopper(target, this, { + placement: 'bottom' + }); + } + } + + openPopover(target) { + this.attachToTarget(target); + this.style.display = 'block'; + setTimeout(() => { + this.shadowRoot.getElementById('parent-div').focus(); + }, 50); + } + + closePopover() { + this.style.display = 'none'; + if (this.popperInstance) { + this.popperInstance.destroy(); + this.popperInstance = null; + } + this.requestUpdate(); + } + handleBlur() { + setTimeout(() => { + this.closePopover(); + }, 0); + } + + async getUserAddress() { + try { + const url = `${this.nodeUrl}/names/${this.name}`; + const res = await fetch(url); + const result = await res.json(); + if (result.error === 401) { + return ''; + } else { + return result.owner; + } + } catch (error) { + return ''; + } + } + + render() { + return html` +
    + close +
    +
    + edit + ${translate('friends.friend10')} +
    +
    + send + ${translate('friends.friend8')} +
    +
    + mail + ${translate('friends.friend9')} +
    +
    +
    + `; + } +} + +customElements.define('friend-item-actions', FriendItemActions); diff --git a/core/src/components/friends-view/friends-feed.js b/core/src/components/friends-view/friends-feed.js new file mode 100644 index 00000000..288d04d7 --- /dev/null +++ b/core/src/components/friends-view/friends-feed.js @@ -0,0 +1,471 @@ +import { LitElement, html, css } from 'lit'; +import '@material/mwc-icon'; +import './friends-view' +import { friendsViewStyles } from './friends-view-css'; +import { connect } from 'pwa-helpers'; +import { store } from '../../store'; +import './feed-item' +import { translate } from 'lit-translate'; + +import '@polymer/paper-spinner/paper-spinner-lite.js' + + +const perEndpointCount = 20; +const totalDesiredCount = 100; +const maxResultsInMemory = 300; +class FriendsFeed extends connect(store)(LitElement) { + static get properties() { + return { + feed: {type: Array}, + setHasNewFeed: {attribute:false}, + isLoading: {type: Boolean}, + hasFetched: {type: Boolean}, + mySelectedFeeds: {type: Array} + }; + } + constructor(){ + super() + this.feed = [] + this.feedToRender = [] + this.nodeUrl = this.getNodeUrl(); + this.myNode = this.getMyNode(); + this.endpoints = [] + this.endpointOffsets = [] // Initialize offsets for each endpoint to 0 + + this.loadAndMergeData = this.loadAndMergeData.bind(this) + this.hasInitialFetch = false + this.observerHandler = this.observerHandler.bind(this); + this.elementObserver = this.elementObserver.bind(this) + this.mySelectedFeeds = [] + this.getSchemas = this.getSchemas.bind(this) + this.hasFetched = false + this._updateFeeds = this._updateFeeds.bind(this) + + } + + static get styles() { + return [friendsViewStyles]; + } + + + + 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; + } + + _updateFeeds(event) { + const detail = event.detail + this.mySelectedFeeds = detail + this.reFetchFeedData() + this.requestUpdate() + } + + connectedCallback() { + super.connectedCallback() + window.addEventListener('friends-my-selected-feeds-event', this._updateFeeds) } + + disconnectedCallback() { + window.removeEventListener('friends-my-selected-feeds-event', this._updateFeeds) + super.disconnectedCallback() + } + + async getSchemas(){ + this.mySelectedFeeds = JSON.parse(localStorage.getItem('friends-my-selected-feeds') || "[]") + const schemas = this.mySelectedFeeds + const getAllSchemas = (schemas || []).map( + async (schema) => { + try { + const url = `${this.nodeUrl}/arbitrary/${schema.service}/${schema.name}/${schema.identifier}`; + const res = await fetch(url) + const data = await res.json() + if(data.error) return false + return data + } catch (error) { + console.log(error); + return false + } + } + ); + const res = await Promise.all(getAllSchemas); + return res.filter((item)=> !!item) + } + + getFeedOnInterval(){ + let interval = null; + let stop = false; + const getAnswer = async () => { + + if (!stop) { + stop = true; + try { + await this.reFetchFeedData() + } catch (error) {} + stop = false; + } + }; + interval = setInterval(getAnswer, 900000); + + } + + async getEndpoints(){ + const dynamicVars = { + + } + const schemas = await this.getSchemas() + const friendList = JSON.parse(localStorage.getItem('friends-my-friend-list') || "[]") + const names = friendList.map(friend => `name=${friend.name}`).join('&'); + if(names.length === 0){ + this.endpoints= [] + this.endpointOffsets = Array(this.endpoints.length).fill(0); + return + } + const baseurl = `${this.nodeUrl}/arbitrary/resources/search?reverse=true&exactmatchnames=true&${names}` + let formEndpoints = [] + schemas.forEach((schema)=> { + const feedData = schema.feed[0] + if(feedData){ + const copyFeedData = {...feedData} + const fullUrl = constructUrl(baseurl, copyFeedData.search, dynamicVars); + if(fullUrl){ + formEndpoints.push({ + url: fullUrl, schemaName: schema.name, schema: copyFeedData + }) + } + } + + + }) + this.endpoints= formEndpoints + this.endpointOffsets = Array(this.endpoints.length).fill(0); + } + + async firstUpdated(){ + this.viewElement = this.shadowRoot.getElementById('viewElement'); + this.downObserverElement = + this.shadowRoot.getElementById('downObserver'); + this.elementObserver(); + + + try { + await new Promise((res)=> { + setTimeout(() => { + res() + }, 5000); + }) + if(this.mySelectedFeeds.length === 0){ + await this.getEndpoints() + +this.loadAndMergeData(); + } + +this.getFeedOnInterval() + + } catch (error) { + console.log(error) + } + } + + getMoreFeed(){ + if(!this.hasInitialFetch) return + if(this.feedToRender.length === this.feed.length ) return + this.feedToRender = this.feed.slice(0, this.feedToRender.length + 20) + this.requestUpdate() + } + + async refresh(){ + try { + await this.getEndpoints() + this.reFetchFeedData() + } catch (error) { + + } + } + + + elementObserver() { + const options = { + rootMargin: '0px', + threshold: 1, + }; + // identify an element to observe + const elementToObserve = this.downObserverElement; + // passing it a callback function + const observer = new IntersectionObserver( + this.observerHandler, + options + ); + // call `observe()` on that MutationObserver instance, + // passing it the element to observe, and the options object + observer.observe(elementToObserve); + } + + observerHandler(entries) { + if (!entries[0].isIntersecting) { + return; + } else { + if (this.feedToRender.length < 20) { + return; + } + this.getMoreFeed(); + } + } + + async fetchDataFromEndpoint(endpointIndex, count) { + const offset = this.endpointOffsets[endpointIndex]; + const url = `${this.endpoints[endpointIndex].url}&limit=${count}&offset=${offset}`; + const res = await fetch(url) + const data = await res.json() + return data.map((i)=> { + return { + ...this.endpoints[endpointIndex], + ...i + } + }) + + } + + + async initialLoad() { + let results = []; + let totalFetched = 0; + let i = 0; + let madeProgress = true; + let exhaustedEndpoints = new Set(); + + while (totalFetched < totalDesiredCount && madeProgress) { + madeProgress = false; + this.isLoading = true + for (i = 0; i < this.endpoints.length; i++) { + if (exhaustedEndpoints.has(i)) { + continue; + } + + const remainingCount = totalDesiredCount - totalFetched; + + // If we've already reached the desired count, break + if (remainingCount <= 0) { + break; + } + + let fetchCount = Math.min(perEndpointCount, remainingCount); + let data = await this.fetchDataFromEndpoint(i, fetchCount); + + // Increment the offset for this endpoint by the number of items fetched + this.endpointOffsets[i] += data.length; + + if (data.length > 0) { + madeProgress = true; + } + + if (data.length < fetchCount) { + exhaustedEndpoints.add(i); + } + + results = results.concat(data); + totalFetched += data.length; + } + + if (exhaustedEndpoints.size === this.endpoints.length) { + break; + } + } + this.isLoading = false + this.hasFetched = true; + // Trim the results if somehow they are over the totalDesiredCount + return results.slice(0, totalDesiredCount); + } + + + + + + trimDataToLimit(data, limit) { + return data.slice(0, limit); + } + + mergeData(newData, existingData) { + const existingIds = new Set(existingData.map(item => item.identifier)); // Assume each item has a unique 'id' + const uniqueNewData = newData.filter(item => !existingIds.has(item.identifier)); + return uniqueNewData.concat(existingData); + } + + + async addExtraData(data){ + let newData = [] + for (let item of data) { + let newItem = { + ...item, + schema: { + ...item.schema, + customParams: {...item.schema.customParams} + + } + } + let newResource = { + identifier: newItem.identifier, + service: newItem.service, + name: newItem.name + } + if(newItem.schema){ + const resource = newItem + + let clickValue1 = newItem.schema.click; + + const resolvedClickValue1 = replacePlaceholders(clickValue1, resource, newItem.schema.customParams); + newItem.link = resolvedClickValue1 + newData.push(newItem) + } + } + return newData + + } + async reFetchFeedData() { + // Resetting offsets to start fresh. + this.endpointOffsets = Array(this.endpoints.length).fill(0); + await this.getEndpoints() + const oldIdentifiers = new Set(this.feed.map(item => item.identifier)); + const newData = await this.initialLoad(); + + // Filter out items that are already in the feed + const trulyNewData = newData.filter(item => !oldIdentifiers.has(item.identifier)); + + if (trulyNewData.length > 0) { + // Adding extra data and merging with old data + const enhancedNewData = await this.addExtraData(trulyNewData); + + // Merge new data with old data immutably + this.feed = [...enhancedNewData, ...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); + this.hasInitialFetch = true; + + const created = trulyNewData[0].created; + let value = localStorage.getItem('lastSeenFeed'); + if (((+value || 0) < created)) { + this.setHasNewFeed(true); + } + } + } + + + + async loadAndMergeData() { + let allData = this.feed + const newData = await this.initialLoad(); + allData = await this.addExtraData(newData) + 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 + this.feed = [...allData] + this.feedToRender = this.feed.slice(0,20) + this.hasInitialFetch = true + if(allData.length > 0){ + const created = allData[0].created + let value = localStorage.getItem('lastSeenFeed') + if (((+value || 0) < created)) { + this.setHasNewFeed(true) + } + } + } + + + + + + render() { + return html` +
    +
    + ${this.isLoading ? html` +
    + +
    + ` : ''} + ${this.hasFetched && !this.isLoading && this.feed.length === 0 ? html` +
    +

    ${translate('friends.friends18')}

    +
    + ` : ''} + ${this.feedToRender.map((item) => { + return html``; + })} +
    +
    +
    + `; + } + +} + +customElements.define('friends-feed', FriendsFeed); + +export function substituteDynamicVar(value, dynamicVars) { + if (typeof value !== 'string') return value; + + const pattern = /\$\$\{([a-zA-Z0-9_]+)\}\$\$/g; // Adjusted pattern to capture $${name}$$ with curly braces + + return value.replace(pattern, (match, p1) => { + return dynamicVars[p1] !== undefined ? dynamicVars[p1] : match; + }); +} + +export function constructUrl(base, search, dynamicVars) { + let queryStrings = []; + + for (const [key, value] of Object.entries(search)) { + const substitutedValue = substituteDynamicVar(value, dynamicVars); + queryStrings.push(`${key}=${encodeURIComponent(substitutedValue)}`); + } + + return queryStrings.length > 0 ? `${base}&${queryStrings.join('&')}` : base; +} + + + + + + + + +export function replacePlaceholders(template, resource, customParams) { + const dataSource = { resource, customParams }; + + return template.replace(/\$\$\{(.*?)\}\$\$/g, (match, p1) => { + const keys = p1.split('.'); + let value = dataSource; + + for (let key of keys) { + if (value[key] !== undefined) { + value = value[key]; + } else { + return match; // Return placeholder unchanged + } + } + return value; + }); +} + + + + diff --git a/core/src/components/friends-view/friends-side-panel-parent.js b/core/src/components/friends-view/friends-side-panel-parent.js new file mode 100644 index 00000000..f77bac91 --- /dev/null +++ b/core/src/components/friends-view/friends-side-panel-parent.js @@ -0,0 +1,81 @@ +import { LitElement, html, css } from 'lit'; +import '@material/mwc-icon'; +import './friends-side-panel.js'; +import '@vaadin/tooltip'; +import { get } from 'lit-translate'; + + +class FriendsSidePanelParent extends LitElement { + static get properties() { + return { + isOpen: {type: Boolean}, + hasNewFeed: {type: Boolean} + }; + } + + + constructor() { + super(); + this.isOpen = false + this.hasNewFeed = false + } + static styles = css` + .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 */ + + } + `; + + setHasNewFeed(val){ + this.hasNewFeed = val + } + render() { + return html` + { + this.isOpen = !this.isOpen + if(this.isOpen && this.hasNewFeed){ + localStorage.setItem('lastSeenFeed', Date.now()); + this.hasNewFeed = false + this.shadowRoot.querySelector("friends-side-panel").selected = 'feed' + } + }} style="color: ${this.hasNewFeed ? 'green' : 'var(--black)'}; cursor:pointer;user-select:none" + >group + + + this.setHasNewFeed(val)} ?isOpen=${this.isOpen} .setIsOpen=${(val)=> this.isOpen = val}> + + `; + } + + +} + +customElements.define('friends-side-panel-parent', FriendsSidePanelParent); diff --git a/core/src/components/friends-view/friends-side-panel.js b/core/src/components/friends-view/friends-side-panel.js new file mode 100644 index 00000000..ce47d9db --- /dev/null +++ b/core/src/components/friends-view/friends-side-panel.js @@ -0,0 +1,151 @@ +import { LitElement, html, css } from 'lit'; +import '@material/mwc-icon'; +import './friends-view' +import './friends-feed' +import { translate } from 'lit-translate'; +class FriendsSidePanel extends LitElement { + static get properties() { + return { + setIsOpen: { attribute: false}, + isOpen: {type: Boolean}, + selected: {type: String}, + setHasNewFeed: {attribute: false}, + closeSidePanel: {attribute: false, type: Object} + }; + } + + constructor(){ + super() + this.selected = 'friends' + this.closeSidePanel = this.closeSidePanel.bind(this) + } + + static styles = css` + :host { + display: block; + position: fixed; + top: 55px; + right: 0px; + width: 420px; + max-width: 95%; + height: calc(100vh - 55px); + background-color: var(--white); + border-left: 1px solid rgb(224, 224, 224); + z-index: 1; + transform: translateX(100%); /* start from outside the right edge */ + transition: transform 0.3s ease-in-out; + } + :host([isOpen]) { + transform: unset; /* slide in to its original position */ + } + + .header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + border-bottom: 1px solid #e0e0e0; + } + + .content { + padding: 16px; + display: flex; + flex-direction: column; + flex-grow: 1; + overflow: auto; + } + .content::-webkit-scrollbar-track { + background-color: whitesmoke; + border-radius: 7px; + } + + .content::-webkit-scrollbar { + width: 12px; + border-radius: 7px; + background-color: whitesmoke; + } + + .content::-webkit-scrollbar-thumb { + background-color: rgb(180, 176, 176); + border-radius: 7px; + transition: all 0.3s ease-in-out; + } + .parent { + display: flex; + flex-direction: column; + height: 100%; + } + + .active { + font-size: 16px; + background: var(--black); + color: var(--white); + padding: 5px; + border-radius: 2px; + cursor: pointer; + } + + .default { + font-size: 16px; + color: var(--black); + padding: 5px; + border-radius: 2px; + cursor: pointer; + } + + .default-content { + visibility: hidden; + position: absolute; + z-index: -50; + } + + `; + + refreshFeed(){ + + this.shadowRoot.querySelector('friends-feed').refresh() + + + } + + closeSidePanel(){ + this.setIsOpen(false) + } + + render() { + return html` +
    +
    +
    + this.selected = 'friends'} class="${this.selected === 'friends' ? 'active' : 'default'}">${translate('friends.friend12')} + this.selected = 'feed'} class="${this.selected === 'feed' ? 'active' : 'default'}">${translate('friends.friend13')} +
    +
    + { + this.refreshFeed() + }} style="color: var(--black); cursor:pointer;">refresh + { + this.setIsOpen(false) + }}>close +
    + +
    +
    +
    + this.refreshFeed()}> +
    +
    + this.setHasNewFeed(val)}> +
    + + + +
    +
    + + `; + } + +} + +customElements.define('friends-side-panel', FriendsSidePanel); diff --git a/core/src/components/friends-view/friends-view-css.js b/core/src/components/friends-view/friends-view-css.js new file mode 100644 index 00000000..303d510d --- /dev/null +++ b/core/src/components/friends-view/friends-view-css.js @@ -0,0 +1,182 @@ +import { css } from 'lit' + +export const friendsViewStyles = css` +* { + box-sizing: border-box; +} + .top-bar-icon { + cursor: pointer; + height: 18px; + width: 18px; + transition: 0.2s all; + } + + .top-bar-icon:hover { + 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; + } + + .close-row { + width: 100%; + display: flex; + justify-content: flex-end; + height: 50px; + flex:0 + + } + + .container-body { + width: 100%; + display: flex; + flex-direction: column; + flex-grow: 1; + margin-top: 5px; + padding: 0px 6px; + box-sizing: border-box; + align-items: center; + gap: 10px; + } + + .container-body::-webkit-scrollbar-track { + background-color: whitesmoke; + border-radius: 7px; + } + + .container-body::-webkit-scrollbar { + width: 6px; + border-radius: 7px; + background-color: whitesmoke; + } + + .container-body::-webkit-scrollbar-thumb { + background-color: rgb(180, 176, 176); + border-radius: 7px; + transition: all 0.3s ease-in-out; + } + + .container-body::-webkit-scrollbar-thumb:hover { + background-color: rgb(148, 146, 146); + cursor: pointer; + } + + p { + color: var(--black); + margin: 0px; + padding: 0px; + word-break: break-all; + } + + .container { + display: flex; + width: 100%; + flex-direction: column; + height: 100%; + } + + .chat-right-panel-label { + font-family: Montserrat, sans-serif; + color: var(--group-header); + padding: 5px; + font-size: 13px; + user-select: none; + } + + .group-info { + display: flex; + flex-direction: column; + justify-content: flex-start; + gap: 10px; + } + + .group-name { + font-family: Raleway, sans-serif; + font-size: 20px; + color: var(--chat-bubble-msg-color); + text-align: center; + user-select: none; + } + + .group-description { + font-family: Roboto, sans-serif; + color: var(--chat-bubble-msg-color); + letter-spacing: 0.3px; + font-weight: 300; + font-size: 14px; + margin-top: 15px; + word-break: break-word; + user-select: none; + } + + .group-subheader { + font-family: Montserrat, sans-serif; + font-size: 14px; + color: var(--chat-bubble-msg-color); + } + + .group-data { + font-family: Roboto, sans-serif; + letter-spacing: 0.3px; + font-weight: 300; + font-size: 14px; + color: var(--chat-bubble-msg-color); + } + .search-results-div { + position: absolute; + top: 25px; + right: 25px; + } + + .name-input { + width: 100%; + 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; + } + + .name-input::selection { + background-color: var(--mdc-theme-primary); + color: white; + } + + .name-input::placeholder { + opacity: 0.9; + color: var(--black); + } + + .search-field { + width: 100%; + position: relative; + } + + .search-icon { + position: absolute; + right: 3px; + color: var(--chat-bubble-msg-color); + transition: hover 0.3s ease-in-out; + background: none; + border-radius: 50%; + padding: 6px 3px; + font-size: 21px; + } + + .search-icon:hover { + cursor: pointer; + background: #d7d7d75c; + } +` diff --git a/core/src/components/friends-view/friends-view.js b/core/src/components/friends-view/friends-view.js new file mode 100644 index 00000000..ab90fe32 --- /dev/null +++ b/core/src/components/friends-view/friends-view.js @@ -0,0 +1,398 @@ +import { LitElement, html, css } from 'lit'; +import { render } from 'lit/html.js'; +import { connect } from 'pwa-helpers'; + +import '@material/mwc-button'; +import '@material/mwc-dialog'; +import '@polymer/paper-spinner/paper-spinner-lite.js'; +import '@polymer/paper-progress/paper-progress.js'; +import '@material/mwc-icon'; +import '@vaadin/icon' +import '@vaadin/icons' +import '@vaadin/button'; +import './ChatSideNavHeads'; +import '../../../../plugins/plugins/core/components/ChatSearchResults' +import './add-friends-modal' + +import { + use, + get, + translate, + translateUnsafeHTML, + registerTranslateConfig, +} from 'lit-translate'; +import { store } from '../../store'; +import { friendsViewStyles } from './friends-view-css'; +import { parentEpml } from '../show-plugin'; + +class FriendsView extends connect(store)(LitElement) { + static get properties() { + return { + error: { type: Boolean }, + toggle: { attribute: false }, + userName: { type: String }, + errorMessage: { type: String }, + successMessage: { type: String }, + setUserName: { attribute: false }, + friendList: { type: Array }, + userSelected: { type: Object }, + isLoading: {type: Boolean}, + userFoundModalOpen: {type: Boolean}, + userFound: { type: Array}, + isOpenAddFriendsModal: {type: Boolean}, + editContent: {type: Object}, + mySelectedFeeds: {type: Array}, + refreshFeed: {attribute: false}, + closeSidePanel: {attribute: false, type: Object} + }; + } + static get styles() { + return [friendsViewStyles]; + } + + constructor() { + super(); + this.error = false; + this.observerHandler = this.observerHandler.bind(this); + this.viewElement = ''; + this.downObserverElement = ''; + this.myAddress = + window.parent.reduxStore.getState().app.selectedAddress.address; + this.errorMessage = ''; + this.successMessage = ''; + this.friendList = []; + this.userSelected = {}; + this.isLoading = false; + this.userFoundModalOpen = false + this.userFound = []; + this.nodeUrl = this.getNodeUrl(); + this.myNode = this.getMyNode(); + this.isOpenAddFriendsModal = false + this.editContent = null + this.addToFriendList = this.addToFriendList.bind(this) + this._updateFriends = this._updateFriends.bind(this) + this._updateFeed = this._updateFeed.bind(this) + + } + + 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; + } + + getMoreFriends() {} + + firstUpdated() { + this.viewElement = this.shadowRoot.getElementById('viewElement'); + this.downObserverElement = + this.shadowRoot.getElementById('downObserver'); + this.elementObserver(); + this.mySelectedFeeds = JSON.parse(localStorage.getItem('friends-my-selected-feeds') || "[]") + this.friendList = JSON.parse(localStorage.getItem('friends-my-friend-list') || "[]") + + + + + } + + _updateFriends(event) { + const detail = event.detail + this.friendList = detail + } + _updateFeed(event) { + console.log({event}) + const detail = event.detail + console.log({detail}) + this.mySelectedFeeds = detail + this.requestUpdate() + } + + connectedCallback() { + super.connectedCallback() + console.log('callback') + window.addEventListener('friends-my-friend-list-event', this._updateFriends) + window.addEventListener('friends-my-selected-feeds-event', this._updateFeed) + } + + disconnectedCallback() { + window.removeEventListener('friends-my-friend-list-event', this._updateFriends) + window.addEventListener('friends-my-selected-feeds-event', this._updateFeed) + super.disconnectedCallback() + } + + elementObserver() { + const options = { + root: this.viewElement, + rootMargin: '0px', + threshold: 1, + }; + // identify an element to observe + const elementToObserve = this.downObserverElement; + // passing it a callback function + const observer = new IntersectionObserver( + this.observerHandler, + options + ); + // call `observe()` on that MutationObserver instance, + // passing it the element to observe, and the options object + observer.observe(elementToObserve); + } + + observerHandler(entries) { + if (!entries[0].isIntersecting) { + return; + } else { + if (this.friendList.length < 20) { + return; + } + this.getMoreFriends(); + } + } + + async userSearch() { + const nameValue = this.shadowRoot.getElementById('sendTo').value + if(!nameValue) { + this.userFound = [] + this.userFoundModalOpen = true + return; + } + try { + const url = `${this.nodeUrl}/names/${nameValue}` + const res = await fetch(url) + const result = await res.json() + if (result.error === 401) { + this.userFound = [] + } else { + this.userFound = [ + result + ]; + } + this.userFoundModalOpen = true; + } catch (error) { + // let err4string = get("chatpage.cchange35"); + // parentEpml.request('showSnackBar', `${err4string}`) + } + } + + getApiKey() { + const apiNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] + let apiKey = apiNode.apiKey + return apiKey + } + + async myFollowName(name) { + let items = [ + name + ] + let namesJsonString = JSON.stringify({ "items": items }) + + let ret = await parentEpml.request('apiCall', { + url: `/lists/followedNames?apiKey=${this.getApiKey()}`, + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: `${namesJsonString}` + }) + + + return ret + } + + async unFollowName(name) { + let items = [ + name + ] + let namesJsonString = JSON.stringify({ "items": items }) + + let ret = await parentEpml.request('apiCall', { + url: `/lists/followedNames?apiKey=${this.getApiKey()}`, + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + }, + body: `${namesJsonString}` + }) + + + return ret + } + async addToFriendList(val, isRemove){ + const copyVal = {...val} + delete copyVal.mySelectedFeeds + if(isRemove){ + this.friendList = this.friendList.filter((item)=> item.name !== copyVal.name) + }else if(this.editContent){ + const findFriend = this.friendList.findIndex(item=> item.name === copyVal.name) + if(findFriend !== -1){ + const copyList = [...this.friendList] + copyList[findFriend] = copyVal + this.friendList = copyList + + } + + } else { + this.friendList = [...this.friendList, copyVal] + } + if(!copyVal.willFollow || isRemove) { + this.unFollowName(copyVal.name) + } else if(copyVal.willFollow){ + this.myFollowName(copyVal.name) + } + this.setMySelectedFeeds(val.mySelectedFeeds) + await new Promise((res)=> { + setTimeout(()=> { + res() + },50) + }) + this.userSelected = {}; + this.shadowRoot.getElementById('sendTo').value = '' + this.isLoading = false; + this.isOpenAddFriendsModal = false + this.editContent = null + this.setMyFriends(this.friendList) + if(!isRemove && this.friendList.length === 1){ + this.refreshFeed() + } + } + setMyFriends(friendList){ + localStorage.setItem('friends-my-friend-list', JSON.stringify(friendList)); + const tempSettingsData= JSON.parse(localStorage.getItem('temp-settings-data') || "{}") + const newTemp = { + ...tempSettingsData, + userLists: { + data: [friendList], + timestamp: Date.now() + } + } + + localStorage.setItem('temp-settings-data', JSON.stringify(newTemp)); + this.dispatchEvent( + new CustomEvent('temp-settings-data-event', { + bubbles: true, + composed: true + }), + ); + + } + setMySelectedFeeds(mySelectedFeeds){ + this.mySelectedFeeds = mySelectedFeeds + const tempSettingsData= JSON.parse(localStorage.getItem('temp-settings-data') || "{}") + const newTemp = { + ...tempSettingsData, + friendsFeed: { + data: mySelectedFeeds, + timestamp: Date.now() + } + } + + localStorage.setItem('temp-settings-data', JSON.stringify(newTemp)); + localStorage.setItem('friends-my-selected-feeds', JSON.stringify(mySelectedFeeds)); + } + openEditFriend(val){ + this.isOpenAddFriendsModal = true + this.userSelected = val + this.editContent = {...val, mySelectedFeeds: this.mySelectedFeeds} + } + + onClose(){ + this.isLoading = false; + this.isOpenAddFriendsModal = false + this.editContent = null + this.userSelected = {} + } + + render() { + console.log('rendered1') + return html` +
    +
    +

    My Friends

    +
    + { + if(e.key === 'Enter'){ + this.userSearch() + } + }} + /> + + + + +
    +
    + { + this.userSelected = result; + this.isOpenAddFriendsModal = true + + this.userFound = []; + this.userFoundModalOpen = false; + }} + .closeFunc=${() => { + this.userFoundModalOpen = false; + this.userFound = []; + }} + .searchResults=${this.userFound} + ?isOpen=${this.userFoundModalOpen} + ?loading=${this.isLoading}> + +
    + + + ${this.friendList.map((item) => { + return html` { + + }} + .chatInfo=${item} + .openEditFriend=${(val)=> this.openEditFriend(val)} + .closeSidePanel=${this.closeSidePanel} + >`; + })} +
    +
    +
    + { + this.isOpenAddFriendsModal = val + }} + .userSelected=${this.userSelected} + .onSubmit=${(val, isRemove)=> this.addToFriendList(val, isRemove)} + .editContent=${this.editContent} + .onClose=${()=> this.onClose()} + .mySelectedFeeds=${this.mySelectedFeeds} + > + + `; + } +} + +customElements.define('friends-view', FriendsView); diff --git a/core/src/components/friends-view/save-settings-qdn.js b/core/src/components/friends-view/save-settings-qdn.js new file mode 100644 index 00000000..86647a06 --- /dev/null +++ b/core/src/components/friends-view/save-settings-qdn.js @@ -0,0 +1,593 @@ +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 WebWorker from 'web-worker:./computePowWorkerFile.src.js'; +import '@polymer/paper-spinner/paper-spinner-lite.js'; +import '@vaadin/tooltip'; +import { get, translate } from 'lit-translate'; +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'; + +class SaveSettingsQdn 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 }, + }; + } + + constructor() { + super(); + this.isOpen = false; + this.getGeneralSettingsQdn = this.getGeneralSettingsQdn.bind(this); + this._updateTempSettingsData = this._updateTempSettingsData.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; + } + static styles = css` + :host { + margin-right: 20px; + } + .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; + } + `; + + 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}?encoding=base64`; + const res = await fetch(url); + const data = await res.text(); + if (data.error) throw new Error('Cannot retrieve your data from qdn'); + const decryptedData = decryptGroupData(data); + const decryptedDataToBase64 = uint8ArrayToObject(decryptedData); + return decryptedDataToBase64; + } + + async setValues(response, resource) { + this.settingsRawData = response; + const rawDataTimestamp = resource.updated; + + const tempSettingsData = JSON.parse( + localStorage.getItem('temp-settings-data') || '{}' + ); + + const userLists = response.userLists || []; + const friendsFeed = response.friendsFeed; + const myMenuPlugs = response.myMenuPlugs; + + this.valuesToBeSavedOnQdn = {}; + if ( + userLists.length > 0 && + (!tempSettingsData.userLists || + (tempSettingsData.userLists && + tempSettingsData.userLists.timestamp < rawDataTimestamp)) + ) { + const friendList = userLists[0]; + const copyPayload = [...friendList]; + + localStorage.setItem( + 'friends-my-friend-list', + JSON.stringify(friendList) + ); + this.dispatchEvent( + new CustomEvent('friends-my-friend-list-event', { + bubbles: true, + composed: true, + detail: copyPayload, + }) + ); + } else if ( + tempSettingsData.userLists && + tempSettingsData.userLists.timestamp > rawDataTimestamp + ) { + this.valuesToBeSavedOnQdn = { + ...this.valuesToBeSavedOnQdn, + userLists: { + data: tempSettingsData.userLists.data, + }, + }; + } + + if ( + friendsFeed && + (!tempSettingsData.friendsFeed || + (tempSettingsData.friendsFeed && + tempSettingsData.friendsFeed.timestamp < rawDataTimestamp)) + ) { + const copyPayload = [...friendsFeed]; + + localStorage.setItem( + 'friends-my-selected-feeds', + JSON.stringify(friendsFeed) + ); + this.dispatchEvent( + new CustomEvent('friends-my-selected-feeds-event', { + bubbles: true, + composed: true, + detail: copyPayload, + }) + ); + } else if ( + tempSettingsData.friendsFeed && + tempSettingsData.friendsFeed.timestamp > rawDataTimestamp + ) { + this.valuesToBeSavedOnQdn = { + ...this.valuesToBeSavedOnQdn, + friendsFeed: { + data: tempSettingsData.friendsFeed.data, + }, + }; + } + + if ( + myMenuPlugs && + (!tempSettingsData.myMenuPlugs || + (tempSettingsData.myMenuPlugs && + tempSettingsData.myMenuPlugs.timestamp < rawDataTimestamp)) + ) { + if (Array.isArray(myMenuPlugs)) { + const copyPayload = [...myMenuPlugs]; + + localStorage.setItem( + 'myMenuPlugs', + JSON.stringify(myMenuPlugs) + ); + + this.dispatchEvent( + new CustomEvent('myMenuPlugs-event', { + bubbles: true, + composed: true, + detail: copyPayload, + }) + ); + } + } else if ( + tempSettingsData.myMenuPlugs && + tempSettingsData.myMenuPlugs.timestamp > rawDataTimestamp + ) { + this.valuesToBeSavedOnQdn = { + ...this.valuesToBeSavedOnQdn, + myMenuPlugs: { + data: tempSettingsData.myMenuPlugs.data, + }, + }; + } + } + + async getGeneralSettingsQdn() { + try { + 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'); + const name = nameObject.name; + 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); + let data = ''; + try { + data = await res.json(); + if (Array.isArray(data)) { + data = data.filter( + (item) => item.identifier === 'qortal_general_settings' + ); + + if (data.length > 0) { + this.resourceExists = true; + const dataItem = data[0]; + try { + const response = await this.getRawData(dataItem); + if (response.version) { + 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.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.accountInfo && + state.app.accountInfo.names.length && + state.app.nodeStatus && + state.app.nodeStatus.syncPercent !== this.syncPercentage + ) { + this.syncPercentage = state.app.nodeStatus.syncPercent; + + if ( + !this.hasAttemptedToFetchResource && + state.app.nodeStatus.syncPercent === 100 + ) { + this.getGeneralSettingsQdn(); + } + } + } + + 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() { + 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 name = nameObject.name; + const identifer = 'qortal_general_settings'; + const filename = 'qortal_general_settings.json'; + const selectedAddress = store.getState().app.selectedAddress; + const getArbitraryFee = await this.getArbitraryFee(); + const feeAmount = getArbitraryFee.fee; + const friendsList = JSON.parse( + localStorage.getItem('friends-my-friend-list') || '[]' + ); + const friendsFeed = JSON.parse( + localStorage.getItem('friends-my-selected-feeds') || '[]' + ); + const myMenuPlugs = JSON.parse( + localStorage.getItem('myMenuPlugs') || '[]' + ); + + let newObject; + + if (this.resourceExists === false) { + newObject = { + version: 1, + userLists: [friendsList], + friendsFeed, + myMenuPlugs, + }; + } else if (this.settingsRawData) { + const tempSettingsData = JSON.parse( + localStorage.getItem('temp-settings-data') || '{}' + ); + newObject = { + ...this.settingsRawData, + }; + for (const key in tempSettingsData) { + if (tempSettingsData[key].hasOwnProperty('data')) { + if ( + key === 'userLists' && + !Array.isArray(tempSettingsData[key].data) + ) + continue; + if ( + key === 'friendsFeed' && + !Array.isArray(tempSettingsData[key].data) + ) + continue; + if ( + key === 'myMenuPlugs' && + !Array.isArray(tempSettingsData[key].data) + ) + continue; + newObject[key] = tempSettingsData[key].data; + } + } + } + + const newObjectToBase64 = await objectToBase64(newObject); + const encryptedData = encryptDataGroup({ + data64: newObjectToBase64, + publicKeys: [], + }); + + const worker = new WebWorker(); + try { + const resPublish = await publishData({ + registeredName: encodeURIComponent(name), + file: encryptedData, + service: 'DOCUMENT_PRIVATE', + 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.setValues(newObject, { + updated: Date.now(), + }); + localStorage.setItem('temp-settings-data', JSON.stringify({})); + this.valuesToBeSavedOnQdn = {}; + worker.terminate(); + } catch (error) { + worker.terminate(); + } + } catch (error) { + console.log({ error }); + } finally { + this.isSaving = false; + } + } + + _updateTempSettingsData() { + this.valuesToBeSavedOnQdn = JSON.parse( + localStorage.getItem('temp-settings-data') || '{}' + ); + } + + connectedCallback() { + super.connectedCallback(); + window.addEventListener( + 'temp-settings-data-event', + this._updateTempSettingsData + ); + } + + disconnectedCallback() { + window.removeEventListener( + 'temp-settings-data-event', + this._updateTempSettingsData + ); + super.disconnectedCallback(); + } + render() { + return html` + ${this.isSaving || + (!this.error && this.resourceExists === undefined) + ? html` + + ` + : html` + 0 || this.resourceExists === false + ? 'active' + : 'notActive'} + @click=${() => { + if ( + Object.values(this.valuesToBeSavedOnQdn) + .length > 0 || + this.resourceExists === false + ) { + if (!this.fee) return; + // this.saveToQdn() + const target = + this.shadowRoot.getElementById( + 'popover-notification' + ); + const popover = + this.shadowRoot.querySelector( + 'popover-component' + ); + if (popover) { + popover.openPopover(target); + } + } + // this.isOpen = !this.isOpen + }} + style="user-select:none" + >save + 0 || + this.resourceExists === false + ? get('save.saving3') + : get('save.saving2')} + > + + +
    +

    + ${`${get('walletpage.wchange12')}: ${ + this.fee ? this.fee.feeToShow : '' + }`} +

    +
    +
    +
    + ${translate('save.saving4')} +
    +
    + ${translate('browserpage.bchange28')} +
    +
    +
    + `} + `; + } +} + +customElements.define('save-settings-qdn', SaveSettingsQdn); diff --git a/core/src/components/friends-view/webworkerParseFeedData.js b/core/src/components/friends-view/webworkerParseFeedData.js new file mode 100644 index 00000000..e69de29b diff --git a/core/src/components/login-view/welcome-page.js b/core/src/components/login-view/welcome-page.js index 905c5777..0929476d 100644 --- a/core/src/components/login-view/welcome-page.js +++ b/core/src/components/login-view/welcome-page.js @@ -49,7 +49,7 @@ class WelcomePage extends LitElement { this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light' } - firstUpdate() { + firstUpdated() { // ... } diff --git a/core/src/components/notification-view/notification-bell-general.js b/core/src/components/notification-view/notification-bell-general.js index 125c8095..393e96d5 100644 --- a/core/src/components/notification-view/notification-bell-general.js +++ b/core/src/components/notification-view/notification-bell-general.js @@ -111,15 +111,30 @@ class NotificationBellGeneral extends connect(store)(LitElement) { > ${hasOngoing ? html` - notifications + + ` : html` notifications + + `} ${hasOngoing @@ -147,6 +162,9 @@ class NotificationBellGeneral extends connect(store)(LitElement) { @blur=${this.handleBlur} >
    + ${this.notifications.length === 0 ? html` +

    ${translate('notifications.notify3')}

    + ` : ''} ${repeat( this.notifications, (notification) => notification.reference.signature, // key function @@ -169,7 +187,6 @@ class NotificationBellGeneral extends connect(store)(LitElement) { } _toggleNotifications() { - if (this.notifications.length === 0) return; this.showNotifications = !this.showNotifications; if (this.showNotifications) { requestAnimationFrame(() => { @@ -184,7 +201,6 @@ class NotificationBellGeneral extends connect(store)(LitElement) { flex-direction: column; align-items: center; position: relative; - margin-right: 20px; } .count { diff --git a/core/src/components/notification-view/notification-bell.js b/core/src/components/notification-view/notification-bell.js index f5d960c3..af7e4ec1 100644 --- a/core/src/components/notification-view/notification-bell.js +++ b/core/src/components/notification-view/notification-bell.js @@ -8,6 +8,8 @@ import '@polymer/iron-icons/iron-icons.js' import { store } from '../../store.js' import { setNewTab } from '../../redux/app/app-actions.js' import { routes } from '../../plugins/routes.js' +import '@material/mwc-icon'; + import config from '../../notifications/config.js' import '../../../../plugins/plugins/core/components/TimeAgo.js' @@ -138,11 +140,29 @@ class NotificationBell extends connect(store)(LitElement) { return html`
    ${this.notificationCount ? html` - this._toggleNotifications()} title="Q-Mail"> + this._toggleNotifications()} id="notification-mail-icon" style="color: green;cursor:pointer;user-select:none" + >mail + + + ` : html` - { - this._openTabQmail() - }} title="Q-Mail"> + this._openTabQmail()} id="notification-mail-icon" style="color: var(--black); cursor:pointer;user-select:none" + >mail + + + `} ${this.notificationCount ? html` @@ -218,8 +238,8 @@ class NotificationBell extends connect(store)(LitElement) { .count { position: absolute; - top: 2px; - right: 0px; + top: -5px; + right: -5px; font-size: 12px; background-color: red; color: white; @@ -229,6 +249,7 @@ class NotificationBell extends connect(store)(LitElement) { display: flex; align-items: center; justify-content: center; + user-select: none; } .nocount { diff --git a/core/src/components/notification-view/popover.js b/core/src/components/notification-view/popover.js index 43e85a94..e44d2610 100644 --- a/core/src/components/notification-view/popover.js +++ b/core/src/components/notification-view/popover.js @@ -23,6 +23,8 @@ export class PopoverComponent extends LitElement { margin-left: 10px; color: var(--black) } + + `; static properties = { @@ -40,7 +42,6 @@ export class PopoverComponent extends LitElement { } attachToTarget(target) { - console.log({target}) if (!this.popperInstance && target) { this.popperInstance = createPopper(target, this, { placement: 'bottom', @@ -66,7 +67,8 @@ export class PopoverComponent extends LitElement { render() { return html` close -
    info ${this.message}
    +
    info ${this.message} +
    `; } } diff --git a/core/src/components/qort-theme-toggle.js b/core/src/components/qort-theme-toggle.js index 02833cea..0a411080 100644 --- a/core/src/components/qort-theme-toggle.js +++ b/core/src/components/qort-theme-toggle.js @@ -114,7 +114,6 @@ class QortThemeToggle extends LitElement { } else { this.theme = 'light'; } - this.dispatchEvent( new CustomEvent('qort-theme-change', { bubbles: true, diff --git a/core/src/components/show-plugin.js b/core/src/components/show-plugin.js index c6ff4d34..266df4a2 100644 --- a/core/src/components/show-plugin.js +++ b/core/src/components/show-plugin.js @@ -26,11 +26,9 @@ import '@vaadin/grid' import '@vaadin/text-field' import '../custom-elements/frag-file-input.js' -const chatLastSeen = localForage.createInstance({ - name: "chat-last-seen", -}) -const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }) + +export const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }) class ShowPlugin extends connect(store)(LitElement) { static get properties() { @@ -435,9 +433,21 @@ class ShowPlugin extends connect(store)(LitElement) { @click="${() => { this.currentTab = index }}" + @mousedown="${(event) => { + if (event.button === 1) { + event.preventDefault(); + this.removeTab(index, tab.id); + } + }}" >
    - ${icon} + ${tab.myPlugObj && tab.myPlugObj.url === "myapp" ? html` + + ` : html` + ${icon} + `} + +
    ${count ? html` @@ -996,6 +1006,7 @@ class NavBar extends connect(store)(LitElement) { border-top-right-radius: 20px; border-bottom-left-radius: 20px; border-bottom-right-radius: 10px; + position: relative; } .app-list .app-icon:hover .removeIcon { @@ -1009,14 +1020,14 @@ class NavBar extends connect(store)(LitElement) { } .menuIconPos { - position: relative; right: -2px; } .removeIconPos { position: absolute; - top: -36px; - left: 0; + top: -10px; + right: -10px; + z-index: 1; } .menuIconPos:hover .removeIcon { @@ -1028,9 +1039,7 @@ class NavBar extends connect(store)(LitElement) { color: var(--black); --mdc-icon-size: 28px; cursor: pointer; - position: absolute; - top: 30px; - left: 123px; + position: relative; z-index: 1; } @@ -1192,6 +1201,7 @@ class NavBar extends connect(store)(LitElement) { this.myFollowedNamesList = [] this.searchContentString = '' this.searchNameResources = [] + this._updateMyMenuPlugins = this._updateMyMenuPlugins.bind(this) } render() { @@ -1458,6 +1468,49 @@ class NavBar extends connect(store)(LitElement) { await this.getMyFollowedNamesList() } + async _updateMyMenuPlugins(event) { + await new Promise((res)=> { + setTimeout(() => { + res() + }, 1000); + }) + const detail = event.detail + this.myMenuPlugins = detail + const addressInfo = this.addressInfo + const isMinter = addressInfo?.error !== 124 && +addressInfo?.level > 0 + const isSponsor = +addressInfo?.level >= 5 + + + if (!isMinter) { + this.newMenuList = this.myMenuPlugins.filter((minter) => { + return minter.url !== 'minting' + }) + } else { + this.newMenuList = this.myMenuPlugins.filter((minter) => { + return minter.url !== 'become-minter' + }) + } + + if (!isSponsor) { + this.myMenuList = this.newMenuList.filter((sponsor) => { + return sponsor.url !== 'sponsorship-list' + }) + } else { + this.myMenuList = this.newMenuList + } + + this.requestUpdate() + } + + connectedCallback() { + super.connectedCallback() + window.addEventListener('myMenuPlugs-event', this._updateMyMenuPlugins) } + + disconnectedCallback() { + window.removeEventListener('myMenuPlugs-event', this._updateMyMenuPlugins) + super.disconnectedCallback() + } + openImportDialog() { this.shadowRoot.getElementById('importTabMenutDialog').show() } @@ -1468,7 +1521,9 @@ class NavBar extends connect(store)(LitElement) { localStorage.removeItem("myMenuPlugs") myFile = file const newTabMenu = JSON.parse((myFile) || "[]") + const copyPayload = [...newTabMenu] localStorage.setItem("myMenuPlugs", JSON.stringify(newTabMenu)) + this.saveSettingToTemp(copyPayload) this.shadowRoot.getElementById('importTabMenutDialog').close() this.myMenuPlugins = JSON.parse(localStorage.getItem("myMenuPlugs") || "[]") this.firstUpdated() @@ -1951,8 +2006,10 @@ class NavBar extends connect(store)(LitElement) { if (myNameRes !== false) { oldMenuPlugs.push(newMenuPlugsItem) + const copyPayload = [...oldMenuPlugs] localStorage.setItem("myMenuPlugs", JSON.stringify(oldMenuPlugs)) + this.saveSettingToTemp(copyPayload) let myplugstring2 = get("walletpage.wchange52") parentEpml.request('showSnackBar', `${myplugstring2}`) @@ -2014,9 +2071,10 @@ class NavBar extends connect(store)(LitElement) { if (myNameRes !== false) { oldMenuPlugs.push(newMenuPlugsItem) + const copyPayload = [...oldMenuPlugs] localStorage.setItem("myMenuPlugs", JSON.stringify(oldMenuPlugs)) - + this.saveSettingToTemp(copyPayload) let myplugstring2 = get("walletpage.wchange52") parentEpml.request('showSnackBar', `${myplugstring2}`) @@ -2084,9 +2142,10 @@ class NavBar extends connect(store)(LitElement) { } oldMenuPlugs.push(newMenuPlugsItem) + const copyPayload = [...oldMenuPlugs] localStorage.setItem("myMenuPlugs", JSON.stringify(oldMenuPlugs)) - + this.saveSettingToTemp(copyPayload) let myplugstring2 = get("walletpage.wchange52") parentEpml.request('showSnackBar', `${myplugstring2}`) @@ -2149,11 +2208,20 @@ class NavBar extends connect(store)(LitElement) { renderRemoveIcon(appurl, appicon, appname, appid, appplugin) { return html` -
    + +