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) {
this.closeLockScreenActive()}">
+
+
`
}
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`
+
+
+
+
+
+
+ ${this.editContent
+ ? translate('friends.friend10')
+ : translate('friends.friend2')}
+
+
+
+
${translate('friends.friend3')}
+
+
+ {
+ this.willFollow = e.target.checked;
+ }}
+ ?checked=${this.willFollow}
+ >
+
+
+
+
+
+
+
+
+
+ {
+ this.alias = e.target.value
+ }}
+ />
+
+
+
+
+
+
+
${translate('friends.friend15')}
+
+
${translate('friends.friend16')}
+
+
+ ${this.isLoadingSchemas ? html`
+
+ ` : ''}
+ ${this.availableFeeedSchemas.map((schema) => {
+ const isAlreadySelected = this.mySelectedFeeds.find(
+ (item) => item.name === schema.name
+ );
+ let avatarImgApp;
+ const avatarUrl2 = `${this.nodeUrl}/arbitrary/THUMBNAIL/${schema.name}/qortal_avatar?async=true&apiKey=${this.myNode.apiKey}`;
+ avatarImgApp = html`

`;
+ return html`
+
{
+ const copymySelectedFeeds = [
+ ...this.mySelectedFeeds,
+ ];
+ const findIndex =
+ copymySelectedFeeds.findIndex(
+ (item) =>
+ item.name === schema.name
+ );
+ if (findIndex === -1) {
+ if(this.mySelectedFeeds.length > 4) return
+ copymySelectedFeeds.push({
+ name: schema.name,
+ identifier: schema.identifier,
+ service: schema.service,
+ });
+ this.mySelectedFeeds =
+ copymySelectedFeeds;
+ } else {
+ this.mySelectedFeeds =
+ copymySelectedFeeds.filter(
+ (item) =>
+ item.name !==
+ schema.name
+ );
+ }
+ }}
+ >
+
${avatarImgApp}
+
${schema.name}
+
+ `;
+ })}
+
+
+
+
+ ${this.editContent
+ ? 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
+
+
{
+ this.openEditFriend();
+ this.closePopover();
+ }}"
+ >
+ edit
+ ${translate('friends.friend10')}
+
+
{
+ const address = await this.getUserAddress();
+ if (!address) return;
+ store.dispatch(
+ setNewTab({
+ url: `q-chat`,
+ id: this.uid.rnd(),
+ myPlugObj: {
+ url: 'q-chat',
+ domain: 'core',
+ page: 'messaging/q-chat/index.html',
+ title: 'Q-Chat',
+ icon: 'vaadin:chat',
+ mwcicon: 'forum',
+ pluginNumber: 'plugin-qhsyOnpRhT',
+ menus: [],
+ parent: false,
+ },
+
+ openExisting: true,
+ })
+ );
+ store.dispatch(
+ setSideEffectAction({
+ type: 'openPrivateChat',
+ data: {
+ address,
+ name: this.name
+ },
+ })
+ );
+ this.closePopover();
+ this.closeSidePanel()
+ }}"
+ >
+ send
+ ${translate('friends.friend8')}
+
+
{
+ const query = `?service=APP&name=Q-Mail/to/${this.name}`;
+ 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: 'Q-Mail',
+ icon: 'vaadin:mailbox',
+ mwcicon: 'mail_outline',
+ menus: [],
+ parent: false,
+ },
+ openExisting: true,
+ })
+ );
+ this.closePopover();
+ this.closeSidePanel()
+ }}"
+ >
+ 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.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 : ''
+ }`}
+
+
+
+
{
+ localStorage.setItem(
+ 'temp-settings-data',
+ JSON.stringify({})
+ );
+ this.valuesToBeSavedOnQdn = {};
+ const popover =
+ this.shadowRoot.querySelector(
+ 'popover-component'
+ );
+ if (popover) {
+ popover.closePopover();
+ }
+ this.getGeneralSettingsQdn();
+ }}"
+ >
+ ${translate('save.saving4')}
+
+
{
+ this.saveToQdn();
+ const popover =
+ this.shadowRoot.querySelector(
+ 'popover-component'
+ );
+ if (popover) {
+ popover.closePopover();
+ }
+ }}"
+ >
+ ${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`
-
this.openRemoveApp(appname, appid, appurl)}">
+
+