diff --git a/core/language/us.json b/core/language/us.json index e53d4411..fea1be8b 100644 --- a/core/language/us.json +++ b/core/language/us.json @@ -1254,5 +1254,29 @@ "profile24": "You must fill out both field name and field value to add a custom property", "profile25": "Is your friend", "profile26": "Add as friend" + }, + "tour": { + "tour1": "In order to use Qortal, the Core must be synced. This icon will be in blue when it's synced.", + "tour2": "Synced", + "tour3": "Synced and minting", + "tour4": "Syncing", + "tour5": "Sync your core", + "tour6": "The Unstoppable Force of Qortal", + "tour7": "Only you control your data on Qortal", + "tour8": "Qortal cannot be taken down", + "tour9": "Completely peer-to-peer with no centralized intermediaries", + "tour10": "This is the default tab view where you can access important Qortal settings and Q-apps such as Q-Tube.", + "tour11": "Get the full experience", + "tour12": "To get the full Qortal experience, we recommend following this checklist.", + "tour13": "You are fully synced! You can now experience the power of the Qortal blockchain.", + "tour14":"Let's try visiting Q-Tube!", + "tour15":"Visit Q-Tube", + "tour16": "Checklist", + "tour17": "Please start the Core to access the Qortal blockchain.", + "tour18": "Bootstrap", + "tour19":"Currently syncing... please wait to use Qortal to its full potential.", + "tour20": "blocks behind. Would you like to bootstrap to speed up the syncing process?", + "tour21": "blocks remaining.", + "tour22": "Bootstrap requested. Please wait." } } diff --git a/core/src/components/app-view.js b/core/src/components/app-view.js index 11c83388..afe909cd 100644 --- a/core/src/components/app-view.js +++ b/core/src/components/app-view.js @@ -42,6 +42,9 @@ import './friends-view/friends-side-panel-parent.js' import './friends-view/save-settings-qdn.js' import './friends-view/core-sync-status.js' import './friends-view/profile.js' +import './beginner-tour/tour-component.js' +import './beginner-tour/sync-indicator.js' +import './friends-view/beginner-checklist.js' import './controllers/coin-balances-controller.js' const chatLastSeen = localForage.createInstance({ @@ -540,6 +543,26 @@ class AppView extends connect(store)(LitElement) { this.myLockScreenPass = '' this.myLockScreenSet = '' this.helperMessage = '' + this.getTourElements = this.getTourElements.bind(this) + } + + getTourElements(){ + let els = {} + console.log('this.shadowRoot.querySelector("core-sync-status")', this.shadowRoot.querySelector("core-sync-status")) + const el1 = this.shadowRoot.querySelector("core-sync-status").shadowRoot.getElementById("core-sync-status-id") + const el2 = this.shadowRoot.querySelector("show-plugin").shadowRoot.getElementById("showPluginId") + const el3 = this.shadowRoot.querySelector("beginner-checklist").shadowRoot.getElementById("popover-notification") + if(el1) { + els['core-sync-status-id'] = el1 + } + if(el2) { + els['tab'] = el2 + } + if(el3) { + els['checklist'] = el3 + } + + return els } render() { @@ -570,6 +593,7 @@ class AppView extends connect(store)(LitElement) {
+ @@ -600,6 +624,8 @@ class AppView extends connect(store)(LitElement) { + + diff --git a/core/src/components/beginner-tour/sync-indicator.js b/core/src/components/beginner-tour/sync-indicator.js new file mode 100644 index 00000000..f4029800 --- /dev/null +++ b/core/src/components/beginner-tour/sync-indicator.js @@ -0,0 +1,281 @@ +import { LitElement, html, css } from 'lit'; +import { store } from '../../store'; +import { connect } from 'pwa-helpers'; +import '@material/mwc-icon'; +import { translate } from '../../../translate'; +import { parentEpml } from '../show-plugin'; + +class SyncIndicator extends connect(store)(LitElement) { + static get properties() { + return { + isBehind: { type: Boolean }, + blocksBehind: { type: Number }, + isSynchronizing: { type: Boolean }, + hasCoreRunning: { type: Boolean }, + }; + } + + constructor() { + super(); + this.isBehind = null; + this.blocksBehind = 0; + this.nodeUrl = this.getNodeUrl(); + this.myNode = this.getMyNode(); + this.interval = null; + this.hasCoreRunning = true; + this.seenWelcomeSync = false; + this.numberOfTries = 0; + } + + static get styles() { + return css` + * { + --mdc-theme-text-primary-on-background: var(--black); + box-sizing: border-box; + } + :host { + box-sizing: border-box; + position: fixed; + bottom: 25px; + right: 25px; + z-index: 50000; + } + .parent { + width: 360px; + padding: 10px; + border-radius: 8px; + border: 1px solid var(--black); + display: flex; + align-items: center; + gap: 10px; + user-select: none; + background: var(--white); + } + .row { + display: flex; + gap: 10px; + width: 100%; + } + .column { + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; + } + .bootstrap-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; + } + + .bootstrap-button:hover { + cursor: pointer; + background-color: #03a8f475; + } + `; + } + async firstUpdated() { + const seenWelcomeSync = JSON.parse( + localStorage.getItem('welcome-sync') || 'false' + ); + this.seenWelcomeSync = seenWelcomeSync; + } + + 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 getDaySummary() { + try { + const endpoint = `${this.nodeUrl}/admin/summary/?apiKey=${this.myNode.apiKey}`; + const res = await fetch(endpoint); + const data = await res.json(); + let blockTimeInSeconds = null; + if (data.blockCount) { + const blockTime = 1440 / data.blockCount; + blockTimeInSeconds = blockTime * 60; + } + + const endpointLastBlock = `${this.nodeUrl}/blocks/last`; + const resLastBlock = await fetch(endpointLastBlock); + const dataLastBlock = await resLastBlock.json(); + const timestampNow = Date.now(); + const currentBlockTimestamp = dataLastBlock.timestamp; + if (blockTimeInSeconds && currentBlockTimestamp < timestampNow) { + const diff = timestampNow - currentBlockTimestamp; + const inSeconds = diff / 1000; // millisecs to secs + const inBlocks = inSeconds / blockTimeInSeconds; + this.blocksBehind = parseInt(inBlocks); + if (inBlocks >= 1000) { + this.isBehind = true; + } else { + this.isBehind = false; + } + } else { + this.blocksBehind = 0; + this.isBehind = false; + } + } catch (error) {} + } + + async checkHowManyBlocksBehind() { + try { + this.getDaySummary(); + this.interval = setInterval(() => { + if (this.isBehind === false) { + this.isBehind = null; + clearInterval(this.interval); + } + this.getDaySummary(); + }, 60000); + } catch (error) {} + } + + stateChanged(state) { + console.log({state}) + if ( + state.app.nodeStatus && + Object.keys(state.app.nodeStatus).length === 0 + ) { + if (this.numberOfTries > 5) { + this.hasCoreRunning = false; + } else { + this.numberOfTries = this.numberOfTries + 1; + } + } else if ( + state.app.nodeStatus && + state.app.nodeStatus.syncPercent !== this.syncPercentage + ) { + this.syncPercentage = state.app.nodeStatus.syncPercent; + + if (state.app.nodeStatus.syncPercent !== 100) { + this.isSynchronizing = true; + } else { + this.isSynchronizing = false; + } + if ( + this.isBehind === null && + state.app.nodeStatus.syncPercent === 100 + ) { + this.isBehind = false; + this.blocksBehind = 0; + if (!this.seenWelcomeSync) { + this.dispatchEvent( + new CustomEvent('open-welcome-modal-sync', { + bubbles: true, + composed: true, + }) + ); + } + } else if ( + !this.interval && + this.isBehind === null && + state.app.nodeStatus.isSynchronizing && + state.app.nodeStatus.syncPercent !== 100 + ) { + this.checkHowManyBlocksBehind(); + } + } else { + this.hasCoreRunning = true; + } + } + + async bootstrap(){ + try { + const endpoint = `${this.nodeUrl}/admin/bootstrap/?apiKey=${this.myNode.apiKey}`; + const res = await fetch(endpoint); + const data = await res.json(); + if(data === true){ + parentEpml.request('showSnackBar', get('tour.tour22')); + } + console.log({data}) + } catch (error) { + + } + } + + render() { + return html` + ${!this.hasCoreRunning + ? html` +
+ priority_high +

+ ${translate("tour.tour17")} +

+
+ ` + : (this.isBehind && this.isSynchronizing) + ? html` +
+
+
+ +

+ ${this.blocksBehind} ${translate("tour.tour20")} +

+
+
+ +
+
+
+ ` + : this.isSynchronizing + ? html` +
+ +

+ ${translate("tour.tour19")} ${this.blocksBehind ? this.blocksBehind : ""} ${this.blocksBehind ? translate("tour.tour21"): ""} +

+
+ ` + : "" } + `; + } +} +customElements.define('sync-indicator', SyncIndicator); diff --git a/core/src/components/beginner-tour/tour-component.js b/core/src/components/beginner-tour/tour-component.js new file mode 100644 index 00000000..938cf393 --- /dev/null +++ b/core/src/components/beginner-tour/tour-component.js @@ -0,0 +1,396 @@ +import { LitElement, html, css } from 'lit'; +import { driver } from 'driver.js'; +import 'driver.js/dist/driver.css'; +import '@material/mwc-icon'; +import '@polymer/paper-spinner/paper-spinner-lite.js'; +import '@vaadin/tooltip'; +import '@material/mwc-button'; +import { get, translate } from '../../../translate/index.js'; +import '@polymer/paper-dialog/paper-dialog.js'; +import { setNewTab } from '../../redux/app/app-actions.js'; +import { store } from '../../store.js'; +import { connect } from 'pwa-helpers'; +import './tour.css'; +class TourComponent extends connect(store)(LitElement) { + static get properties() { + return { + getElements: { attribute: false }, + dialogOpenedCongrats: { type: Boolean }, + hasViewedTour: { type: Boolean }, + }; + } + + constructor() { + super(); + this.dialogOpenedCongrats = false; + this._controlOpenWelcomeModal = + this._controlOpenWelcomeModal.bind(this); + this.hasName = false; + this.nodeUrl = this.getNodeUrl(); + this.myNode = this.getMyNode(); + } + + 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); + box-sizing: border-box; + color: var(--black); + background: var(--white); + } + + :host { + box-sizing: border-box; + position: fixed; + bottom: 25px; + right: 25px; + z-index: 50000; + } + + .full-info-wrapper { + width: 100%; + min-width: 600px; + max-width: 600px; + text-align: center; + background: var(--white); + border: 1px solid var(--black); + border-radius: 15px; + padding: 25px; + box-shadow: 0px 10px 15px rgba(0, 0, 0, 0.1); + display: block !important; + } + + .buttons { + display: inline; + } + .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(--black); + transition: all 0.3s ease-in-out; + display: flex; + align-items: center; + gap: 10px; + font-size: 18px; + justify-content: center; + outline: 1px solid var(--black); + } + + .accept-button:hover { + cursor: pointer; + background-color: #03a8f485; + } + + .close-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; + width:auto; + } + + .close-button:hover { + cursor: pointer; + background-color: #f4433663; + } + `; + } + + _controlOpenWelcomeModal() { + this.isSynced = true + + const seenWelcomeSync = JSON.parse( + localStorage.getItem('welcome-sync') || 'false' + ); + if (this.hasName) return; + if (seenWelcomeSync) return; + if(!this.hasViewedTour) return + this.dialogOpenedCongrats = true; + } + + openWelcomeModal() { + this.dispatchEvent( + new CustomEvent('send-tour-finished', { + bubbles: true, + composed: true, + }) + ); + const seenWelcomeSync = JSON.parse( + localStorage.getItem('welcome-sync') || 'false' + ); + if (this.hasName) return; + if (seenWelcomeSync) return; + if(!this.isSynced) return + this.dialogOpenedCongrats = true; + } + + connectedCallback() { + super.connectedCallback(); + window.addEventListener( + 'open-welcome-modal-sync', + this._controlOpenWelcomeModal + ); + } + + disconnectedCallback() { + window.removeEventListener( + 'open-welcome-modal-sync', + this._controlOpenWelcomeModal + ); + + super.disconnectedCallback(); + } + + 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 getName(recipient) { + try { + const endpoint = `${this.nodeUrl}/names/address/${recipient}`; + const res = await fetch(endpoint); + const getNames = await res.json(); + + if (Array.isArray(getNames) && getNames.length > 0) { + return getNames[0].name; + } else { + return ''; + } + } catch (error) { + return ''; + } + } + async firstUpdated() { + const hasViewedTour = JSON.parse( + localStorage.getItem('hasViewedTour') || 'false' + ); + const selectedAddress = store.getState().app.selectedAddress || ''; + const name = await this.getName(selectedAddress.address); + if (name) { + this.hasName = true; + } + this.hasViewedTour = hasViewedTour; + if (!hasViewedTour) { + try { + if (name) { + this.hasViewedTour = true; + this.hasName = true; + localStorage.setItem("hasViewedTour", JSON.stringify(true)) + } + } catch (error) { + console.log({ error }); + } + } + await new Promise((res) => { + setTimeout(() => { + res(); + }, 1000); + }); + if (!this.hasViewedTour) { + const elements = this.getElements(); + let steps = [ + { + popover: { + title: get("tour.tour6"), + description: ` +
+ +
+
+

${get("tour.tour7")}

+
+
+

${get("tour.tour8")}

+
+
+

${get("tour.tour9")}

+
+ `, + // ... other options + }, + }, + ]; + const step2 = elements['core-sync-status-id']; + const step3 = elements['tab']; + const step4 = elements['checklist']; + + if (step2) { + steps.push({ + element: step2, + popover: { + title: get("tour.tour5"), + description: ` +
+

${get("tour.tour1")}

+
+
+ +

${get("tour.tour2")}

+
+
+ +

${get("tour.tour3")}

+
+
+ +

${get("tour.tour4")}

+
+ + `, + }, + }); + } + if (step3) { + steps.push({ + element: step3, + popover: { + title: 'Tab View', + description: ` +
+

${get("tour.tour10")} +

+
+
+ +

You can also bookmark other Q-Apps and Plugins by clicking on the ${get( + 'tabmenu.tm19' + )} button

+
+ `, + }, + }); + } + if (step4) { + steps.push( + { + element: step4, + popover: { + title: get("tour.tour11"), + description: get("tour.tour12"), + }, + } + );this.hasViewedTour + } + let currentStepIndex = 0; + const driverObj = driver({ + popoverClass: 'driverjs-theme', + showProgress: true, + showButtons: ['next', 'previous'], + steps: steps, + allowClose: false, + onDestroyed: () => { + localStorage.setItem("hasViewedTour", JSON.stringify(true)) + this.hasViewedTour = true; + this.openWelcomeModal(); + } + }); + + driverObj.drive(); + } else { + this.dispatchEvent( + new CustomEvent('send-tour-finished', { + bubbles: true, + composed: true, + }) + ); + } + } + + visitQtube() { + this.onClose(); + const query = `?service=APP&name=Q-Tube`; + store.dispatch( + setNewTab({ + url: `qdn/browser/index.html${query}`, + id: 'q-mail-notification', + myPlugObj: { + url: 'myapp', + domain: 'core', + page: `qdn/browser/index.html${query}`, + title: 'Q-Tube', + menus: [], + parent: false, + }, + }) + ); + } + + onClose() { + localStorage.setItem("welcome-sync", JSON.stringify(true)) + this.dialogOpenedCongrats = false; + } + + render() { + return html` + + ${this.dialogOpenedCongrats && this.hasViewedTour + ? html` + +

Congratulations!

+
+ ${translate("tour.tour13")} +
+
+ ${translate("tour.tour14")} +
+ +
+ ${translate("tour.tour15")} +
+
+
{ + this.dialogOpenedCongrats = false + }} + > + ${translate("general.close")} +
+
+
+ ` + : ''} + `; + } +} +customElements.define('tour-component', TourComponent); diff --git a/core/src/components/beginner-tour/tour.css b/core/src/components/beginner-tour/tour.css new file mode 100644 index 00000000..9f9e971f --- /dev/null +++ b/core/src/components/beginner-tour/tour.css @@ -0,0 +1,77 @@ +.driver-popover.driverjs-theme { + background-color: var(--white); + color: var(--black); + max-width: 500px; + width: auto; + } + + .driver-popover.driverjs-theme .driver-popover-title { + font-size: 20px; + text-align: center; + } + + .driver-popover.driverjs-theme .driver-popover-title, + .driver-popover.driverjs-theme .driver-popover-description, + .driver-popover.driverjs-theme .driver-popover-progress-text { + color: var(--black); + font-family: Roboto, sans-serif; + } + .driver-popover.driverjs-theme .driver-popover-description { + font-size: 16px; + } + + .driver-popover.driverjs-theme button { + flex: 1; + text-align: center; + background-color: #000; + color: #ffffff; + border: 2px solid #000; + text-shadow: none; + font-size: 14px; + padding: 5px 8px; + border-radius: 6px; + } + + .driver-popover.driverjs-theme .test-span { + color: green; + } + + .driver-popover.driverjs-theme button:hover { + background-color: #000; + color: #ffffff; + } + + .driver-popover.driverjs-theme .driver-popover-navigation-btns { + justify-content: space-between; + gap: 3px; + } + + .driver-popover.driverjs-theme .driver-popover-close-btn { + color: #9b9b9b; + } + + .driver-popover.driverjs-theme .driver-popover-close-btn:hover { + color: #000; + } + .driver-popover.driverjs-theme .driver-popover-footer { + gap: 20px; + } + .driver-popover.driverjs-theme .driver-popover-footer button { + background-color: #000 !important; + } + + .driver-popover.driverjs-theme .driver-popover-arrow-side-left.driver-popover-arrow { + border-left-color: #fde047; + } + + .driver-popover.driverjs-theme .driver-popover-arrow-side-right.driver-popover-arrow { + border-right-color: #fde047; + } + + .driver-popover.driverjs-theme .driver-popover-arrow-side-top.driver-popover-arrow { + border-top-color: #fde047; + } + + .driver-popover.driverjs-theme .driver-popover-arrow-side-bottom.driver-popover-arrow { + border-bottom-color: #fde047; + } \ No newline at end of file diff --git a/core/src/components/friends-view/beginner-checklist.js b/core/src/components/friends-view/beginner-checklist.js new file mode 100644 index 00000000..6d77b016 --- /dev/null +++ b/core/src/components/friends-view/beginner-checklist.js @@ -0,0 +1,343 @@ +import { css, html, LitElement } from 'lit'; +import { connect } from 'pwa-helpers'; + +import '@vaadin/item'; +import '@vaadin/list-box'; +import '@polymer/paper-icon-button/paper-icon-button.js'; +import '@polymer/iron-icons/iron-icons.js'; +import { store } from '../../store.js'; +import { setNewTab } from '../../redux/app/app-actions.js'; +import '@material/mwc-icon'; +import { get, translate } from '../../../translate/index.js'; +import { repeat } from 'lit/directives/repeat.js'; +import '../../../../plugins/plugins/core/components/TimeAgo.js'; +import '../notification-view/popover.js'; +import ShortUniqueId from 'short-unique-id'; + +class BeginnerChecklist extends connect(store)(LitElement) { + static properties = { + notifications: { type: Array }, + showChecklist: { type: Boolean }, + theme: { type: String, reflect: true }, + isSynced: { type: Boolean }, + hasName: { type: Boolean }, + hasTourFinished: { type: Boolean }, + }; + + constructor() { + super(); + this.showChecklist = false; + this.initialFetch = false; + this.theme = localStorage.getItem('qortalTheme') + ? localStorage.getItem('qortalTheme') + : 'light'; + this.isSynced = false; + this.hasName = null; + this.nodeUrl = this.getNodeUrl(); + this.myNode = this.getMyNode(); + this.hasTourFinished = false; + this._controlTourFinished = this._controlTourFinished.bind(this); + this.uid = new ShortUniqueId(); + } + + _controlTourFinished() { + this.hasTourFinished = true; + } + + connectedCallback() { + super.connectedCallback(); + window.addEventListener( + 'send-tour-finished', + this._controlTourFinished + ); + } + + disconnectedCallback() { + window.removeEventListener( + 'send-tour-finished', + this._controlTourFinished + ); + + super.disconnectedCallback(); + } + + 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 getName(recipient) { + try { + const endpoint = `${this.nodeUrl}/names/address/${recipient}`; + const res = await fetch(endpoint); + const getNames = await res.json(); + + if (Array.isArray(getNames) && getNames.length > 0) { + this.hasName = true; + } else { + this.hasName = false; + } + } catch (error) { + return ''; + } + } + + stateChanged(state) { + if ( + state.app.nodeStatus && + state.app.nodeStatus.syncPercent !== this.syncPercentage + ) { + this.syncPercentage = state.app.nodeStatus.syncPercent; + + if ( + !this.hasAttempted && + state.app.nodeStatus.syncPercent === 100 + ) { + this.hasAttempted = true; + this.getName(); + } + } + if ( + state.app.accountInfo && + state.app.accountInfo.names.length && + state.app.nodeStatus && + state.app.nodeStatus.syncPercent === 100 && + this.hasName === false && + this.hasAttempted && + state.app.accountInfo && + state.app.accountInfo.names && + state.app.accountInfo.names.length > 0 + ) { + this.hasName = true; + } + } + + handleBlur() { + setTimeout(() => { + if (!this.shadowRoot.contains(document.activeElement)) { + this.showChecklist = false; + } + }, 0); + } + + + + render() { + + return this.hasName === false || !this.hasTourFinished + ? html` +
+ +
this._toggleChecklist()} + > + checklist + + +
+ + +
+
+
+

Are you synced?

+ ${this.syncPercentage === 100 + ? html` + task_alt + ` + : html` + radio_button_unchecked + `} +
+ +
{ + store.dispatch( + setNewTab({ + url: `group-management`, + id: this.uid.rnd(), + myPlugObj: { + url: 'name-registration', + domain: 'core', + page: 'name-registration/index.html', + title: 'Name Registration', + icon: 'vaadin:user-check', + mwcicon: 'manage_accounts', + pluginNumber: + 'plugin-qCmtXAQmtu', + menus: [], + parent: false, + }, + openExisting: true, + }) + ); + this.handleBlur() + }}> +

Do you have a name registered?

+ ${this.hasName + ? html` + task_alt + ` + : html` + radio_button_unchecked + `} +
+
+
+
+ ` + : ''; + } + + _toggleChecklist() { + this.showChecklist = !this.showChecklist; + if (this.showChecklist) { + requestAnimationFrame(() => { + this.shadowRoot.getElementById('checklist-panel').focus(); + }); + } + } + + static styles = css` + .layout { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + } + + .count { + position: absolute; + top: -5px; + right: -5px; + font-size: 12px; + background-color: red; + color: white; + border-radius: 50%; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + } + + .nocount { + display: none; + } + + .popover-panel { + position: absolute; + width: 200px; + padding: 10px; + background-color: var(--white); + border: 1px solid var(--black); + border-radius: 4px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + top: 40px; + max-height: 350px; + overflow: auto; + scrollbar-width: thin; + scrollbar-color: #6a6c75 #a1a1a1; + } + + .popover-panel::-webkit-scrollbar { + width: 11px; + } + + .popover-panel::-webkit-scrollbar-track { + background: #a1a1a1; + } + + .popover-panel::-webkit-scrollbar-thumb { + background-color: #6a6c75; + border-radius: 6px; + border: 3px solid #a1a1a1; + } + + .list { + display: flex; + flex-direction: column; + gap: 15px; + } + + .task-list-item { + display: flex; + gap: 15px; + justify-content: space-between; + align-items: center; + } + + .checklist-item { + padding: 5px; + border-bottom: 1px solid; + display: flex; + justify-content: space-between; + cursor: pointer; + transition: 0.2s all; + } + + .checklist-item:hover { + background: var(--nav-color-hover); + } + + p { + font-size: 16px; + color: var(--black); + margin: 0px; + padding: 0px; + } + `; +} + +customElements.define('beginner-checklist', BeginnerChecklist); diff --git a/core/src/components/friends-view/core-sync-status.js b/core/src/components/friends-view/core-sync-status.js index 4ec0d633..733123aa 100644 --- a/core/src/components/friends-view/core-sync-status.js +++ b/core/src/components/friends-view/core-sync-status.js @@ -81,7 +81,9 @@ class CoreSyncStatus extends connect(store)(LitElement) { render() { return html` +
${this.renderSyncStatusIcon()} +
` } diff --git a/core/src/components/friends-view/friends-view.js b/core/src/components/friends-view/friends-view.js index be15c87f..148422c9 100644 --- a/core/src/components/friends-view/friends-view.js +++ b/core/src/components/friends-view/friends-view.js @@ -139,8 +139,8 @@ class FriendsView extends connect(store)(LitElement) { disconnectedCallback() { window.removeEventListener('friends-my-friend-list-event', this._updateFriends) - window.addEventListener('friends-my-selected-feeds-event', this._updateFeed) - window.addEventListener('add-friend', this._addFriend) + window.removeEventListener('friends-my-selected-feeds-event', this._updateFeed) + window.removeEventListener('add-friend', this._addFriend) super.disconnectedCallback() } diff --git a/core/src/components/show-plugin.js b/core/src/components/show-plugin.js index 44166dcb..824da3ab 100644 --- a/core/src/components/show-plugin.js +++ b/core/src/components/show-plugin.js @@ -19,6 +19,7 @@ import '@polymer/paper-dialog/paper-dialog.js' import '@vaadin/grid' import '@vaadin/text-field' import '../custom-elements/frag-file-input.js' +import { defaultQappsTabs } from '../data/defaultQapps.js' registerTranslateConfig({ loader: lang => fetch(`/language/${lang}.json`).then(res => res.json()) @@ -322,6 +323,7 @@ class ShowPlugin extends connect(store)(LitElement) { constructor() { super() this.registeredUrls = [] + this.initialRegisteredUrls = [] this.currentTab = 0 this.tabs = [] this.uid = new ShortUniqueId() @@ -339,6 +341,7 @@ class ShowPlugin extends connect(store)(LitElement) { } return html` +
${this.tabs.map((tab, index) => { let title = '' @@ -1725,7 +1728,9 @@ class NavBar extends connect(store)(LitElement) { if (localStorage.getItem("myMenuPlugs") === null) { await appDelay(1000) - const myObj = JSON.stringify(this.menuList) + const listOfPlugins = this.menuList.filter(plugin=> plugin.url !== "puzzles") + const addQapps = [...listOfPlugins, ...defaultQappsTabs] + const myObj = JSON.stringify(addQapps) localStorage.setItem("myMenuPlugs", myObj) this.myMenuPlugins = JSON.parse(localStorage.getItem("myMenuPlugs") || "[]") } else { diff --git a/core/src/data/defaultQapps.js b/core/src/data/defaultQapps.js new file mode 100644 index 00000000..0378984a --- /dev/null +++ b/core/src/data/defaultQapps.js @@ -0,0 +1,79 @@ +export const defaultQappsTabs = [ + { + "url": "myapp", + "domain": "core", + "page": "qdn/browser/index.html?name=Q-Tube&service=APP", + "title": "Q-Tube", + "icon": "vaadin:external-browser", + "mwcicon": "apps", + "pluginNumber": "plugin-04tlGhUzzd", + "menus": [], + "parent": false + }, + { + "url": "myapp", + "domain": "core", + "page": "qdn/browser/index.html?name=Q-Shop&service=APP", + "title": "Q-Shop", + "icon": "vaadin:external-browser", + "mwcicon": "apps", + "pluginNumber": "plugin-gAogkIS5z6", + "menus": [], + "parent": false + }, + { + "url": "myapp", + "domain": "core", + "page": "qdn/browser/index.html?name=Q-Blog&service=APP", + "title": "Q-Blog", + "icon": "vaadin:external-browser", + "mwcicon": "rss_feed", + "pluginNumber": "plugin-4KX5PHKSbM", + "menus": [], + "parent": false + }, + { + "url": "myapp", + "domain": "core", + "page": "qdn/browser/index.html?name=Q-Fund&service=APP", + "title": "Q-Fund", + "icon": "vaadin:external-browser", + "mwcicon": "apps", + "pluginNumber": "plugin-POw2jHDdHo", + "menus": [], + "parent": false + }, + { + "url": "myapp", + "domain": "core", + "page": "qdn/browser/index.html?name=Q-Share&service=APP", + "title": "Q-Share", + "icon": "vaadin:external-browser", + "mwcicon": "apps", + "pluginNumber": "plugin-POw24oDdHo", + "menus": [], + "parent": false + }, + { + "url": "myapp", + "domain": "core", + "page": "qdn/browser/index.html?name=Ear-Bump&service=APP", + "title": "Ear-Bump", + "icon": "vaadin:external-browser", + "mwcicon": "apps", + "pluginNumber": "plugin-POx8jHDdHo", + "menus": [], + "parent": false + }, + { + "url": "myapp", + "domain": "core", + "page": "qdn/browser/index.html?name=Q-Mail&service=APP", + "title": "Q-Mail", + "icon": "vaadin:external-browser", + "mwcicon": "mail", + "pluginNumber": "plugin-GGHiHzW6pe", + "menus": [], + "parent": false + } +] \ No newline at end of file diff --git a/core/src/styles/switch-theme.css b/core/src/styles/switch-theme.css index b7a342ba..cd195572 100644 --- a/core/src/styles/switch-theme.css +++ b/core/src/styles/switch-theme.css @@ -136,4 +136,5 @@ html[theme="dark"] { --app-hr: rgba(255, 255, 255, .3); --code-block-text-color: #008fd5; --noavatar: url("/img/noavatar_dark.png"); -} \ No newline at end of file +} + diff --git a/img/addplugin.webp b/img/addplugin.webp new file mode 100644 index 00000000..13795259 Binary files /dev/null and b/img/addplugin.webp differ diff --git a/package-lock.json b/package-lock.json index 3dc571b8..7fdcc544 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "buffer": "6.0.3", "compressorjs": "1.2.1", "crypto-js": "4.2.0", + "driver.js": "^1.3.1", "electron-dl": "3.5.1", "electron-log": "5.0.1", "electron-store": "8.1.0", @@ -5450,6 +5451,11 @@ "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", "dev": true }, + "node_modules/driver.js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/driver.js/-/driver.js-1.3.1.tgz", + "integrity": "sha512-MvUdXbqSgEsgS/H9KyWb5Rxy0aE6BhOVT4cssi2x2XjmXea6qQfgdx32XKVLLSqTaIw7q/uxU5Xl3NV7+cN6FQ==" + }, "node_modules/ejs": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", diff --git a/package.json b/package.json index ad4b1f4c..6f1f679f 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "buffer": "6.0.3", "compressorjs": "1.2.1", "crypto-js": "4.2.0", + "driver.js": "^1.3.1", "electron-dl": "3.5.1", "electron-log": "5.0.1", "electron-store": "8.1.0", diff --git a/plugins/plugins/core/streams/onNewBlock.js b/plugins/plugins/core/streams/onNewBlock.js index 495f920b..2aceab39 100644 --- a/plugins/plugins/core/streams/onNewBlock.js +++ b/plugins/plugins/core/streams/onNewBlock.js @@ -1,8 +1,9 @@ import {parentEpml} from '../connect.js' +const MIN_RECONNECT_INTERVAL = 1000; // 1 second +const MAX_RECONNECT_INTERVAL = 300000; // 5 minutes let socketObject let activeBlockSocketTimeout -let initial = 0 let closeGracefully = false let isCalled = false let retryOnClose = false @@ -10,11 +11,12 @@ let blockFirstCall = true let nodeStatusSocketObject let nodeStatusSocketTimeout let nodeStatusSocketcloseGracefully = false -let nodeStatusCount = 0 let nodeStatusRetryOnClose = false let nodeStateCall = false let isLoggedIn = false let oldAccountInfo +let blockSocketReconnectInterval = MIN_RECONNECT_INTERVAL; +let nodeStatusSocketReconnectInterval = MIN_RECONNECT_INTERVAL; parentEpml.subscribe('logged_in', loggedIn => { if (loggedIn === 'true') { @@ -24,6 +26,7 @@ parentEpml.subscribe('logged_in', loggedIn => { } }) + const setAccountInfo = async (addr) => { const names = await parentEpml.request('apiCall', { url: `/names/address/${addr}` @@ -115,6 +118,21 @@ const initNodeStatusCall = (nodeConfig) => { } } +function attemptReconnectBlockSocket() { + setTimeout(() => { + initBlockSocket(); + blockSocketReconnectInterval = Math.min(blockSocketReconnectInterval * 2, MAX_RECONNECT_INTERVAL); + }, blockSocketReconnectInterval); +} + +function attemptReconnectNodeStatusSocket() { + setTimeout(() => { + initNodeStatusSocket(); + nodeStatusSocketReconnectInterval = Math.min(nodeStatusSocketReconnectInterval * 2, MAX_RECONNECT_INTERVAL); + }, nodeStatusSocketReconnectInterval); +} + + const initBlockSocket = () => { let myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] let nodeUrl = myNode.domain + ":" + myNode.port @@ -127,9 +145,9 @@ const initBlockSocket = () => { const activeBlockSocket = new WebSocket(activeBlockSocketLink) // Open Connection activeBlockSocket.onopen = (e) => { + blockSocketReconnectInterval = MIN_RECONNECT_INTERVAL; closeGracefully = false socketObject = activeBlockSocket - initial = initial + 1 } // Message Event activeBlockSocket.onmessage = (e) => { @@ -141,21 +159,10 @@ const initBlockSocket = () => { } // Closed Event activeBlockSocket.onclose = () => { - processBlock({}) - blockFirstCall = true - clearInterval(activeBlockSocketTimeout) - - if (closeGracefully === false && initial <= 52) { - if (initial <= 52) { - retryOnClose = true - setTimeout(pingactiveBlockSocket, 10000) - initial = initial + 1 - } else { - // ... Stop retrying... - retryOnClose = false - } - } - } + processBlock({}); + blockFirstCall = true; + attemptReconnectBlockSocket(); + }; // Error Event activeBlockSocket.onerror = (e) => { blockFirstCall = true @@ -200,31 +207,26 @@ const initNodeStatusSocket = () => { const activeNodeStatusSocket = new WebSocket(activeNodeStatusSocketLink) // Open Connection activeNodeStatusSocket.onopen = (e) => { + console.log('onopen') + nodeStatusSocketReconnectInterval = MIN_RECONNECT_INTERVAL; + nodeStatusSocketcloseGracefully = false nodeStatusSocketObject = activeNodeStatusSocket - nodeStatusCount = nodeStatusCount + 1 } // Message Event activeNodeStatusSocket.onmessage = (e) => { + console.log('onmessage') doNodeStatus(JSON.parse(e.data)) } // Closed Event activeNodeStatusSocket.onclose = () => { - doNodeStatus({}) - clearInterval(nodeStatusSocketTimeout) - if (nodeStatusSocketcloseGracefully === false && nodeStatusCount <= 52) { - if (nodeStatusCount <= 52) { - nodeStatusRetryOnClose = true - setTimeout(pingNodeStatusSocket, 10000) - nodeStatusCount = nodeStatusCount + 1 - } else { - // ... Stop retrying... - nodeStatusRetryOnClose = false - } - } - } + console.log('onclose') + doNodeStatus({}); + attemptReconnectNodeStatusSocket(); + }; // Error Event activeNodeStatusSocket.onerror = (e) => { + console.log('onerror') doNodeStatus({}) } }