Redsign qortal-ui repo

This commit is contained in:
AlphaX-Projects
2023-05-11 18:40:52 +02:00
parent edbe674fb9
commit 6d3dbcdfe8
3745 changed files with 20028 additions and 10558 deletions

160
plugins/build-config.js Normal file
View File

@@ -0,0 +1,160 @@
require('events').EventEmitter.defaultMaxListeners = 0;
const path = require('path');
const { nodeResolve } = require('@rollup/plugin-node-resolve');
const progress = require('rollup-plugin-progress');
const replace = require('@rollup/plugin-replace');
const globals = require('rollup-plugin-node-globals');
const commonjs = require('@rollup/plugin-commonjs');
const alias = require('@rollup/plugin-alias');
const terser = require('@rollup/plugin-terser');
const babel = require('@rollup/plugin-babel');
const webWorkerLoader = require('@qortal/rollup-plugin-web-worker-loader');
const aliases = {};
const generateRollupConfig = (inputFile, outputFile) => {
return {
inputOptions: {
onwarn: (warning, rollupWarn) => {
if (warning.code !== 'CIRCULAR_DEPENDENCY') {
rollupWarn(warning);
}
},
input: inputFile,
plugins: [
alias({
entries: Object.keys(aliases).map((find) => {
return {
find,
replacement: aliases[find],
};
}),
}),
nodeResolve({
preferBuiltins: false,
mainFields: ['module', 'browser'],
}),
replace({
preventAssignment: true,
'process.env.NODE_ENV': JSON.stringify('production'),
}),
commonjs(),
globals(),
progress(),
webWorkerLoader(),
babel.babel({
babelHelpers: 'bundled',
exclude: 'node_modules/**',
}),
terser({
compress: true,
output: {
comments: false,
},
}),
],
},
outputOptions: {
file: outputFile,
format: 'umd',
},
};
};
const generateForPlugins = () => {
const configs = [
{
in: 'plugins/core/main.src.js',
out: 'plugins/core/main.js',
},
{
in: 'plugins/core/trade-portal/trade-portal.src.js',
out: 'plugins/core/trade-portal/trade-portal.js',
},
{
in: 'plugins/core/trade-bot/trade-bot-portal.src.js',
out: 'plugins/core/trade-bot/trade-bot-portal.js',
},
{
in: 'plugins/core/wallet/wallet-app.src.js',
out: 'plugins/core/wallet/wallet-app.js',
},
{
in: 'plugins/core/reward-share/reward-share.src.js',
out: 'plugins/core/reward-share/reward-share.js',
},
{
in: 'plugins/core/node-management/node-management.src.js',
out: 'plugins/core/node-management/node-management.js',
},
{
in: 'plugins/core/group-management/group-management.src.js',
out: 'plugins/core/group-management/group-management.js',
},
{
in: 'plugins/core/name-registration/name-registration.src.js',
out: 'plugins/core/name-registration/name-registration.js',
},
{
in: 'plugins/core/names-market/names-market.src.js',
out: 'plugins/core/names-market/names-market.js',
},
{
in: 'plugins/core/qdn/websites.src.js',
out: 'plugins/core/qdn/websites.js',
},
{
in: 'plugins/core/qdn/publish/publish.src.js',
out: 'plugins/core/qdn/publish/publish.js',
},
{
in: 'plugins/core/qdn/browser/browser.src.js',
out: 'plugins/core/qdn/browser/browser.js',
},
{
in: 'plugins/core/qdn/data-management/data-management.src.js',
out: 'plugins/core/qdn/data-management/data-management.js',
},
{
in: 'plugins/core/messaging/messaging.src.js',
out: 'plugins/core/messaging/messaging.js',
},
{
in: 'plugins/core/messaging/chain-messaging/chain-messaging.src.js',
out: 'plugins/core/messaging/chain-messaging/chain-messaging.js',
},
{
in: 'plugins/core/messaging/q-chat/q-chat.src.js',
out: 'plugins/core/messaging/q-chat/q-chat.js',
},
{
in: 'plugins/core/minting/minting-info.src.js',
out: 'plugins/core/minting/minting-info.js',
},
{
in: 'plugins/core/become-minter/become-minter.src.js',
out: 'plugins/core/become-minter/become-minter.js',
},
{
in: 'plugins/core/sponsorship-list/sponsorship-list.src.js',
out: 'plugins/core/sponsorship-list/sponsorship-list.js',
},
{
in: 'plugins/core/puzzles/puzzles.src.js',
out: 'plugins/core/puzzles/puzzles.js',
},
{
in: 'plugins/core/q-app/q-apps.src.js',
out: 'plugins/core/q-app/q-apps.js',
},
].map((file) => {
return generateRollupConfig(
path.join(__dirname, file.in),
path.join(__dirname, file.out)
);
});
return configs;
};
module.exports = generateForPlugins;

22
plugins/build.js Normal file
View File

@@ -0,0 +1,22 @@
const rollup = require('rollup')
const configs = require('./build-config.js')()
const build = () => {
configs.forEach(async file => {
const bundle = await rollup.rollup(file.inputOptions);
const { output } = await bundle.generate(file.outputOptions);
for (const chunkOrAsset of output) {
if (chunkOrAsset.type === 'asset') {
} else {
// ..
}
}
await bundle.write(file.outputOptions);
})
console.log('BUILD PLUGINS ==> Bundling Done 🎉');
}
module.exports = build

View File

@@ -0,0 +1,35 @@
const PLUGINS = 'plugins'
const BUILD = 'build'
const WATCH = 'watch'
/**
* @package Plugins Controller
* @param { String } type
*/
const pluginsController = (type) => {
switch (type) {
case PLUGINS:
const path = require('path')
const plugins = [
{
folder: path.join(__dirname, 'plugins/core'),
name: 'core'
}
]
return plugins
case BUILD:
const build = require('./build.js')
return build
case WATCH:
const watch = require('./watch.js')
return watch
default:
return
}
}
module.exports = pluginsController

10
plugins/epml.js Normal file
View File

@@ -0,0 +1,10 @@
import { Epml, EpmlReadyPlugin, RequestPlugin, ContentWindow as EpmlContentWindowPlugin, EpmlStreamPlugin, EpmlProxyPlugin, EpmlStream } from 'epml'
Epml.registerPlugin(RequestPlugin)
Epml.registerPlugin(EpmlReadyPlugin)
Epml.registerPlugin(EpmlContentWindowPlugin)
Epml.registerPlugin(EpmlStreamPlugin)
Epml.registerPlugin(EpmlProxyPlugin)
Epml.allowProxying = true
export { Epml, EpmlStream }

View File

@@ -0,0 +1,253 @@
import { css } from 'lit';
export const pageStyles = css`
* {
box-sizing: border-box;
--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);
--lumo-primary-text-color: rgb(0, 167, 245);
--lumo-primary-color-50pct: rgba(0, 167, 245, 0.5);
--lumo-primary-color-10pct: rgba(0, 167, 245, 0.1);
--lumo-primary-color: hsl(199, 100%, 48%);
--lumo-base-color: var(--white);
--lumo-body-text-color: var(--black);
--_lumo-grid-border-color: var(--border);
--_lumo-grid-secondary-border-color: var(--border2);
}
.header-title {
font-size: 40px;
color: var(--black);
font-weight: 400;
text-align: center;
}
.divider {
color: #eee;
border-radius: 80%;
margin-bottom: 2rem;
}
.fullWidth {
width: 100%;
}
.page-container {
display: flex;
align-items: center;
flex-direction: column;
margin-bottom: 75px;
width: 100%;
}
.inner-container {
display: flex;
align-items: center;
flex-direction: column;
width: 100%;
}
.description {
color: var(--black);
}
.message {
color: var(--gray);
}
.sub-main {
width: 95%;
display: flex;
flex-direction: column;
max-width: 800px;
}
.level-black {
font-size: 32px;
color: var(--black);
font-weight: 400;
text-align: center;
margin-top: 2rem;
}
.form-wrapper {
display: flex;
align-items: center;
width: 100%;
height: 50px;
}
.row {
display: flex;
width: 100%;
}
.column {
display: flex;
flex-direction: column;
width: 100%;
}
.column-center {
align-items: center;
}
.no-margin {
margin: 0;
}
.no-wrap {
flex-wrap: nowrap !important;
}
.row-center {
justify-content: center;
flex-wrap: wrap;
}
.form-item {
display: flex;
height: 100%;
}
.form-item--button {
flex-grow: 0;
}
.form-item--input {
flex-grow: 1;
margin-right: 25px;
}
.center-box {
position: absolute;
width: 100%;
top: 50%;
left: 50%;
transform: translate(-50%, 0%);
text-align: center;
}
.content-box {
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px 25px;
text-align: center;
display: inline-block;
margin-bottom: 15px;
flex-basis: 600px;
}
.gap {
gap: 10px;
}
.level-black {
font-size: 32px;
color: var(--black);
font-weight: 400;
text-align: center;
margin-top: 2rem;
text-align: center;
}
.title {
font-weight: 600;
font-size: 20px;
line-height: 28px;
opacity: 0.66;
color: var(--switchborder);
}
.address {
overflow-wrap: anywhere;
color: var(--black);
}
h4 {
font-weight: 600;
font-size: 20px;
line-height: 28px;
color: var(--black);
}
mwc-textfield {
width: 100%;
}
vaadin-button {
height: 100%;
margin: 0;
cursor: pointer;
min-width: 80px;
}
.loader,
.loader:after {
border-radius: 50%;
width: 10em;
height: 10em;
}
.loadingContainer {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
}
.backdrop {
height: 100vh;
width: 100vw;
opacity: 0.6;
background-color: var(--border);
z-index: 9;
position: fixed;
}
.loading,
.loading:after {
border-radius: 50%;
width: 5em;
height: 5em;
}
.loading {
margin: 10px auto;
border-width: 0.6em;
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: 10px;
position: relative;
text-indent: -9999em;
transform: translateZ(0px);
animation: 1.1s linear 0s infinite normal none running loadingAnimation;
}
@-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);
}
}
`;

View File

@@ -0,0 +1,203 @@
import { LitElement, html } from 'lit';
import { Epml } from '../../../epml.js';
import '../components/ButtonIconCopy.js';
import { use, translate, registerTranslateConfig } from 'lit-translate';
import { blocksNeed } from '../../utils/blocks-needed.js';
registerTranslateConfig({
loader: (lang) => fetch(`/language/${lang}.json`).then((res) => res.json()),
});
import '@polymer/paper-spinner/paper-spinner-lite.js';
import '@material/mwc-button';
import '@material/mwc-textfield';
import '@vaadin/button';
import { pageStyles } from './become-minter-css.src.js';
import './components/not-sponsored.src';
import './components/yes-sponsored.src';
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent });
class BecomeMinter extends LitElement {
static get properties() {
return {
theme: { type: String, reflect: true },
sponsorshipKeyValue: { type: String },
nodeInfo: { type: Object },
isPageLoading: { type: Boolean },
addressInfo: { type: Object },
rewardSharePublicKey: { type: String },
mintingAccountData: { type: Array },
};
}
static styles = [pageStyles];
constructor() {
super();
this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light';
this.sponsorshipKeyValue = '';
this.isPageLoading = true;
this.nodeInfo = {};
this.addressInfo = {};
this.rewardSharePublicKey = '';
this.mintingAccountData = null;
}
changeLanguage() {
const checkLanguage = localStorage.getItem('qortalLanguage');
if (checkLanguage === null || checkLanguage.length === 0) {
localStorage.setItem('qortalLanguage', 'us');
use('us');
} else {
use(checkLanguage);
}
}
_handleStorage() {
const checkLanguage = localStorage.getItem('qortalLanguage');
const checkTheme = localStorage.getItem('qortalTheme');
use(checkLanguage);
if (checkTheme === 'dark') {
this.theme = 'dark';
} else {
this.theme = 'light';
}
document.querySelector('html').setAttribute('theme', this.theme);
}
connectedCallback() {
super.connectedCallback();
window.addEventListener('storage', this._handleStorage);
}
disconnectedCallback() {
window.removeEventListener('storage', this._handleStorage);
super.disconnectedCallback();
}
async getNodeInfo() {
const nodeInfo = await parentEpml.request('apiCall', {
url: `/admin/status`,
});
return nodeInfo;
}
async getMintingAcccounts() {
const mintingAccountData = await parentEpml.request('apiCall', {
url: `/admin/mintingaccounts`,
});
return mintingAccountData;
}
async atMount() {
this.changeLanguage();
this.isPageLoading = true;
try {
const [nodeInfo, myRewardShareArray, mintingaccounts] =
await Promise.all([
this.getNodeInfo(),
this.getRewardShareRelationship(
window.parent.reduxStore.getState().app?.selectedAddress
?.address
),
this.getMintingAcccounts(),
]);
this.nodeInfo = nodeInfo;
this.rewardSharePublicKey =
myRewardShareArray[0]?.rewardSharePublicKey;
this.isPageLoading = false;
this.mintingAccountData = mintingaccounts;
this.addressInfo =
window.parent.reduxStore.getState().app.accountInfo.addressInfo;
} catch (error) {
console.error(error);
this.isPageLoading = false;
}
}
async firstUpdated() {
await this.atMount();
}
async getRewardShareRelationship(recipientAddress) {
const myRewardShareArray = await parentEpml.request('apiCall', {
type: 'api',
url: `/addresses/rewardshares?recipients=${recipientAddress}`,
});
return myRewardShareArray;
}
_levelUpBlocks() {
let countBlocksString = (
blocksNeed(0) -
(this.addressInfo?.blocksMinted +
this.addressInfo?.blocksMintedAdjustment)
).toString();
return countBlocksString;
}
render() {
const findMintingAccount = this.mintingAccountData?.find(
(ma) => ma.recipientAccount === window.parent.reduxStore.getState().app?.selectedAddress
?.address
);
const isAlreadySponsored =
this.addressInfo?.error !== 124 &&
this.addressInfo?.level === 0 &&
this.addressInfo?.blocksMinted > 0 && this.addressInfo?.blocksMinted < 7200
return html`
${this.isPageLoading
? html`
<div class="loadingContainer">
<div class="loading"></div>
</div>
<div class="backdrop"></div>
`
: ''}
<div class="page-container">
<h1 class="header-title">
${translate('mintingpage.mchange32')}
</h1>
<div class="fullWidth">
<hr class="divider" />
</div>
${isAlreadySponsored
? ''
: html`
<not-sponsored
.atMount="${() => this.atMount()}"
>
</not-sponsored>
`}
${!isAlreadySponsored
? ''
: html`
<yes-sponsored
.rewardSharePublicKey=${this
.rewardSharePublicKey}
.addressInfo=${this.addressInfo}
.isMinting=${!!findMintingAccount}
>
</yes-sponsored>
`}
</div>
`;
}
}
window.customElements.define('become-minter', BecomeMinter);

View File

@@ -0,0 +1,137 @@
import { LitElement, html } from 'lit';
import { Epml } from '../../../../epml.js';
import '../../components/ButtonIconCopy.js';
import { use, translate, registerTranslateConfig } from 'lit-translate';
registerTranslateConfig({
loader: (lang) => fetch(`/language/${lang}.json`).then((res) => res.json()),
});
import '@polymer/paper-spinner/paper-spinner-lite.js';
import '@material/mwc-button';
import '@material/mwc-textfield';
import '@vaadin/button';
import { pageStyles } from '../become-minter-css.src.js';
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent });
class NotSponsored extends LitElement {
static properties = {
atMount: { type: Function },
isLoadingSponsorshipKeySubmit: { type: Boolean },
sponsorshipKeyValue: { type: String },
addMintingAccountMessage: { type: String },
};
static styles = [pageStyles];
constructor() {
super();
this.isLoadingSponsorshipKeySubmit = false;
this.sponsorshipKeyValue = '';
this.addMintingAccountMessage = '';
this.atMount = () => {};
}
renderErr1Text() {
return html`${translate('nodepage.nchange27')}`;
}
renderErr2Text() {
return html`${translate('nodepage.nchange28')}`;
}
getApiKey() {
const myNode =
window.parent.reduxStore.getState().app.nodeConfig.knownNodes[
window.parent.reduxStore.getState().app.nodeConfig.node
];
let apiKey = myNode.apiKey;
return apiKey;
}
addMintingAccount(e) {
this.isLoadingSponsorshipKeySubmit = true;
this.addMintingAccountMessage = 'Loading...';
parentEpml
.request('apiCall', {
url: `/admin/mintingaccounts?apiKey=${this.getApiKey()}`,
method: 'POST',
body: this.sponsorshipKeyValue,
})
.then((res) => {
if (res === true) {
// refetch data
this.atMount();
this.sponsorshipKeyValue = '';
this.addMintingAccountMessage = this.renderErr1Text();
this.isLoadingSponsorshipKeySubmit = false;
} else {
this.sponsorshipKeyValue = '';
this.addMintingAccountMessage = this.renderErr2Text();
this.isLoadingSponsorshipKeySubmit = false;
}
});
}
inputHandler(e) {
this.sponsorshipKeyValue = e.target.value;
}
render() {
return html`
<div class="inner-container">
<div class="sub-main">
<h2 class="level-black">
${translate('mintingpage.mchange33')}
</h2>
<p class="description">
${translate('mintingpage.mchange34')}
</p>
<h2 class="level-black">
${translate('mintingpage.mchange35')}
</h2>
<p class="description">
${translate('mintingpage.mchange36')}
</p>
<p class="description">
${translate('mintingpage.mchange37')}
</p>
<p class="message">${this.addMintingAccountMessage}</p>
<div class="form-wrapper">
<div class="form-item form-item--input">
<mwc-textfield
?disabled="${this
.isLoadingSponsorshipKeySubmit}"
label="${translate('becomeMinterPage.bchange8')}"
id="addSponsorshipKey"
@input="${this.inputHandler}"
.value="${this.sponsorshipKeyValue || ''}"
fullWidth
>
</mwc-textfield>
</div>
<div class="form-item form-item--button">
<vaadin-button
theme="primary"
?disabled="${this.isLoadingSponsorshipKeySubmit}"
@click="${this.addMintingAccount}"
>
${this.isLoadingSponsorshipKeySubmit === false
? html`${translate('puzzlepage.pchange15')}`
: html`
<paper-spinner-lite active></paper-spinner-lite>
`}
</vaadin-button>
</div>
</div>
</div>
</div>
`;
}
}
window.customElements.define('not-sponsored', NotSponsored);

View File

@@ -0,0 +1,114 @@
import { LitElement, html } from 'lit';
import { Epml } from '../../../../epml.js';
import '../../components/ButtonIconCopy.js';
import { use, translate, registerTranslateConfig } from 'lit-translate';
registerTranslateConfig({
loader: (lang) => fetch(`/language/${lang}.json`).then((res) => res.json()),
});
import '@polymer/paper-spinner/paper-spinner-lite.js';
import '@material/mwc-button';
import '@material/mwc-textfield';
import '@vaadin/button';
import { blocksNeed } from '../../../utils/blocks-needed.js';
import { pageStyles } from '../become-minter-css.src.js';
class YesSponsored extends LitElement {
static get properties() {
return {
addressInfo: { type: Object },
rewardSharePublicKey: { type: String },
isMinting: {type: Boolean}
};
}
constructor() {
super();
this.addressInfo = {};
this.rewardSharePublicKey = '';
this.isMinting = false
}
static styles = [pageStyles];
_levelUpBlocks() {
let countBlocksString = (
blocksNeed(0) -
(this.addressInfo?.blocksMinted +
this.addressInfo?.blocksMintedAdjustment)
).toString();
return countBlocksString;
}
render() {
return html`
<div class="inner-container">
<div class="column column-center">
<div class="column column-center">
<span class="level-black">
${translate('becomeMinterPage.bchange10')}
</span>
<hr style="width: 75%; color: #eee; border-radius: 80%; margin-bottom: 2rem;">
</div>
<br />
<div class="row row-center gap">
<div class="content-box">
<span class="title">
${translate('walletpage.wchange41')}
</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
${this.isMinting ? html`
<h4>${translate('becomeMinterPage.bchange12')}</h4>
` : html`
<h4>${translate('mintingpage.mchange9')}</h4>
`}
</div>
</div>
<div class="row row-center gap">
<div class="content-box">
<span class="title">
${translate('becomeMinterPage.bchange13')}
</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>
${this._levelUpBlocks()}
${translate('mintingpage.mchange26')}
</h4>
</div>
</div>
<div class="row row-center gap">
<div class="content-box">
<span class="title">
${translate('becomeMinterPage.bchange15')}
</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4 class="no-margin">
${translate('becomeMinterPage.bchange16')}
</h4>
<div class="row row-center column-center no-wrap">
<p class="address">
${this.rewardSharePublicKey}
</p>
<button-icon-copy
title="${translate('walletpage.wchange3')}"
onSuccessMessage="${translate('walletpage.wchange4')}"
onErrorMessage="${translate('walletpage.wchange39')}"
textToCopy=${this.rewardSharePublicKey}
buttonSize="28px"
iconSize="16px"
color="var(--copybutton)"
offsetLeft="4px"
>
</button-icon-copy>
</div>
</div>
</div>
</div>
</div>
`;
}
}
window.customElements.define('yes-sponsored', YesSponsored);

View File

@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css" />
<link rel="stylesheet" href="/font/switch-theme.css" />
<script>
const checkBack = localStorage.getItem('qortalTheme');
if (checkBack === 'dark') {
newtheme = 'dark';
} else {
newtheme = 'light';
}
document.querySelector('html').setAttribute('theme', newtheme);
</script>
<style>
html {
--scrollbarBG: #a1a1a1;
--thumbBG: #6a6c75;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}
html,
body {
margin: 0;
font-family: 'Roboto', sans-serif;
background: var(--plugback);
}
</style>
</head>
<body>
<become-minter></become-minter>
<script src="become-minter.js"></script>
</body>
</html>

View File

@@ -0,0 +1,68 @@
import { LitElement, html, css } from 'lit'
import { Epml } from '../../../epml.js'
import '@material/mwc-icon-button'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class ButtonIconCopy extends LitElement {
static get properties() {
return {
textToCopy: { type: String },
title: { type: String },
onSuccessMessage: { type: String },
onErrorMessage: { type: String },
buttonSize: { type: String },
iconSize: { type: String },
color: { type: String },
offsetLeft: { type: String },
offsetRight: { type: String }
}
}
constructor() {
super()
this.textToCopy = ''
this.title = 'Copy to clipboard'
this.onSuccessMessage = 'Copied to clipboard'
this.onErrorMessage = 'Unable to copy'
this.buttonSize = '48px'
this.iconSize = '24px'
this.color = 'inherit'
this.offsetLeft = '0'
this.offsetRight = '0'
}
connectedCallback() {
super.connectedCallback();
this.style.setProperty('--mdc-icon-button-size', this.buttonSize)
this.style.setProperty('--mdc-icon-size', this.iconSize)
this.style.setProperty('color', this.color)
this.style.setProperty('margin-left', this.offsetLeft)
this.style.setProperty('margin-right', this.offsetRight)
}
render() {
return html`
<mwc-icon-button
title=${this.title}
label=${this.title}
icon="content_copy"
@click=${() => this.saveToClipboard(this.textToCopy)}
>
</mwc-icon-button>
`
}
async saveToClipboard(text) {
try {
await navigator.clipboard.writeText(text)
parentEpml.request('showSnackBar', this.onSuccessMessage)
} catch (err) {
parentEpml.request('showSnackBar', this.onErrorMessage)
console.error('Copy to clipboard error:', err)
}
}
}
window.customElements.define('button-icon-copy', ButtonIconCopy)

View File

@@ -0,0 +1,521 @@
import { css } from 'lit';
export const gifExplorerStyles = css`
.gifs-container {
position: relative;
display: flex;
padding: 10px 15px;
border-radius: 12px;
box-shadow: rgba(0, 0, 0, 0.09) 0px 3px 12px;
background-color: var(--chat-menu-bg);
width: fit-content;
justify-self: flex-end;
place-self: end flex-end;
min-height: 400px;
max-height: calc(95vh - 90px);
min-width: 370px;
max-width: 370px;
box-shadow: var(--gifs-drop-shadow);
}
.gif-explorer-container {
min-height: 400px;
display: flex;
flex-direction: column;
justify-content: flex-start;
width: 100%;
align-items: center;
gap: 15px;
}
.title-row {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
.gif-explorer-title {
flex: 1;
text-align: center;
font-family: Roboto, sans-serif;
letter-spacing: 0.8px;
font-size: 25px;
color: var(--chat-bubble-msg-color);
margin: 0;
user-select: none;
}
.explore-collections-icon {
text-align: right;
font-size: 20px;
color: var(--chat-group);
box-shadow: var(--gif-search-icon-bs);
padding: 7px;
background-color: var(--gif-search-icon);
border: none;
border-radius: 8px;
cursor: pointer;
}
.create-collections-icon {
position: absolute;
bottom: 5px;
left: 50%;
transform: translateX(-50%);
padding: 4px;
font-size: 22px;
background-color: var(--mdc-theme-primary);
color: white;
border-radius: 8px;
box-shadow: 0 0 0 rgba(0, 0, 0, 0.2);
transition: all 0.3s ease-in-out;
}
.create-collections-icon:hover {
cursor: pointer;
box-shadow: 0px 4px 5px 0px hsla(0, 0%, 0%, 0.14),
0px 1px 10px 0px hsla(0, 0%, 0%, 0.12),
0px 2px 4px -1px hsla(0, 0%, 0%, 0.2);
}
.collections-button-row {
width: auto;
background-color: var(--gif-button-row-bg);
border-radius: 35px;
padding: 2px;
margin-top: 10px;
}
.collections-button-innerrow {
display: flex;
flex-direction: row;
align-items: center;
}
.my-collections-button {
font-size: 16px;
font-family: 'Maven Pro', sans-serif;
letter-spacing: 0.5px;
color: var(--gif-button-row-color);
border-radius: 35px;
padding: 8px 20px;
margin: 2px 0;
cursor: pointer;
user-select: none;
}
.subscribed-collections-button {
font-size: 16px;
font-family: 'Maven Pro', sans-serif;
letter-spacing: 0.5px;
color: var(--gif-button-row-color);
border-radius: 35px;
padding: 8px 20px;
margin: 2px 0;
cursor: pointer;
user-select: none;
}
.collections-button-active {
display: flex;
align-items: center;
justify-content: center;
background-color: white;
color: var(--mdc-theme-primary);
border-radius: 25px;
padding: 8px 20px;
margin: 2px 0;
box-shadow: rgb(0 0 0 / 14%) 0px 1px 1px 0px,
rgb(0 0 0 / 12%) 0px 2px 1px -1px, rgb(0 0 0 / 20%) 0px 1px 3px 0px;
transition: all 0.3s ease-in-out;
cursor: auto;
}
.collection-wrapper {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow-x: hidden;
}
.collection-gifs {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 10px;
margin-top: 10px;
overflow-y: auto;
overflow-x: hidden;
}
.collection-gifs::-webkit-scrollbar-track {
background-color: whitesmoke;
border-radius: 7px;
}
.collection-gifs::-webkit-scrollbar {
width: 6px;
border-radius: 7px;
background-color: whitesmoke;
}
.collection-gifs::-webkit-scrollbar-thumb {
background-color: rgb(180, 176, 176);
border-radius: 7px;
transition: all 0.3s ease-in-out;
}
.collection-gif {
border-radius: 15px;
background-color: transparent;
cursor: pointer;
width: 100%;
height: 150px;
object-fit: cover;
border: 1px solid transparent;
transition: all 0.2s cubic-bezier(0, 0.55, 0.45, 1);
box-shadow: rgb(50 50 93 / 25%) 0px 6px 12px -2px,
rgb(0 0 0 / 30%) 0px 3px 7px -3px;
}
.collection-gif:hover {
border: 1px solid var(--mdc-theme-primary);
}
.new-collection-row {
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
}
.new-collection-subrow {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 5px;
}
.new-collection-title {
font-family: Maven Pro, sans-serif;
color: var(--chat-bubble-msg-color);
font-size: 18px;
letter-spacing: 0.6px;
margin: 0;
user-select: none;
}
.new-collection-subtitle {
font-family: Roboto, sans-serif;
color: var(--chat-bubble-msg-color);
font-weight: 300;
opacity: 0.9;
font-size: 14px;
letter-spacing: 0.3px;
margin: 0;
user-select: none;
}
.new-collection-container {
display: flex;
margin: 15px 20px;
border: 3.5px dashed #b898c1;
border-radius: 10px;
background-color: #d7d3db2e;
align-items: center;
justify-content: center;
cursor: pointer;
}
.new-collection-icon {
font-size: 30px;
color: var(--mdc-theme-primary);
}
.gifs-added-col {
display: flex;
flex-direction: column;
justify-content: flex-end;
flex: 1 1 0%;
margin-top: 10px;
overflow-y: auto;
max-height: 300px;
}
.gifs-added-row {
display: flex;
flex-direction: column;
gap: 5px;
overflow-y: auto;
}
.gifs-added-row .gif-input:last-child {
border-bottom: none;
}
.gifs-added-row::-webkit-scrollbar-track {
background-color: whitesmoke;
border-radius: 7px;
}
.gifs-added-row::-webkit-scrollbar {
width: 6px;
border-radius: 7px;
background-color: whitesmoke;
}
.gifs-added-row::-webkit-scrollbar-thumb {
background-color: rgb(180, 176, 176);
border-radius: 7px;
transition: all 0.3s ease-in-out;
}
.gif-input {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
background-color: transparent;
padding: 15px 5px;
border-bottom: 1px solid #7b787888;
}
.gif-input-img {
width: 70px;
height: 70px;
border-radius: 10px;
}
.gif-input-field {
height: 30px;
background-color: transparent;
border: none;
color: var(--chat-bubble-msg-color);
border-bottom: 1px solid var(--chat-bubble-msg-color);
width: 100%;
padding: 0;
margin: 0;
outline: 0;
font-size: 16px;
font-family: Roboto, sans-serif;
letter-spacing: 0.3px;
font-weight: 300;
}
.upload-collection-row {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin-top: 10px;
}
.upload-collection-name {
display: block;
padding: 8px 10px;
font-size: 16px;
font-family: Montserrat, sans-serif;
font-weight: 600;
background-color: #ebeaea21;
border: 1px solid var(--mdc-theme-primary);
border-radius: 5px;
color: var(--chat-bubble-msg-color);
outline: none;
}
.upload-collection-name::placeholder {
font-size: 16px;
font-family: Montserrat, sans-serif;
font-weight: 600;
opacity: 0.6;
color: var(--chat-bubble-msg-color);
}
.collection-back-button {
display: flex;
font-family: Roboto, sans-serif;
font-weight: 300;
letter-spacing: 0.3px;
font-size: 16px;
width: fit-content;
gap: 10px;
color: var(--chat-bubble-msg-color);
flex-direction: row;
align-items: center;
transition: box-shadow 0.2s ease-in-out;
background-color: var(--gif-button-row-bg);
border-radius: 3px;
box-shadow: rgb(0 0 0 / 20%) 0px 0px 0px;
padding: 8px 10px;
cursor: pointer;
}
.collection-back-button:hover {
border: none;
box-sizing: border-box;
box-shadow: rgb(0 0 0 / 14%) 0px 4px 5px 0px,
rgb(0 0 0 / 12%) 0px 1px 10px 0px, rgb(0 0 0 / 20%) 0px 2px 4px -1px;
}
.collection-back-button-arrow {
font-size: 10px;
}
.no-collections {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
color: var(--chat-bubble-msg-color);
font-size: 20px;
font-family: Paytone One, sans-serif;
margin-top: 20px;
user-select: none;
}
.collection-card {
display: flex;
font-family: Roboto, sans-serif;
font-weight: 300;
letter-spacing: 0.3px;
font-size: 19px;
color: var(--chat-bubble-msg-color);
flex-direction: row;
align-items: center;
transition: all 0.3s ease-in-out;
box-shadow: none;
padding: 10px;
cursor: pointer;
}
.collection-card:hover {
border: none;
border-radius: 5px;
background-color: var(--gif-collection-hover-bg);
}
.upload-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.4s ease-in-out;
}
.upload-back-button {
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;
}
.upload-back-button:hover {
cursor: pointer;
background-color: #f4433663;
}
.upload-button:hover {
cursor: pointer;
background-color: #03a8f475;
}
.lds-circle {
display: flex;
align-items: center;
justify-content: center;
margin-top: 70px;
}
.lds-circle > div {
display: inline-block;
width: 80px;
height: 80px;
margin: 8px;
border-radius: 50%;
background: var(--mdc-theme-primary);
animation: lds-circle 2.4s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
@keyframes lds-circle {
0%,
100% {
animation-timing-function: cubic-bezier(0.5, 0, 1, 0.5);
}
0% {
transform: rotateY(0deg);
}
50% {
transform: rotateY(1800deg);
animation-timing-function: cubic-bezier(0, 0.5, 0.5, 1);
}
100% {
transform: rotateY(3600deg);
}
}
.gifs-loading-message {
font-family: Montserrat, sans-serif;
font-size: 20px;
font-weight: 600;
color: var(--chat-bubble-msg-color);
margin: 0 0 10px 0;
text-align: center;
user-select: none;
}
.subscribe-button {
position: absolute;
bottom: 3px;
left: 50%;
transform: translateX(-50%);
font-family: Raleway, sans-serif;
font-weight: 500;
font-size: 14px;
background-color: var(--mdc-theme-primary);
border: none;
border-radius: 8px;
outline: none;
padding: 5px 10px;
transition: all 0.3s cubic-bezier(0.5, 1, 0.89, 1);
}
.subscribe-button:hover {
cursor: pointer;
box-shadow: 0px 3px 4px 0px hsla(0, 0%, 0%, 0.14),
0px 3px 3px -2px hsla(0, 0%, 0%, 0.12),
0px 1px 8px 0px hsla(0, 0%, 0%, 0.2);
}
.unsubscribe-button {
position: absolute;
width: max-content;
bottom: 3px;
left: 50%;
transform: translateX(-50%);
font-family: Raleway, sans-serif;
font-weight: 500;
font-size: 14px;
background-color: #f44336;
border: none;
border-radius: 8px;
outline: none;
padding: 5px 10px;
transition: all 0.3s cubic-bezier(0.5, 1, 0.89, 1);
}
.unsubscribe-button:hover {
cursor: pointer;
box-shadow: 0px 3px 4px 0px hsla(0, 0%, 0%, 0.14),
0px 3px 3px -2px hsla(0, 0%, 0%, 0.12),
0px 1px 8px 0px hsla(0, 0%, 0%, 0.2);
}
`;

View File

@@ -0,0 +1,996 @@
import {LitElement, html, css} from 'lit';
import {render} from 'lit/html.js';
import {Epml} from '../../../../epml.js';
import * as zip from '@zip.js/zip.js';
import '@material/mwc-icon';
import ShortUniqueId from 'short-unique-id';
import {publishData} from '../../../utils/publish-image.js';
import {translate, get} from 'lit-translate';
import {gifExplorerStyles} from './ChatGifs-css.js';
import { bytesToMegabytes } from '../../../utils/bytesToMegabytes.js';
import './ChatGifsExplore.js';
import '../ImageComponent.js';
import '@vaadin/tooltip';
const parentEpml = new Epml({type: 'WINDOW', source: window.parent});
class ChatGifs extends LitElement {
static get properties() {
return {
selectedAddress: {type: Object},
myGifCollections: {type: Array},
mySubscribedCollections: {type: Array},
exploreCollections: {type: Array},
gifsToBeAdded: {type: Array},
webWorkerImage: {type: Object},
mode: {type: String},
currentCollection: {type: String},
isLoading: {type: String},
newCollectionName: {type: String},
editor: {type: Object},
isSubscribed: { type: Boolean },
setGifsLoading: { attribute: false },
sendMessage: { attribute: false },
setOpenGifModal: { attribute: false }
};
}
static styles = [gifExplorerStyles];
constructor() {
super();
this.uid = new ShortUniqueId();
this.selectedAddress = window.parent.reduxStore.getState().app.selectedAddress;
this.myGifCollections = [];
this.mySubscribedCollections = [];
this.exploreCollections = [];
this.myAccountName = '';
this.gifsToBeAdded = [];
this.mode = 'myCollection';
this.currentCollection = null;
this.pageNumber = 0;
this.isLoading = false;
this.isSubscribed = false;
this.newCollectionName = '';
this.getAllCollections = this.getAllCollections.bind(this);
}
async firstUpdated() {
const tooltip = this.shadowRoot.querySelector('vaadin-tooltip');
const overlay = tooltip.shadowRoot.querySelector(
'vaadin-tooltip-overlay'
);
overlay.shadowRoot.getElementById('overlay').style.cssText =
'background-color: transparent; border-radius: 10px; box-shadow: rgb(50 50 93 / 25%) 0px 2px 5px -1px, rgb(0 0 0 / 30%) 0px 1px 3px -1px';
overlay.shadowRoot.getElementById('content').style.cssText =
'background-color: var(--gif-tooltip-bg); color: var(--chat-bubble-msg-color); text-align: center; padding: 20px 10px; font-family: Roboto, sans-serif; letter-spacing: 0.3px; font-weight: 300; font-size: 13.5px; transition: all 0.3s ease-in-out;';
try {
this.isLoading = true;
const myCollections = await this.getMyGifCollections();
const savedCollections = await this.getSavedCollections();
const allCollections = await this.getAllCollections();
if (!Array.isArray(myCollections) && !Array.isArray(savedCollections)) {
parentEpml.request('showSnackBar', get('gifs.gchange12'));
return;
}
await new Promise((res) => {
setTimeout(() => {
res();
}, 1000)
});
this.myGifCollections = myCollections;
this.mySubscribedCollections = savedCollections;
this.exploreCollections = allCollections;
} catch (error) {
console.error(error);
} finally {
this.isLoading = false;
}
}
async updated(changedProperties) {
if (changedProperties && changedProperties.has('mode')) {
const mode = this.mode;
if (mode === 'myCollection') {
try {
this.myGifCollections = [];
this.isLoading = true;
const collections = await this.getMyGifCollections();
await new Promise((res) => {
setTimeout(() => {
res();
}, 1000)
});
this.myGifCollections = collections;
} catch (error) {
console.error(error);
} finally {
this.isLoading = false;
}
}
if (mode === 'explore') {
try {
this.exploreCollections = [];
this.isLoading = true;
const allCollections = await this.getAllCollections();
await new Promise((res) => {
setTimeout(() => {
res();
}, 1000)
});
this.exploreCollections = allCollections;
} catch (error) {
console.error(error);
} finally {
this.isLoading = false;
}
}
if (mode === 'subscribedCollection') {
try {
this.mySubscribedCollections = [];
this.isLoading = true;
const savedCollections = await this.getSavedCollections();
await new Promise((res) => {
setTimeout(() => {
res();
}, 1000)
});
this.mySubscribedCollections = savedCollections;
} catch (error) {
console.error(error);
} finally {
this.isLoading = false;
}
}
}
if (changedProperties && changedProperties.has('currentCollection')) {
if (this.mode === 'explore') {
const subbedCollection = this.mySubscribedCollections.find((collection) => ((collection.name === this.currentCollection.name) && (collection.identifier === this.currentCollection.identifier)));
if (subbedCollection) {
this.isSubscribed = true;
} else {
this.isSubscribed = false;
}
}
}
}
async structureCollections(gifCollections) {
const userName = await this.getName(this.selectedAddress.address);
if (!userName) {
return;
}
try {
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 getMetaDataGifs = (gifCollections || []).map(
async (collection) => {
let collectionObj = collection;
try {
const metaData = await parentEpml.request('apiCall', {
url: `/arbitrary/metadata/GIF_REPOSITORY/${collection.name}/${collection.identifier}?apiKey=${this.getApiKey()}`,
});
collectionObj = {
...collection,
gifUrls: [],
};
if (metaData.files) {
const metaDataArray = metaData.files.map((data) => {
return {
url: `${nodeUrl}/arbitrary/GIF_REPOSITORY/${collection.name}/${collection.identifier}?filepath=${data}&apiKey=${this.getApiKey()}`,
filePath: data,
identifier: collection.identifier,
name: collection.name
};
});
collectionObj = {
...collection,
gifUrls: metaDataArray,
};
}
} catch (error) {
console.log(error);
}
return collectionObj;
}
);
return await Promise.all(getMetaDataGifs);
} catch (error) {}
}
getApiKey() {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
let apiKey = myNode.apiKey;
return apiKey;
}
async getMoreExploreGifs() {
try {
const getAllGifCollections = await parentEpml.request('apiCall', {
type: 'api',
url: `/arbitrary/resources?service=GIF_REPOSITORY&limit=20&offset=${
this.pageNumber * 20
}&apiKey=${this.getApiKey()}`,
});
const gifCollectionWithMetaData = await this.structureCollections(
getAllGifCollections
);
this.exploreCollections = [
...this.exploreCollections,
...gifCollectionWithMetaData,
];
this.pageNumber = this.pageNumber + 1;
} catch (error) {
console.error(error);
}
}
async getCollectionList() {
try {
return await parentEpml.request('apiCall', {
type: 'api',
url: `/lists/gifSubscribedRepos?apiKey=${this.getApiKey()}`,
});
} catch (error) {}
}
async addCollectionToList(collection) {
try {
const body = {
items: [collection],
};
const bodyToString = JSON.stringify(body);
await parentEpml.request('apiCall', {
type: 'api',
method: 'POST',
url: `/lists/gifSubscribedRepos?apiKey=${this.getApiKey()}`,
body: bodyToString,
headers: {
'Content-Type': 'application/json',
},
});
} catch (error) {}
}
async removeCollectionFromList(collection) {
try {
const body = {
items: [collection],
};
const bodyToString = JSON.stringify(body);
await parentEpml.request('apiCall', {
type: 'api',
method: 'DELETE',
url: `/lists/gifSubscribedRepos?apiKey=${this.getApiKey()}`,
body: bodyToString,
headers: {
'Content-Type': 'application/json',
},
});
} catch (error) {}
}
async getMyGifCollections() {
const userName = await this.getName(this.selectedAddress.address);
this.myAccountName = userName;
if (this.myAccountName) {
const getMyGifCollections = await parentEpml.request('apiCall', {
url: `/arbitrary/resources/search?service=GIF_REPOSITORY&query=${this.myAccountName}&apiKey=${this.getApiKey()}`,
});
const gifCollectionWithMetaData = await this.structureCollections(
getMyGifCollections
);
return gifCollectionWithMetaData;
} else {
return [];
}
}
async getAllCollections() {
this.pageNumber = 0;
// for the explore section
const getAllGifCollections = await parentEpml.request('apiCall', {
type: 'api',
url: `/arbitrary/resources?service=GIF_REPOSITORY&limit=20&offset=${
this.pageNumber * 20
}&apiKey=${this.getApiKey()}`,
});
const gifCollectionWithMetaData = await this.structureCollections(
getAllGifCollections
);
this.pageNumber = this.pageNumber + 1;
return gifCollectionWithMetaData;
}
async getSavedCollections() {
const getCollectionList = await this.getCollectionList();
let savedCollections = [];
const getSavedGifRepos = (getCollectionList || []).map(
async (collection) => {
let splitCollection = collection.split('/');
const name = splitCollection[0];
const identifier = splitCollection[1];
try {
const data = await parentEpml.request('apiCall', {
url: `/arbitrary/metadata/GIF_REPOSITORY/${name}/${identifier}?apiKey=${this.getApiKey()}`,
});
if (data.title) {
savedCollections.push({
identifier,
name,
service: 'GIF_REPOSITORY'
});
}
} catch (error) {
console.log(error);
}
return collection;
}
);
await Promise.all(getSavedGifRepos);
const savedCollectionsWithMetaData = await this.structureCollections(
savedCollections
);
return savedCollectionsWithMetaData;
}
async getName(recipient) {
try {
const getNames = await parentEpml.request('apiCall', {
type: 'api',
url: `/names/address/${recipient}?apiKey=${this.getApiKey()}`,
});
if (Array.isArray(getNames) && getNames.length > 0) {
return getNames[0].name;
} else {
return '';
}
} catch (error) {
return '';
}
}
removeDotGIF(arr) {
return arr.map(obj => {
const newObj = { ...obj };
if (newObj.hasOwnProperty('name') && newObj.name.endsWith('.gif')) {
newObj.name = newObj.name.slice(0, -4);
}
return newObj;
});
}
addDotGIF(arr) {
return arr.map(obj => {
const newObj = { ...obj };
if (newObj.hasOwnProperty('name') && !newObj.name.endsWith('.gif')) {
newObj.name += '.gif';
}
return newObj;
});
}
addGifs(gifs) {
const mapGifs = gifs.map((file) => {
return {
file,
name: file.name,
size: file.size
};
});
const removedExtensions = this.removeDotGIF(mapGifs);
this.gifsToBeAdded = [...this.gifsToBeAdded, ...removedExtensions];
}
async uploadGifCollection() {
if (!this.newCollectionName) {
parentEpml.request('showSnackBar', get('gifs.gchange8'));
return;
}
try {
this.setGifsLoading(true);
this.isLoading = true;
const userName = await this.getName(this.selectedAddress.address);
const doesNameExist = await parentEpml.request('apiCall', {
url: `/arbitrary/metadata/GIF_REPOSITORY/${userName}/${this.newCollectionName}?apiKey=${this.getApiKey()}`,
});
if (!userName) {
parentEpml.request('showSnackBar', get('chatpage.cchange27'));
this.setGifsLoading(false);
this.isLoading = false;
return;
}
if (doesNameExist.title) {
parentEpml.request('showSnackBar', get('gifs.gchange24'));
this.isLoading = false;
this.setGifsLoading(false);
return;
}
function validateGifSizes(gifs) {
const maxSizeInMB = 0.7;
const invalidGifs = [];
for (let i = 0; i < gifs.length; i++) {
const gif = gifs[i];
const gifSize = gif.size;
const gifSizeMB = bytesToMegabytes(gifSize);
if (gifSizeMB > maxSizeInMB) {
invalidGifs.push(gif);
}
}
if (invalidGifs.length > 0) {
return false;
} else {
return true;
}
}
let validatedSize = validateGifSizes(this.gifsToBeAdded);
if (!validatedSize) {
parentEpml.request('showSnackBar', get('gifs.gchange28'));
this.isLoading = false;
this.setGifsLoading(false);
return;
}
function validateDuplicateGifNames(arr) {
let names = [];
for (let i = 0; i < arr.length; i++) {
if (names.includes(arr[i].name)) {
return false;
}
names.push(arr[i].name);
}
return true;
}
let validatedNames = validateDuplicateGifNames(this.gifsToBeAdded);
if (!validatedNames) {
parentEpml.request('showSnackBar', get('gifs.gchange23'));
this.isLoading = false;
this.setGifsLoading(false);
return;
}
const addedGifExtensionsArr = this.addDotGIF(this.gifsToBeAdded);
function blobToBase64(blob) {
return new Promise((resolve, _) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
}
const zipFileWriter = new zip.BlobWriter('application/zip');
const zipWriter = new zip.ZipWriter(zipFileWriter, {
bufferedWrite: true,
});
for (let i = 0; i < addedGifExtensionsArr.length; i++) {
await zipWriter.add(
addedGifExtensionsArr[i].name,
new zip.BlobReader(addedGifExtensionsArr[i].file)
);
}
await zipWriter.close();
const zipFileBlob = await zipFileWriter.getData();
const zipSize = bytesToMegabytes(zipFileBlob.size);
if (zipSize > 10) {
parentEpml.request('showSnackBar', get('gifs.gchange27'));
this.isLoading = false;
this.setGifsLoading(false);
return;
}
const blobTobase = await blobToBase64(zipFileBlob);
await publishData({
registeredName: userName,
file: blobTobase.split(',')[1],
service: 'GIF_REPOSITORY',
identifier: this.newCollectionName,
parentEpml,
title: this.newCollectionName,
uploadType: 'zip',
selectedAddress: this.selectedAddress,
worker: this.webWorkerImage,
isBase64: true,
});
await new Promise((res) => {
let interval = null;
let stop = false;
const getAnswer = async () => {
if (!stop) {
stop = true;
try {
let myCollection = await parentEpml.request(
'apiCall',
{
url: `/arbitrary/metadata/GIF_REPOSITORY/${userName}/${this.newCollectionName}?apiKey=${this.getApiKey()}`,
}
);
if (myCollection.title) {
clearInterval(interval);
res();
}
} catch (error) {
console.error(error);
this.isLoading = false;
this.setGifsLoading(false);
this.mode = 'myCollection';
this.gifsToBeAdded = [];
this.newCollectionName = '';
parentEpml.request('showSnackBar', get('gifs.gchange12'));
}
stop = false;
}
};
interval = setInterval(getAnswer, 5000);
});
this.isLoading = false;
this.setGifsLoading(false);
this.mode = 'myCollection';
this.gifsToBeAdded = [];
this.newCollectionName = '';
parentEpml.request('showSnackBar', get('gifs.gchange10'));
} catch (error) {
console.log(error);
parentEpml.request('showSnackBar', get('gifs.gchange12'));
this.setGifsLoading(false);
this.isLoading = false;
}
}
setCurrentCollection(val) {
this.currentCollection = val;
}
clearGifSelections() {
this.mode = 'myCollection';
this.gifsToBeAdded = [];
}
async subscribeToCollection() {
await this.addCollectionToList(
`${this.currentCollection.name}/${this.currentCollection.identifier}`
);
parentEpml.request('showSnackBar', get('gifs.gchange20'));
this.isSubscribed = true;
const savedCollections = await this.getSavedCollections();
this.mySubscribedCollections = savedCollections;
}
async unsubscribeToCollection() {
await this.removeCollectionFromList(
`${this.currentCollection.name}/${this.currentCollection.identifier}`
);
parentEpml.request('showSnackBar', get('gifs.gchange21'));
this.isSubscribed = false;
const savedCollections = await this.getSavedCollections();
this.mySubscribedCollections = savedCollections;
}
render() {
return html`
<div class="gifs-container">
<div class="gif-explorer-container">
<vaadin-icon
style=${
(this.mode === 'newCollection' ||
(this.mode === 'explore' && this.currentCollection))
? 'display: none;'
: 'display: block;'
}
id="create-collection-button"
class="create-collections-icon"
@click=${() => {
if (this.isLoading) return;
this.mode = 'newCollection';
}}
icon="vaadin:plus"
slot="icon">
</vaadin-icon>
<div class="title-row">
<div
style=${((this.currentCollection && (this.mode === 'myCollection' || this.mode === 'subscribedCollection')) || this.mode === 'explore') ? "visibility: visible;" : "visibility: hidden;"}
class='collection-back-button'
@click=${() => {
if (this.mode === 'explore' && !this.currentCollection) {
this.mode = 'myCollection';
this.currentCollection = null;
} else if (this.mode === 'explore' && this.currentCollection) {
this.mode = 'explore';
this.currentCollection = null;
this.isSubscribed = false;
} else {
this.currentCollection = null;
}
}}
>
<vaadin-icon class='collection-back-button-arrow' icon='vaadin:arrow-left' slot='icon'></vaadin-icon>
</div>
<p class="gif-explorer-title">
${translate(
'gifs.gchange1'
)}
</p>
<vaadin-icon
style=${
(this.mode === 'newCollection' || this.mode === 'explore')
? 'display: none'
: 'display: block'
}
id="explore-collections-icon"
class="explore-collections-icon"
@click=${() => {
if (this.isLoading) return;
this.mode = 'explore';
this.currentCollection = null;
}}
icon="vaadin:search"
slot="icon">
</vaadin-icon>
<vaadin-tooltip
for="explore-collections-icon"
position="top"
hover-delay=${400}
hide-delay=${1}
text=${get('gifs.gchange2')}>
</vaadin-tooltip>
</div>
<div
class="collections-button-row"
style=${(this.mode === 'newCollection' || this.mode === 'explore')
? 'display: none' : 'display: block'}>
<div class="collections-button-innerrow">
<div
id="my-collections-button"
class=${[
'my-collections-button',
this.mode === 'myCollection'
? 'collections-button-active'
: null,
].join(' ')}
@click=${() => {
if (this.isLoading) return;
if (this.mode === 'myCollection') return;
this.mode = 'myCollection';
this.currentCollection = null;
}}>
${translate('gifs.gchange3')}
</div>
<div
id="subscribed-collections-button"
class=${[
'subscribed-collections-button',
this.mode === 'subscribedCollection'
? 'collections-button-active'
: null,
].join(' ')}
@click=${() => {
if (this.isLoading) return;
if (this.mode === 'subscribedCollection') return;
this.mode = 'subscribedCollection';
this.currentCollection = null;
}}
>
${translate('gifs.gchange4')}
</div>
</div>
</div>
<div class="collection-wrapper">
${this.mode === 'myCollection' && !this.currentCollection
? html`
${this.isLoading === true
? html`<div class="lds-circle"><div></div></div>`
: ''}
${(this.myGifCollections.length === 0 && !this.isLoading) ? (
html`
<div class='no-collections'>${translate('gifs.gchange13')}</div>
`
) : (
html`
${(this.myGifCollections || []).map((collection) => {
return html`
<div @click=${() => {
this.currentCollection =
collection;
}} class='collection-card'>
${collection.identifier}
</div>
`;
})}
`
)}
`
: ''
}
${this.mode === 'subscribedCollection' &&
!this.currentCollection
? html`
${this.isLoading === true
? html`<div class="lds-circle"><div></div></div>`
: ''}
${(this.mySubscribedCollections.length === 0 && !this.isLoading) ? (
html`
<div class='no-collections'>${translate('gifs.gchange14')}</div>
`
) : (
html`
${this.mySubscribedCollections.map(
(collection) => {
return html`
<div @click=${() => {
this.currentCollection =
collection;
}} class='collection-card'>
${collection.identifier}
</div>
`;
}
)}
`
)}
`
: ''
}
${this.mode === 'explore' && !this.currentCollection
? html`
${this.isLoading === true
? html`
<div class="lds-circle"><div></div></div>
`
: html`
<chat-gifs-explore
currentCollection=${this.currentCollection}
.getAllCollections=${(val) =>
this.getAllCollections(val)}
.getMoreExploreGifs=${(val) =>
this.getMoreExploreGifs(val)}
.exploreCollections=${this
.exploreCollections}
.setCurrentCollection=${(val) =>
this.setCurrentCollection(val)}
></chat-gifs-explore>
`
}
`
: ''
}
${this.currentCollection && this.mode === 'myCollection'
? html`
<div class='collection-gifs'>
${this.currentCollection.gifUrls.map((gif) => {
return html`
<image-component
.sendMessage=${(val) => this.sendMessage(val)}
.setOpenGifModal=${(val) => this.setOpenGifModal(val)}
.class=${'gif-image'}
.gif=${gif}
.alt=${'gif-image'}>
</image-component>
`;
})}
</div>
`
: ''
}
${this.currentCollection &&
this.mode === 'subscribedCollection'
? html`
<div class='collection-gifs'>
${this.currentCollection.gifUrls.map((gif) => {
return html`
<image-component
.sendMessage=${(val) => this.sendMessage(val)}
.setOpenGifModal=${(val) => this.setOpenGifModal(val)}
.class=${'gif-image'}
.gif=${gif}
.alt=${'gif-image'}>
</image-component>
`;
})}
</div>
`
: ''
}
${this.currentCollection && this.mode === 'explore'
? html`
<div class="collection-gifs">
${this.currentCollection.gifUrls.map((gif) => {
return html`
<image-component
.sendMessage=${(val) => this.sendMessage(val)}
.setOpenGifModal=${(val) => this.setOpenGifModal(val)}
.class=${'gif-image'}
.gif=${gif}
.alt=${'gif-image'}>
</image-component>
`;
})}
</div>
${this.isSubscribed ? (
html`
<button
class='unsubscribe-button'
@click=${this.unsubscribeToCollection}>
${translate('gifs.gchange22')}
</button>
`
) : (
html`
<button
class='subscribe-button'
@click=${this.subscribeToCollection}
>
${translate('gifs.gchange17')}
</button>
`
)}
`
: ''
}
${this.mode === 'newCollection' && this.isLoading === false
? html`
<div class="new-collection-row" style=${this.gifsToBeAdded.length === 0 ? "" : "flex: 1;"}>
<div class="new-collection-subrow">
<p class="new-collection-title">
${translate('gifs.gchange5')}
</p>
<p class="new-collection-subtitle">
${translate('gifs.gchange6')}
</p>
</div>
<div
@click=${() =>
this.shadowRoot
.getElementById(
'file-picker-gif'
)
.click()}
class="new-collection-container"
style=${this.gifsToBeAdded.length > 0 ? "padding: 10px 0;" : "padding: 60px 0;"}
>
<vaadin-icon
id="new-collection-icon"
class="new-collection-icon"
icon="vaadin:folder"
slot="icon"
>
</vaadin-icon>
<input
@change="${(e) => {
this.addGifs(
Array.from(e.target.files)
);
const filePickerInput =
this.shadowRoot.getElementById(
'file-picker-gif'
);
if (filePickerInput) {
filePickerInput.value = '';
}
}}"
id="file-picker-gif"
?multiple=${true}
type="file"
name="myGif"
accept="image/gif"
style=${'display: none;'}
/>
</div>
<input
class="upload-collection-name"
style=${this.gifsToBeAdded.length === 0 ? "display: none;" : "display: block;"}
placeholder=${get("gifs.gchange9")}
.value=${this.newCollectionName}
@change=${(e) => {
this.newCollectionName =
e.target.value;
}}
/>
<div
class="gifs-added-col"
>
<div class="gifs-added-row">
${this.gifsToBeAdded.map((gif, i) => {
return html`
<div class="gif-input">
<img
class="gif-input-img"
src=${URL.createObjectURL(
gif.file
)}
/>
<input
class="gif-input-field"
.value=${gif.name}
@change=${(e) => {
this.gifsToBeAdded[i] = {
...gif,
name: e.target
.value,
};
}}
/>
</div>
`;
})}
</div>
<div class="upload-collection-row">
<button
class="upload-back-button"
@click=${() => {
this.mode = 'myCollection';
this.gifsToBeAdded = [];
}}
>
${translate('general.back')}
</button>
<button
style=${this.gifsToBeAdded.length === 0 ? "display: none;" : "display: block;"}
class="upload-button"
@click=${() => {
this.uploadGifCollection();
}}
>
${translate('gifs.gchange7')}
</button>
</div>
</div>
</div>
`
: this.mode === 'newCollection' && this.isLoading === true ? (
html`
<div>
<p class='gifs-loading-message'>${translate("gifs.gchange11")}</p>
<div class="lds-circle"><div></div></div>
</div>
`
)
: ''
}
</div>
</div>
</div>
</div>
`;
}
}
window.customElements.define('chat-gifs', ChatGifs);

View File

@@ -0,0 +1,157 @@
import { css } from 'lit';
export const chatGifsExploreStyles = css`
.container-body {
display: flex;
flex-direction: column;
align-items: center;
max-width: 100%;
height: 100%;
}
.collection-wrapper {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
max-height: 500px;
overflow-y: auto;
overflow-x: hidden;
}
.collection-wrapper::-webkit-scrollbar-track {
background-color: whitesmoke;
border-radius: 7px;
}
.collection-wrapper::-webkit-scrollbar {
width: 6px;
border-radius: 7px;
background-color: whitesmoke;
}
.collection-wrapper::-webkit-scrollbar-thumb {
background-color: rgb(180, 176, 176);
border-radius: 7px;
transition: all 0.3s ease-in-out;
}
.collection-card {
display: flex;
font-family: Roboto, sans-serif;
font-weight: 300;
letter-spacing: 0.3px;
font-size: 19px;
color: var(--chat-bubble-msg-color);
flex-direction: row;
align-items: center;
transition: all 0.3s ease-in-out;
box-shadow: none;
padding: 10px;
cursor: pointer;
}
.collection-card:hover {
border: none;
border-radius: 5px;
background-color: var(--gif-collection-hover-bg);
}
.search-collection-name {
display: block;
padding: 8px 10px;
font-size: 16px;
font-family: Montserrat, sans-serif;
font-weight: 600;
background-color: #ebeaea21;
border: 1px solid var(--mdc-theme-primary);
border-radius: 5px;
color: var(--chat-bubble-msg-color);
width: 90%;
margin: 10px 0;
outline: none;
}
.search-collection-name::placeholder {
font-size: 16px;
font-family: Montserrat, sans-serif;
font-weight: 600;
opacity: 0.6;
color: var(--chat-bubble-msg-color);
}
.search-collection-wrapper {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
position: relative;
}
.explore-collections-icon {
position: absolute;
right: 20px;
font-size: 13px;
color: var(--chat-group);
cursor: pointer;
}
.clear-search-icon {
position: absolute;
right: 15px;
font-size: 16px;
color: var(--chat-group);
padding: 1px;
border-radius: 50%;
background-color: transparent;
transition: all 0.3s ease-in-out;
}
.clear-search-icon:hover {
cursor: pointer;
background-color: #e4e3e389;
}
.gifs-loading-message {
font-family: Montserrat, sans-serif;
font-size: 20px;
font-weight: 600;
color: var(--chat-bubble-msg-color);
margin: 0 0 10px 0;
text-align: center;
user-select: none;
}
.lds-circle {
display: flex;
align-items: center;
justify-content: center;
}
.lds-circle > div {
display: inline-block;
width: 80px;
height: 80px;
margin: 8px;
border-radius: 50%;
background: var(--mdc-theme-primary);
animation: lds-circle 2.4s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
@keyframes lds-circle {
0%,
100% {
animation-timing-function: cubic-bezier(0.5, 0, 1, 0.5);
}
0% {
transform: rotateY(0deg);
}
50% {
transform: rotateY(1800deg);
animation-timing-function: cubic-bezier(0, 0.5, 0.5, 1);
}
100% {
transform: rotateY(3600deg);
}
}
`;

View File

@@ -0,0 +1,172 @@
import { LitElement, html, css } from 'lit';
import { Epml } from '../../../../epml.js';
import { chatGifsExploreStyles } from './ChatGifsExplore-css.js';
import { translate, get } from 'lit-translate';
import '@material/mwc-icon';
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent });
class ChatGifsExplore extends LitElement {
static get properties() {
return {
currentCollection: { type: String },
searchCollectionName: {type: String},
getMoreExploreGifs: { attribute: false },
exploreCollections: { type: Array },
setCurrentCollection: { attribute: false },
isLoading: { type: Boolean },
isSearched: { type: Boolean },
getAllCollections: { attribute: false }
};
}
static styles = [chatGifsExploreStyles];
constructor() {
super();
this.searchCollectionName = '';
this.downObserverElement = '';
this.viewElement = '';
this.exploreCollections = [];
this.isLoading = false;
this.isSearched = false;
}
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.exploreCollections.length < 20) {
return;
}
this.getMoreExploreGifs();
}
}
async firstUpdated() {
this.viewElement = this.shadowRoot.getElementById('viewElement');
this.downObserverElement =
this.shadowRoot.getElementById('downObserver');
this.elementObserver();
}
getApiKey() {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
let apiKey = myNode.apiKey;
return apiKey;
}
async searchCollections() {
this.isSearched = true;
try {
this.exploreCollections = [];
this.isLoading = true;
const response = await parentEpml.request('apiCall', {
url: `/arbitrary/resources/search?service=GIF_REPOSITORY&query=${this.searchCollectionName}&limit=0&apiKey=${this.getApiKey()}
`,
});
await new Promise((res) => {
setTimeout(() => {
res();
}, 1000)
});
this.exploreCollections = response;
} catch (error) {
console.error(error);
} finally {
this.isLoading = false;
}
}
render() {
return html`
<div id='viewElement' class='container-body'>
<div class='search-collection-wrapper'>
<input
class='search-collection-name'
placeholder=${get('gifs.gchange9')}
.value=${this.searchCollectionName}
@change=${(e) => {
this.searchCollectionName =
e.target.value;
}}
@keyup=${async (e) => {
if (e.key === 'Enter' && this.searchCollectionName) {
await this.searchCollections()
}
}}
/>
${this.isSearched ? (
html`
<vaadin-icon
class='clear-search-icon'
@click=${async () => {
if (this.isLoading) return;
const latestCollections = await this.getAllCollections();
this.exploreCollections = latestCollections;
this.searchCollectionName = '';
this.isSearched = false;
}}
icon='vaadin:close-small'
slot='icon'>
</vaadin-icon>
`
) : html`
<vaadin-icon
class='explore-collections-icon'
@click=${async () => {
if (this.isLoading || !this.searchCollectionName) return;
await this.searchCollections();
}}
icon='vaadin:search'
slot='icon'>
</vaadin-icon>
`}
</div>
<div class='collection-wrapper'>
${this.isLoading ? html`
<div style=${'margin-top: 10px;'}>
<p class='gifs-loading-message'>${translate('gifs.gchange18')}
</p>
<div class='lds-circle'><div></div></div>
</div>`
: this.isSearched && this.exploreCollections.length === 0 ? (
html`<p style=${'margin-top: 10px;'} class='gifs-loading-message'>${translate('gifs.gchange19')}</p>`
) : (
html`${this.exploreCollections.map((collection) => {
return html`
<div class='collection-card' @click=${() => {
this.setCurrentCollection(collection);
}}>
${collection.identifier}
</div>
`;
})}`
)}
</div>
<div id='downObserver'></div>
</div>
`;
}
}
window.customElements.define('chat-gifs-explore', ChatGifsExplore);

View File

@@ -0,0 +1,335 @@
import { LitElement, html, css } from "lit"
import { render } from "lit/html.js"
import { get, translate } from "lit-translate"
import { Epml } from "../../../epml"
import snackbar from "./snackbar.js"
import "@material/mwc-button"
import "@material/mwc-dialog"
import "@polymer/paper-spinner/paper-spinner-lite.js"
import "@material/mwc-icon"
import "./WrapperModal"
const parentEpml = new Epml({ type: "WINDOW", source: window.parent })
class ChatGroupInvites extends LitElement {
static get properties() {
return {
isLoading: { type: Boolean },
isOpenLeaveModal: { type: Boolean },
leaveGroupObj: { type: Object },
error: { type: Boolean },
message: { type: String },
chatHeads: { type: Array },
groupAdmin: { attribute: false },
groupMembers: { attribute: false },
selectedHead: { type: Object },
}
}
constructor() {
super()
this.isLoading = false
this.isOpenLeaveModal = false
this.leaveGroupObj = {}
this.leaveFee = 0.001
this.error = false
this.message = ""
this.chatHeads = []
this.groupAdmin = []
this.groupMembers = []
}
static get styles() {
return css`
.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;
}
`
}
firstUpdated() {}
timeIsoString(timestamp) {
let myTimestamp = timestamp === undefined ? 1587560082346 : timestamp
let time = new Date(myTimestamp)
return time.toISOString()
}
resetDefaultSettings() {
this.error = false
this.message = ""
this.isLoading = false
}
renderErr9Text() {
return html`${translate("grouppage.gchange49")}`
}
async confirmRelationship(reference) {
let interval = null
let stop = false
const getAnswer = async () => {
if (!stop) {
stop = true
try {
let myRef = await parentEpml.request("apiCall", {
type: "api",
url: `/transactions/reference/${reference}`,
})
if (myRef && myRef.type) {
clearInterval(interval)
this.isLoading = false
this.isOpenLeaveModal = false
}
} catch (error) {}
stop = false
}
}
interval = setInterval(getAnswer, 5000)
}
async getLastRef() {
let myRef = await parentEpml.request("apiCall", {
type: "api",
url: `/addresses/lastreference/${this.selectedAddress.address}`,
})
return myRef
}
getTxnRequestResponse(txnResponse, reference) {
if (txnResponse === true) {
this.message = this.renderErr9Text()
this.error = false
this.confirmRelationship(reference)
} else {
this.error = true
this.message = ""
throw new Error(txnResponse)
}
}
async convertBytesForSigning(transactionBytesBase58) {
let convertedBytes = await parentEpml.request("apiCall", {
type: "api",
method: "POST",
url: `/transactions/convert`,
body: `${transactionBytesBase58}`,
})
return convertedBytes
}
async signTx(body){
return await parentEpml.request("apiCall", {
type: "api",
method: "POST",
url: `/transactions/sign`,
body: body,
headers: {
'Content-Type': 'application/json'
}
})
}
async process(body){
return await parentEpml.request("apiCall", {
type: "api",
method: "POST",
url: `/transactions/process`,
body: body,
})
}
async _addAdmin(groupId) {
// Reset Default Settings...
this.resetDefaultSettings()
const leaveFeeInput = this.leaveFee
this.isLoading = true
// Get Last Ref
const validateReceiver = async () => {
let lastRef = await this.getLastRef()
let myTransaction = await makeTransactionRequest(lastRef)
this.getTxnRequestResponse(myTransaction, lastRef )
}
// Make Transaction Request
const makeTransactionRequest = async (lastRef) => {
const body = {
timestamp: Date.now(),
reference: lastRef,
fee: leaveFeeInput,
ownerPublicKey: window.parent.Base58.encode(
window.parent.reduxStore.getState().app.selectedAddress
.keyPair.publicKey
),
groupId: groupId,
member: this.selectedHead.address,
}
const bodyToString = JSON.stringify(body)
let transactionBytes = await parentEpml.request("apiCall", {
type: "api",
method: "POST",
url: `/groups/addadmin`,
body: bodyToString,
headers: {
"Content-Type": "application/json",
},
})
const readforsign = await this.convertBytesForSigning(
transactionBytes
)
const body2 = {
privateKey: window.parent.Base58.encode(
window.parent.reduxStore.getState().app.selectedAddress
.keyPair.privateKey
),
transactionBytes: readforsign,
}
const bodyToString2 = JSON.stringify(body2)
let signTransaction = await this.signTx(bodyToString2)
let processTransaction = await this.process(signTransaction)
return processTransaction
}
validateReceiver()
}
async _removeAdmin(groupId) {
// Reset Default Settings...
this.resetDefaultSettings()
const leaveFeeInput = this.leaveFee
this.isLoading = true
// Get Last Ref
const validateReceiver = async () => {
let lastRef = await this.getLastRef()
let myTransaction = await makeTransactionRequest(lastRef)
this.getTxnRequestResponse(myTransaction, lastRef)
}
// Make Transaction Request
const makeTransactionRequest = async (lastRef) => {
const body = {
timestamp: Date.now(),
reference: lastRef,
fee: leaveFeeInput,
ownerPublicKey: window.parent.Base58.encode(
window.parent.reduxStore.getState().app.selectedAddress
.keyPair.publicKey
),
groupId: groupId,
admin: this.selectedHead.address,
}
const bodyToString = JSON.stringify(body)
let transactionBytes = await parentEpml.request("apiCall", {
type: "api",
method: "POST",
url: `/groups/removeadmin`,
body: bodyToString,
headers: {
"Content-Type": "application/json",
},
})
const readforsign = await this.convertBytesForSigning(
transactionBytes
)
const body2 = {
privateKey: window.parent.Base58.encode(
window.parent.reduxStore.getState().app.selectedAddress
.keyPair.privateKey
),
transactionBytes: readforsign,
}
const bodyToString2 = JSON.stringify(body2)
let signTransaction = await this.signTx(bodyToString2)
let processTransaction = await this.process(signTransaction)
return processTransaction
}
validateReceiver()
}
render() {
console.log("leaveGroupObj", this.leaveGroupObj)
return html`
<vaadin-icon @click=${()=> {
this.isOpenLeaveModal = true
}} class="top-bar-icon" style="margin: 0px 20px" icon="vaadin:users" slot="icon"></vaadin-icon>
<wrapper-modal
.removeImage=${() => {
if (this.isLoading) return
this.isOpenLeaveModal = false
}}
style=${
this.isOpenLeaveModal ? "display: block" : "display: none"
}>
<div style="text-align:center">
<h1>${translate("grouppage.gchange35")}</h1>
<hr>
</div>
<button @click=${() =>
this._addAdmin(
this.leaveGroupObj.groupId
)}>Promote to Admin</button>
<button @click=${() =>
this._removeAdmin(
this.leaveGroupObj.groupId
)}>Remove as Admin</button>
<div style="text-align:right; height:36px;">
<span ?hidden="${!this.isLoading}">
<!-- loading message -->
${translate("grouppage.gchange36")} &nbsp;
<paper-spinner-lite
style="margin-top:12px;"
?active="${this.isLoading}"
alt="Leaving"
>
</paper-spinner-lite>
</span>
<span ?hidden=${this.message === ""} style="${
this.error ? "color:red;" : ""
}">
${this.message}
</span>
</div>
<button
@click=${() => {
this.isOpenLeaveModal = false
}}
class="modal-button"
?disabled="${this.isLoading}"
>
${translate("general.close")}
</button>
</wrapper-modal >
`
}
}
customElements.define("chat-right-panel", ChatGroupInvites)

View File

@@ -0,0 +1,283 @@
import { LitElement, html, css } from 'lit';
import { render } from 'lit/html.js';
import { get, translate } from 'lit-translate';
import { Epml } from '../../../epml';
import snackbar from './snackbar.js'
import '@material/mwc-button';
import '@material/mwc-dialog';
import '@polymer/paper-spinner/paper-spinner-lite.js'
import '@material/mwc-icon';
import './WrapperModal';
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class ChatGroupSettings extends LitElement {
static get properties() {
return {
isLoading: { type: Boolean },
isOpenLeaveModal: {type: Boolean},
leaveGroupObj: { type: Object },
error: {type: Boolean},
message: {type: String},
chatHeads: {type: Array},
setActiveChatHeadUrl: {attribute: false}
}
}
constructor() {
super();
this.isLoading = false;
this.isOpenLeaveModal = false
this.leaveGroupObj = {}
this.leaveFee = 0.001
this.error = false
this.message = ''
this.chatHeads = []
}
static get styles() {
return css`
.top-bar-icon {
cursor: pointer;
height: 18px;
width: 18px;
transition: .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;
}
`
}
firstUpdated() {
}
timeIsoString(timestamp) {
let myTimestamp = timestamp === undefined ? 1587560082346 : timestamp
let time = new Date(myTimestamp)
return time.toISOString()
}
resetDefaultSettings() {
this.error = false
this.message = ''
this.isLoading = false
}
renderErr9Text() {
return html`${translate("grouppage.gchange49")}`
}
async confirmRelationship() {
let interval = null
let stop = false
const getAnswer = async () => {
const currentChats = this.chatHeads
if (!stop) {
stop = true;
try {
const findGroup = currentChats.find((item)=> item.groupId === this.leaveGroupObj.groupId)
if (!findGroup) {
clearInterval(interval)
this.isLoading = false
this.isOpenLeaveModal= false
this.setActiveChatHeadUrl('')
}
} catch (error) {
}
stop = false
}
};
interval = setInterval(getAnswer, 5000);
}
async _convertToPrivate(groupId) {
// Reset Default Settings...
this.resetDefaultSettings()
const leaveFeeInput = this.leaveFee
this.isLoading = true
// Get Last Ref
const getLastRef = async () => {
let myRef = await parentEpml.request('apiCall', {
type: 'api',
url: `/addresses/lastreference/${this.selectedAddress.address}`
})
return myRef
};
const validateReceiver = async () => {
let lastRef = await getLastRef();
let myTransaction = await makeTransactionRequest(lastRef)
getTxnRequestResponse(myTransaction)
}
const convertBytesForSigning = async (transactionBytesBase58) => {
let convertedBytes = await parentEpml.request("apiCall", {
type: "api",
method: "POST",
url: `/transactions/convert`,
body: `${transactionBytesBase58}`,
})
return convertedBytes
}
// Make Transaction Request
const makeTransactionRequest = async (lastRef) => {
let groupdialog3 = get("transactions.groupdialog3")
let groupdialog4 = get("transactions.groupdialog4")
const body = {
"timestamp": Date.now(),
"reference": lastRef,
"fee": leaveFeeInput,
"ownerPublicKey": window.parent.Base58.encode(window.parent.reduxStore.getState().app.selectedAddress.keyPair.publicKey),
"groupId": groupId,
"newOwner": "QdR4bQ1fJFnSZgswtW27eE8ToXwHqUQyaU",
"newIsOpen": false,
"newDescription": "my group for accounts I like",
"newApprovalThreshold": "NONE",
"newMinimumBlockDelay": 5,
"newMaximumBlockDelay": 60
}
console.log('STRING3')
// const bodyToString = JSON.stringify(body)
// let transactionBytes = await parentEpml.request("apiCall", {
// type: "api",
// method: "POST",
// url: `/groups/update`,
// body: bodyToString,
// headers: {
// 'Content-Type': 'application/json'
// }
// })
// console.log({transactionBytes})
// const readforsign = await convertBytesForSigning(transactionBytes)
// // const res = await signAndProcess(transactionBytes)
// const body2 = {
// "privateKey": window.parent.Base58.encode(window.parent.reduxStore.getState().app.selectedAddress.keyPair.privateKey),
// "transactionBytes": readforsign
// }
// const bodyToString2 = JSON.stringify(body2)
// let signTransaction = await parentEpml.request("apiCall", {
// type: "api",
// method: "POST",
// url: `/transactions/sign`,
// body: bodyToString2,
// headers: {
// 'Content-Type': 'application/json'
// }
// })
// let processTransaction = await parentEpml.request("apiCall", {
// type: "api",
// method: "POST",
// url: `/transactions/process`,
// body: signTransaction,
// })
// return processTransaction
console.log('this.selectedAddress.nonce', this.selectedAddress.nonce)
let myTxnrequest = await parentEpml.request('transaction', {
type: 23,
nonce: this.selectedAddress.nonce,
params: {
_groupId: groupId,
lastReference: lastRef,
fee: leaveFeeInput,
"newOwner": "QdR4bQ1fJFnSZgswtW27eE8ToXwHqUQyaU",
"newIsOpen": false,
"newDescription": "my group for accounts I like",
"newApprovalThreshold": "NONE",
"newMinimumBlockDelay": 5,
"newMaximumBlockDelay": 60
}
})
return myTxnrequest
}
const getTxnRequestResponse = (txnResponse) => {
if (txnResponse === true) {
this.message = this.renderErr9Text()
this.error = false
this.confirmRelationship()
} else {
this.error = true
this.message = ""
throw new Error(txnResponse)
}
}
validateReceiver()
}
render() {
console.log('leaveGroupObj', this.leaveGroupObj)
return html`
<vaadin-icon @click=${()=> {
this.isOpenLeaveModal = true
}} class="top-bar-icon" style="margin: 0px 20px" icon="vaadin:cog" slot="icon"></vaadin-icon>
<!-- Leave Group Dialog -->
<wrapper-modal
.removeImage=${() => {
if(this.isLoading) return
this.isOpenLeaveModal = false
} }
style=${(this.isOpenLeaveModal) ? "display: block" : "display: none"}>
<div style="text-align:center">
<h1>${translate("grouppage.gchange35")}</h1>
<hr>
</div>
<
<button @click=${() => this._convertToPrivate(this.leaveGroupObj.groupId, this.leaveGroupObj.groupName)}> Convert a public group to private</button>
<div style="text-align:right; height:36px;">
<span ?hidden="${!this.isLoading}">
<!-- loading message -->
${translate("grouppage.gchange36")} &nbsp;
<paper-spinner-lite
style="margin-top:12px;"
?active="${this.isLoading}"
alt="Leaving"
>
</paper-spinner-lite>
</span>
<span ?hidden=${this.message === ''} style="${this.error ? 'color:red;' : ''}">
${this.message}
</span>
</div>
<button
@click=${() => {
this.isOpenLeaveModal= false
}}
class="modal-button"
?disabled="${this.isLoading}"
>
${translate("general.close")}
</button>
</wrapper-modal >
`;
}
}
customElements.define('chat-group-settings', ChatGroupSettings);

View File

@@ -0,0 +1,296 @@
import { LitElement, html, css } from 'lit';
import { render } from 'lit/html.js';
import { get, translate } from 'lit-translate';
import { Epml } from '../../../epml';
import snackbar from './snackbar.js'
import '@material/mwc-button';
import '@material/mwc-dialog';
import '@polymer/paper-spinner/paper-spinner-lite.js'
import '@material/mwc-icon';
import './WrapperModal';
import '@vaadin/tabs'
import '@vaadin/tabs/theme/material/vaadin-tabs.js';
import '@vaadin/avatar';
import '@vaadin/grid';
import '@vaadin/grid/vaadin-grid-filter-column.js';
import { columnBodyRenderer } from '@vaadin/grid/lit.js';
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class ChatGroupsManagement extends LitElement {
static get properties() {
return {
isLoading: { type: Boolean },
isOpenLeaveModal: {type: Boolean},
leaveGroupObj: { type: Object },
error: {type: Boolean},
message: {type: String},
chatHeads: {type: Array},
setActiveChatHeadUrl: {attribute: false},
selectedAddress: {attribute: Object},
currentTab: {type: Number},
groups: {type: Array}
}
}
constructor() {
super();
this.isLoading = false;
this.isOpenLeaveModal = false
this.leaveGroupObj = {}
this.fee = null
this.error = false
this.message = ''
this.chatHeads = []
this.currentTab = 0
this.groups = []
}
static get styles() {
return css`
.top-bar-icon {
cursor: pointer;
height: 18px;
width: 18px;
transition: .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;
}
`
}
async getJoinedGroups(){
let joinedG = await parentEpml.request('apiCall', {
url: `/groups/member/${this.selectedAddress.address}`
})
return joinedG
}
async firstUpdated() {
try {
let _joinedGroups = await this.getJoinedGroups()
this.joinedGroups = _joinedGroups
} catch (error) {
}
}
_tabChanged(e) {
this.currentTab = e.detail.value
}
async unitFee() {
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 url = `${nodeUrl}/transactions/unitfee?txType=LEAVE_GROUP`
let fee = null
try {
const res = await fetch(url)
const data = await res.json()
fee = (Number(data) / 1e8).toFixed(3)
} catch (error) {
fee = null
}
return fee
}
timeIsoString(timestamp) {
let myTimestamp = timestamp === undefined ? 1587560082346 : timestamp
let time = new Date(myTimestamp)
return time.toISOString()
}
resetDefaultSettings() {
this.error = false
this.message = ''
this.isLoading = false
}
renderErr9Text() {
return html`${translate("grouppage.gchange49")}`
}
async confirmRelationship() {
let interval = null
let stop = false
const getAnswer = async () => {
const currentChats = this.chatHeads
if (!stop) {
stop = true;
try {
const findGroup = currentChats.find((item)=> item.groupId === this.leaveGroupObj.groupId)
if (!findGroup) {
clearInterval(interval)
this.isLoading = false
this.isOpenLeaveModal= false
this.setActiveChatHeadUrl('')
}
} catch (error) {
}
stop = false
}
};
interval = setInterval(getAnswer, 5000);
}
async _leaveGroup(groupId, groupName) {
// Reset Default Settings...
this.resetDefaultSettings()
const leaveFeeInput = await this.unitFee()
if(!leaveFeeInput){
throw Error()
}
this.isLoading = true
// Get Last Ref
const getLastRef = async () => {
let myRef = await parentEpml.request('apiCall', {
type: 'api',
url: `/addresses/lastreference/${this.selectedAddress.address}`
})
return myRef
};
const validateReceiver = async () => {
let lastRef = await getLastRef();
let myTransaction = await makeTransactionRequest(lastRef)
getTxnRequestResponse(myTransaction)
}
// Make Transaction Request
const makeTransactionRequest = async (lastRef) => {
let groupdialog3 = get("transactions.groupdialog3")
let groupdialog4 = get("transactions.groupdialog4")
let myTxnrequest = await parentEpml.request('transaction', {
type: 32,
nonce: this.selectedAddress.nonce,
params: {
fee: leaveFeeInput,
registrantAddress: this.selectedAddress.address,
rGroupName: groupName,
rGroupId: groupId,
lastReference: lastRef,
groupdialog3: groupdialog3,
groupdialog4: groupdialog4,
}
})
return myTxnrequest
}
const getTxnRequestResponse = (txnResponse) => {
if (txnResponse.success === false && txnResponse.message) {
this.error = true
this.message = txnResponse.message
throw new Error(txnResponse)
} else if (txnResponse.success === true && !txnResponse.data.error) {
this.message = this.renderErr9Text()
this.error = false
this.confirmRelationship()
} else {
this.error = true
this.message = txnResponse.data.message
throw new Error(txnResponse)
}
}
validateReceiver()
}
nameRenderer(person){
console.log({person})
return html`
<vaadin-horizontal-layout style="align-items: center;display:flex" theme="spacing">
<vaadin-avatar style="margin-right:5px" img="${person.pictureUrl}" .name="${person.displayName}"></vaadin-avatar>
<span> ${person.displayName} </span>
</vaadin-horizontal-layout>
`;
};
render() {
return html`
<!-- <vaadin-icon @click=${()=> {
this.isOpenLeaveModal = true
}} class="top-bar-icon" style="margin: 0px 20px" icon="vaadin:exit" slot="icon"></vaadin-icon> -->
<!-- Leave Group Dialog -->
<wrapper-modal
.removeImage=${() => {
if(this.isLoading) return
this.isOpenLeaveModal = false
} }
customStyle=${"width: 90%; max-width: 900px; height: 90%"}
style=${(this.isOpenLeaveModal) ? "display: block" : "display: none"}>
<div style="width: 100%;height: 100%;display: flex; flex-direction: column;background:var(--mdc-theme-surface)">
<div style="height: 50px;display: flex; flex:0">
<vaadin-tabs id="tabs" selected="${this.currentTab}" @selected-changed="${this._tabChanged}" style="width: 100%">
<vaadin-tab>Groups</vaadin-tab>
<vaadin-tab>Group Join Requests</vaadin-tab>
<vaadin-tab>Invites</vaadin-tab>
<vaadin-tab>Blocked Users</vaadin-tab>
</vaadin-tabs>
</div>
<div style="width: 100%;display: flex; flex-direction: column; flex-grow: 1; overflow:auto;background:var(--mdc-theme-surface)">
${this.currentTab === 0 ? html`
<div>
<!-- Groups tab -->
<!-- Search groups and be able to join -->
<p>Search groups</p>
<!-- Click group and it goes to that group and open right panel and settings -->
<p>Current groups as owner</p>
<p>Current groups as member</p>
</div>
` : ''}
</div>
<div style="width: 100%;height: 50;display: flex; flex: 0">
<button
class="modal-button"
?disabled="${this.isLoading}"
@click=${() => this._leaveGroup(this.leaveGroupObj.groupId, this.leaveGroupObj.groupName)}
>
${translate("grouppage.gchange37")}
</button>
<button
@click=${() => {
this.isOpenLeaveModal= false
}}
class="modal-button"
?disabled="${this.isLoading}"
>
${translate("general.close")}
</button>
</div>
</div>
</wrapper-modal >
`;
}
}
customElements.define('chat-groups-management', ChatGroupsManagement);

View File

@@ -0,0 +1,251 @@
import { LitElement, html, css } from 'lit'
import { render } from 'lit/html.js'
import { Epml } from '../../../epml.js'
import localForage from "localforage";
import { translate} from 'lit-translate';
import '@material/mwc-icon'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
const chatLastSeen = localForage.createInstance({
name: "chat-last-seen",
});
class ChatHead 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},
lastReadMessageTimestamp: {type: Number}
}
}
static get styles() {
return css`
li {
width: 100%;
padding: 10px 5px 10px 5px;
cursor: pointer;
width: 100%;
box-sizing: border-box;
display: flex;
align-items: flex-start;
}
li:hover {
background-color: var(--menuhover);
}
.active {
background: var(--menuactive);
border-left: 4px solid #3498db;
}
.img-icon {
float: left;
font-size:40px;
color: var(--chat-group);
}
.about {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin: 0px;
}
.inner-container {
display: flex;
width: calc(100% - 45px);
flex-direction: column;
justify-content: center;
}
.status {
color: #92959e;
}
.clearfix:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
.name {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
`
}
constructor() {
super()
this.selectedAddress = {}
this.config = {
user: {
node: {
}
}
}
this.chatInfo = {}
this.iconName = ''
this.activeChatHeadUrl = ''
this.isImageLoaded = false
this.imageFetches = 0
this.lastReadMessageTimestamp = 0
this.loggedInAddress = window.parent.reduxStore.getState().app.selectedAddress.address
}
createImage(imageUrl) {
const imageHTMLRes = new Image();
imageHTMLRes.src = imageUrl;
imageHTMLRes.style= "width:40px; height:40px; float: left; border-radius:50%";
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;
}, 750);
} else {
this.isImageLoaded = false
}
};
return imageHTMLRes;
}
render() {
let avatarImg = '';
let backupAvatarImg = ''
let isUnread = false
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)
}
if(this.lastReadMessageTimestamp && this.chatInfo.timestamp){
if(this.lastReadMessageTimestamp < this.chatInfo.timestamp){
isUnread = true
}
}
if(this.activeChatHeadUrl === this.chatInfo.url){
isUnread = false
}
if(this.chatInfo.sender === this.loggedInAddress){
isUnread = false
}
return html`
<li @click=${() => this.getUrl(this.chatInfo.url)} class="clearfix ${this.activeChatHeadUrl === this.chatInfo.url ? 'active' : ''}">
${this.isImageLoaded ? html`${avatarImg}` : html`` }
${!this.isImageLoaded && !this.chatInfo.name && !this.chatInfo.groupName ? html`<mwc-icon class="img-icon">account_circle</mwc-icon>` : html`` }
${!this.isImageLoaded && this.chatInfo.name ? html`<div style="width:40px; height:40px; flex-shrink: 0; border-radius:50%; background: ${this.activeChatHeadUrl === this.chatInfo.url ? 'var(--chatHeadBgActive)' : 'var(--chatHeadBg)' }; color: ${this.activeChatHeadUrl === this.chatInfo.url ? 'var(--chatHeadTextActive)' : 'var(--chatHeadText)' }; font-weight:bold; display: flex; justify-content: center; align-items: center; text-transform: capitalize">${this.chatInfo.name.charAt(0)}</div>`: ''}
${!this.isImageLoaded && this.chatInfo.groupName ? html`<div style="width:40px; height:40px; flex-shrink: 0; border-radius:50%; background: ${this.activeChatHeadUrl === this.chatInfo.url ? 'var(--chatHeadBgActive)' : 'var(--chatHeadBg)' }; color: ${this.activeChatHeadUrl === this.chatInfo.url ? 'var(--chatHeadTextActive)' : 'var(--chatHeadText)' }; font-weight:bold; display: flex; justify-content: center; align-items: center; text-transform: capitalize">${this.chatInfo.groupName.charAt(0)}</div>`: ''}
<div class="inner-container">
<div class="about">
<div class="name"><span style="font-weight: bold;float:left; padding-left: 8px; color: var(--chat-group);font-size:14px;word-break:${this.chatInfo.groupName ? this.chatInfo.groupName : this.chatInfo.name !== undefined ? 'break-word': 'break-all'}">${this.chatInfo.groupName ? this.chatInfo.groupName : this.chatInfo.name !== undefined ? this.chatInfo.name : this.chatInfo.address.substr(0, 15)} </span> <mwc-icon style="font-size:18px; color: var(--chat-group);">${this.chatInfo.groupId !== undefined ? 'lock_open' : 'lock'}</mwc-icon> </div>
</div>
<div class="about" style="margin-top:7px">
<div class="name"><span style="float:left; padding-left: 8px; color: var(--chat-group);font-size:14px"></span>
<div style="color: var(--black); display: flex;font-size: 12px; align-items:center">
<div style="width: 8px; height: 8px;border-radius: 50%;background: ${isUnread ? 'var(--error)' : 'none'} ; margin-right:5px;"></div>
<message-time style="display: ${(this.chatInfo.timestamp && this.chatInfo.timestamp > 100000) ? 'block' : 'none'}" timestamp=${this.chatInfo.timestamp}></message-time>
<span style="font-size:12px;color:var(--black);display: ${(!this.chatInfo.timestamp || this.chatInfo.timestamp > 100000) ? 'none' : 'block'}">${translate('chatpage.cchange90')}</span>
</div>
</div>
</div>
</div>
</li>
`
}
async firstUpdated() {
let configLoaded = false
this.lastReadMessageTimestamp = await chatLastSeen.getItem(this.chatInfo.url) || 0
parentEpml.ready().then(() => {
parentEpml.subscribe('selected_address', async selectedAddress => {
this.selectedAddress = {}
selectedAddress = JSON.parse(selectedAddress)
if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
this.selectedAddress = selectedAddress
})
parentEpml.subscribe('chat_last_seen', async chatList => {
const parsedChatList = JSON.parse(chatList)
const findChatSeen = parsedChatList.find(chat=> chat.key === this.chatInfo.url)
if(findChatSeen && this.lastReadMessageTimestamp !== findChatSeen.timestamp){
this.lastReadMessageTimestamp = findChatSeen.timestamp
this.requestUpdate()
}
})
parentEpml.subscribe('config', c => {
if (!configLoaded) {
configLoaded = true
}
this.config = JSON.parse(c)
})
})
parentEpml.imReady()
}
shouldUpdate(changedProperties) {
if(changedProperties.has('activeChatHeadUrl')){
return true
}
if(changedProperties.has('lastReadMessageTimestamp')){
return true
}
if(changedProperties.has('chatInfo')){
const prevChatInfo = changedProperties.get('chatInfo')
if(prevChatInfo.address !== this.chatInfo.address){
this.isImageLoaded = false
this.requestUpdate()
}
return true
}
return false
}
getUrl(chatUrl) {
this.setActiveChatHeadUrl(chatUrl)
}
onPageNavigation(pageUrl) {
parentEpml.request('setPageUrl', pageUrl)
}
}
window.customElements.define('chat-head', ChatHead)

View File

@@ -0,0 +1,268 @@
import { LitElement, html, css } from 'lit';
import { render } from 'lit/html.js';
import { get, translate } from 'lit-translate';
import { Epml } from '../../../epml';
import snackbar from './snackbar.js'
import '@material/mwc-button';
import '@material/mwc-dialog';
import '@polymer/paper-spinner/paper-spinner-lite.js'
import '@material/mwc-icon';
import './WrapperModal';
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class ChatLeaveGroup extends LitElement {
static get properties() {
return {
isLoading: { type: Boolean },
isOpenLeaveModal: {type: Boolean},
leaveGroupObj: { type: Object },
error: {type: Boolean},
message: {type: String},
chatHeads: {type: Array},
setActiveChatHeadUrl: {attribute: false},
selectedAddress: {attribute: Object}
}
}
constructor() {
super();
this.isLoading = false;
this.isOpenLeaveModal = false
this.leaveGroupObj = {}
this.fee = null
this.error = false
this.message = ''
this.chatHeads = []
}
static get styles() {
return css`
.top-bar-icon {
cursor: pointer;
height: 18px;
width: 18px;
transition: .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;
}
`
}
firstUpdated() {
}
async unitFee() {
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 url = `${nodeUrl}/transactions/unitfee?txType=LEAVE_GROUP`
let fee = null
try {
const res = await fetch(url)
const data = await res.json()
fee = (Number(data) / 1e8).toFixed(3)
} catch (error) {
fee = null
}
return fee
}
timeIsoString(timestamp) {
let myTimestamp = timestamp === undefined ? 1587560082346 : timestamp
let time = new Date(myTimestamp)
return time.toISOString()
}
resetDefaultSettings() {
this.error = false
this.message = ''
this.isLoading = false
}
renderErr9Text() {
return html`${translate("grouppage.gchange49")}`
}
async confirmRelationship() {
let interval = null
let stop = false
const getAnswer = async () => {
const currentChats = this.chatHeads
if (!stop) {
stop = true;
try {
const findGroup = currentChats.find((item)=> item.groupId === this.leaveGroupObj.groupId)
if (!findGroup) {
clearInterval(interval)
this.isLoading = false
this.isOpenLeaveModal= false
this.setActiveChatHeadUrl('')
}
} catch (error) {
}
stop = false
}
};
interval = setInterval(getAnswer, 5000);
}
async _leaveGroup(groupId, groupName) {
// Reset Default Settings...
this.resetDefaultSettings()
const leaveFeeInput = await this.unitFee()
if(!leaveFeeInput){
throw Error()
}
this.isLoading = true
// Get Last Ref
const getLastRef = async () => {
let myRef = await parentEpml.request('apiCall', {
type: 'api',
url: `/addresses/lastreference/${this.selectedAddress.address}`
})
return myRef
};
const validateReceiver = async () => {
let lastRef = await getLastRef();
let myTransaction = await makeTransactionRequest(lastRef)
getTxnRequestResponse(myTransaction)
}
// Make Transaction Request
const makeTransactionRequest = async (lastRef) => {
let groupdialog3 = get("transactions.groupdialog3")
let groupdialog4 = get("transactions.groupdialog4")
let myTxnrequest = await parentEpml.request('transaction', {
type: 32,
nonce: this.selectedAddress.nonce,
params: {
fee: leaveFeeInput,
registrantAddress: this.selectedAddress.address,
rGroupName: groupName,
rGroupId: groupId,
lastReference: lastRef,
groupdialog3: groupdialog3,
groupdialog4: groupdialog4,
}
})
return myTxnrequest
}
const getTxnRequestResponse = (txnResponse) => {
if (txnResponse.success === false && txnResponse.message) {
this.error = true
this.message = txnResponse.message
throw new Error(txnResponse)
} else if (txnResponse.success === true && !txnResponse.data.error) {
this.message = this.renderErr9Text()
this.error = false
this.confirmRelationship()
} else {
this.error = true
this.message = txnResponse.data.message
throw new Error(txnResponse)
}
}
validateReceiver()
}
render() {
return html`
<vaadin-icon @click=${()=> {
this.isOpenLeaveModal = true
}} class="top-bar-icon" style="margin: 0px 20px" icon="vaadin:exit" slot="icon"></vaadin-icon>
<!-- Leave Group Dialog -->
<wrapper-modal
.removeImage=${() => {
if(this.isLoading) return
this.isOpenLeaveModal = false
} }
style=${(this.isOpenLeaveModal) ? "display: block" : "display: none"}>
<div style="text-align:center">
<h1>${translate("grouppage.gchange35")}</h1>
<hr>
</div>
<div class="itemList">
<span class="title">${translate("grouppage.gchange4")}</span>
<br>
<div><span>${this.leaveGroupObj.groupName}</span></div>
<span class="title">${translate("grouppage.gchange5")}</span>
<br>
<div><span>${this.leaveGroupObj.description}</span></div>
<span class="title">${translate("grouppage.gchange10")}</span>
<br>
<div><span>${this.leaveGroupObj.owner}</span></div>
<span class="title">${translate("grouppage.gchange31")}</span>
<br>
<div><span><time-ago datetime=${this.timeIsoString(this.leaveGroupObj.created)}></time-ago></span></div>
${!this.leaveGroupObj.updated ? "" : html`<span class="title">${translate("grouppage.gchange32")}</span>
<br>
<div><span><time-ago datetime=${this.timeIsoString(this.leaveGroupObj.updated)}></time-ago></span></div>`}
</div>
<div style="text-align:right; height:36px;">
<span ?hidden="${!this.isLoading}">
<!-- loading message -->
${translate("grouppage.gchange36")} &nbsp;
<paper-spinner-lite
style="margin-top:12px;"
?active="${this.isLoading}"
alt="Leaving"
>
</paper-spinner-lite>
</span>
<span ?hidden=${this.message === ''} style="${this.error ? 'color:red;' : ''}">
${this.message}
</span>
</div>
<button
class="modal-button"
?disabled="${this.isLoading}"
@click=${() => this._leaveGroup(this.leaveGroupObj.groupId, this.leaveGroupObj.groupName)}
>
${translate("grouppage.gchange37")}
</button>
<button
@click=${() => {
this.isOpenLeaveModal= false
}}
class="modal-button"
?disabled="${this.isLoading}"
>
${translate("general.close")}
</button>
</wrapper-modal >
`;
}
}
customElements.define('chat-leave-group', ChatLeaveGroup);

View File

@@ -0,0 +1,131 @@
import { LitElement, html, css } from 'lit'
import { render } from 'lit/html.js'
import { Epml } from '../../../epml.js'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class ChatMessage extends LitElement {
static get properties() {
return {
selectedAddress: { type: Object },
config: { type: Object },
message: { type: Object, reflect: true }
}
}
static get styles() {
return css`
.message-data {
margin-bottom: 15px;
}
.message-data-time {
color: #a8aab1;
font-size: 13px;
padding-left: 6px;
}
.message {
color: black;
padding: 12px 10px;
line-height: 19px;
font-size: 16px;
border-radius: 7px;
margin-bottom: 20px;
width: 90%;
position: relative;
}
.message:after {
bottom: 100%;
left: 93%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-bottom-color: #ddd;
border-width: 10px;
margin-left: -10px;
}
.my-message {
background: #ddd;
border: 2px #ccc solid;
}
.other-message {
background: #f1f1f1;
border: 2px solid #dedede;
}
.other-message:after {
border-bottom-color: #f1f1f1;
left: 7%;
}
.align-left {
text-align: left;
}
.align-right {
text-align: right;
}
.float-right {
float: right;
}
.clearfix:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
`
}
constructor() {
super()
this.selectedAddress = {}
this.config = {
user: {
node: {
}
}
}
this.message = {}
}
render() {
return html`
<li class="clearfix">
<div class="message-data ${this.message.sender === this.selectedAddress.address ? "align-right" : ""}">
<span class="message-data-name">${this.message.sender}</span> &nbsp;
<span class="message-data-time">10:10 AM, Today</span>
</div>
<div class="message ${this.message.sender === this.selectedAddress.address ? "my-message float-right" : "other-message float-left"}">
${this.message.decodedMessage}
</div>
</li>
`
}
firstUpdated() {
let configLoaded = false
parentEpml.ready().then(() => {
parentEpml.subscribe('selected_address', async selectedAddress => {
this.selectedAddress = {}
selectedAddress = JSON.parse(selectedAddress)
if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
this.selectedAddress = selectedAddress
})
parentEpml.subscribe('config', c => {
if (!configLoaded) {
configLoaded = true
}
this.config = JSON.parse(c)
})
})
parentEpml.imReady()
}
}
window.customElements.define('chat-message', ChatMessage)

View File

@@ -0,0 +1,415 @@
import { LitElement, html, css } from 'lit';
import { render } from 'lit/html.js';
import { get, translate } from 'lit-translate';
import { Epml } from '../../../epml';
import snackbar from './snackbar.js'
import '@material/mwc-button';
import '@material/mwc-dialog';
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class ChatModals extends LitElement {
static get properties() {
return {
openDialogPrivateMessage: {type: Boolean},
openDialogBlockUser: {type: Boolean},
isLoading: { type: Boolean },
nametodialog: { type: String, attribute: true },
hidePrivateMessageModal: {type: Function},
hideBlockUserModal: {type: Function},
toblockaddress: { type: String, attribute: true },
chatBlockedAdresses: { type: Array },
}
}
constructor() {
super();
this.isLoading = false;
this.hidePrivateMessageModal = () => {};
this.hideBlockUserModal = () => {};
this.chatBlockedAdresses = []
}
static get styles() {
return css`
.input {
width: 90%;
border: none;
display: inline-block;
font-size: 16px;
padding: 10px 20px;
border-radius: 5px;
resize: none;
background: #eee;
}
.textarea {
width: 90%;
border: none;
display: inline-block;
font-size: 16px;
padding: 10px 20px;
border-radius: 5px;
height: 120px;
resize: none;
background: #eee;
}
.close-button {
display:block;
--mdc-theme-primary: red;
}
`
}
firstUpdated() {
const stopKeyEventPropagation = (e) => {
e.stopPropagation();
return false;
}
this.shadowRoot.getElementById('sendTo').addEventListener('keydown', stopKeyEventPropagation);
this.shadowRoot.getElementById('messageBox').addEventListener('keydown', stopKeyEventPropagation);
parentEpml.ready().then(() => {
parentEpml.subscribe('selected_address', async selectedAddress => {
this.selectedAddress = {}
selectedAddress = JSON.parse(selectedAddress)
if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
this.selectedAddress = selectedAddress
})
parentEpml.request('apiCall', {
url: `/addresses/balance/${window.parent.reduxStore.getState().app.selectedAddress.address}`
}).then(res => {
this.balance = res
})
})
parentEpml.imReady()
}
// Send Private Message
_sendMessage() {
this.isLoading = true;
const recipient = this.shadowRoot.getElementById('sendTo').value;
const messageBox = this.shadowRoot.getElementById('messageBox');
const messageText = messageBox.value;
if (recipient.length === 0) {
this.isLoading = false
} else if (messageText.length === 0) {
this.isLoading = false
} else {
this.sendMessage()
}
};
async sendMessage() {
this.isLoading = true;
const _recipient = this.shadowRoot.getElementById('sendTo').value;
const messageBox = this.shadowRoot.getElementById('messageBox');
const messageText = messageBox.value;
let recipient;
const validateName = async (receiverName) => {
let myRes;
let myNameRes = await parentEpml.request('apiCall', {
type: 'api',
url: `/names/${receiverName}`
});
if (myNameRes.error === 401) {
myRes = false
} else {
myRes = myNameRes
}
return myRes;
}
const myNameRes = await validateName(_recipient)
if (!myNameRes) {
recipient = _recipient
} else {
recipient = myNameRes.owner
}
let _reference = new Uint8Array(64);
window.crypto.getRandomValues(_reference);
let sendTimestamp = Date.now()
let reference = window.parent.Base58.encode(_reference)
const getAddressPublicKey = async () => {
let isEncrypted
let _publicKey
let addressPublicKey = await parentEpml.request('apiCall', {
type: 'api',
url: `/addresses/publickey/${recipient}`
})
if (addressPublicKey.error === 102) {
_publicKey = false
// Do something here...
let err1string = get('welcomepage.wcchange7')
parentEpml.request('showSnackBar', `${err1string}`)
this.isLoading = false
} else if (addressPublicKey !== false) {
isEncrypted = 1
_publicKey = addressPublicKey
sendMessageRequest(isEncrypted, _publicKey)
} else {
isEncrypted = 0
_publicKey = this.selectedAddress.address
sendMessageRequest(isEncrypted, _publicKey)
}
};
const sendMessageRequest = async (isEncrypted, _publicKey) => {
const messageObject = {
messageText,
images: [''],
repliedTo: '',
version: 1
}
const stringifyMessageObject = JSON.stringify(messageObject)
let chatResponse = await parentEpml.request('chat', {
type: 18,
nonce: this.selectedAddress.nonce,
params: {
timestamp: sendTimestamp,
recipient: recipient,
recipientPublicKey: _publicKey,
hasChatReference: 0,
message: stringifyMessageObject,
lastReference: reference,
proofOfWorkNonce: 0,
isEncrypted: isEncrypted,
isText: 1
}
})
_computePow(chatResponse)
}
const _computePow = async (chatBytes) => {
const _chatBytesArray = Object.keys(chatBytes).map(function (key) { return chatBytes[key]; });
const chatBytesArray = new Uint8Array(_chatBytesArray)
const chatBytesHash = new window.parent.Sha256().process(chatBytesArray).finish().result
const hashPtr = window.parent.sbrk(32, window.parent.heap);
const hashAry = new Uint8Array(window.parent.memory.buffer, hashPtr, 32);
hashAry.set(chatBytesHash);
const difficulty = this.balance < 4 ? 18 : 8;
const workBufferLength = 8 * 1024 * 1024;
const workBufferPtr = window.parent.sbrk(workBufferLength, window.parent.heap);
let nonce = window.parent.computePow(hashPtr, workBufferPtr, workBufferLength, difficulty)
let _response = await parentEpml.request('sign_chat', {
nonce: this.selectedAddress.nonce,
chatBytesArray: chatBytesArray,
chatNonce: nonce
})
getSendChatResponse(_response)
}
const getSendChatResponse = (response) => {
if (response === true) {
messageBox.value = ''
let err2string = get('welcomepage.wcchange8')
parentEpml.request('showSnackBar', `${err2string}`)
this.isLoading = false
this.shadowRoot.querySelector('#startPmDialog').close()
} else if (response.error) {
parentEpml.request('showSnackBar', response.message)
this.isLoading = false
this.shadowRoot.querySelector('#startPmDialog').close()
} else {
let err3string = get('welcomepage.wcchange9')
parentEpml.request('showSnackBar', `${err3string}`)
this.isLoading = false
this.shadowRoot.querySelector('#startPmDialog').close()
}
}
getAddressPublicKey()
}
_textArea(e) {
if (e.keyCode === 13 && !e.shiftKey) this._sendMessage()
}
getApiKey() {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
let apiKey = myNode.apiKey;
return apiKey;
}
getChatBlockedList() {
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 blockedAddressesUrl = `${nodeUrl}/lists/blockedAddresses?apiKey=${this.getApiKey()}`
const err3string = 'No regitered name'
localStorage.removeItem("ChatBlockedAddresses")
var obj = [];
fetch(blockedAddressesUrl).then(response => {
return response.json()
}).then(data => {
return data.map(item => {
const noName = {
name: err3string,
owner: item
}
fetch(`${nodeUrl}/names/address/${item}?limit=0&reverse=true`).then(res => {
return res.json()
}).then(jsonRes => {
if(jsonRes.length) {
jsonRes.map (item => {
obj.push(item)
})
} else {
obj.push(noName)
}
localStorage.setItem("ChatBlockedAddresses", JSON.stringify(obj))
})
})
})
}
relMessages() {
setTimeout(() => {
window.location.href = window.location.href.split( '#' )[0]
}, 500)
}
async getChatBlockedAdresses() {
const chatBlockedAdresses = await parentEpml.request('apiCall', {
url: `/lists/blockedAddresses?apiKey=${this.getApiKey()}`
})
this.chatBlockedAdresses = chatBlockedAdresses
}
// Chat Block Address
async chatBlockAddress() {
let address = this.toblockaddress
let items = [
address
]
let addressJsonString = JSON.stringify({ "items": items })
let ret = await parentEpml.request('apiCall', {
url: `/lists/blockedAddresses?apiKey=${this.getApiKey()}`,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: `${addressJsonString}`
})
if (ret === true) {
this.chatBlockedAdresses = this.chatBlockedAdresses.filter(item => item != address)
this.chatBlockedAdresses.push(address)
this.getChatBlockedList()
this.hideBlockUserModal()
let err1string = get("blockpage.bcchange2")
snackbar.add({
labelText: `${err1string}`,
dismiss: true
})
this.relMessages()
} else {
this.hideBlockUserModal()
let err2string = get("blockpage.bcchange2")
snackbar.add({
labelText: `${err2string}`,
dismiss: true
})
}
return ret
}
render() {
return html`
<mwc-dialog
id='sendPMDialog'
tabindex='0'
?open=${this.openDialogPrivateMessage}
scrimClickAction='${this.isLoading ? '' : 'close'}'
escapeKeyAction='close'
defaultAction='close'
@blur=${() => this.hidePrivateMessageModal()}
>
<div style='text-align:center'>
<h1>${translate('welcomepage.wcchange2')}</h1>
<hr>
</div>
<p>${translate('welcomepage.wcchange3')}</p>
<textarea class='input' ?disabled=${this.isLoading} id='sendTo' rows='1'>${this.nametodialog}</textarea>
<p style='margin-bottom:0;'>
<textarea class='textarea' @keydown=${(e) => this._textArea(e)} ?disabled=${this.isLoading} id='messageBox' placeholder='${translate('welcomepage.wcchange5')}' rows='1'></textarea>
</p>
<mwc-button ?disabled='${this.isLoading}' slot='primaryAction' @click=${() => {
this._sendMessage();
}
}>${translate('welcomepage.wcchange6')}
</mwc-button>
<mwc-button
?disabled='${this.isLoading}'
slot='secondaryAction'
@click='${() => this.hidePrivateMessageModal()}'
class='close-button'
>
${translate('general.close')}
</mwc-button>
</mwc-dialog>
<mwc-dialog
id='blockNameDialog'
tabindex='0'
?open=${this.openDialogBlockUser}
escapeKeyAction='close'
defaultAction='close'
@blur=${() => this.hideBlockUserModal()}
>
<div style='text-align:center'>
<h1>${translate('blockpage.bcchange5')}</h1>
<hr>
<h2>${translate('blockpage.bcchange6')}</h2><br>
<h2>${this.nametodialog}</h2>
</div>
<mwc-button
slot='secondaryAction'
@click='${() => this.chatBlockAddress()}'
class='block'
>
${translate('general.yes')}
</mwc-button>
<mwc-button
slot='primaryAction'
@click='${() => this.hideBlockUserModal()}'
class='close-button'
>
${translate('general.no')}
</mwc-button>
</mwc-dialog>
`;
}
}
customElements.define('chat-modals', ChatModals);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,306 @@
import { LitElement, html, css } from "lit";
import { render } from "lit/html.js";
import { get, translate } from "lit-translate";
import { Epml } from "../../../epml";
import { getUserNameFromAddress } from "../../utils/getUserNameFromAddress";
import snackbar from "./snackbar.js";
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/button';
import "./WrapperModal";
import "./TipUser"
import "./UserInfo/UserInfo";
class ChatRightPanel extends LitElement {
static get properties() {
return {
leaveGroupObj: { type: Object },
error: { type: Boolean },
chatHeads: { type: Array },
groupAdmin: { attribute: false },
groupMembers: { attribute: false },
selectedHead: { type: Object },
toggle: { attribute: false },
getMoreMembers:{ attribute: false },
setOpenPrivateMessage: { attribute: false },
userName: { type: String },
walletBalance: { type: Number },
sendMoneyLoading: { type: Boolean },
btnDisable: { type: Boolean },
errorMessage: { type: String },
successMessage: { type: String },
setOpenTipUser: { attribute: false },
setOpenUserInfo: { attribute: false },
setUserName: { attribute: false },
}
}
constructor() {
super()
this.leaveGroupObj = {}
this.leaveFee = 0.001
this.error = false
this.chatHeads = []
this.groupAdmin = []
this.groupMembers = []
this.observerHandler = this.observerHandler.bind(this)
this.viewElement = ''
this.downObserverElement = ''
this.myAddress = window.parent.reduxStore.getState().app.selectedAddress.address
this.sendMoneyLoading = false
this.btnDisable = false
this.errorMessage = ""
this.successMessage = ""
}
static get styles() {
return css`
.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;
overflow:auto;
margin-top: 5px;
padding: 0px 6px;
box-sizing: border-box;
}
.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);
}
`
}
firstUpdated() {
this.viewElement = this.shadowRoot.getElementById('viewElement');
this.downObserverElement = this.shadowRoot.getElementById('downObserver');
this.elementObserver();
}
async updated(changedProperties) {
if (changedProperties && changedProperties.has('selectedHead')) {
if (this.selectedHead !== {}) {
const userName = await getUserNameFromAddress(this.selectedHead.address);
this.userName = userName;
}
}
}
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.groupMembers.length < 20){
return
}
console.log('this.leaveGroupObjp', this.leaveGroupObj)
this.getMoreMembers(this.leaveGroupObj.groupId)
}
}
render() {
const owner = this.groupAdmin.filter((admin)=> admin.address === this.leaveGroupObj.owner)
return html`
<div class="container">
<div class="close-row" style="margin-top: 15px">
<vaadin-icon class="top-bar-icon" @click=${()=> this.toggle(false)} style="margin: 0px 10px" icon="vaadin:close" slot="icon"></vaadin-icon>
</div>
<div id="viewElement" class="container-body">
<p class="group-name">${this.leaveGroupObj && this.leaveGroupObj.groupName}</p>
<div class="group-info">
<p class="group-description">${this.leaveGroupObj && this.leaveGroupObj.description}</p>
<p class="group-subheader">Members: <span class="group-data">${this.leaveGroupObj && this.leaveGroupObj.memberCount}</span></p>
<p class="group-subheader">Date created : <span class="group-data">${new Date(this.leaveGroupObj.created).toLocaleDateString("en-US")}</span></p>
</div>
<br />
<p class="chat-right-panel-label">GROUP OWNER</p>
${owner.map((item) => {
return html`<chat-side-nav-heads
activeChatHeadUrl=""
.setActiveChatHeadUrl=${(val) => {
if (val.address === this.myAddress) return;
console.log({ val });
this.selectedHead = val;
this.setOpenUserInfo(true);
this.setUserName({
sender: val.address,
senderName: val.name ? val.name : ""
});
}}
chatInfo=${JSON.stringify(item)}
></chat-side-nav-heads>`
})}
<p class="chat-right-panel-label">ADMINS</p>
${this.groupAdmin.map((item) => {
return html`<chat-side-nav-heads
activeChatHeadUrl=""
.setActiveChatHeadUrl=${(val) => {
if (val.address === this.myAddress) return;
console.log({ val });
this.selectedHead = val;
this.setOpenUserInfo(true);
this.setUserName({
sender: val.address,
senderName: val.name ? val.name : ""
});
}}
chatInfo=${JSON.stringify(item)}
></chat-side-nav-heads>`
})}
<p class="chat-right-panel-label">MEMBERS</p>
${this.groupMembers.map((item) => {
return html`<chat-side-nav-heads
activeChatHeadUrl=""
.setActiveChatHeadUrl=${(val) => {
if (val.address === this.myAddress) return;
console.log({ val });
this.selectedHead = val;
this.setOpenUserInfo(true);
this.setUserName({
sender: val.address,
senderName: val.name ? val.name : ""
});
}}
chatInfo=${JSON.stringify(item)}
></chat-side-nav-heads>`
})}
<div id='downObserver'></div>
</div>
</div>
</div>
`
}
}
customElements.define("chat-right-panel", ChatRightPanel)

View File

@@ -0,0 +1,807 @@
import { css } from 'lit'
export const chatStyles = css`
html {
--scrollbarBG: #a1a1a1;
--thumbBG: #6a6c75;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
--mdc-theme-primary: rgb(3, 169, 244);
--mdc-theme-secondary: var(--mdc-theme-primary);
--mdc-dialog-max-width: 85vw;
--mdc-dialog-max-height: 95vh;
}
* :focus-visible {
outline: none;
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}
a {
color: var(--black);
text-decoration: none;
}
ul {
list-style: none;
margin: 0;
padding: 20px 17px;
}
.chat-list {
overflow-y: auto;
overflow-x: hidden;
height: 100%;
box-sizing: border-box;
}
.message-data {
width: 92%;
margin-bottom: 15px;
margin-left: 55px;
}
.message-data-name {
user-select: none;
color: #03a9f4;
margin-bottom: 5px;
}
.forwarded-text {
user-select: none;
color: #03a9f4;
margin-bottom: 5px;
}
.message-data-forward {
user-select: none;
color: var(--mainmenutext);
margin-bottom: 5px;
font-size: 12px;
}
.message-data-my-name {
color: #05be0e;
font-weight: bold;
}
.message-data-time {
color: #888888;
font-size: 13px;
user-select: none;
display: flex;
justify-content: space-between;
width: 100%;
padding-top: 2px;
}
.message-data-time-hidden {
visibility: hidden;
transition: all 0.1s ease-in-out;
color: #888888;
font-size: 13px;
user-select: none;
display: flex;
justify-content: space-between;
width: 100%;
padding-top: 2px;
}
.message-user-info {
display: flex;
justify-content: space-between;
width: 100%;
gap: 10px;
}
.chat-bubble-container {
display:flex;
gap: 7px;
}
.message-container {
position: relative;
}
.message-subcontainer1 {
position: relative;
display: flex;
align-items: flex-end;
}
.message-subcontainer2 {
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;
width: fit-content;
min-width: 150px;
}
.message-myBg {
background-color: var(--chat-bubble-myBg) !important;
}
.message-triangle {
position: relative;
}
.message-triangle:after {
content: "";
position: absolute;
bottom: 0px;
left: -9px;
width: 0;
height: 0;
border-style: solid;
border-width: 0px 0px 7px 9px;
border-color: transparent transparent var(--chat-bubble-bg) transparent;
}
.message-myTriangle {
position: relative;
}
.message-myTriangle:after {
content: "";
position: absolute;
bottom: 0px;
left: -9px;
width: 0;
height: 0;
border-style: solid;
border-width: 0px 0px 7px 9px;
border-color: transparent transparent var(--chat-bubble-myBg) transparent;
}
.message-reactions {
background-color: transparent;
width: calc(100% - 54px);
margin-left: 54px;
display: flex;
flex-flow: row wrap;
justify-content: left;
gap: 8px;
}
.original-message {
position: relative;
display: flex;
flex-direction: column;
color: var(--chat-bubble-msg-color);
line-height: 19px;
user-select: text;
font-size: 15px;
width: 90%;
border-radius: 5px;
padding: 8px 5px 8px 25px;
margin-bottom: 10px;
cursor: pointer;
}
.original-message:before {
content: "";
position: absolute;
top: 5px;
left: 10px;
height: 75%;
width: 2.6px;
background-color: var(--mdc-theme-primary);
}
.original-message-sender {
color: var(--mdc-theme-primary);
}
.replied-message {
margin: 0;
padding: 0;
}
.replied-message p {
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 500px;
max-height: 40px;
margin: 0;
padding: 0;
}
.message {
display: flex;
flex-direction: column;
color: var(--chat-bubble-msg-color);
line-height: 19px;
overflow-wrap: anywhere;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
font-size: 16px;
width: 100%;
position: relative;
}
.message-data-avatar {
margin: 0px 10px 0px 3px;
width: 42px;
height: 42px;
float: left;
}
.message-parent {
padding: 3px;
background: rgba(245, 245, 245, 0);
transition: all 0.1s ease-in-out;
}
.message-parent:hover {
background: var(--chat-bubble);
border-radius: 8px;
}
.message-parent:hover .chat-hover {
display: block;
}
.message-parent:hover .message-data-time-hidden {
visibility: visible;
}
.chat-hover {
display: none;
position: absolute;
top: -25px;
right: 5px;
}
.emoji {
width: 1.7em;
height: 1.5em;
margin-bottom: -2px;
vertical-align: bottom;
object-fit: contain;
}
.align-left {
text-align: left;
}
.align-right {
text-align: right;
}
.float-left {
float: left;
}
.float-right {
float: right;
}
.clearfix:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
img {
border-radius: 25%;
}
.container {
display: flex;
flex-direction: row;
align-items: center;
background-color: var(--chat-menu-bg);
border: 1px solid var(--chat-menu-outline);
border-radius: 5px;
height:100%;
position: relative;
}
.container:focus-visible {
outline: none;
}
.menu-icon {
width: 100%;
padding: 5px 7px;
display: flex;
align-items: center;
font-size: 13px;
color: var(--chat-menu-icon);
}
.menu-icon:hover {
border-radius: 5px;
background-color: var(--chat-menu-icon-hover);
transition: all 0.1s ease-in-out;
cursor: pointer;
}
.tooltip {
position: relative;
}
.tooltip:before {
content: attr(data-text);
display: none;
position: absolute;
top: -47px;
left: 50%;
transform: translateX(-50%);
width: auto;
padding: 10px;
border-radius: 10px;
background:#fff;
color: #000;
text-align: center;
box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px;
font-size: 12px;
z-index: 5;
white-space: nowrap;
overflow: hidden;
}
.tooltip:hover:before {
display: block;
}
.tooltip:after {
content: "";
position: absolute;
margin-top: -7px;
top: -7px;
border: 10px solid #fff;
border-color: white transparent transparent transparent;
z-index: 5;
display: none;
}
.tooltip:hover:before, .tooltip:hover:after {
display: block;
}
.block-user-container {
display: block;
position: absolute;
left: -5px;
}
.block-user {
width: 100%;
padding: 5px 7px;
display: flex;
align-items: center;
font-size: 13px;
color: var(--chat-menu-icon);
justify-content: space-evenly;
border: 1px solid rgb(218, 217, 217);
border-radius: 5px;
background-color: var(--chat-menu-bg);
width: 150px;
height: 32px;
padding: 3px 8px;
box-shadow: rgba(77, 77, 82, 0.2) 0px 7px 29px 0px;
}
.block-user:hover {
cursor:pointer;
background-color: var(--block-user-bg-hover);
transition: all 0.1s ease-in-out 0s;
}
.reactions-bg {
position: relative;
background-color: #d5d5d5;
border-radius: 10px;
padding: 5px;
color: black;
transition: all 0.1s ease-in-out;
border: 0.5px solid transparent;
cursor: pointer;
}
.reactions-bg:hover {
border: 0.5px solid var(--reaction-bubble-outline);
}
.image-container {
display: flex;
}
.message-data-level {
height: 21px;
width: auto;
overflow: hidden;
}
.defaultSize {
width: 45vh;
height: 40vh;
}
.hideImg {
visibility: hidden;
}
.image-deleted-msg {
font-family: Roboto, sans-serif;
font-size: 14px;
font-style: italic;
color: var(--chat-bubble-msg-color);
margin: 0;
padding-top: 10px;
}
.image-delete-icon {
margin-left: 5px;
height: 20px;
cursor: pointer;
visibility: hidden;
transition: .2s all;
opacity: 0.8;
color: rgb(228, 222, 222);
padding-left: 7px;
}
.image-delete-icon:hover {
opacity: 1;
}
.message-parent:hover .image-delete-icon {
visibility: visible;
}
.imageContainer {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.spinnerContainer {
display: flex;
width: 100%;
justify-content: center
}
.delete-image-msg {
font-family: Livvic, sans-serif;
font-size: 20px;
color: var(--chat-bubble-msg-color);
letter-spacing: 0.3px;
font-weight: 300;
text-align: center;
}
.modal-button-row {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.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;
}
#messageContent p {
margin: 0px;
padding: 0px;
}
#messageContent p mark {
background-color: #ffe066;
border-radius: 0.25em;
box-decoration-break: clone;
padding: 0.125em 0;
}
#messageContent > * + * {
outline: none;
}
#messageContent ul,
ol {
padding: 0 1rem;
}
#messageContent h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
#messageContent code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
#messageContent pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
white-space: pre-wrap;
}
#messageContent pre code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
#messageContent img {
width: 1.7em;
height: 1.5em;
margin: 0px;
}
#messageContent blockquote {
padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
}
#messageContent hr {
border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1);
margin: 2rem 0;
}
.replied-message p {
margin: 0px;
padding: 0px;
}
.replied-message > * + * {
margin-top: 0.75em;
outline: none;
}
.replied-message ul,
ol {
padding: 0 1rem;
}
.replied-message h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
.replied-message code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
.replied-message pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
white-space: pre-wrap;
margin: 0px;
}
.replied-message pre code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
.replied-message img {
width: 1.7em;
height: 1.5em;
margin: 0px;
}
.replied-message blockquote {
padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
}
.replied-message hr {
border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1);
margin: 2rem 0;
}
.attachment-container {
display: flex;
align-items: center;
justify-content: space-evenly;
padding: 5px 0 10px 0;
gap: 20px;
cursor: pointer;
}
.attachment-icon-container {
display: flex;
align-items: center;
justify-content: center;
height: 50px;
width: 50px;
border-radius: 50%;
border: none;
background-color: var(--mdc-theme-primary);
}
.attachment-icon {
width: 70%;
}
.attachment-info {
display: flex;
flex-direction: column;
gap: 5px;
}
.attachment-name {
font-family: Work Sans, sans-serif;
font-size: 16px;
color: var(--chat-bubble-msg-color);
margin: 0;
letter-spacing: 0.4px;
padding: 5px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.attachment-size {
font-family: Roboto, sans-serif;
font-size: 16px;
color: var(--chat-bubble-msg-color);
margin: 0;
letter-spacing: 0.3px;
font-weight: 300;
}
.download-icon {
position: relative;
color: var(--chat-bubble-msg-color);
width: 19px;
background-color: transparent;
}
.download-icon:hover::before {
background-color: rgb(161 158 158 / 41%);
}
.download-icon::before {
content: "";
position: absolute;
border-radius: 50%;
padding: 18px;
background-color: transparent;
transition: all 0.3s ease-in-out;
}
.edited-message-style {
font-family: "Work Sans", sans-serif;
font-style: italic;
font-size: 13px;
visibility: visible;
}
.blink-bg{
border-radius: 8px;
animation: blinkingBackground 3s;
}
@keyframes blinkingBackground{
0% { background-color: rgba(var(--menuactivergb), 1)}
100% { background-color:rgba(var(--menuactivergb), 0)}
}
.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;
}
@-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);
}
}
`

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,66 @@
import { LitElement, html } from 'lit';
import { render } from 'lit/html.js';
import { chatSearchResultsStyles } from './ChatSearchResults-css.js'
import { translate } from 'lit-translate';
export class ChatSearchResults extends LitElement {
static get properties() {
return {
onClickFunc: { attribute: false },
closeFunc: { attribute: false },
searchResults: { type: Array },
isOpen: { type: Boolean },
loading: { type: Boolean }
}
}
static styles = [chatSearchResultsStyles]
render() {
return html`
<div class="chat-results-card" style=${this.isOpen ? "display: block;" : "display: none;"}>
<vaadin-icon
@click=${() => this.closeFunc()}
icon="vaadin:close-small"
slot="icon"
class="close-icon"
>
</vaadin-icon>
${this.loading ? (
html`
<div class="spinner-container">
<paper-spinner-lite active></paper-spinner-lite>
</div>
`
) : (
html`
<p class="chat-result-header">${translate("chatpage.cchange36")}</p>
<div class="divider"></div>
<div class="chat-result-container">
${this.searchResults.length === 0 ? (
html`<p class="no-results">${translate("chatpage.cchange37")}</p>`
) : (
html`
${this.searchResults.map((result) => {
return (
html`
<div class="chat-result-card" @click=${() => {
this.shadowRoot.querySelector(".chat-result-card").classList.add("active");
this.onClickFunc(result);
}}>
<p class="chat-result">
${result.name}
</p>
</div>
`
)}
)}
`
)}
</div>
`
)}
</div>
`;
}
}
customElements.define('chat-search-results', ChatSearchResults);

View File

@@ -0,0 +1,120 @@
import { css } from 'lit'
export const chatSearchResultsStyles = css`
.chat-results-card {
position: relative;
padding: 25px 20px;
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
width: 300px;
min-height: 200px;
height: auto;
border-radius: 5px;
background-color: var(--white);
}
.chat-result-header {
color: var(--chat-bubble-msg-color);
font-size: 18px;
font-family: Montserrat, sans-serif;
text-align: center;
margin: 0 0 10px 0;
user-select: none;
}
.divider {
height: 1px;
background: var(--chat-bubble-msg-color);
margin: 0 40px;
user-select: none;
}
.no-results {
font-family: Roboto, sans-serif;
font-weight: 300;
letter-spacing: 0.3px;
font-size: 16px;
color: var(--chat-bubble-msg-color);
text-align: center;
margin: 20px 0 0 0;
user-select: none;
}
.chat-result-container {
height: 200px;
overflow-y: auto;
padding: 0 10px;
}
.chat-result-container::-webkit-scrollbar-track {
background-color: whitesmoke;
border-radius: 7px;
}
.chat-result-container::-webkit-scrollbar {
width: 6px;
border-radius: 7px;
background-color: whitesmoke;
}
.chat-result-container::-webkit-scrollbar-thumb {
background-color: rgb(180, 176, 176);
border-radius: 7px;
transition: all 0.3s ease-in-out;
}
.chat-result-container::-webkit-scrollbar-thumb:hover {
background-color: rgb(148, 146, 146);
cursor: pointer;
}
.chat-result-card {
padding: 12px;
margin-bottom: 15px;
margin-top: 15px;
transition: all 0.2s ease-in-out;
box-shadow: none;
}
.chat-result-card:active {
background-color: #09b814;
}
.chat-result-card:hover {
cursor: pointer;
border: none;
border-radius: 4px;
box-sizing: border-box;
-webkit-box-shadow: rgba(132, 132, 132, 40%) 0px 0px 6px -1px;
box-shadow: rgba(132, 132, 132, 40%) 0px 0px 6px -1px;
}
.chat-result {
font-family: Roboto, sans-serif;
font-weight: 300;
letter-spacing: 0.3px;
font-size: 15px;
color: var(--chat-bubble-msg-color);
margin: 0;
user-select: none;
}
.spinner-container {
display: flex;
width: 100%;
justify-content: center
}
.close-icon {
position: absolute;
top: 5px;
right: 5px;
color: var(--chat-bubble-msg-color);
font-size: 14px;
transition: all 0.1s ease-in-out;
}
.close-icon:hover {
cursor: pointer;
font-size: 15px;
}
`

View File

@@ -0,0 +1,229 @@
import { LitElement, html, css } from 'lit'
import { Epml } from '../../../epml.js'
import '@material/mwc-icon'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class ChatSelect 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}
}
}
static get styles() {
return css`
ul {
list-style-type: none;
}
li {
padding: 10px 2px 20px 5px;
cursor: pointer;
width: 100%;
display: flex;
box-sizing: border-box;
}
li:hover {
background-color: var(--menuhover);
}
.active {
background: var(--menuactive);
border-left: 4px solid #3498db;
}
.img-icon {
font-size:40px;
color: var(--chat-group);
}
.about {
margin-top: 8px;
}
.about {
padding-left: 8px;
}
.status {
color: #92959e;
}
.name {
user-select: none;
}
.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:40px; height:40px; float: left; border-radius:50%";
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 = '';
let backupAvatarImg = ''
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`
<li
@click=${() => this.getUrl(this.chatInfo.url)}
class="clearfix ${this.activeChatHeadUrl === this.chatInfo.url ? 'active' : ''}">
${this.isImageLoaded ? html`${avatarImg}` : html``}
${!this.isImageLoaded && !this.chatInfo.name && !this.chatInfo.groupName ? html`<mwc-icon class="img-icon">account_circle</mwc-icon>` :
html``
}
${!this.isImageLoaded && this.chatInfo.name ?
html`
<div
style="width:40px; height:40px; float: left; border-radius:50%; background: ${this.activeChatHeadUrl === this.chatInfo.url ?
'var(--chatHeadBgActive)' :
'var(--chatHeadBg)' };
color: ${this.activeChatHeadUrl === this.chatInfo.url ?
'var(--chatHeadTextActive)' :
'var(--chatHeadText)'};
font-weight:bold;
display: flex;
justify-content: center;
align-items: center;
text-transform: capitalize">
${this.chatInfo.name.charAt(0)}
</div>`:
''}
${!this.isImageLoaded && this.chatInfo.groupName ?
html`
<div
style="width:40px;
height:40px;
float: left;
border-radius:50%;
background: ${this.activeChatHeadUrl === this.chatInfo.url ?
'var(--chatHeadBgActive)' :
'var(--chatHeadBg)' };
color: ${this.activeChatHeadUrl === this.chatInfo.url ?
'var(--chatHeadTextActive)' :
'var(--chatHeadText)' };
font-weight:bold;
display: flex;
justify-content: center;
align-items: center;
text-transform: capitalize">
${this.chatInfo.groupName.charAt(0)}
</div>`:
''}
<div class="about">
<div class="name">
<span style="float:left; padding-left: 8px; color: var(--chat-group);">
${this.chatInfo.groupName ?
this.chatInfo.groupName :
this.chatInfo.name !== undefined ? this.chatInfo.name :
this.chatInfo.address.substr(0, 15)}
</span>
</div>
</div>
</li>
`
}
firstUpdated() {
let configLoaded = false
parentEpml.ready().then(() => {
parentEpml.subscribe('selected_address', async selectedAddress => {
this.selectedAddress = {}
selectedAddress = JSON.parse(selectedAddress)
if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
this.selectedAddress = selectedAddress
})
parentEpml.subscribe('config', c => {
if (!configLoaded) {
configLoaded = true
}
this.config = JSON.parse(c)
})
})
parentEpml.imReady()
}
shouldUpdate(changedProperties) {
if(changedProperties.has('activeChatHeadUrl')){
return true
}
if(changedProperties.has('chatInfo')){
return true
}
return false
}
getUrl(chatUrl) {
this.setActiveChatHeadUrl(chatUrl)
}
onPageNavigation(pageUrl) {
parentEpml.request('setPageUrl', pageUrl)
}
}
window.customElements.define('chat-select', ChatSelect)

View File

@@ -0,0 +1,205 @@
import { LitElement, html, css } from 'lit'
import { Epml } from '../../../epml.js'
import '@material/mwc-icon'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
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}
}
}
static get styles() {
return css`
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`
<li @click=${() => this.getUrl(this.chatInfo)} class="clearfix">
${this.isImageLoaded ? html`${avatarImg}` : html``}
${!this.isImageLoaded && !this.chatInfo.name && !this.chatInfo.groupName
? html`<mwc-icon class="img-icon">account_circle</mwc-icon>`
: html``}
${!this.isImageLoaded && this.chatInfo.name
? html`<div
style="width:30px; height:30px; float: left; border-radius:50%; background: ${this.activeChatHeadUrl === this.chatInfo.url
? "var(--chatHeadBgActive)"
: "var(--chatHeadBg)"}; color: ${this.activeChatHeadUrl ===
this.chatInfo.url
? "var(--chatHeadTextActive)"
: "var(--chatHeadText)"}; font-weight:bold; display: flex; justify-content: center; align-items: center; text-transform: capitalize"
>
${this.chatInfo.name.charAt(0)}
</div>`
: ""}
${!this.isImageLoaded && this.chatInfo.groupName
? html`<div
style="width:30px; height:30px; float: left; border-radius:50%; background: ${this.activeChatHeadUrl === this.chatInfo.url
? "var(--chatHeadBgActive)"
: "var(--chatHeadBg)"}; color: ${this.activeChatHeadUrl === this.chatInfo.url
? "var(--chatHeadTextActive)"
: "var(--chatHeadText)"}; font-weight:bold; display: flex; justify-content: center; align-items: center; text-transform: capitalize"
>
${this.chatInfo.groupName.charAt(0)}
</div>`
: ""}
<div>
<div class="name">
<span style="float:left; padding-left: 8px; color: var(--chat-group);">
${this.chatInfo.groupName
? this.chatInfo.groupName
: this.chatInfo.name !== undefined
? this.chatInfo.name
: this.chatInfo.address.substr(0, 15)}
</span>
</div>
</div>
</li>
`
}
firstUpdated() {
let configLoaded = false
parentEpml.ready().then(() => {
parentEpml.subscribe('selected_address', async selectedAddress => {
this.selectedAddress = {}
selectedAddress = JSON.parse(selectedAddress)
if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
this.selectedAddress = selectedAddress
})
parentEpml.subscribe('config', c => {
if (!configLoaded) {
configLoaded = true
}
this.config = JSON.parse(c)
})
})
parentEpml.imReady();
}
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)
}
onPageNavigation(pageUrl) {
parentEpml.request('setPageUrl', pageUrl)
}
}
window.customElements.define('chat-side-nav-heads', ChatSideNavHeads)

View File

@@ -0,0 +1,828 @@
import { LitElement, html, css } from "lit";
import { get } from 'lit-translate';
import { escape, unescape } from 'html-escaper';
import { EmojiPicker } from 'emoji-picker-js';
import { inputKeyCodes } from '../../utils/keyCodes.js';
import { Epml } from '../../../epml.js';
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent });
class ChatTextEditor extends LitElement {
static get properties() {
return {
isLoading: { type: Boolean },
isLoadingMessages: { type: Boolean },
_sendMessage: { attribute: false },
placeholder: { type: String },
imageFile: { type: Object },
insertImage: { attribute: false },
iframeHeight: { type: Number },
editedMessageObj: { type: Object },
chatEditor: { type: Object },
setChatEditor: { attribute: false },
iframeId: { type: String },
hasGlobalEvents: { type: Boolean },
chatMessageSize: { type: Number },
isEditMessageOpen: { type: Boolean },
theme: {
type: String,
reflect: true
}
}
}
static get styles() {
return css`
:host {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: auto;
overflow-y: hidden;
width: 100%;
}
.chatbar-container {
width: 100%;
display: flex;
height: auto;
overflow: hidden;
}
.chatbar-caption {
border-bottom: 2px solid var(--mdc-theme-primary);
}
.emoji-button {
width: 45px;
height: 40px;
padding-top: 4px;
border: none;
outline: none;
background: transparent;
cursor: pointer;
max-height: 40px;
color: var(--black);
}
.message-size-container {
display: flex;
justify-content: flex-end;
width: 100%;
}
.message-size {
font-family: Roboto, sans-serif;
font-size: 12px;
color: black;
}
.paperclip-icon {
color: var(--paperclip-icon);
width: 25px;
}
.paperclip-icon:hover {
cursor: pointer;
}
.send-icon {
width: 30px;
margin-left: 5px;
transition: all 0.1s ease-in-out;
cursor: pointer;
}
.send-icon:hover {
filter: brightness(1.1);
}
.file-picker-container {
position: relative;
height: 25px;
width: 25px;
}
.file-picker-input-container {
position: absolute;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
z-index: 10;
opacity: 0;
overflow: hidden;
}
input[type=file]::-webkit-file-upload-button {
cursor: pointer;
}
.chatbar-container textarea {
display: none;
}
.chatbar-container .chat-editor {
display: flex;
max-height: -webkit-fill-available;
width: 100%;
border-color: transparent;
margin: 0;
padding: 0;
border: none;
}
.checkmark-icon {
width: 30px;
color: var(--mdc-theme-primary);
margin-bottom: 6px;
}
.checkmark-icon:hover {
cursor: pointer;
}
`
}
constructor() {
super()
this.isLoadingMessages = true
this.isLoading = false
this.getMessageSize = this.getMessageSize.bind(this)
this.calculateIFrameHeight = this.calculateIFrameHeight.bind(this)
this.resetIFrameHeight = this.resetIFrameHeight.bind(this)
this.addGlobalEventListener = this.addGlobalEventListener.bind(this)
this.sendMessageFunc = this.sendMessageFunc.bind(this)
this.removeGlobalEventListener = this.removeGlobalEventListener.bind(this)
this.initialChat = this.initialChat.bind(this)
this.iframeHeight = 42
this.chatMessageSize = 0
this.userName = window.parent.reduxStore.getState().app.accountInfo.names[0]
this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light'
}
render() {
let scrollHeightBool = false;
try {
if (this.chatMessageInput && this.chatMessageInput.contentDocument.body.scrollHeight > 60 && this.shadowRoot.querySelector(".chat-editor").contentDocument.body.querySelector("#chatbarId").innerHTML.trim() !== "") {
scrollHeightBool = true;
}
} catch (error) {
scrollHeightBool = false;
}
return html`
<div
class=${["chatbar-container", (this.iframeId === "newChat" || this.iframeId === "privateMessage") ? "chatbar-caption" : ""].join(" ")}
style="${scrollHeightBool ? 'align-items: flex-end' : "align-items: center"}">
<div
style=${this.iframeId === "privateMessage" ? "display: none" : "display: block"}
class="file-picker-container"
@click=${(e) => {
this.preventUserSendingImage(e)
}}>
<vaadin-icon
class="paperclip-icon"
icon="vaadin:paperclip"
slot="icon"
>
</vaadin-icon>
<div class="file-picker-input-container">
<input
@change="${e => {
this.insertImage(e.target.files[0]);
const filePickerInput = this.shadowRoot.getElementById('file-picker')
if(filePickerInput){
filePickerInput.value = ""
}
}
}"
id="file-picker"
class="file-picker-input" type="file" name="myImage" accept="image/*" />
</div>
</div>
<textarea style="color: var(--black);" tabindex='1' ?autofocus=${true} ?disabled=${this.isLoading || this.isLoadingMessages} id="messageBox" rows="1"></textarea>
<iframe style=${(this.iframeId === "newChat" && this.iframeHeight > 42) && "height: 100%;"} id=${this.iframeId} class="chat-editor" tabindex="-1" height=${this.iframeHeight}></iframe>
<button class="emoji-button" ?disabled=${this.isLoading || this.isLoadingMessages}>
${html`<img class="emoji" draggable="false" alt="😀" src="/emoji/svg/1f600.svg" />`}
</button>
${this.editedMessageObj ? (
html`
<div>
${this.isLoading === false ? html`
<vaadin-icon
class="checkmark-icon"
icon="vaadin:check"
slot="icon"
@click=${() => {
this.sendMessageFunc();
}}
>
</vaadin-icon>
` :
html`
<paper-spinner-lite active></paper-spinner-lite>
`}
</div>
`
) :
html`
<div
style="${scrollHeightBool
? 'margin-bottom: 5px;'
: "margin-bottom: 0;"}
${this.iframeId === 'newChat'
? 'display: none;'
: 'display: flex;'}">
${this.isLoading === false ? html`
<img
src="/img/qchat-send-message-icon.svg"
alt="send-icon"
class="send-icon"
@click=${() => {
this.sendMessageFunc();
}}
/>
` :
html`
<paper-spinner-lite active></paper-spinner-lite>
`}
</div>
`
}
</div>
${this.chatMessageSize >= 750 ?
html`
<div class="message-size-container" style=${this.imageFile && "margin-top: 10px;"}>
<div class="message-size" style="${this.chatMessageSize > 1000 && 'color: #bd1515'}">
${`Your message size is of ${this.chatMessageSize} bytes out of a maximum of 1000`}
</div>
</div>
` :
html``}
</div>
`
}
preventUserSendingImage(e) {
if (!this.userName) {
e.preventDefault();
parentEpml.request('showSnackBar', get("chatpage.cchange27"));
};
}
initialChat(e) {
if (!this.chatEditor?.contentDiv.matches(':focus')) {
// WARNING: Deprecated methods from KeyBoard Event
if (e.code === "Space" || e.keyCode === 32 || e.which === 32) {
this.chatEditor.insertText('&nbsp;');
} else if (inputKeyCodes.includes(e.keyCode)) {
this.chatEditor.insertText(e.key);
return this.chatEditor.focus();
} else {
return this.chatEditor.focus();
}
}
}
addGlobalEventListener(){
document.addEventListener('keydown', this.initialChat);
}
removeGlobalEventListener(){
document.removeEventListener('keydown', this.initialChat);
}
async firstUpdated() {
if (this.hasGlobalEvents) {
this.addGlobalEventListener();
}
window.addEventListener('storage', () => {
const checkTheme = localStorage.getItem('qortalTheme');
const chatbar = this.shadowRoot.getElementById(this.iframeId).contentWindow.document.getElementById('chatbarId');
if (checkTheme === 'dark') {
this.theme = 'dark';
chatbar.style.cssText = "color:#ffffff;"
} else {
this.theme = 'light';
chatbar.style.cssText = "color:#080808;"
}
})
this.emojiPickerHandler = this.shadowRoot.querySelector('.emoji-button');
this.mirrorChatInput = this.shadowRoot.getElementById('messageBox');
this.chatMessageInput = this.shadowRoot.getElementById(this.iframeId);
this.emojiPicker = new EmojiPicker({
style: "twemoji",
twemojiBaseUrl: '/emoji/',
showPreview: false,
showVariants: false,
showAnimation: false,
position: 'top-start',
boxShadow: 'rgba(4, 4, 5, 0.15) 0px 0px 0px 1px, rgba(0, 0, 0, 0.24) 0px 8px 16px 0px',
zIndex: 100
});
this.emojiPicker.on('emoji', selection => {
const emojiHtmlString = `<img class="emoji" draggable="false" alt="${selection.emoji}" src="${selection.url}">`;
this.chatEditor.insertEmoji(emojiHtmlString);
});
this.emojiPickerHandler.addEventListener('click', () => this.emojiPicker.togglePicker(this.emojiPickerHandler));
await this.updateComplete;
this.initChatEditor();
}
async updated(changedProperties) {
if (changedProperties && changedProperties.has('editedMessageObj')) {
if (this.editedMessageObj) {
this.chatEditor.insertText(this.editedMessageObj.message);
this.getMessageSize(this.editedMessageObj.message);
} else {
this.chatEditor.insertText("");
this.chatMessageSize = 0;
}
}
if (changedProperties && changedProperties.has('placeholder')) {
const captionEditor = this.shadowRoot.getElementById(this.iframeId).contentWindow.document.getElementById('chatbarId');
captionEditor.setAttribute('data-placeholder', this.placeholder);
}
if (changedProperties && changedProperties.has("imageFile")) {
this.chatMessageInput = "newChat";
}
}
shouldUpdate(changedProperties) {
// Only update element if prop1 changed.
if(changedProperties.has('setChatEditor') && changedProperties.size === 1) return false
return true
}
sendMessageFunc(props) {
if (this.chatMessageSize > 1000 ) {
parentEpml.request('showSnackBar', get("chatpage.cchange29"));
return;
};
this.chatMessageSize = 0;
this.chatEditor.updateMirror();
this._sendMessage(props);
}
getMessageSize(message){
try {
const messageText = message;
// Format and Sanitize Message
const sanitizedMessage = messageText.replace(/&nbsp;/gi, ' ').replace(/<br\s*[\/]?>/gi, '\n');
const trimmedMessage = sanitizedMessage.trim();
let messageObject = {};
if (this.repliedToMessageObj) {
let chatReference = this.repliedToMessageObj.reference;
if (this.repliedToMessageObj.chatReference) {
chatReference = this.repliedToMessageObj.chatReference;
}
messageObject = {
messageText: trimmedMessage,
images: [''],
repliedTo: chatReference,
version: 1
}
} else if (this.editedMessageObj) {
let message = "";
try {
const parsedMessageObj = JSON.parse(this.editedMessageObj.decodedMessage);
message = parsedMessageObj;
} catch (error) {
message = this.messageObj.decodedMessage
}
messageObject = {
...message,
messageText: trimmedMessage,
}
} else if(this.imageFile && this.iframeId === 'newChat') {
messageObject = {
messageText: trimmedMessage,
images: [{
service: "QCHAT_IMAGE",
name: '123456789123456789123456789',
identifier: '123456'
}],
repliedTo: '',
version: 1
};
} else {
messageObject = {
messageText: trimmedMessage,
images: [''],
repliedTo: '',
version: 1
};
}
const stringified = JSON.stringify(messageObject);
const size = new Blob([stringified]).size;
this.chatMessageSize = size;
} catch (error) {
console.error(error)
}
}
calculateIFrameHeight(height) {
setTimeout(()=> {
const editorTest = this.shadowRoot.getElementById(this.iframeId).contentWindow.document.getElementById('chatbarId').scrollHeight;
this.iframeHeight = editorTest + 20;
}, 50)
}
resetIFrameHeight(height) {
this.iframeHeight = 42;
}
initChatEditor() {
const ChatEditor = function (editorConfig) {
const ChatEditor = function () {
const editor = this;
editor.init();
};
ChatEditor.prototype.getValue = function () {
const editor = this;
if (editor.contentDiv) {
return editor.contentDiv.innerHTML;
}
};
ChatEditor.prototype.setValue = function (value) {
const editor = this;
if (value) {
editor.contentDiv.innerHTML = value;
editor.updateMirror();
}
editor.focus();
};
ChatEditor.prototype.resetValue = function () {
const editor = this;
editor.contentDiv.innerHTML = '';
editor.updateMirror();
editor.focus();
editorConfig.resetIFrameHeight()
};
ChatEditor.prototype.styles = function () {
const editor = this;
editor.styles = document.createElement('style');
editor.styles.setAttribute('type', 'text/css');
editor.styles.innerText = `
html {
cursor: text;
}
.chatbar-body {
display: flex;
align-items: center;
}
.chatbar-body::-webkit-scrollbar-track {
background-color: whitesmoke;
border-radius: 7px;
}
.chatbar-body::-webkit-scrollbar {
width: 6px;
border-radius: 7px;
background-color: whitesmoke;
}
.chatbar-body::-webkit-scrollbar-thumb {
background-color: rgb(180, 176, 176);
border-radius: 7px;
transition: all 0.3s ease-in-out;
}
.chatbar-body::-webkit-scrollbar-thumb:hover {
background-color: rgb(148, 146, 146);
cursor: pointer;
}
div {
font-size: 1rem;
line-height: 1.38rem;
font-weight: 400;
font-family: "Open Sans", helvetica, sans-serif;
padding-right: 3px;
text-align: left;
white-space: break-spaces;
word-break: break-word;
outline: none;
min-height: 20px;
width: 100%;
}
div[contentEditable=true]:empty:before {
content: attr(data-placeholder);
display: block;
text-overflow: ellipsis;
overflow: hidden;
user-select: none;
white-space: nowrap;
opacity: 0.7;
}
div[contentEditable=false]{
background: rgba(0,0,0,0.1);
width: 100%;
}
img.emoji {
width: 1.7em;
height: 1.5em;
margin-bottom: -2px;
vertical-align: bottom;
}
`;
editor.content.head.appendChild(editor.styles);
};
ChatEditor.prototype.enable = function () {
const editor = this;
editor.contentDiv.setAttribute('contenteditable', 'true');
editor.focus();
};
ChatEditor.prototype.getMirrorElement = function (){
return editor.mirror
}
ChatEditor.prototype.disable = function () {
const editor = this;
editor.contentDiv.setAttribute('contenteditable', 'false');
};
ChatEditor.prototype.state = function () {
const editor = this;
return editor.contentDiv.getAttribute('contenteditable');
};
ChatEditor.prototype.focus = function () {
const editor = this;
editor.contentDiv.focus();
};
ChatEditor.prototype.clearSelection = function () {
const editor = this;
let selection = editor.content.getSelection().toString();
if (!/^\s*$/.test(selection)) editor.content.getSelection().removeAllRanges();
};
ChatEditor.prototype.insertEmoji = function (emojiImg) {
const editor = this;
const doInsert = () => {
if (editor.content.queryCommandSupported("InsertHTML")) {
editor.content.execCommand("insertHTML", false, emojiImg);
editor.updateMirror();
}
};
editor.focus();
return doInsert();
};
ChatEditor.prototype.insertText = function (text) {
const editor = this;
const parsedText = editorConfig.emojiPicker.parse(text);
const doPaste = () => {
if (editor.content.queryCommandSupported("InsertHTML")) {
editor.content.execCommand("insertHTML", false, parsedText);
editor.updateMirror();
}
};
editor.focus();
return doPaste();
};
ChatEditor.prototype.updateMirror = function () {
const editor = this;
const chatInputValue = editor.getValue();
const filteredValue = chatInputValue.replace(/<img.*?alt=".*?/g, '').replace(/".?src=.*?>/g, '');
let unescapedValue = editorConfig.unescape(filteredValue);
editor.mirror.value = unescapedValue;
};
ChatEditor.prototype.listenChanges = function () {
const editor = this;
const events = ['drop', 'contextmenu', 'mouseup', 'click', 'touchend', 'keydown', 'blur', 'paste']
for (let i = 0; i < events.length; i++) {
const event = events[i]
editor.content.body.addEventListener(event, async function (e) {
if (e.type === 'click') {
e.preventDefault();
e.stopPropagation();
}
if (e.type === 'paste') {
e.preventDefault();
const item_list = await navigator.clipboard.read();
let image_type; // we will feed this later
const item = item_list.find( item => // choose the one item holding our image
item.types.some( type => {
if (type.startsWith( 'image/')) {
image_type = type;
return true;
}
})
);
if(item){
const blob = item && await item.getType( image_type );
var file = new File([blob], "name", {
type: image_type
});
editorConfig.insertImage(file)
} else {
navigator.clipboard.readText()
.then(clipboardText => {
let escapedText = editorConfig.escape(clipboardText);
editor.insertText(escapedText);
})
.then(() => {
editorConfig.getMessageSize(editorConfig.editableElement.contentDocument.body.querySelector("#chatbarId").innerHTML);
})
.catch(err => {
// Fallback if everything fails...
let textData = (e.originalEvent || e).clipboardData.getData('text/plain');
editor.insertText(textData);
})
}
return false;
}
if (e.type === 'contextmenu') {
e.preventDefault();
e.stopPropagation();
return false;
}
if (e.type === 'keydown') {
await new Promise((res, rej) => {
setTimeout(() => {
editorConfig.calculateIFrameHeight(editorConfig.editableElement.contentDocument.body.scrollHeight);
editorConfig.getMessageSize(editorConfig.editableElement.contentDocument.body.querySelector("#chatbarId").innerHTML);
}, 0);
res();
})
// Handle Enter
if (e.keyCode === 13 && !e.shiftKey) {
if (editor.state() === 'false') return false;
if (editorConfig.iframeId === 'newChat') {
editorConfig.sendFunc(
{
type: 'image',
imageFile: editorConfig.imageFile,
}
);
} else {
editorConfig.sendFunc();
}
e.preventDefault();
return false;
}
// Handle Commands with CTR or CMD
if (e.ctrlKey || e.metaKey) {
switch (e.keyCode) {
case 66:
case 98: e.preventDefault();
return false;
case 73:
case 105: e.preventDefault();
return false;
case 85:
case 117: e.preventDefault();
return false;
}
return false;
}
}
if (e.type === 'blur') {
editor.clearSelection();
}
if (e.type === 'drop') {
e.preventDefault();
let droppedText = e.dataTransfer.getData('text/plain')
let escapedText = editorConfig.escape(droppedText)
editor.insertText(escapedText);
return false;
}
editor.updateMirror();
});
}
editor.content.addEventListener('click', function (event) {
event.preventDefault();
editor.focus();
});
};
ChatEditor.prototype.remove = function () {
const editor = this;
var old_element = editor.content.body;
var new_element = old_element.cloneNode(true);
editor.content.body.parentNode.replaceChild(new_element, old_element);
while (editor.content.body.firstChild) {
editor.content.body.removeChild(editor.content.body.lastChild);
}
};
ChatEditor.prototype.init = function () {
const editor = this;
editor.frame = editorConfig.editableElement;
editor.mirror = editorConfig.mirrorElement;
editor.content = (editor.frame.contentDocument || editor.frame.document);
editor.content.body.classList.add("chatbar-body");
let elemDiv = document.createElement('div');
elemDiv.setAttribute('contenteditable', 'true');
elemDiv.setAttribute('spellcheck', 'false');
elemDiv.setAttribute('data-placeholder', editorConfig.placeholder);
elemDiv.style.cssText = `width:100%; ${editorConfig.theme === "dark" ? "color:#ffffff;" : "color: #080808"}`;
elemDiv.id = 'chatbarId';
editor.content.body.appendChild(elemDiv);
editor.contentDiv = editor.frame.contentDocument.body.firstChild;
editor.styles();
editor.listenChanges();
};
function doInit() {
return new ChatEditor();
}
return doInit();
};
const editorConfig = {
getMessageSize: this.getMessageSize,
calculateIFrameHeight: this.calculateIFrameHeight,
mirrorElement: this.mirrorChatInput,
editableElement: this.chatMessageInput,
sendFunc: this.sendMessageFunc,
emojiPicker: this.emojiPicker,
escape: escape,
unescape: unescape,
placeholder: this.placeholder,
imageFile: this.imageFile,
requestUpdate: this.requestUpdate,
insertImage: this.insertImage,
chatMessageSize: this.chatMessageSize,
addGlobalEventListener: this.addGlobalEventListener,
removeGlobalEventListener: this.removeGlobalEventListener,
iframeId: this.iframeId,
theme: this.theme,
resetIFrameHeight: this.resetIFrameHeight
};
const newChat = new ChatEditor(editorConfig);
this.setChatEditor(newChat);
}
}
window.customElements.define("chat-text-editor", ChatTextEditor)

View File

@@ -0,0 +1,743 @@
import { LitElement, html, css } from "lit";
import { get, translate } from 'lit-translate';
import { EmojiPicker } from 'emoji-picker-js';
import { Epml } from '../../../epml.js';
import '@material/mwc-icon'
import '@material/mwc-checkbox'
// import { addAutoLoadImageChat } from "../../../../qortal-ui-core/src/redux/app/app-actions.js";
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent });
class ChatTextEditor extends LitElement {
static get properties() {
return {
isLoading: { type: Boolean },
isLoadingMessages: { type: Boolean },
_sendMessage: { attribute: false },
placeholder: { type: String },
attachment: { type: Object },
insertFile: { attribute: false },
imageFile: { type: Object },
insertImage: { attribute: false },
iframeHeight: { type: Number },
editedMessageObj: { type: Object },
repliedToMessageObj: {type: Object},
setChatEditor: { attribute: false },
iframeId: { type: String },
hasGlobalEvents: { type: Boolean },
chatMessageSize: { type: Number },
isEditMessageOpen: { type: Boolean },
editor: {type: Object},
theme: {
type: String,
reflect: true
},
toggleEnableChatEnter: {attribute: false},
isEnabledChatEnter: {type: Boolean},
openGifModal: { type: Boolean },
setOpenGifModal: { attribute: false },
chatId: {type: String}
}
}
static get styles() {
return css`
:host {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: auto;
width: 100%;
overflow: hidden;
}
* {
--mdc-checkbox-unchecked-color: var(--black);
}
.chatbar-container {
width: 100%;
display: flex;
height: auto;
overflow: hidden;
}
.chatbar-caption {
border-bottom: 2px solid var(--mdc-theme-primary);
}
.privateMessageMargin {
margin-bottom: 12px;
}
.emoji-button {
width: 45px;
height: 40px;
padding-top: 4px;
border: none;
outline: none;
background: transparent;
cursor: pointer;
max-height: 40px;
color: var(--black);
margin-bottom: 5px;
}
.message-size-container {
display: flex;
justify-content: flex-end;
width: 100%;
}
.message-size {
font-family: Roboto, sans-serif;
font-size: 12px;
color: black;
}
.paperclip-icon {
color: var(--paperclip-icon);
width: 25px;
}
.paperclip-icon:hover {
cursor: pointer;
}
.send-icon {
width: 30px;
margin-left: 5px;
transition: all 0.1s ease-in-out;
cursor: pointer;
}
.send-icon:hover {
filter: brightness(1.1);
}
.file-picker-container {
position: relative;
height: 25px;
width: 25px;
margin-bottom: 10px;
}
.file-picker-input-container {
position: absolute;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
z-index: 10;
opacity: 0;
overflow: hidden;
}
input[type=file]::-webkit-file-upload-button {
cursor: pointer;
}
.chatbar-container textarea {
display: none;
}
.chatbar-container .chat-editor {
display: flex;
max-height: -webkit-fill-available;
width: 100%;
border-color: transparent;
margin: 0;
padding: 0;
border: none;
}
.checkmark-icon {
width: 30px;
color: var(--mdc-theme-primary);
margin-bottom: 6px;
}
.checkmark-icon:hover {
cursor: pointer;
}
.element {
width: 100%;
max-height: 100%;
overflow: auto;
color: var(--black);
padding: 0px 10px;
height: 100%;
display: flex;
align-items: center;
}
.element::-webkit-scrollbar-track {
background-color: whitesmoke;
border-radius: 7px;
}
.element::-webkit-scrollbar {
width: 6px;
border-radius: 7px;
background-color: whitesmoke;
}
.element::-webkit-scrollbar-thumb {
background-color: rgb(180, 176, 176);
border-radius: 7px;
transition: all 0.3s ease-in-out;
}
.element::-webkit-scrollbar-thumb:hover {
background-color: rgb(148, 146, 146);
cursor: pointer;
}
.ProseMirror:focus {
outline: none;
}
.is-active {
background-color: var(--white)
}
.ProseMirror > * + * {
margin-top: 0.75em;
outline: none;
}
.ProseMirror ul,
ol {
padding: 0 1rem;
}
.ProseMirror h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
.ProseMirror code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
.ProseMirror pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
white-space: pre-wrap;
}
.ProseMirror pre code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
.ProseMirror img {
width: 1.7em;
height: 1.5em;
margin: 0px;
}
.ProseMirror blockquote {
padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
}
.ProseMirror hr {
border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1);
margin: 2rem 0;
}
.chatbar-button-single {
background: var(--white);
outline: none;
border: none;
color: var(--black);
padding: 4px;
border-radius: 5px;
cursor: pointer;
margin-right: 2px;
filter: brightness(100%);
transition: all 0.2s;
display: none;
}
.removeBg {
background: none;
}
.chatbar-button-single label {
font-size: 13px;
}
.chatbar-button-single:hover {
filter: brightness(120%);
}
.chatbar-buttons {
margin-bottom: 5px;
flex-shrink: 0;
}
.show-chatbar-buttons {
display: flex;
align-items: center;
justify-content: center;
}
:host(:hover) .chatbar-button-single {
display: flex;
align-items: center;
justify-content: center;
}
.ProseMirror p.is-editor-empty:first-child::before {
color: #adb5bd;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
.ProseMirror p {
font-size: 18px;
margin-block-start: 0px;
margin-block-end: 0px;
overflow-wrap: anywhere;
}
.ProseMirror {
width: 100%;
box-sizing: border-box;
word-break: break-word;
}
.ProseMirror mark {
background-color: #ffe066;
border-radius: 0.25em;
box-decoration-break: clone;
padding: 0.125em 0;
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
/* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
}
.material-symbols-outlined {
font-family: 'Material Symbols Outlined';
font-weight: normal;
font-style: normal;
font-size: 18px; /* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
}
.hide-styling {
display: none;
}
mwc-checkbox::shadow .mdc-checkbox::after, mwc-checkbox::shadow .mdc-checkbox::before {
background-color:var(--mdc-theme-primary)
}
--mdc-checkbox-unchecked-color
`
}
constructor() {
super()
this.isLoadingMessages = true
this.isLoading = false
this.getMessageSize = this.getMessageSize.bind(this)
this.sendMessageFunc = this.sendMessageFunc.bind(this)
this.iframeHeight = 42
this.chatMessageSize = 0
this.userName = window.parent.reduxStore.getState().app.accountInfo.names[0]
this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light'
this.editor = null
}
render() {
return html`
<div
class=${["chatbar-container", "chatbar-buttons", this.iframeId !=="_chatEditorDOM" && 'hide-styling'].join(" ")}
style="align-items: center;justify-content:space-between">
<div style="display: flex;align-items: center">
<button
@click=${() => this.editor.chain().focus().toggleBold().run()}
?disabled=${
this.editor &&
!this.editor.can()
.chain()
.focus()
.toggleBold()
.run()
}
class=${["chatbar-button-single", (this.editedMessageObj || this.repliedToMessageObj || this.openGifModal) && 'show-chatbar-buttons', this.editor && this.editor.isActive('bold') ? 'is-active' : ''].join(" ")}
>
<!-- <mwc-icon >format_bold</mwc-icon> -->
<span class="material-symbols-outlined">&#xe238;</span>
</button>
<button
@click=${() => this.editor.chain().focus().toggleItalic().run()}
?disabled=${ this.editor &&
!this.editor.can()
.chain()
.focus()
.toggleItalic()
.run()
}
class=${["chatbar-button-single", (this.editedMessageObj || this.repliedToMessageObj || this.openGifModal) && 'show-chatbar-buttons', this.editor && this.editor.isActive('italic') ? 'is-active' : ''].join(' ')}
>
<span class="material-symbols-outlined">&#xe23f;</span>
</button>
<button
@click=${() => this.editor.chain().focus().toggleUnderline().run()}
class=${["chatbar-button-single", (this.editedMessageObj || this.repliedToMessageObj || this.openGifModal) && 'show-chatbar-buttons', this.editor && this.editor.isActive('underline') ? 'is-active' : ''].join(' ')}
>
<span class="material-symbols-outlined">&#xe249;</span>
</button>
<button
@click=${() => this.editor.chain().focus().toggleHighlight().run()}
class=${["chatbar-button-single", (this.editedMessageObj || this.repliedToMessageObj || this.openGifModal) && 'show-chatbar-buttons', this.editor && this.editor.isActive('highlight') ? 'is-active' : ''].join(' ')}
>
<span class="material-symbols-outlined">&#xf82b;</span>
</button>
<button
@click=${() => this.editor.chain().focus().toggleCodeBlock().run()}
class=${["chatbar-button-single",(this.editedMessageObj || this.repliedToMessageObj || this.openGifModal) && 'show-chatbar-buttons', this.editor && this.editor.isActive('codeBlock') ? 'is-active' : ''].join(' ')}
>
<span class="material-symbols-outlined">&#xf84d;</span>
</button>
<button
@click=${()=> this.toggleEnableChatEnter() }
style="height: 26px; box-sizing: border-box;"
class=${["chatbar-button-single",(this.editedMessageObj || this.repliedToMessageObj || this.openGifModal) && 'show-chatbar-buttons', this.editor && this.editor.isActive('codeBlock') ? 'is-active' : ''].join(' ')}
>
${this.isEnabledChatEnter ? html`
${translate("chatpage.cchange63")}
` : html`
${translate("chatpage.cchange64")}
`}
</button>
</div>
${this.iframeId === "_chatEditorDOM" ? html`
<div
style="height: 26px; box-sizing: border-box"
class=${["chatbar-button-single", "removeBg"].join(' ')}
>
<label
for="qChatShowAutoMsg"
@click=${() => this.shadowRoot.getElementById('qChatShowAutoMsg').click()}
>${translate('chatpage.cchange69')}</label>
<mwc-checkbox style="margin-right: -15px;" id="qChatShowAutoMsg" @click=${e => {
if(e.target.checked){
window.parent.reduxStore.dispatch( window.parent.reduxAction.removeAutoLoadImageChat(this.chatId))
return
}
window.parent.reduxStore.dispatch( window.parent.reduxAction.addAutoLoadImageChat(this.chatId))
}} ?checked=${(window.parent.reduxStore.getState().app?.autoLoadImageChats || []).includes(this.chatId)}></mwc-checkbox>
</div>
` : ''}
</div>
<div
class=${["chatbar-container", (this.iframeId === "newChat" || this.iframeId === "privateMessage") ? "chatbar-caption" : ""].join(" ")}
style="align-items: flex-end; position: relative">
<div
style=${this.iframeId === "privateMessage" ? "display: none" : "display: block"}
class="file-picker-container"
@click=${(e) => {
this.preventUserSendingImage(e)
}}>
<vaadin-icon
class="paperclip-icon"
icon="vaadin:paperclip"
slot="icon"
>
</vaadin-icon>
<div class="file-picker-input-container">
<input
@change="${e => {
this.insertFile(e.target.files[0]);
const filePickerInput = this.shadowRoot.getElementById('file-picker');
if (filePickerInput) {
filePickerInput.value = "";
}
}
}"
id="file-picker"
class="file-picker-input"
type="file"
name="myImage"
accept="image/*, .doc, .docx, .pdf, .zip, .pdf, .txt, .odt, .ods, .xls, .xlsx, .ppt, .pptx" />
</div>
</div>
<textarea style="color: var(--black);" tabindex='1' ?autofocus=${true} ?disabled=${this.isLoading || this.isLoadingMessages} id="messageBox" rows="1"></textarea>
<div id=${this.iframeId}
class=${["element", this.iframeId === "privateMessage" ? "privateMessageMargin" : ""].join(" ")}
></div>
<button class="emoji-button" ?disabled=${this.isLoading || this.isLoadingMessages}>
${html`<img class="emoji" draggable="false" alt="😀" src="/emoji/svg/1f600.svg" />`}
</button>
${this.setOpenGifModal ?
html`
<button
class="emoji-button"
@click=${()=> {
if (!this.userName) {
parentEpml.request('showSnackBar', get("gifs.gchange26"));
return;
}
this.setOpenGifModal(true)
}}>
<span style="font-size: 30px" class="material-symbols-outlined">&#xe7a3;</span>
</button>
`
: ''}
${this.editedMessageObj ? (
html`
<div style="margin-bottom: 10px">
${this.isLoading === false ? html`
<vaadin-icon
class="checkmark-icon"
icon="vaadin:check"
slot="icon"
@click=${() => {
this.sendMessageFunc();
}}
>
</vaadin-icon>
` :
html`
<paper-spinner-lite active></paper-spinner-lite>
`}
</div>
`
) :
html`
<div
style="margin-bottom: 10px;
${(this.iframeId === 'newChat' || this.iframeId === "newAttachmentChat")
? 'display: none;'
: 'display: flex;'}">
${this.isLoading === false ? html`
<img
src="/img/qchat-send-message-icon.svg"
alt="send-icon"
class="send-icon"
@click=${() => {
this.sendMessageFunc();
}}
/>
` :
html`
<paper-spinner-lite active></paper-spinner-lite>
`}
</div>
`
}
</div>
${this.chatMessageSize >= 750 ?
html`
<div class="message-size-container" style=${this.imageFile && "margin-top: 10px;"}>
<div class="message-size" style="${this.chatMessageSize > 4000 && 'color: #bd1515'}">
${`Your message size is of ${this.chatMessageSize} bytes out of a maximum of 4000`}
</div>
</div>
` :
html``}
</div>
`
}
preventUserSendingImage(e) {
if (!this.userName) {
e.preventDefault();
parentEpml.request('showSnackBar', get("chatpage.cchange27"));
}
}
async firstUpdated() {
window.addEventListener('storage', () => {
const checkTheme = localStorage.getItem('qortalTheme');
const chatbar = this.shadowRoot.querySelector('.element')
if (checkTheme === 'dark') {
this.theme = 'dark';
chatbar.style.cssText = "color:#ffffff;"
} else {
this.theme = 'light';
chatbar.style.cssText = "color:#080808;"
}
})
this.emojiPickerHandler = this.shadowRoot.querySelector('.emoji-button');
this.mirrorChatInput = this.shadowRoot.getElementById('messageBox');
this.chatMessageInput = this.shadowRoot.querySelector('.element')
this.emojiPicker = new EmojiPicker({
style: "twemoji",
twemojiBaseUrl: '/emoji/',
showPreview: false,
showVariants: false,
showAnimation: false,
position: 'top-start',
boxShadow: 'rgba(4, 4, 5, 0.15) 0px 0px 0px 1px, rgba(0, 0, 0, 0.24) 0px 8px 16px 0px',
zIndex: 100
});
this.emojiPicker.on('emoji', selection => {
this.editor.commands.insertContent(selection.emoji, {
parseOptions: {
preserveWhitespace: false
}
})
});
this.emojiPickerHandler.addEventListener('click', () => this.emojiPicker.togglePicker(this.emojiPickerHandler));
await this.updateComplete;
// this.initChatEditor();
}
async updated(changedProperties) {
if (changedProperties && changedProperties.has('editedMessageObj')) {
if (this.editedMessageObj) {
this.editor.commands.setContent(this.editedMessageObj.message)
this.getMessageSize(this.editedMessageObj.message);
} else {
this.chatMessageSize = 0;
}
}
if (changedProperties && changedProperties.has('placeholder') && this.updatePlaceholder && this.editor) {
this.updatePlaceholder(this.editor, this.placeholder )
}
if (changedProperties && changedProperties.has("imageFile")) {
this.chatMessageInput = "newChat";
}
}
shouldUpdate(changedProperties) {
// Only update element if prop1 changed.
if(changedProperties.has('setChatEditor') && changedProperties.size === 1) return false
return true
}
sendMessageFunc(props) {
if(this.editor.isEmpty && (this.iframeId !== 'newChat' && this.iframeId !== 'newAttachmentChat')) return
this.getMessageSize(this.editor.getJSON())
if (this.chatMessageSize > 4000 ) {
parentEpml.request('showSnackBar', get("chatpage.cchange29"));
return;
}
this.chatMessageSize = 0;
this._sendMessage(props, this.editor.getJSON());
}
getMessageSize(message){
try {
const trimmedMessage = message
let messageObject = {};
if (this.repliedToMessageObj) {
let chatReference = this.repliedToMessageObj.signature;
if (this.repliedToMessageObj.chatReference) {
chatReference = this.repliedToMessageObj.chatReference;
}
messageObject = {
messageText: trimmedMessage,
images: [''],
repliedTo: chatReference,
version: 3
}
} else if (this.editedMessageObj) {
let message = "";
try {
const parsedMessageObj = JSON.parse(this.editedMessageObj.decodedMessage);
message = parsedMessageObj;
} catch (error) {
message = this.messageObj.decodedMessage
}
messageObject = {
...message,
messageText: trimmedMessage,
}
} else if(this.imageFile && this.iframeId === 'newChat') {
messageObject = {
messageText: trimmedMessage,
images: [{
service: "QCHAT_IMAGE",
name: '123456789123456789123456789',
identifier: '123456'
}],
repliedTo: '',
version: 3
};
} else if (this.attachment && this.iframeId === 'newAttachmentChat') {
messageObject = {
messageText: trimmedMessage,
attachments: [{
service: "QCHAT_ATTACHMENT",
name: '123456789123456789123456789',
identifier: '123456',
attachmentName: "123456789123456789123456789",
attachmentSize: "123456"
}],
repliedTo: '',
version: 2
};
} else {
messageObject = {
messageText: trimmedMessage,
images: [''],
repliedTo: '',
version: 3
};
}
const stringified = JSON.stringify(messageObject);
const size = new Blob([stringified]).size;
this.chatMessageSize = size;
} catch (error) {
console.error(error)
}
}
}
window.customElements.define("chat-text-editor", ChatTextEditor)

View File

@@ -0,0 +1,522 @@
import { LitElement, html, css } from 'lit'
import { render } from 'lit/html.js'
import { Epml } from '../../../epml.js'
import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate'
registerTranslateConfig({
loader: lang => fetch(`/language/${lang}.json`).then(res => res.json())
})
import '@material/mwc-icon'
import '@material/mwc-button'
import '@material/mwc-dialog'
import '@polymer/paper-spinner/paper-spinner-lite.js'
import '@vaadin/grid'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class ChatWelcomePage extends LitElement {
static get properties() {
return {
selectedAddress: { type: Object },
config: { type: Object },
myAddress: { type: Object, reflect: true },
messages: { type: Array },
btnDisable: { type: Boolean },
isLoading: { type: Boolean },
balance: { type: Number },
theme: { type: String, reflect: true },
setOpenPrivateMessage: { attribute: false }
}
}
static get styles() {
return css`
* {
--mdc-theme-primary: rgb(3, 169, 244);
--paper-input-container-focus-color: var(--mdc-theme-primary);
--lumo-primary-text-color: rgb(0, 167, 245);
--lumo-primary-color-50pct: rgba(0, 167, 245, 0.5);
--lumo-primary-color-10pct: rgba(0, 167, 245, 0.1);
--lumo-primary-color: hsl(199, 100%, 48%);
--lumo-base-color: var(--white);
--lumo-body-text-color: var(--black);
}
@keyframes moveInBottom {
0% {
opacity: 0;
transform: translateY(30px);
}
100% {
opacity: 1;
transform: translate(0);
}
}
paper-spinner-lite{
height: 24px;
width: 24px;
--paper-spinner-color: var(--mdc-theme-primary);
--paper-spinner-stroke-width: 2px;
}
.welcome-title {
display: block;
overflow: hidden;
font-size: 40px;
color: var(--black);
font-weight: 400;
text-align: center;
white-space: pre-wrap;
overflow-wrap: break-word;
word-break: break-word;
cursor: inherit;
margin-top: 2rem;
}
.sub-main {
position: relative;
text-align: center;
}
.center-box {
position: absolute;
top: 45%;
left: 50%;
transform: translate(-50%, 0%);
text-align: center;
}
.img-icon {
font-size: 150px;
color: var(--black);
}
.start-chat {
display: inline-flex;
flex-direction: column;
justify-content: center;
align-content: center;
border: none;
border-radius: 20px;
padding-left: 25px;
padding-right: 25px;
color: var(--white);
background: var(--tradehead);
width: 50%;
font-size: 17px;
cursor: pointer;
height: 50px;
margin-top: 1rem;
text-transform: uppercase;
text-decoration: none;
transition: all .2s;
position: relative;
animation: moveInBottom .3s ease-out .50s;
animation-fill-mode: backwards;
}
.start-chat:hover {
transform: translateY(-3px);
box-shadow: 0 10px 20px rgba(0, 0, 0, .2);
}
.start-chat::after {
content: "";
display: inline-flex;
height: 100%;
width: 100%;
border-radius: 100px;
position: absolute;
top: 0;
left: 0;
z-index: -1;
transition: all .4s;
}
.red {
--mdc-theme-primary: red;
}
h2 {
margin:0;
}
h2, h3, h4, h5 {
color:# var(--black);
font-weight: 400;
}
[hidden] {
display: hidden !important;
visibility: none !important;
}
.details {
display: flex;
font-size: 18px;
}
.title {
font-weight:600;
font-size:12px;
line-height: 32px;
opacity: 0.66;
}
.input {
width: 90%;
border: none;
display: inline-block;
font-size: 16px;
padding: 10px 20px;
border-radius: 5px;
resize: none;
background: #eee;
}
.textarea {
width: 90%;
border: none;
display: inline-block;
font-size: 16px;
padding: 10px 20px;
border-radius: 5px;
height: 120px;
resize: none;
background: #eee;
}
`
}
constructor() {
super()
this.selectedAddress = window.parent.reduxStore.getState().app.selectedAddress.address
this.myAddress = {}
this.balance = 1
this.messages = []
this.btnDisable = false
this.isLoading = false
this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light'
}
render() {
return html`
<div>
<div>
<span class="welcome-title">${translate("welcomepage.wcchange1")}</span>
<hr style="color: #eee; border-radius: 80%; margin-bottom: 2rem;">
</div>
<div class="sub-main">
<div class="center-box">
<mwc-icon class="img-icon">chat</mwc-icon><br>
<span style="font-size: 20px; color: var(--black);">${this.myAddress.address}</span>
<div
class="start-chat"
@click="${() => this.setOpenPrivateMessage({
name: "",
open: true
})}">
${translate("welcomepage.wcchange2")}
</div>
</div>
</div>
<!-- Start Chatting Dialog -->
<mwc-dialog id="startSecondChatDialog" scrimClickAction="${this.isLoading ? '' : 'close'}">
<div style="text-align:center">
<h1>${translate("welcomepage.wcchange2")}</h1>
<hr>
</div>
<p>${translate("welcomepage.wcchange3")}</p>
<textarea class="input" ?disabled=${this.isLoading} id="sendTo" placeholder="${translate("welcomepage.wcchange4")}" rows="1"></textarea>
<p style="margin-bottom:0;">
<textarea class="textarea" @keydown=${(e) => this._textArea(e)} ?disabled=${this.isLoading} id="messageBox" placeholder="${translate("welcomepage.wcchange5")}" rows="1"></textarea>
</p>
<mwc-button ?disabled="${this.isLoading}" slot="primaryAction" @click=${() => {
this._sendMessage();
}
}>
${translate("welcomepage.wcchange6")}</mwc-button>
<mwc-button
?disabled="${this.isLoading}"
slot="secondaryAction"
dialogAction="cancel"
class="red"
>
${translate("general.close")}
</mwc-button>
</mwc-dialog>
</div>
`
}
firstUpdated() {
this.changeTheme()
const stopKeyEventPropagation = (e) => {
e.stopPropagation();
return false;
}
this.shadowRoot.getElementById('sendTo').addEventListener('keydown', stopKeyEventPropagation);
this.shadowRoot.getElementById('messageBox').addEventListener('keydown', stopKeyEventPropagation);
const getDataFromURL = () => {
let tempUrl = document.location.href
let splitedUrl = decodeURI(tempUrl).split('?')
let urlData = splitedUrl[1]
if (urlData !== undefined) {
this.chatId = urlData
}
}
window.addEventListener('storage', () => {
const checkLanguage = localStorage.getItem('qortalLanguage')
const checkTheme = localStorage.getItem('qortalTheme')
use(checkLanguage)
if (checkTheme === 'dark') {
this.theme = 'dark'
} else {
this.theme = 'light'
}
document.querySelector('html').setAttribute('theme', this.theme)
})
let configLoaded = false
parentEpml.ready().then(() => {
parentEpml.subscribe('selected_address', async selectedAddress => {
this.selectedAddress = {}
selectedAddress = JSON.parse(selectedAddress)
if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
this.selectedAddress = selectedAddress
})
parentEpml.request('apiCall', {
url: `/addresses/balance/${window.parent.reduxStore.getState().app.selectedAddress.address}`
}).then(res => {
this.balance = res
})
})
parentEpml.imReady()
}
changeTheme() {
const checkTheme = localStorage.getItem('qortalTheme')
if (checkTheme === 'dark') {
this.theme = 'dark';
} else {
this.theme = 'light';
}
document.querySelector('html').setAttribute('theme', this.theme);
}
changeLanguage() {
const checkLanguage = localStorage.getItem('qortalLanguage')
if (checkLanguage === null || checkLanguage.length === 0) {
localStorage.setItem('qortalLanguage', 'us')
use('us')
} else {
use(checkLanguage)
}
}
_sendMessage() {
this.isLoading = true;
const recipient = this.shadowRoot.getElementById('sendTo').value;
const messageBox = this.shadowRoot.getElementById('messageBox');
const messageText = messageBox.value;
if (recipient.length === 0) {
this.isLoading = false;
} else if (messageText.length === 0) {
this.isLoading = false;
} else {
this.sendMessage();
}
};
async sendMessage() {
this.isLoading = true;
const _recipient = this.shadowRoot.getElementById('sendTo').value;
const messageBox = this.shadowRoot.getElementById('messageBox');
const messageText = messageBox.value;
let recipient;
const validateName = async (receiverName) => {
let myRes;
let myNameRes = await parentEpml.request('apiCall', {
type: 'api',
url: `/names/${receiverName}`
});
if (myNameRes.error === 401) {
myRes = false;
} else {
myRes = myNameRes;
};
return myRes;
};
const myNameRes = await validateName(_recipient);
if (!myNameRes) {
recipient = _recipient;
} else {
recipient = myNameRes.owner;
};
let _reference = new Uint8Array(64);
window.crypto.getRandomValues(_reference);
let sendTimestamp = Date.now();
let reference = window.parent.Base58.encode(_reference);
const getAddressPublicKey = async () => {
let isEncrypted;
let _publicKey;
let addressPublicKey = await parentEpml.request('apiCall', {
type: 'api',
url: `/addresses/publickey/${recipient}`
})
if (addressPublicKey.error === 102) {
_publicKey = false;
// Do something here...
let err1string = get("welcomepage.wcchange7");
parentEpml.request('showSnackBar', `${err1string}`);
this.isLoading = false;
} else if (addressPublicKey !== false) {
isEncrypted = 1;
_publicKey = addressPublicKey;
sendMessageRequest(isEncrypted, _publicKey);
} else {
isEncrypted = 0;
_publicKey = this.selectedAddress.address;
sendMessageRequest(isEncrypted, _publicKey);
};
};
const sendMessageRequest = async (isEncrypted, _publicKey) => {
const messageObject = {
messageText,
images: [''],
repliedTo: '',
version: 3
};
const stringifyMessageObject = JSON.stringify(messageObject);
let chatResponse = await parentEpml.request('chat', {
type: 18,
nonce: this.selectedAddress.nonce,
params: {
timestamp: sendTimestamp,
recipient: recipient,
recipientPublicKey: _publicKey,
hasChatReference: 0,
message: stringifyMessageObject,
lastReference: reference,
proofOfWorkNonce: 0,
isEncrypted: isEncrypted,
isText: 1
}
})
_computePow(chatResponse)
}
const _computePow = async (chatBytes) => {
const _chatBytesArray = Object.keys(chatBytes).map(function (key) { return chatBytes[key]; });
const chatBytesArray = new Uint8Array(_chatBytesArray)
const chatBytesHash = new window.parent.Sha256().process(chatBytesArray).finish().result
const hashPtr = window.parent.sbrk(32, window.parent.heap);
const hashAry = new Uint8Array(window.parent.memory.buffer, hashPtr, 32);
hashAry.set(chatBytesHash);
const difficulty = this.balance < 4 ? 18 : 8;
const workBufferLength = 8 * 1024 * 1024;
const workBufferPtr = window.parent.sbrk(workBufferLength, window.parent.heap);
let nonce = window.parent.computePow(hashPtr, workBufferPtr, workBufferLength, difficulty)
let _response = await parentEpml.request('sign_chat', {
nonce: this.selectedAddress.nonce,
chatBytesArray: chatBytesArray,
chatNonce: nonce
})
getSendChatResponse(_response)
}
const getSendChatResponse = (response) => {
if (response === true) {
messageBox.value = ""
let err2string = get("welcomepage.wcchange8")
parentEpml.request('showSnackBar', `${err2string}`)
this.isLoading = false
} else if (response.error) {
parentEpml.request('showSnackBar', response.message)
this.isLoading = false
} else {
let err3string = get("welcomepage.wcchange9")
parentEpml.request('showSnackBar', `${err3string}`)
this.isLoading = false
}
}
getAddressPublicKey()
}
_textMenu(event) {
const getSelectedText = () => {
var text = "";
if (typeof window.getSelection != "undefined") {
text = window.getSelection().toString();
} else if (typeof this.shadowRoot.selection != "undefined" && this.shadowRoot.selection.type == "Text") {
text = this.shadowRoot.selection.createRange().text;
}
return text;
}
const checkSelectedTextAndShowMenu = () => {
let selectedText = getSelectedText();
if (selectedText && typeof selectedText === 'string') {
let _eve = { pageX: event.pageX, pageY: event.pageY, clientX: event.clientX, clientY: event.clientY }
let textMenuObject = { selectedText: selectedText, eventObject: _eve, isFrame: true }
parentEpml.request('openCopyTextMenu', textMenuObject)
}
}
checkSelectedTextAndShowMenu()
}
_textArea(e) {
if (e.keyCode === 13 && !e.shiftKey) this._sendMessage()
}
isEmptyArray(arr) {
if (!arr) { return true }
return arr.length === 0
}
getApiKey() {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
let apiKey = myNode.apiKey;
return apiKey;
}
}
window.customElements.define('chat-welcome-page', ChatWelcomePage)

View File

@@ -0,0 +1,127 @@
import { LitElement, html, css } from 'lit';
import { translate, get } from 'lit-translate';
import { render } from 'lit/html.js';
export class ImageComponent extends LitElement {
static get properties() {
return {
class: { type: String },
gif: { type: Object },
alt: { type: String },
attempts: { type: Number },
maxAttempts: { type: Number },
error: { type: Boolean },
sendMessage: { attribute: false },
setOpenGifModal: { attribute: false }
}
}
static get styles() {
return css`
.gif-error-msg {
margin: 0;
font-family: Roboto, sans-serif;
font-size: 17px;
letter-spacing: 0.3px;
color: var(--chat-bubble-msg-color);
font-weight: 300;
padding: 10px 10px;
}
.gif-image {
border-radius: 15px;
background-color: transparent;
cursor: pointer;
width: 100%;
height: 150px;
object-fit: cover;
border: 1px solid transparent;
transition: all 0.2s cubic-bezier(0, 0.55, 0.45, 1);
box-shadow: rgb(50 50 93 / 25%) 0px 6px 12px -2px, rgb(0 0 0 / 30%) 0px 3px 7px -3px;
}
.gif-image:hover {
border: 1px solid var(--mdc-theme-primary );
}
`
}
constructor() {
super();
this.attempts = 0;
this.maxAttempts = 5;
}
getApiKey() {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
let apiKey = myNode.apiKey;
return apiKey;
}
async _fetchImage() {
this.attempts++;
if (this.attempts > this.maxAttempts) return;
await new Promise((res) => {
setTimeout(() => {
res();
}, 1000)
});
try {
const response = await fetch(this.gif.url);
const data = await response.json();
console.log({data});
if (data.ok) {
this.error = false;
this.gif = {
...this.gif,
url: data.src
};
this.requestUpdate();
} else if (!data.ok || data.error) {
this.error = true;
} else {
this.error = false;
}
} catch (error) {
this.error = true;
console.error(error);
this._fetchImage();
}
}
render() {
if (this.error && this.attempts <= this.maxAttempts) {
setTimeout(() => {
this._fetchImage();
}, 1000);
}
return html`
${this.gif && !this.error
? html`
<img
class=${this.class}
src=${this.gif.url}
alt=${this.alt}
@click=${() => {
this.sendMessage({
type: 'gif',
identifier: this.gif.identifier,
name: this.gif.name,
filePath: this.gif.filePath,
service: "GIF_REPOSITORY"
})
this.setOpenGifModal(false);
}}
@error=${this._fetchImage}
/>`
: this.error && this.attempts <= this.maxAttempts ? html`
<p class='gif-error-msg'>${translate('gifs.gchange15')}</p>
`
: html`
<p class='gif-error-msg'>${translate('gifs.gchange16')}</p>
`
}`
}
}
customElements.define('image-component', ImageComponent);

View File

@@ -0,0 +1,204 @@
import { LitElement, html, css } from 'lit'
import { render } from 'lit/html.js'
import { Epml } from '../../../epml.js'
import snackbar from './snackbar.js'
import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate'
registerTranslateConfig({
loader: lang => fetch(`/language/${lang}.json`).then(res => res.json())
})
import '@polymer/paper-tooltip/paper-tooltip.js'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class LevelFounder extends LitElement {
static get properties() {
return {
checkleveladdress: { type: String, attribute: true },
selectedAddress: { type: Object },
config: { type: Object },
memberInfo: { type: Array }
}
}
static get styles() {
return css`
* {
--mdc-theme-primary: rgb(3, 169, 244);
--mdc-theme-secondary: var(--mdc-theme-primary);
--mdc-dialog-content-ink-color: var(--black);
--mdc-theme-surface: var(--white);
--mdc-theme-text-primary-on-background: var(--black);
--paper-input-container-focus-color: var(--mdc-theme-primary);
--lumo-primary-text-color: rgb(0, 167, 245);
--lumo-primary-color-50pct: rgba(0, 167, 245, 0.5);
--lumo-primary-color-10pct: rgba(0, 167, 245, 0.1);
--lumo-primary-color: hsl(199, 100%, 48%);
--lumo-base-color: var(--white);
--lumo-body-text-color: var(--black);
}
h2 {
margin:0;
}
h2, h3, h4, h5 {
color:# var(--black);
font-weight: 400;
}
.custom {
--paper-tooltip-background: #03a9f4;
--paper-tooltip-text-color: #fff;
}
.level-img-tooltip {
--paper-tooltip-background: #000000;
--paper-tooltip-text-color: #fff;
--paper-tooltip-delay-in: 300;
--paper-tooltip-delay-out: 3000;
}
.message-data {
display: flex;
justify-content: center;
align-items: center;
gap: 5px;
}
.message-data-level {
width: 20px;
height: 20px;
}
.badge {
align-items: center;
background: rgb(3, 169, 244);
border: 1px solid transparent;
border-radius: 50%;
color: rgb(255, 255, 255);
display: flex;
font-size: 10px;
font-weight: 400;
height: 12px;
width: 12px;
justify-content: center;
cursor: pointer;
}
`
}
constructor() {
super()
this.memberInfo = []
this.selectedAddress = window.parent.reduxStore.getState().app.selectedAddress.address
}
render() {
return html`
<div class="message-data">
${this.renderFounder()}
${this.renderLevel()}
</div>
`
}
firstUpdated() {
this.checkAddressInfo()
window.addEventListener('storage', () => {
const checkLanguage = localStorage.getItem('qortalLanguage')
use(checkLanguage)
})
let configLoaded = false
parentEpml.ready().then(() => {
parentEpml.subscribe('selected_address', async selectedAddress => {
this.selectedAddress = {}
selectedAddress = JSON.parse(selectedAddress)
if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
this.selectedAddress = selectedAddress
})
})
parentEpml.imReady()
}
changeLanguage() {
const checkLanguage = localStorage.getItem('qortalLanguage')
if (checkLanguage === null || checkLanguage.length === 0) {
localStorage.setItem('qortalLanguage', 'us')
use('us')
} else {
use(checkLanguage)
}
}
async checkAddressInfo() {
let toCheck = this.checkleveladdress
const memberInfo = await parentEpml.request('apiCall', {
url: `/addresses/${toCheck}`
})
this.memberInfo = memberInfo
}
renderFounder() {
let adressfounder = this.memberInfo.flags;
if (adressfounder === 1) {
return html `
<span id="founderTooltip" class="badge">F</span>
<paper-tooltip class="custom" for="founderTooltip" position="top">FOUNDER</paper-tooltip>
`
} else {
return null;
}
}
renderLevel() {
let adresslevel = this.memberInfo.level;
return adresslevel ? html `
<img id="level-img" src=${`/img/badges/level-${adresslevel}.png`} alt=${`badge-${adresslevel}`} class="message-data-level" />
<paper-tooltip class="level-img-tooltip" for="level-img" position="top" >
${translate("mintingpage.mchange27")} ${adresslevel}
</paper-tooltip>
` : ''
}
_textMenu(event) {
const getSelectedText = () => {
var text = "";
if (typeof window.getSelection != "undefined") {
text = window.getSelection().toString();
} else if (typeof this.shadowRoot.selection != "undefined" && this.shadowRoot.selection.type == "Text") {
text = this.shadowRoot.selection.createRange().text;
}
return text;
}
const checkSelectedTextAndShowMenu = () => {
let selectedText = getSelectedText();
if (selectedText && typeof selectedText === 'string') {
let _eve = { pageX: event.pageX, pageY: event.pageY, clientX: event.clientX, clientY: event.clientY }
let textMenuObject = { selectedText: selectedText, eventObject: _eve, isFrame: true }
parentEpml.request('openCopyTextMenu', textMenuObject)
}
}
checkSelectedTextAndShowMenu()
}
isEmptyArray(arr) {
if (!arr) { return true }
return arr.length === 0
}
getApiKey() {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
let apiKey = myNode.apiKey;
return apiKey;
}
}
window.customElements.define('level-founder', LevelFounder)

View File

@@ -0,0 +1,638 @@
import { LitElement, html, css } from 'lit'
import { render } from 'lit/html.js'
import { Epml } from '../../../epml.js'
import snackbar from './snackbar.js'
import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate'
registerTranslateConfig({
loader: lang => fetch(`/language/${lang}.json`).then(res => res.json())
})
import '@material/mwc-snackbar'
import '@material/mwc-button'
import '@material/mwc-dialog'
import '@material/mwc-icon'
import '@polymer/paper-tooltip/paper-tooltip.js'
import '@polymer/paper-spinner/paper-spinner-lite.js'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class NameMenu extends LitElement {
static get properties() {
return {
toblockaddress: { type: String, attribute: true },
nametodialog: { type: String, attribute: true },
chatBlockedAdresses: { type: Array },
selectedAddress: { type: Object },
config: { type: Object },
myAddress: { type: Object, reflect: true },
messages: { type: Array },
btnDisable: { type: Boolean },
isLoading: { type: Boolean },
balance: { type: Number }
}
}
static get styles() {
return css`
* {
--mdc-theme-primary: rgb(3, 169, 244);
--mdc-theme-secondary: var(--mdc-theme-primary);
--mdc-dialog-content-ink-color: var(--black);
--mdc-theme-surface: var(--white);
--mdc-theme-text-primary-on-background: var(--black);
--paper-input-container-focus-color: var(--mdc-theme-primary);
--lumo-primary-text-color: rgb(0, 167, 245);
--lumo-primary-color-50pct: rgba(0, 167, 245, 0.5);
--lumo-primary-color-10pct: rgba(0, 167, 245, 0.1);
--lumo-primary-color: hsl(199, 100%, 48%);
--lumo-base-color: var(--white);
--lumo-body-text-color: var(--black);
}
a {
background-color: transparent;
color: var(--black);
text-decoration: none;
display: inline;
position: relative;
}
a:hover {
background-color: transparent;
color: var(--black);
text-decoration: none;
display: inline;
position: relative;
}
a:after {
content: '';
position: absolute;
width: 100%;
transform: scaleX(0);
height: 2px;
bottom: 0;
left: 0;
background-color: #03a9f4;
transform-origin: bottom right;
transition: transform 0.25s ease-out;
}
a:hover:after {
transform: scaleX(1);
transform-origin: bottom left;
}
.block {
}
.red {
--mdc-theme-primary: red;
}
h2 {
margin:0;
}
h2, h3, h4, h5 {
color:# var(--black);
font-weight: 400;
}
.custom {
--paper-tooltip-background: #03a9f4;
--paper-tooltip-text-color: #fff;
}
.dropdown {
position: relative;
display: inline;
}
.dropdown a:hover {
background-color: transparent;
}
.dropdown-content {
display: none;
position: absolute;
bottom: 25px;
left: 10px;
background-color: var(--white);
min-width: 200px;
overflow: auto;
border: 1px solid transparent;
border-radius: 10px;
box-shadow: var(--qchatshadow);
z-index: 1;
}
.dropdown-content span {
color: var(--nav-text-color);
text-align: center;
padding-top: 12px;
display: block;
}
.dropdown-content a {
color: var(--nav-text-color);
padding: 12px 12px;
text-decoration: none;
display: block;
}
.dropdown-content a:hover {
background-color: var(--nav-color-hover);
}
.showList {
display: block;
}
.input {
width: 90%;
border: none;
display: inline-block;
font-size: 16px;
padding: 10px 20px;
border-radius: 5px;
resize: none;
background: #eee;
}
.textarea {
width: 90%;
border: none;
display: inline-block;
font-size: 16px;
padding: 10px 20px;
border-radius: 5px;
height: 120px;
resize: none;
background: #eee;
}
`
}
constructor() {
super()
this.chatBlockedAdresses = []
this.selectedAddress = window.parent.reduxStore.getState().app.selectedAddress.address
this.myAddress = {}
this.balance = 1
this.messages = []
this.btnDisable = false
this.isLoading = false
}
render() {
return html`
<div class="dropdown">
<a class="block" id="myNameMenu" href="#" @click="${() => this.myMenu()}">${this.nametodialog}</a>
<paper-tooltip class="custom" for="myNameMenu" position="right">${translate("blockpage.bcchange7")}</paper-tooltip>
<div id="myDropdown" class="dropdown-content">
<span>${this.nametodialog}</span>
<hr style="color: var(--nav-text-color); border-radius: 90%;">
<a href="#" @click="${() => this.shadowRoot.querySelector('#blockNameDialog').show()}">${translate("blockpage.bcchange1")}</a>
<a href="#" @click="${() => this.copyToClipboard(this.toblockaddress)}">${translate("blockpage.bcchange8")}</a>
<a href="#" @click="${() => this.shadowRoot.querySelector('#startPmDialog').show()}">${translate("blockpage.bcchange9")}</a>
<a class="block" href="#" @click="${() => this.myMenu()}">${translate("general.close")}</a>
</div>
</div>
<mwc-dialog id="blockNameDialog">
<div style="text-align:center">
<h1>${translate("blockpage.bcchange5")}</h1>
<hr>
<h2>${translate("blockpage.bcchange6")}</h2><br>
<h2>${this.nametodialog}</h2>
</div>
<mwc-button
slot="secondaryAction"
@click="${() => this.chatBlockAddress()}"
class="block"
>
${translate("general.yes")}
</mwc-button>
<mwc-button
slot="primaryAction"
@click="${() => this.myMenu()}"
class="block red"
>
${translate("general.no")}
</mwc-button>
</mwc-dialog>
<mwc-dialog id="startPmDialog" scrimClickAction="${this.isLoading ? '' : 'close'}">
<div style="text-align:center">
<h1>${translate("welcomepage.wcchange2")}</h1>
<hr>
</div>
<p>${translate("welcomepage.wcchange3")}</p>
<textarea class="input" ?disabled=${this.isLoading} id="sendTo" rows="1">${this.nametodialog}</textarea>
<p style="margin-bottom:0;">
<textarea class="textarea" @keydown=${(e) => this._textArea(e)} ?disabled=${this.isLoading} id="messageBox" placeholder="${translate("welcomepage.wcchange5")}" rows="1"></textarea>
</p>
<mwc-button ?disabled="${this.isLoading}" slot="primaryAction" @click=${() => {
this._sendMessage();
}
}>
${translate("welcomepage.wcchange6")}</mwc-button>
<mwc-button
?disabled="${this.isLoading}"
slot="secondaryAction"
@click="${() => this.myMenu()}"
class="block red"
>
${translate("general.close")}
</mwc-button>
</mwc-dialog>
`
}
firstUpdated() {
this.getChatBlockedAdresses();
setInterval(() => {
this.getChatBlockedAdresses();
}, 60000)
window.addEventListener('storage', () => {
const checkLanguage = localStorage.getItem('qortalLanguage');
use(checkLanguage);
})
window.onclick = function(event) {
if (!event.target.matches('.block')) {
var dropdowns = document.getElementsByClassName('dropdown-content');
var i;
for (i = 0; i < dropdowns.length; i++) {
var openDropdown = dropdowns[i];
if (openDropdown.classList.contains('showList')) {
openDropdown.classList.remove('showList');
}
}
}
}
const stopKeyEventPropagation = (e) => {
e.stopPropagation();
return false;
}
this.shadowRoot.getElementById('sendTo').addEventListener('keydown', stopKeyEventPropagation);
this.shadowRoot.getElementById('messageBox').addEventListener('keydown', stopKeyEventPropagation);
const getDataFromURL = () => {
let tempUrl = document.location.href
let splitedUrl = decodeURI(tempUrl).split('?')
let urlData = splitedUrl[1]
if (urlData !== undefined) {
this.chatId = urlData
}
}
let configLoaded = false
parentEpml.ready().then(() => {
parentEpml.subscribe('selected_address', async selectedAddress => {
this.selectedAddress = {}
selectedAddress = JSON.parse(selectedAddress)
if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
this.selectedAddress = selectedAddress
})
parentEpml.request('apiCall', {
url: `/addresses/balance/${window.parent.reduxStore.getState().app.selectedAddress.address}`
}).then(res => {
this.balance = res
})
})
parentEpml.imReady()
}
changeLanguage() {
const checkLanguage = localStorage.getItem('qortalLanguage')
if (checkLanguage === null || checkLanguage.length === 0) {
localStorage.setItem('qortalLanguage', 'us')
use('us')
} else {
use(checkLanguage)
}
}
myMenu() {
this.shadowRoot.getElementById('myDropdown').classList.toggle('showList')
this.shadowRoot.querySelector('#blockNameDialog').close()
this.shadowRoot.querySelector('#startPmDialog').close()
}
closeMenu() {
this.shadowRoot.getElementById('myDropdown').classList.toggle('showList')
var dropdowns = document.getElementsByClassName('dropdown-content');
var i;
for (i = 0; i < dropdowns.length; i++) {
var openDropdown = dropdowns[i];
if (openDropdown.classList.contains('showList')) {
openDropdown.classList.remove('showList');
}
}
}
async copyToClipboard(text) {
try {
let copyString1 = get("walletpage.wchange4")
await navigator.clipboard.writeText(text)
parentEpml.request('showSnackBar', `${copyString1}`)
this.closeMenu()
} catch (err) {
let copyString2 = get("walletpage.wchange39")
parentEpml.request('showSnackBar', `${copyString2}`)
console.error('Copy to clipboard error:', err)
this.closeMenu()
}
}
relMessages() {
setTimeout(() => {
window.location.href = window.location.href.split( '#' )[0]
}, 500)
}
async getChatBlockedAdresses() {
const chatBlockedAdresses = await parentEpml.request('apiCall', {
url: `/lists/blockedAddresses?apiKey=${this.getApiKey()}`
})
this.chatBlockedAdresses = chatBlockedAdresses
}
async chatBlockAddress() {
let address = this.toblockaddress
let items = [
address
]
let addressJsonString = JSON.stringify({ "items": items })
let ret = await parentEpml.request('apiCall', {
url: `/lists/blockedAddresses?apiKey=${this.getApiKey()}`,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: `${addressJsonString}`
})
if (ret === true) {
this.chatBlockedAdresses = this.chatBlockedAdresses.filter(item => item != address)
this.chatBlockedAdresses.push(address)
this.getChatBlockedList()
this.closeMenu()
this.shadowRoot.querySelector('#blockNameDialog').close()
let err1string = get("blockpage.bcchange2")
snackbar.add({
labelText: `${err1string}`,
dismiss: true
})
this.relMessages()
} else {
this.closeMenu()
this.shadowRoot.querySelector('#blockNameDialog').close()
let err2string = get("blockpage.bcchange2")
snackbar.add({
labelText: `${err2string}`,
dismiss: true
})
}
return ret
}
getChatBlockedList() {
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 blockedAddressesUrl = `${nodeUrl}/lists/blockedAddresses?apiKey=${this.getApiKey()}`
const err3string = 'No registered name'
localStorage.removeItem("ChatBlockedAddresses")
var obj = [];
fetch(blockedAddressesUrl).then(response => {
return response.json()
}).then(data => {
return data.map(item => {
const noName = {
name: err3string,
owner: item
}
fetch(`${nodeUrl}/names/address/${item}?limit=0&reverse=true`).then(res => {
return res.json()
}).then(jsonRes => {
if(jsonRes.length) {
jsonRes.map (item => {
obj.push(item)
})
} else {
obj.push(noName)
}
localStorage.setItem("ChatBlockedAddresses", JSON.stringify(obj))
})
})
})
}
_sendMessage() {
this.isLoading = true
const recipient = this.shadowRoot.getElementById('sendTo').value
const messageBox = this.shadowRoot.getElementById('messageBox')
const messageText = messageBox.value
if (recipient.length === 0) {
this.isLoading = false
} else if (messageText.length === 0) {
this.isLoading = false
} else {
this.sendMessage()
}
}
async sendMessage(e) {
this.isLoading = true
const _recipient = this.shadowRoot.getElementById('sendTo').value
const messageBox = this.shadowRoot.getElementById('messageBox')
const messageText = messageBox.value
let recipient
const validateName = async (receiverName) => {
let myRes
let myNameRes = await parentEpml.request('apiCall', {
type: 'api',
url: `/names/${receiverName}`
})
if (myNameRes.error === 401) {
myRes = false
} else {
myRes = myNameRes
}
return myRes
}
const myNameRes = await validateName(_recipient)
if (!myNameRes) {
recipient = _recipient
} else {
recipient = myNameRes.owner
}
let _reference = new Uint8Array(64);
window.crypto.getRandomValues(_reference);
let sendTimestamp = Date.now()
let reference = window.parent.Base58.encode(_reference)
const getAddressPublicKey = async () => {
let isEncrypted
let _publicKey
let addressPublicKey = await parentEpml.request('apiCall', {
type: 'api',
url: `/addresses/publickey/${recipient}`
})
if (addressPublicKey.error === 102) {
_publicKey = false
// Do something here...
let err1string = get("welcomepage.wcchange7")
parentEpml.request('showSnackBar', `${err1string}`)
this.isLoading = false
} else if (addressPublicKey !== false) {
isEncrypted = 1
_publicKey = addressPublicKey
sendMessageRequest(isEncrypted, _publicKey)
} else {
isEncrypted = 0
_publicKey = this.selectedAddress.address
sendMessageRequest(isEncrypted, _publicKey)
}
};
const sendMessageRequest = async (isEncrypted, _publicKey) => {
const messageObject = {
messageText,
images: [''],
repliedTo: '',
version: 1
}
const stringifyMessageObject = JSON.stringify(messageObject)
let chatResponse = await parentEpml.request('chat', {
type: 18,
nonce: this.selectedAddress.nonce,
params: {
timestamp: sendTimestamp,
recipient: recipient,
recipientPublicKey: _publicKey,
hasChatReference: 0,
message: stringifyMessageObject,
lastReference: reference,
proofOfWorkNonce: 0,
isEncrypted: isEncrypted,
isText: 1
}
})
_computePow(chatResponse)
}
const _computePow = async (chatBytes) => {
const _chatBytesArray = Object.keys(chatBytes).map(function (key) { return chatBytes[key]; });
const chatBytesArray = new Uint8Array(_chatBytesArray)
const chatBytesHash = new window.parent.Sha256().process(chatBytesArray).finish().result
const hashPtr = window.parent.sbrk(32, window.parent.heap);
const hashAry = new Uint8Array(window.parent.memory.buffer, hashPtr, 32);
hashAry.set(chatBytesHash);
const difficulty = this.balance < 4 ? 18 : 8;
const workBufferLength = 8 * 1024 * 1024;
const workBufferPtr = window.parent.sbrk(workBufferLength, window.parent.heap);
let nonce = window.parent.computePow(hashPtr, workBufferPtr, workBufferLength, difficulty)
let _response = await parentEpml.request('sign_chat', {
nonce: this.selectedAddress.nonce,
chatBytesArray: chatBytesArray,
chatNonce: nonce
})
getSendChatResponse(_response)
}
const getSendChatResponse = (response) => {
if (response === true) {
messageBox.value = ""
let err2string = get("welcomepage.wcchange8")
parentEpml.request('showSnackBar', `${err2string}`)
this.isLoading = false
this.closeMenu()
} else if (response.error) {
parentEpml.request('showSnackBar', response.message)
this.isLoading = false
this.closeMenu()
} else {
let err3string = get("welcomepage.wcchange9")
parentEpml.request('showSnackBar', `${err3string}`)
this.isLoading = false
this.closeMenu()
}
}
getAddressPublicKey()
}
_textMenu(event) {
const getSelectedText = () => {
var text = "";
if (typeof window.getSelection != "undefined") {
text = window.getSelection().toString();
} else if (typeof this.shadowRoot.selection != "undefined" && this.shadowRoot.selection.type == "Text") {
text = this.shadowRoot.selection.createRange().text;
}
return text;
}
const checkSelectedTextAndShowMenu = () => {
let selectedText = getSelectedText();
if (selectedText && typeof selectedText === 'string') {
let _eve = { pageX: event.pageX, pageY: event.pageY, clientX: event.clientX, clientY: event.clientY }
let textMenuObject = { selectedText: selectedText, eventObject: _eve, isFrame: true }
parentEpml.request('openCopyTextMenu', textMenuObject)
}
}
checkSelectedTextAndShowMenu()
}
_textArea(e) {
if (e.keyCode === 13 && !e.shiftKey) this._sendMessage()
}
isEmptyArray(arr) {
if (!arr) { return true }
return arr.length === 0
}
getApiKey() {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
let apiKey = myNode.apiKey;
return apiKey;
}
}
window.customElements.define('name-menu', NameMenu)

View File

@@ -0,0 +1,599 @@
var VERSIONS = [
null,
[[10, 7,17,13], [ 1, 1, 1, 1], []],
[[16,10,28,22], [ 1, 1, 1, 1], [4,16]],
[[26,15,22,18], [ 1, 1, 2, 2], [4,20]],
[[18,20,16,26], [ 2, 1, 4, 2], [4,24]],
[[24,26,22,18], [ 2, 1, 4, 4], [4,28]],
[[16,18,28,24], [ 4, 2, 4, 4], [4,32]],
[[18,20,26,18], [ 4, 2, 5, 6], [4,20,36]],
[[22,24,26,22], [ 4, 2, 6, 6], [4,22,40]],
[[22,30,24,20], [ 5, 2, 8, 8], [4,24,44]],
[[26,18,28,24], [ 5, 4, 8, 8], [4,26,48]],
[[30,20,24,28], [ 5, 4,11, 8], [4,28,52]],
[[22,24,28,26], [ 8, 4,11,10], [4,30,56]],
[[22,26,22,24], [ 9, 4,16,12], [4,32,60]],
[[24,30,24,20], [ 9, 4,16,16], [4,24,44,64]],
[[24,22,24,30], [10, 6,18,12], [4,24,46,68]],
[[28,24,30,24], [10, 6,16,17], [4,24,48,72]],
[[28,28,28,28], [11, 6,19,16], [4,28,52,76]],
[[26,30,28,28], [13, 6,21,18], [4,28,54,80]],
[[26,28,26,26], [14, 7,25,21], [4,28,56,84]],
[[26,28,28,30], [16, 8,25,20], [4,32,60,88]],
[[26,28,30,28], [17, 8,25,23], [4,26,48,70,92]],
[[28,28,24,30], [17, 9,34,23], [4,24,48,72,96]],
[[28,30,30,30], [18, 9,30,25], [4,28,52,76,100]],
[[28,30,30,30], [20,10,32,27], [4,26,52,78,104]],
[[28,26,30,30], [21,12,35,29], [4,30,56,82,108]],
[[28,28,30,28], [23,12,37,34], [4,28,56,84,112]],
[[28,30,30,30], [25,12,40,34], [4,32,60,88,116]],
[[28,30,30,30], [26,13,42,35], [4,24,48,72,96,120]],
[[28,30,30,30], [28,14,45,38], [4,28,52,76,100,124]],
[[28,30,30,30], [29,15,48,40], [4,24,50,76,102,128]],
[[28,30,30,30], [31,16,51,43], [4,28,54,80,106,132]],
[[28,30,30,30], [33,17,54,45], [4,32,58,84,110,136]],
[[28,30,30,30], [35,18,57,48], [4,28,56,84,112,140]],
[[28,30,30,30], [37,19,60,51], [4,32,60,88,116,144]],
[[28,30,30,30], [38,19,63,53], [4,28,52,76,100,124,148]],
[[28,30,30,30], [40,20,66,56], [4,22,48,74,100,126,152]],
[[28,30,30,30], [43,21,70,59], [4,26,52,78,104,130,156]],
[[28,30,30,30], [45,22,74,62], [4,30,56,82,108,134,160]],
[[28,30,30,30], [47,24,77,65], [4,24,52,80,108,136,164]],
[[28,30,30,30], [49,25,81,68], [4,28,56,84,112,140,168]]];
var MODE_TERMINATOR = 0;
var MODE_NUMERIC = 1, MODE_ALPHANUMERIC = 2, MODE_OCTET = 4, MODE_KANJI = 8;
var NUMERIC_REGEXP = /^\d*$/;
var ALPHANUMERIC_REGEXP = /^[A-Za-z0-9 $%*+\-./:_]*$/;
var ALPHANUMERIC_OUT_REGEXP = /^[A-Z0-9 $%*+\-./:_]*$/;
var ECCLEVEL_L = 1, ECCLEVEL_M = 0, ECCLEVEL_Q = 3, ECCLEVEL_H = 2;
var GF256_MAP = [], GF256_INVMAP = [-1];
for (var i = 0, v = 1; i < 255; ++i) {
GF256_MAP.push(v);
GF256_INVMAP[v] = i;
v = (v * 2) ^ (v >= 128 ? 0x11d : 0);
}
var GF256_GENPOLY = [[]];
for (var i = 0; i < 30; ++i) {
var prevpoly = GF256_GENPOLY[i], poly = [];
for (var j = 0; j <= i; ++j) {
var a = (j < i ? GF256_MAP[prevpoly[j]] : 0);
var b = GF256_MAP[(i + (prevpoly[j-1] || 0)) % 255];
poly.push(GF256_INVMAP[a ^ b]);
}
GF256_GENPOLY.push(poly);
}
var ALPHANUMERIC_MAP = {};
for (var i = 0; i < 45; ++i) {
ALPHANUMERIC_MAP['0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'.charAt(i)] = i;
}
var MASKFUNCS = [
function(i,j) { return (i+j) % 2 == 0; },
function(i,j) { return i % 2 == 0; },
function(i,j) { return j % 3 == 0; },
function(i,j) { return (i+j) % 3 == 0; },
function(i,j) { return (((i/2)|0) + ((j/3)|0)) % 2 == 0; },
function(i,j) { return (i*j) % 2 + (i*j) % 3 == 0; },
function(i,j) { return ((i*j) % 2 + (i*j) % 3) % 2 == 0; },
function(i,j) { return ((i+j) % 2 + (i*j) % 3) % 2 == 0; }];
var needsverinfo = function(ver) { return ver > 6; };
var getsizebyver = function(ver) { return 4 * ver + 17; };
var nfullbits = function(ver) {
var v = VERSIONS[ver];
var nbits = 16*ver*ver + 128*ver + 64;
if (needsverinfo(ver)) nbits -= 36;
if (v[2].length) {
nbits -= 25 * v[2].length * v[2].length - 10 * v[2].length - 55;
}
return nbits;
};
var ndatabits = function(ver, ecclevel) {
var nbits = nfullbits(ver) & ~7;
var v = VERSIONS[ver];
nbits -= 8 * v[0][ecclevel] * v[1][ecclevel];
return nbits;
}
var ndatalenbits = function(ver, mode) {
switch (mode) {
case MODE_NUMERIC: return (ver < 10 ? 10 : ver < 27 ? 12 : 14);
case MODE_ALPHANUMERIC: return (ver < 10 ? 9 : ver < 27 ? 11 : 13);
case MODE_OCTET: return (ver < 10 ? 8 : 16);
case MODE_KANJI: return (ver < 10 ? 8 : ver < 27 ? 10 : 12);
}
};
var getmaxdatalen = function(ver, mode, ecclevel) {
var nbits = ndatabits(ver, ecclevel) - 4 - ndatalenbits(ver, mode);
switch (mode) {
case MODE_NUMERIC:
return ((nbits/10) | 0) * 3 + (nbits%10 < 4 ? 0 : nbits%10 < 7 ? 1 : 2);
case MODE_ALPHANUMERIC:
return ((nbits/11) | 0) * 2 + (nbits%11 < 6 ? 0 : 1);
case MODE_OCTET:
return (nbits/8) | 0;
case MODE_KANJI:
return (nbits/13) | 0;
}
};
var validatedata = function(mode, data) {
switch (mode) {
case MODE_NUMERIC:
if (!data.match(NUMERIC_REGEXP)) return null;
return data;
case MODE_ALPHANUMERIC:
if (!data.match(ALPHANUMERIC_REGEXP)) return null;
return data.toUpperCase();
case MODE_OCTET:
if (typeof data === 'string') {
var newdata = [];
for (var i = 0; i < data.length; ++i) {
var ch = data.charCodeAt(i);
if (ch < 0x80) {
newdata.push(ch);
} else if (ch < 0x800) {
newdata.push(0xc0 | (ch >> 6),
0x80 | (ch & 0x3f));
} else if (ch < 0x10000) {
newdata.push(0xe0 | (ch >> 12),
0x80 | ((ch >> 6) & 0x3f),
0x80 | (ch & 0x3f));
} else {
newdata.push(0xf0 | (ch >> 18),
0x80 | ((ch >> 12) & 0x3f),
0x80 | ((ch >> 6) & 0x3f),
0x80 | (ch & 0x3f));
}
}
return newdata;
} else {
return data;
}
}
};
var encode = function(ver, mode, data, maxbuflen) {
var buf = [];
var bits = 0, remaining = 8;
var datalen = data.length;
var pack = function(x, n) {
if (n >= remaining) {
buf.push(bits | (x >> (n -= remaining)));
while (n >= 8) buf.push((x >> (n -= 8)) & 255);
bits = 0;
remaining = 8;
}
if (n > 0) bits |= (x & ((1 << n) - 1)) << (remaining -= n);
};
var nlenbits = ndatalenbits(ver, mode);
pack(mode, 4);
pack(datalen, nlenbits);
switch (mode) {
case MODE_NUMERIC:
for (var i = 2; i < datalen; i += 3) {
pack(parseInt(data.substring(i-2,i+1), 10), 10);
}
pack(parseInt(data.substring(i-2), 10), [0,4,7][datalen%3]);
break;
case MODE_ALPHANUMERIC:
for (var i = 1; i < datalen; i += 2) {
pack(ALPHANUMERIC_MAP[data.charAt(i-1)] * 45 +
ALPHANUMERIC_MAP[data.charAt(i)], 11);
}
if (datalen % 2 == 1) {
pack(ALPHANUMERIC_MAP[data.charAt(i-1)], 6);
}
break;
case MODE_OCTET:
for (var i = 0; i < datalen; ++i) {
pack(data[i], 8);
}
break;
};
pack(MODE_TERMINATOR, 4);
if (remaining < 8) buf.push(bits);
while (buf.length + 1 < maxbuflen) buf.push(0xec, 0x11);
if (buf.length < maxbuflen) buf.push(0xec);
return buf;
};
var calculateecc = function(poly, genpoly) {
var modulus = poly.slice(0);
var polylen = poly.length, genpolylen = genpoly.length;
for (var i = 0; i < genpolylen; ++i) modulus.push(0);
for (var i = 0; i < polylen; ) {
var quotient = GF256_INVMAP[modulus[i++]];
if (quotient >= 0) {
for (var j = 0; j < genpolylen; ++j) {
modulus[i+j] ^= GF256_MAP[(quotient + genpoly[j]) % 255];
}
}
}
return modulus.slice(polylen);
};
var augumenteccs = function(poly, nblocks, genpoly) {
var subsizes = [];
var subsize = (poly.length / nblocks) | 0, subsize0 = 0;
var pivot = nblocks - poly.length % nblocks;
for (var i = 0; i < pivot; ++i) {
subsizes.push(subsize0);
subsize0 += subsize;
}
for (var i = pivot; i < nblocks; ++i) {
subsizes.push(subsize0);
subsize0 += subsize+1;
}
subsizes.push(subsize0);
var eccs = [];
for (var i = 0; i < nblocks; ++i) {
eccs.push(calculateecc(poly.slice(subsizes[i], subsizes[i+1]), genpoly));
}
var result = [];
var nitemsperblock = (poly.length / nblocks) | 0;
for (var i = 0; i < nitemsperblock; ++i) {
for (var j = 0; j < nblocks; ++j) {
result.push(poly[subsizes[j] + i]);
}
}
for (var j = pivot; j < nblocks; ++j) {
result.push(poly[subsizes[j+1] - 1]);
}
for (var i = 0; i < genpoly.length; ++i) {
for (var j = 0; j < nblocks; ++j) {
result.push(eccs[j][i]);
}
}
return result;
};
var augumentbch = function(poly, p, genpoly, q) {
var modulus = poly << q;
for (var i = p - 1; i >= 0; --i) {
if ((modulus >> (q+i)) & 1) modulus ^= genpoly << i;
}
return (poly << q) | modulus;
};
var makebasematrix = function(ver) {
var v = VERSIONS[ver], n = getsizebyver(ver);
var matrix = [], reserved = [];
for (var i = 0; i < n; ++i) {
matrix.push([]);
reserved.push([]);
}
var blit = function(y, x, h, w, bits) {
for (var i = 0; i < h; ++i) {
for (var j = 0; j < w; ++j) {
matrix[y+i][x+j] = (bits[i] >> j) & 1;
reserved[y+i][x+j] = 1;
}
}
};
blit(0, 0, 9, 9, [0x7f, 0x41, 0x5d, 0x5d, 0x5d, 0x41, 0x17f, 0x00, 0x40]);
blit(n-8, 0, 8, 9, [0x100, 0x7f, 0x41, 0x5d, 0x5d, 0x5d, 0x41, 0x7f]);
blit(0, n-8, 9, 8, [0xfe, 0x82, 0xba, 0xba, 0xba, 0x82, 0xfe, 0x00, 0x00]);
for (var i = 9; i < n-8; ++i) {
matrix[6][i] = matrix[i][6] = ~i & 1;
reserved[6][i] = reserved[i][6] = 1;
}
var aligns = v[2], m = aligns.length;
for (var i = 0; i < m; ++i) {
var minj = (i==0 || i==m-1 ? 1 : 0), maxj = (i==0 ? m-1 : m);
for (var j = minj; j < maxj; ++j) {
blit(aligns[i], aligns[j], 5, 5, [0x1f, 0x11, 0x15, 0x11, 0x1f]);
}
}
if (needsverinfo(ver)) {
var code = augumentbch(ver, 6, 0x1f25, 12);
var k = 0;
for (var i = 0; i < 6; ++i) {
for (var j = 0; j < 3; ++j) {
matrix[i][(n-11)+j] = matrix[(n-11)+j][i] = (code >> k++) & 1;
reserved[i][(n-11)+j] = reserved[(n-11)+j][i] = 1;
}
}
}
return {matrix: matrix, reserved: reserved};
};
var putdata = function(matrix, reserved, buf) {
var n = matrix.length;
var k = 0, dir = -1;
for (var i = n-1; i >= 0; i -= 2) {
if (i == 6) --i;
var jj = (dir < 0 ? n-1 : 0);
for (var j = 0; j < n; ++j) {
for (var ii = i; ii > i-2; --ii) {
if (!reserved[jj][ii]) {
matrix[jj][ii] = (buf[k >> 3] >> (~k&7)) & 1;
++k;
}
}
jj += dir;
}
dir = -dir;
}
return matrix;
};
var maskdata = function(matrix, reserved, mask) {
var maskf = MASKFUNCS[mask];
var n = matrix.length;
for (var i = 0; i < n; ++i) {
for (var j = 0; j < n; ++j) {
if (!reserved[i][j]) matrix[i][j] ^= maskf(i,j);
}
}
return matrix;
}
var putformatinfo = function(matrix, reserved, ecclevel, mask) {
var n = matrix.length;
var code = augumentbch((ecclevel << 3) | mask, 5, 0x537, 10) ^ 0x5412;
for (var i = 0; i < 15; ++i) {
var r = [0,1,2,3,4,5,7,8,n-7,n-6,n-5,n-4,n-3,n-2,n-1][i];
var c = [n-1,n-2,n-3,n-4,n-5,n-6,n-7,n-8,7,5,4,3,2,1,0][i];
matrix[r][8] = matrix[8][c] = (code >> i) & 1;
}
return matrix;
};
var evaluatematrix = function(matrix) {
var PENALTY_CONSECUTIVE = 3;
var PENALTY_TWOBYTWO = 3;
var PENALTY_FINDERLIKE = 40;
var PENALTY_DENSITY = 10;
var evaluategroup = function(groups) {
var score = 0;
for (var i = 0; i < groups.length; ++i) {
if (groups[i] >= 5) score += PENALTY_CONSECUTIVE + (groups[i]-5);
}
for (var i = 5; i < groups.length; i += 2) {
var p = groups[i];
if (groups[i-1] == p && groups[i-2] == 3*p && groups[i-3] == p &&
groups[i-4] == p && (groups[i-5] >= 4*p || groups[i+1] >= 4*p)) {
score += PENALTY_FINDERLIKE;
}
}
return score;
};
var n = matrix.length;
var score = 0, nblacks = 0;
for (var i = 0; i < n; ++i) {
var row = matrix[i];
var groups;
groups = [0];
for (var j = 0; j < n; ) {
var k;
for (k = 0; j < n && row[j]; ++k) ++j;
groups.push(k);
for (k = 0; j < n && !row[j]; ++k) ++j;
groups.push(k);
}
score += evaluategroup(groups);
groups = [0];
for (var j = 0; j < n; ) {
var k;
for (k = 0; j < n && matrix[j][i]; ++k) ++j;
groups.push(k);
for (k = 0; j < n && !matrix[j][i]; ++k) ++j;
groups.push(k);
}
score += evaluategroup(groups);
var nextrow = matrix[i+1] || [];
nblacks += row[0];
for (var j = 1; j < n; ++j) {
var p = row[j];
nblacks += p;
if (row[j-1] == p && nextrow[j] === p && nextrow[j-1] === p) {
score += PENALTY_TWOBYTWO;
}
}
}
score += PENALTY_DENSITY * ((Math.abs(nblacks / n / n - 0.5) / 0.05) | 0);
return score;
};
var generate = function(data, ver, mode, ecclevel, mask) {
var v = VERSIONS[ver];
var buf = encode(ver, mode, data, ndatabits(ver, ecclevel) >> 3);
buf = augumenteccs(buf, v[1][ecclevel], GF256_GENPOLY[v[0][ecclevel]]);
var result = makebasematrix(ver);
var matrix = result.matrix, reserved = result.reserved;
putdata(matrix, reserved, buf);
if (mask < 0) {
maskdata(matrix, reserved, 0);
putformatinfo(matrix, reserved, ecclevel, 0);
var bestmask = 0, bestscore = evaluatematrix(matrix);
maskdata(matrix, reserved, 0);
for (mask = 1; mask < 8; ++mask) {
maskdata(matrix, reserved, mask);
putformatinfo(matrix, reserved, ecclevel, mask);
var score = evaluatematrix(matrix);
if (bestscore > score) {
bestscore = score;
bestmask = mask;
}
maskdata(matrix, reserved, mask);
}
mask = bestmask;
}
maskdata(matrix, reserved, mask);
putformatinfo(matrix, reserved, ecclevel, mask);
return matrix;
};
var QRCode = {
'generate': function(data, options) {
var MODES = {'numeric': MODE_NUMERIC, 'alphanumeric': MODE_ALPHANUMERIC,
'octet': MODE_OCTET};
var ECCLEVELS = {'L': ECCLEVEL_L, 'M': ECCLEVEL_M, 'Q': ECCLEVEL_Q,
'H': ECCLEVEL_H};
options = options || {};
var ver = options.version || -1;
var ecclevel = ECCLEVELS[(options.ecclevel || 'L').toUpperCase()];
var mode = options.mode ? MODES[options.mode.toLowerCase()] : -1;
var mask = 'mask' in options ? options.mask : -1;
if (mode < 0) {
if (typeof data === 'string') {
if (data.match(NUMERIC_REGEXP)) {
mode = MODE_NUMERIC;
} else if (data.match(ALPHANUMERIC_OUT_REGEXP)) {
mode = MODE_ALPHANUMERIC;
} else {
mode = MODE_OCTET;
}
} else {
mode = MODE_OCTET;
}
} else if (!(mode == MODE_NUMERIC || mode == MODE_ALPHANUMERIC ||
mode == MODE_OCTET)) {
throw 'invalid or unsupported mode';
}
data = validatedata(mode, data);
if (data === null) throw 'invalid data format';
if (ecclevel < 0 || ecclevel > 3) throw 'invalid ECC level';
if (ver < 0) {
for (ver = 1; ver <= 40; ++ver) {
if (data.length <= getmaxdatalen(ver, mode, ecclevel)) break;
}
if (ver > 40) throw 'too large data';
} else if (ver < 1 || ver > 40) {
throw 'invalid version';
}
if (mask != -1 && (mask < 0 || mask > 8)) throw 'invalid mask';
return generate(data, ver, mode, ecclevel, mask);
},
'generateHTML': function(data, options) {
options = options || {};
var matrix = QRCode['generate'](data, options);
var modsize = Math.max(options.modulesize || 5, 0.5);
var margin = Math.max(options.margin !== null ? options.margin : 4, 0.0);
var e = document.createElement('div');
var n = matrix.length;
var html = ['<table border="0" cellspacing="0" cellpadding="0" style="border:' + modsize*margin + 'px solid transparent;background:transparent;">'];
for (var i = 0; i < n; ++i) {
html.push('<tr>');
for (var j = 0; j < n; ++j) {
html.push('<td style="width:' + modsize + 'px;height:' + modsize + 'px' +
(matrix[i][j] ? ';background:var(--black);' : '') + '"></td>');
}
html.push('</tr>');
}
e.className = 'qrcode';
e.innerHTML = html.join('') + '</table>';
return e;
},
'generateSVG': function(data, options) {
options = options || {};
var matrix = QRCode['generate'](data, options);
var n = matrix.length;
var modsize = Math.max(options.modulesize || 5, 0.5);
var margin = Math.max(options.margin? options.margin : 4, 0.0);
var size = modsize * (n + 2 * margin);
var common = ' class= "fg"'+' width="'+modsize+'" height="'+modsize+'"/>';
var e = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
e.setAttribute('viewBox', '0 0 '+size+' '+size);
e.setAttribute('style', 'shape-rendering:crispEdges');
var svg = [
'<style scoped>.bg{fill:#FFF}.fg{fill:#03A9F4}</style>',
'<rect class="bg" x="0" y="0"',
'width="'+size+'" height="'+size+'"/>',
];
var yo = margin * modsize;
for (var y = 0; y < n; ++y) {
var xo = margin * modsize;
for (var x = 0; x < n; ++x) {
if (matrix[y][x])
svg.push('<rect x="'+xo+'" y="'+yo+'"', common);
xo += modsize;
}
yo += modsize;
}
e.innerHTML = svg.join('');
return e;
},
'generatePNG': function(data, options) {
options = options || {};
var matrix = QRCode['generate'](data, options);
var modsize = Math.max(options.modulesize || 5, 0.5);
var margin = Math.max(options.margin != null ? options.margin : 4, 0.0);
var n = matrix.length;
var size = modsize * (n + 2 * margin);
var canvas = document.createElement('canvas'), context;
canvas.width = canvas.height = size;
context = canvas.getContext('2d');
if (!context) throw 'canvas support is needed for PNG output';
context.fillStyle = '#fff';
context.fillRect(0, 0, size, size);
context.fillStyle = '#000';
for (var i = 0; i < n; ++i) {
for (var j = 0; j < n; ++j) {
if (matrix[i][j]) {
context.fillRect(modsize * (margin + j),
modsize * (margin + i),
modsize, modsize);
}
}
}
return canvas.toDataURL();
}
};
export { QRCode };
export default QRCode;

View File

@@ -0,0 +1,162 @@
function saveFile({ data, debug, filename }) {
if (debug) console.log("[qortal-file-saver] starting with ", { data, debug, filename });
if (!data) throw new Error("[qortal-file-saver] You must pass in data");
if (!filename) {
if (typeof window !== "undefined" && typeof File !== undefined && data instanceof File) {
filename = data.name;
}
throw new Error("[qortal-file-saver] You must pass in filename");
}
const constructorName = (typeof data === "object" && typeof data.constructor === "function" && data.constructor.name) || null;
if (debug) console.log("constructorName:", constructorName);
const ext = filename.substr(filename.lastIndexOf(".")).toLowerCase();
const A = ({ href, download }) => {
const a = document.createElement("a");
a.href = href;
a.download = download;
return a;
};
function convertImageToCanvas({ debug, img }) {
if (debug) console.log("[qortal-file-saver] starting convertImageToCanvas");
const height = img.height;
const width = img.width;
const canvas = document.createElement("canvas");
canvas.height = height;
canvas.width = width;
const context = canvas.getContext("2d");
context.drawImage(img, 0, 0, width, height);
return canvas;
}
function saveHTML({ data, debug, filename }) {
if (typeof data === "object" && "outerHTML" in data) {
if (debug) console.log("[qortal-file-saver] data appears to be an HTML element, so grabbing it's outer HTML");
data = data.outerHTML;
}
const url = "data:text/html," + encodeURIComponent(data);
saveDataOrBlobURL({ url, debug, filename });
}
function saveCanvas({ data, debug, filename, imageType }) {
const url = data.toDataURL("image/" + imageType);
saveDataOrBlobURL({ url, debug, filename });
}
function saveDataOrBlobURL({ url, debug, filename }) {
A({ href: url, download: filename }).click();
}
function saveImageAsJPG({ data: img, debug, filename }) {
if (debug) console.log("starting saveImageAsJPG");
const canvas = convertImageToCanvas({ debug, img });
saveCanvasAsJPG({ data: canvas, debug, filename });
}
function saveImageAsPNG({ data: img, debug, filename }) {
if (debug) console.log("starting saveImageAsPNG");
const canvas = convertImageToCanvas({ debug, img });
saveCanvasAsPNG({ data: canvas, debug, filename });
}
function saveImageAsWebP({ data: img, debug, filename }) {
if (debug) console.log("starting saveImageAsWebP");
const canvas = convertImageToCanvas({ debug, img });
saveCanvasAsWebP({ data: canvas, debug, filename });
}
function saveCanvasAsJPG({ data, debug, filename }) {
saveCanvas({ data, debug, filename, imageType: "jpeg" });
}
function saveCanvasAsPNG({ data, debug, filename }) {
saveCanvas({ data, debug, filename, imageType: "png" });
}
function saveCanvasAsWebP({ data, debug, filename }) {
saveCanvas({ data, debug, filename, imageType: "webp" });
}
function saveDSV({ data, debug, delimiter, filename, mediatype }) {
if (!Array.isArray(data)) throw new Error("[qortal-saver] data must be an array to save as a CSV");
if (!delimiter) throw new Error("[qortal-saver] delimiter must be set");
if (!mediatype) throw new Error("[qortal-saver] mediatype must be set");
let output = "data:" + mediatype + ";charset=utf-8,";
const columns = Array.from(new Set(data.map(Object.keys).flat())).sort();
const types = new Set(data.map(it => (Array.isArray(it) ? "array" : typeof it)));
const includeHeader = types.has("object");
if (debug) console.log("includeHeader:", includeHeader);
if (includeHeader) output += columns.map(c => '"' + c.replace(/,/g, "\\,") + '"') + "\n";
for (let i = 0; i < data.length; i++) {
const row = data[i];
if (i !== 0) output += "\n";
output += columns.map(col => '"' + row[col].toString().replace(/,/g, "\\,") + '"');
}
const url = encodeURI(output);
saveDataOrBlobURL({ url, debug, filename });
}
function saveCSV({ data, debug, filename }) {
saveDSV({ data, debug, delimiter: ",", filename, mediatype: "text/csv" });
}
function saveTSV({ data, debug, filename }) {
saveDSV({ data, debug, delimiter: "\t", filename, mediatype: "text/tab-separated-values" });
}
function saveJSON({ data, debug, filename }) {
const url = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, undefined, 2));
saveDataOrBlobURL({ url, debug, filename });
}
function saveText({ data, debug, filename }) {
const url = "data:text/plain;charset=utf-8," + encodeURIComponent(data);
saveDataOrBlobURL({ url, debug, filename });
}
function saveBlob({ data, debug, filename }) {
const url = URL.createObjectURL(data);
if (debug) console.log("[qortal-file-saver.saveBlob] url:", url);
saveDataOrBlobURL({ url, debug, filename });
URL.revokeObjectURL(url);
}
if (ext === ".csv") {
saveCSV({ data, debug, filename });
} else if (ext === ".tsv") {
saveTSV({ data, debug, filename });
} else if (ext === ".html") {
saveHTML({ data, debug, filename });
} else if (ext === ".json" || ext === ".geojson" || ext === ".topojson") {
saveJSON({ data, debug, filename });
} else if (ext === ".txt" || ext === ".js" || ext === ".py") {
saveText({ data, debug, filename });
} else if (constructorName === "HTMLCanvasElement" && ext === ".png") {
saveCanvasAsPNG({ data, debug, filename });
} else if (constructorName === "HTMLCanvasElement" && ext === ".jpg") {
saveCanvasAsJPG({ data, debug, filename });
} else if (constructorName === "HTMLCanvasElement" && ext === ".webp") {
saveCanvasAsWebP({ data, debug, filename });
} else if (constructorName === "HTMLImageElement" && ext === ".jpg") {
saveImageAsJPG({ data, debug, filename });
} else if (constructorName === "HTMLImageElement" && ext === ".png") {
saveImageAsPNG({ data, debug, filename });
} else if (constructorName === "HTMLImageElement" && ext === ".webp") {
saveImageAsWebP({ data, debug, filename });
} else if (constructorName === "Blob") {
saveBlob({ data, debug, filename });
} else {
throw new Error('[qortal-file-saver] unrecognized extension "' + ext + '"');
}
}
if (typeof define === "function" && define.amd)
define(function () {
return saveFile;
});
if (typeof module === "object") module.exports = saveFile;
if (typeof window === "object") window.saveFile = saveFile;
if (typeof self === "object") self.saveFile = saveFile;

View File

@@ -0,0 +1,192 @@
import { LitElement, html, css } from 'lit'
import { render } from 'lit/html.js'
import QRCode from './QRCode.js'
export class QortalQrcodeGenerator extends LitElement {
static get properties() {
return {
auto: { type: Boolean },
data: { type: String },
debug: { type: Boolean },
ecclevel: { type: String },
format: { type: String },
margin: { type: Number },
mask: { type: Number, value: -1 },
mode: { type: String },
modulesize: { type: Number },
version: { type: Number },
qrcode: { type: Object }
}
}
static get styles() {
return css`
:host {
display: block;
}
`
}
constructor() {
super()
this.auto = false
this.debug = false
this.ecclevel = 'L'
this.format = 'html'
this.mode = 'numeric'
this.margin = 4
this.mask = -1
this.modulesize = 5
this.version = -1
this.qrcode = null
}
render() {
return html`
<div id="qrCodeContainer">
${this.qrcode ? this.qrcode : ''}
</div>
`
}
validateParams() {
return (
this.validateEcclevel() &&
this.validateFormat() &&
this.validateMask() &&
this.validateMode() &&
this.validateModulesize() &&
this.validateVersion()
)
}
validateEcclevel() {
if (
this.ecclevel === 'L' ||
this.ecclevel === 'M' ||
this.ecclevel === 'Q' ||
this.ecclevel === 'H'
) {
return true
}
console.error('[qortal-qrcode-generator] validateEcclevel - Invalid value of `ecclevel`', this.ecclevel)
return false
}
validateFormat() {
if (this.format == 'html' || this.format == 'png') {
return true
}
console.error('[qortal-qrcode-generator] validateFormat - Invalid value of `format`', this.format)
return false
}
validateMargin() {
if (this.margin >= -1) {
return true
}
console.error('[qortal-qrcode-generator] validateMargin - Invalid value of `margin`', this.margin)
return false
}
validateMask() {
if (this.mask >= -1 && this.mask <= 7) {
return true
}
console.error('[qortal-qrcode-generator] validateMask - Invalid value of `mask`', this.mask)
return false
}
validateMode() {
if (this.mode === 'numeric' || this.mode === 'alphanumeric' || this.mode === 'octet') {
return true
}
console.error('[qortal-qrcode-generator] validateMode - Invalid value of `mode`', this.mode)
return false
}
validateModulesize() {
if (this.modulesize >= 0.5) {
return true
}
console.error('[qortal-qrcode-generator] validateModulesize - Invalid value of `modulesize`', this.modulesize)
return false
}
validateVersion() {
if (this.version == -1 || (this.version >= 0 && this.version <= 40)) {
return true
}
console.error('[qortal-qrcode-generator] validateVersion - Invalid value of `version`', this.version)
return false
}
getOptions() {
return {
modulesize: this.modulesize,
margin: this.margin,
version: this.version,
mode: this.mode,
ecclevel: this.ecclevel,
mask: this.mask,
}
}
generateQRCodePNG() {
let img
try {
img = document.createElement('img')
img.src = QRCode.generatePNG(this.data, this.getOptions())
this.qrcode = img;
} catch (e) {
console.error('[qortal-qrcode-generator] generateQRCodePNG - No canvas support', e)
}
}
generateQRCodeHTML() {
if (this.debug) {
console.debug('[qortal-qrcode-generator] generateQRCodeHTML - data ', this.data)
}
this.qrcode = QRCode.generateHTML(this.data, this.getOptions())
}
generateQRCode() {
if (this.debug) {
console.log('[qortal-qrcode-generator] generateQRCode', this.validateParams());
}
if (!this.validateParams()) {
return
}
if (this.format === 'png') {
this.generateQRCodePNG()
} else {
this.generateQRCodeHTML()
}
this.dispatchEvent(
new CustomEvent('qrcode-generated', {
bubbles: true,
composed: true,
}),
)
}
updated(changedProperties) {
if (this.debug) {
console.log('[qortal-qrcode-generator] updated');
}
if (
(changedProperties.has('auto') ||
changedProperties.has('data') ||
changedProperties.has('ecclevel') ||
changedProperties.has('mask') ||
changedProperties.has('mode') ||
changedProperties.has('version')) &&
this.auto
) {
this.generateQRCode()
}
}
}
window.customElements.define('qortal-qrcode-generator', QortalQrcodeGenerator)

View File

@@ -0,0 +1,51 @@
import { LitElement, html, css } from 'lit'
import './time-elements/index.js'
class TimeAgo extends LitElement {
static get properties() {
return {
selectedAddress: { type: Object },
config: { type: Object },
timestamp: { type: Number },
format: { type: String, reflect: true },
timeIso: { type: String }
}
}
static get styles() {
return css``
}
updated(changedProps) {
changedProps.forEach((OldProp, name) => {
if (name === 'timeIso' || name === 'timestamp') {
this.renderTime(this.timestamp)
}
});
this.shadowRoot.querySelector('time-ago').setAttribute('title', '');
}
constructor() {
super();
this.timestamp = 0
this.timeIso = ''
this.format = ''
}
render() {
return html`
<time-ago datetime=${this.timeIso} format=${this.format}> </time-ago>
`
}
renderTime(timestamp) {
timestamp === undefined ? this.timeIso = '' : this.timeIso = new Date(timestamp).toISOString();
}
firstUpdated() {
}
}
window.customElements.define('message-time', TimeAgo)

View File

@@ -0,0 +1,85 @@
import { css } from 'lit'
export const tipUserStyles = css`
.tip-user-header {
display: flex;
justify-content: center;
align-items: center;
padding: 12px;
border-bottom: 1px solid whitesmoke;
gap: 25px;
user-select: none;
}
.tip-user-header-font {
font-family: Montserrat, sans-serif;
font-size: 20px;
color: var(--chat-bubble-msg-color);
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tip-user-body {
display: flex;
justify-content: center;
align-items: center;
padding: 20px 10px;
flex-direction: column;
gap: 25px;
}
.tip-input {
width: 300px;
margin-bottom: 15px;
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);
}
.tip-input::selection {
background-color: var(--mdc-theme-primary);
color: white;
}
.tip-input::placeholder {
opacity: 0.9;
color: var(--black);
}
.tip-available {
font-family: Roboto, sans-serif;
font-size: 17px;
color: var(--chat-bubble-msg-color);
font-weight: 300;
letter-spacing: 0.3px;
margin: 0;
user-select: none;
}
.success-msg {
font-family: Roboto, sans-serif;
font-size: 18px;
font-weight: 400;
letter-spacing: 0.3px;
margin: 0;
user-select: none;
color: #10880b;
}
.error-msg {
font-family: Roboto, sans-serif;
font-size: 18px;
font-weight: 400;
letter-spacing: 0.3px;
margin: 0;
user-select: none;
color: #f30000;
}
`

View File

@@ -0,0 +1,277 @@
import { LitElement, html } from 'lit';
import { render } from 'lit/html.js';
import { get, translate } from 'lit-translate';
import { tipUserStyles } from './TipUser-css.js';
import { Epml } from '../../../epml';
import '@vaadin/button';
import '@polymer/paper-progress/paper-progress.js';
const parentEpml = new Epml({ type: "WINDOW", source: window.parent });
export class TipUser extends LitElement {
static get properties() {
return {
userName: { type: String },
walletBalance: { type: Number },
sendMoneyLoading: { type: Boolean },
closeTipUser: { type: Boolean },
btnDisable: { type: Boolean },
errorMessage: { type: String },
successMessage: { type: String },
setOpenTipUser: { attribute: false },
}
}
constructor() {
super()
this.sendMoneyLoading = false
this.btnDisable = false
this.errorMessage = ""
this.successMessage = ""
this.myAddress = window.parent.reduxStore.getState().app.selectedAddress
}
static styles = [tipUserStyles]
async firstUpdated() {
await this.fetchWalletDetails();
}
updated(changedProperties) {
if (changedProperties && changedProperties.has("closeTipUser")) {
if (this.closeTipUser) {
this.shadowRoot.getElementById("amountInput").value = "";
this.errorMessage = "";
this.successMessage = "";
}
}
}
async getLastRef() {
let myRef = await parentEpml.request("apiCall", {
type: "api",
url: `/addresses/lastreference/${this.myAddress.address}`,
})
return myRef;
}
renderSuccessText() {
return html`${translate("chatpage.cchange55")}`
}
renderReceiverText() {
return html`${translate("chatpage.cchange54")}`
}
getApiKey() {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
let apiKey = myNode.apiKey;
return apiKey;
}
async fetchWalletDetails() {
await parentEpml.request('apiCall', {
url: `/addresses/balance/${this.myAddress.address}?apiKey=${this.getApiKey()}`,
})
.then((res) => {
if (isNaN(Number(res))) {
let snack4string = get("chatpage.cchange48")
parentEpml.request('showSnackBar', `${snack4string}`)
} else {
this.walletBalance = Number(res).toFixed(8);
}
})
}
async sendQort() {
const amount = this.shadowRoot.getElementById("amountInput").value;
let recipient = this.userName;
this.sendMoneyLoading = true;
this.btnDisable = true;
if (parseFloat(amount) + parseFloat(0.001) > parseFloat(this.walletBalance)) {
this.sendMoneyLoading = false;
this.btnDisable = false;
let snack1string = get("chatpage.cchange51");
parentEpml.request('showSnackBar', `${snack1string}`);
return false;
}
if (parseFloat(amount) <= 0) {
this.sendMoneyLoading = false;
this.btnDisable = false;
let snack2string = get("chatpage.cchange52");
parentEpml.request('showSnackBar', `${snack2string}`);
return false;
}
if (recipient.length === 0) {
this.sendMoneyLoading = false;
this.btnDisable = false;
let snack3string = get("chatpage.cchange53");
parentEpml.request('showSnackBar', `${snack3string}`);
return false;
}
const validateName = async (receiverName) => {
let myRes;
let myNameRes = await parentEpml.request('apiCall', {
type: 'api',
url: `/names/${receiverName}`,
})
if (myNameRes.error === 401) {
myRes = false;
} else {
myRes = myNameRes;
}
return myRes;
}
const validateAddress = async (receiverAddress) => {
let myAddress = await window.parent.validateAddress(receiverAddress);
return myAddress;
}
const validateReceiver = async (recipient) => {
let lastRef = await this.getLastRef();
let isAddress;
try {
isAddress = await validateAddress(recipient);
} catch (err) {
isAddress = false;
}
if (isAddress) {
let myTransaction = await makeTransactionRequest(recipient, lastRef);
getTxnRequestResponse(myTransaction);
} else {
let myNameRes = await validateName(recipient);
if (myNameRes !== false) {
let myNameAddress = myNameRes.owner
let myTransaction = await makeTransactionRequest(myNameAddress, lastRef)
getTxnRequestResponse(myTransaction)
} else {
console.error(this.renderReceiverText())
this.errorMessage = this.renderReceiverText();
this.sendMoneyLoading = false;
this.btnDisable = false;
}
}
}
const getName = async (recipient)=> {
try {
const getNames = await parentEpml.request("apiCall", {
type: "api",
url: `/names/address/${recipient}`,
});
if (getNames?.length > 0 ) {
return getNames[0].name;
} else {
return '';
}
} catch (error) {
return "";
}
}
const makeTransactionRequest = async (receiver, lastRef) => {
let myReceiver = receiver;
let mylastRef = lastRef;
let dialogamount = get("transactions.amount");
let dialogAddress = get("login.address");
let dialogName = get("login.name");
let dialogto = get("transactions.to");
let recipientName = await getName(myReceiver);
let myTxnrequest = await parentEpml.request('transaction', {
type: 2,
nonce: this.myAddress.nonce,
params: {
recipient: myReceiver,
recipientName: recipientName,
amount: amount,
lastReference: mylastRef,
fee: 0.001,
dialogamount: dialogamount,
dialogto: dialogto,
dialogAddress,
dialogName
},
})
return myTxnrequest;
}
const getTxnRequestResponse = (txnResponse) => {
if (txnResponse.success === false && txnResponse.message) {
this.errorMessage = txnResponse.message;
this.sendMoneyLoading = false;
this.btnDisable = false;
throw new Error(txnResponse);
} else if (txnResponse.success === true && !txnResponse.data.error) {
this.shadowRoot.getElementById('amountInput').value = '';
this.errorMessage = '';
this.successMessage = this.renderSuccessText();
this.sendMoneyLoading = false;
this.btnDisable = false;
setTimeout(() => {
this.setOpenTipUser(false);
this.successMessage = "";
}, 3000);
} else {
this.errorMessage = txnResponse.data.message;
this.sendMoneyLoading = false;
this.btnDisable = false;
throw new Error(txnResponse);
}
}
validateReceiver(recipient);
}
render() {
return html`
<div class="tip-user-header">
<img src="/img/qort.png" width="32" height="32">
<p class="tip-user-header-font">${translate("chatpage.cchange43")} ${this.userName}</p>
</div>
<div class="tip-user-body">
<p class="tip-available">${translate("chatpage.cchange47")}: ${this.walletBalance} QORT</p>
<input id="amountInput" class="tip-input" type="number" placeholder="${translate("chatpage.cchange46")}" />
<p class="tip-available">${translate("chatpage.cchange49")}: 0.001 QORT</p>
${this.sendMoneyLoading ?
html`
<paper-progress indeterminate style="width: 100%; margin: 4px;">
</paper-progress>`
: html`
<div style=${"text-align: center;"}>
<vaadin-button
?disabled=${this.btnDisable}
theme="primary medium"
style="width: 100%; cursor: pointer"
@click=${() => this.sendQort()}>
<vaadin-icon icon="vaadin:arrow-forward" slot="prefix"></vaadin-icon>
${translate("chatpage.cchange50")} QORT
</vaadin-button>
</div>
`}
${this.successMessage ?
html`
<p class="success-msg">
${this.successMessage}
</p>
`
: this.errorMessage ?
html`
<p class="error-msg">
${this.errorMessage}
</p>
`
: null}
</div>
`;
}
}
customElements.define('tip-user', TipUser);

View File

@@ -0,0 +1,103 @@
import { LitElement, html, css } from 'lit'
import { Epml } from '../../../epml.js'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class ToolTip extends LitElement {
static get properties() {
return {
selectedAddress: { type: Object },
config: { type: Object },
toolTipMessage: { type: String, reflect: true },
showToolTip: { type: Boolean, reflect: true }
}
}
static get styles() {
return css`
.tooltip {
position: relative;
display: inline-block;
border-bottom: 1px dotted black;
}
.tooltiptext {
margin-bottom: 100px;
display: inline;
visibility: visible;
width: 120px;
background-color: #555;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px 0;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -60px;
opacity: 1;
transition: opacity 0.3s;
}
.tooltiptext::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: #555 transparent transparent transparent;
}
.hide-tooltip {
display: none;
visibility: hidden;
opacity: 0;
}
`
}
constructor() {
super()
this.selectedAddress = {}
this.config = {
user: {
node: {
}
}
}
this.toolTipMessage = ''
this.showToolTip = false
}
render() {
return html`
<span id="myTool" class="tooltiptext">${this.toolTipMessage}</span>
`
}
firstUpdated() {
let configLoaded = false
parentEpml.ready().then(() => {
parentEpml.subscribe('selected_address', async selectedAddress => {
this.selectedAddress = {}
selectedAddress = JSON.parse(selectedAddress)
if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
this.selectedAddress = selectedAddress
})
parentEpml.subscribe('config', c => {
if (!configLoaded) {
configLoaded = true
}
this.config = JSON.parse(c)
})
})
parentEpml.imReady()
}
}
window.customElements.define('tool-tip', ToolTip)

View File

@@ -0,0 +1,69 @@
import { css } from 'lit'
export const userInfoStyles = css`
.user-info-header {
font-family: Montserrat, sans-serif;
text-align: center;
font-size: 28px;
color: var(--chat-bubble-msg-color);
margin-bottom: 10px;
padding: 10px 0;
user-select: none;
}
.avatar-container {
display: flex;
justify-content: center;
}
.user-info-avatar {
width: 100px;
height: 100px;
border-radius: 50%;
margin: 10px 0;
}
.user-info-no-avatar {
display: flex;
justify-content: center;
align-items: center;
text-transform: capitalize;
font-size: 50px;
font-family: Roboto, sans-serif;
width: 100px;
height: 100px;
border-radius:50%;
background: var(--chatHeadBg);
color: var(--chatHeadText);
}
.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;
}
.send-message-button:hover {
cursor: pointer;
background-color: #03a8f485;
}
.close-icon {
position: absolute;
top: 3px;
right: 5px;
color: #676b71;
width: 14px;
transition: all 0.1s ease-in-out;
}
.close-icon:hover {
cursor: pointer;
color: #494c50;
}
`

View File

@@ -0,0 +1,119 @@
import { LitElement, html } from 'lit';
import { render } from 'lit/html.js';
import { translate } from 'lit-translate';
import { userInfoStyles } from './UserInfo-css.js';
import { Epml } from '../../../../epml';
import '@vaadin/button';
import '@polymer/paper-progress/paper-progress.js';
import { cropAddress } from '../../../utils/cropAddress.js';
export class UserInfo extends LitElement {
static get properties() {
return {
setOpenUserInfo: { attribute: false },
setOpenTipUser: { attribute: false },
setOpenPrivateMessage: { attribute: false },
userName: { type: String },
selectedHead: { type: Object },
isImageLoaded: { type: Boolean }
}
}
constructor() {
super()
this.isImageLoaded = false
this.selectedHead = {}
this.imageFetches = 0
}
static styles = [userInfoStyles]
createImage(imageUrl) {
const imageHTMLRes = new Image();
imageHTMLRes.src = imageUrl;
imageHTMLRes.classList.add("user-info-avatar");
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.selectedHead && this.selectedHead.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.selectedHead.name}/qortal_avatar?async=true&apiKey=${myNode.apiKey}`;
avatarImg = this.createImage(avatarUrl);
}
return html`
<div style=${"position: relative;"}>
<vaadin-icon
class="close-icon"
icon="vaadin:close-big"
slot="icon"
@click=${() => {
this.setOpenUserInfo(false)
}}>
</vaadin-icon>
${this.isImageLoaded ?
html`
<div class="avatar-container">
${avatarImg}
</div>` :
html``}
${!this.isImageLoaded && this.selectedHead && this.selectedHead.name ?
html`
<div class="avatar-container">
<div class="user-info-no-avatar">
${this.selectedHead.name.charAt(0)}
</div>
</div>
`
: ""}
${!this.isImageLoaded && this.selectedHead && !this.selectedHead.name ?
html`
<div class="avatar-container">
<img src="/img/incognito.png" alt="avatar" />
</div>`
: ""}
<div class="user-info-header">
${this.selectedHead && this.selectedHead.name ? this.selectedHead.name : this.selectedHead ? cropAddress(this.selectedHead.address) : null}
</div>
<div
class="send-message-button"
@click="${() => {
this.setOpenPrivateMessage({
name: this.userName,
open: true
})
this.setOpenUserInfo(false);
}
}">
${translate("chatpage.cchange58")}
</div>
<div
style=${"margin-top: 5px;"}
class="send-message-button"
@click=${() => {
this.setOpenTipUser(true);
this.setOpenUserInfo(false);
}}>
${translate("chatpage.cchange59")}
</div>
</div>
`
}
}
customElements.define('user-info', UserInfo);

View File

@@ -0,0 +1,57 @@
import { css } from 'lit'
export const wrapperModalStyles = css`
.backdrop {
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
background: rgb(186 186 186 / 26%);
overflow: hidden;
animation: backdrop_blur cubic-bezier(0.22, 1, 0.36, 1) 1s forwards;
z-index: 50
}
.modal-body {
height: auto;
position: fixed;
box-shadow: rgb(60 64 67 / 30%) 0px 1px 2px 0px, rgb(60 64 67 / 15%) 0px 2px 6px 2px;
width: 500px;
z-index: 5;
display: flex;
flex-direction: column;
padding: 15px;
background-color: var(--white);
left: 50%;
top: 0px;
transform: translate(-50%, 10%);
border-radius: 12px;
overflow-y: auto;
animation: 1s cubic-bezier(0.22, 1, 0.36, 1) 0s 1 normal forwards running modal_transition;
max-height: 80%;
z-index: 60
}
@keyframes backdrop_blur {
0% {
backdrop-filter: blur(0px);
background: transparent;
}
100% {
backdrop-filter: blur(5px);
background: rgb(186 186 186 / 26%);
}
}
@keyframes modal_transition {
0% {
visibility: hidden;
opacity: 0;
}
100% {
visibility: visible;
opacity: 1;
}
}
`

View File

@@ -0,0 +1,33 @@
import { LitElement, html } from 'lit';
import { render } from 'lit/html.js';
import { wrapperModalStyles } from './WrapperModal-css.js'
export class WrapperModal extends LitElement {
static get properties() {
return {
customStyle: {type: String},
onClickFunc: { attribute: false },
zIndex: {type: Number}
}
}
static styles = [wrapperModalStyles]
render() {
return html`
<div>
<div
style="z-index: ${this.zIndex || 50}"
class="backdrop"
@click=${() => {
this.onClickFunc();
}}>
</div>
<div class="modal-body" style=${this.customStyle ? this.customStyle : ""}>
<slot></slot>
</div>
</div>
`;
}
}
customElements.define('wrapper-modal', WrapperModal);

View File

@@ -0,0 +1,82 @@
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.chatBytes, e.data.path, e.data.difficulty)
postMessage(response)
})
const memory = new WebAssembly.Memory({ initial: 256, maximum: 256 })
const heap = new Uint8Array(memory.buffer)
const computePow = async (chatBytes, path, difficulty) => {
let response = null
await new Promise((resolve, reject)=> {
const _chatBytesArray = Object.keys(chatBytes).map(function (key) { return chatBytes[key]; });
const chatBytesArray = new Uint8Array(_chatBytesArray);
const chatBytesHash = new Sha256().process(chatBytesArray).finish().result;
const hashPtr = sbrk(32, heap);
const hashAry = new Uint8Array(memory.buffer, hashPtr, 32);
hashAry.set(chatBytesHash);
const workBufferLength = 8 * 1024 * 1024;
const workBufferPtr = sbrk(workBufferLength, heap);
const importObject = {
env: {
memory: memory
},
};
function loadWebAssembly(filename, imports) {
// Fetch the file and compile it
return fetch(filename)
.then(response => response.arrayBuffer())
.then(buffer => WebAssembly.compile(buffer))
.then(module => {
// Create the instance.
return new WebAssembly.Instance(module, importObject);
});
}
loadWebAssembly(path)
.then(wasmModule => {
response = {
nonce : wasmModule.exports.compute2(hashPtr, workBufferPtr, workBufferLength, difficulty),
chatBytesArray
}
resolve()
});
})
return response
}

View File

@@ -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
}

View File

@@ -0,0 +1,115 @@
import { LitElement, html, css } from 'lit'
import '@material/mwc-button'
import '@material/mwc-icon'
import { translate, translateUnsafeHTML } from 'lit-translate'
class FragFileInput extends LitElement {
static get properties () {
return {
accept: { type: String },
readAs: { type: String }
}
}
static get styles () {
return css`
#drop-area {
border: 2px dashed #ccc;
font-family: "Roboto", sans-serif;
padding: 20px;
}
#trigger:hover {
cursor: pointer;
}
#drop-area.highlight {
border-color: var(--mdc-theme-primary, #000);
}
p {
margin-top: 0;
}
form {
margin-bottom: 10px;
}
#fileInput {
display: none;
}
`
}
constructor () {
super()
this.readAs = this.readAs || 'Text'
}
render () {
return html`
<div id="drop-area">
<slot name="info-text"></slot>
<div style="line-height: 40px; text-align: center;">
<slot id="trigger" name="inputTrigger" @click=${() => this.shadowRoot.getElementById('fileInput').click()} style="dispay:inline;">
<mwc-button><mwc-icon>cloud_upload</mwc-icon><span style="color: var(--black);">&nbsp; ${translate("fragfile.selectfile")}</span></mwc-button>
</slot><br>
<span style="text-align: center; padding-top: 4px; color: var(--black);">${translate("fragfile.dragfile")}</span>
</div>
</div>
<input type="file" id="fileInput" accept="${this.accept}" @change="${e => this.readFile(e.target.files[0])}">
`
}
readFile (file) {
const fr = new FileReader()
fr.onload = () => {
this.dispatchEvent(new CustomEvent('file-read-success', {
detail: { result: fr.result },
bubbles: true,
composed: true
}))
}
fr['readAs' + this.readAs](file)
}
firstUpdated () {
this._dropArea = this.shadowRoot.getElementById('drop-area')
const preventDefaults = e => {
e.preventDefault()
e.stopPropagation()
}
;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
this._dropArea.addEventListener(eventName, preventDefaults, false)
})
const highlight = e => {
this._dropArea.classList.add('highlight')
}
const unhighlight = e => {
this._dropArea.classList.remove('highlight')
}
;['dragenter', 'dragover'].forEach(eventName => {
this._dropArea.addEventListener(eventName, highlight, false)
})
;['dragleave', 'drop'].forEach(eventName => {
this._dropArea.addEventListener(eventName, unhighlight, false)
})
this._dropArea.addEventListener('drop', e => {
const dt = e.dataTransfer
const file = dt.files[0]
this.readFile(file)
}, false)
}
}
window.customElements.define('frag-file-input', FragFileInput)

View File

@@ -0,0 +1,56 @@
export const mimeToExtensionMap = {
// Documents
"application/pdf": ".pdf",
"application/msword": ".doc",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
"application/vnd.ms-excel": ".xls",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
"application/vnd.ms-powerpoint": ".ppt",
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
"application/vnd.oasis.opendocument.text": ".odt",
"application/vnd.oasis.opendocument.spreadsheet": ".ods",
"application/vnd.oasis.opendocument.presentation": ".odp",
"text/plain": ".txt",
"text/csv": ".csv",
"text/html": ".html",
"application/xhtml+xml": ".xhtml",
"application/xml": ".xml",
"application/json": ".json",
// Images
"image/jpeg": ".jpg",
"image/png": ".png",
"image/gif": ".gif",
"image/webp": ".webp",
"image/svg+xml": ".svg",
"image/tiff": ".tif",
"image/bmp": ".bmp",
// Audio
"audio/mpeg": ".mp3",
"audio/ogg": ".ogg",
"audio/wav": ".wav",
"audio/webm": ".weba",
"audio/aac": ".aac",
// Video
"video/mp4": ".mp4",
"video/webm": ".webm",
"video/ogg": ".ogv",
"video/x-msvideo": ".avi",
"video/quicktime": ".mov",
"video/x-ms-wmv": ".wmv",
"video/mpeg": ".mpeg",
"video/3gpp": ".3gp",
"video/3gpp2": ".3g2",
"video/x-matroska": ".mkv",
"video/x-flv": ".flv",
// Archives
"application/zip": ".zip",
"application/x-rar-compressed": ".rar",
"application/x-tar": ".tar",
"application/x-7z-compressed": ".7z",
"application/x-gzip": ".gz",
"application/x-bzip2": ".bz2",
};

View File

@@ -0,0 +1,104 @@
import nacl from '../../../../crypto/api/deps/nacl-fast.js'
import ed2curve from '../../../../crypto/api/deps/ed2curve.js'
export const fileToBase64 = (file) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const dataUrl = reader.result;
if (typeof dataUrl === "string") {
const base64String = dataUrl.split(',')[1];
resolve(base64String);
} else {
reject(new Error('Invalid data URL'));
}
};
reader.onerror = (error) => {
reject(error);
};
});
export function uint8ArrayToBase64(uint8Array) {
const length = uint8Array.length;
let binaryString = '';
const chunkSize = 1024 * 1024; // Process 1MB at a time
for (let i = 0; i < length; i += chunkSize) {
const chunkEnd = Math.min(i + chunkSize, length);
const chunk = uint8Array.subarray(i, chunkEnd);
binaryString += Array.from(chunk, byte => String.fromCharCode(byte)).join('');
}
return btoa(binaryString);
}
export function base64ToUint8Array(base64) {
const binaryString = atob(base64)
const len = binaryString.length
const bytes = new Uint8Array(len)
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i)
}
return bytes
}
export const encryptData = ({ data64, recipientPublicKey }) => {
const Uint8ArrayData = base64ToUint8Array(data64)
const uint8Array = Uint8ArrayData
if (!(uint8Array instanceof Uint8Array)) {
throw new Error("The Uint8ArrayData you've submitted is invalid")
}
try {
const privateKey = window.parent.reduxStore.getState().app.selectedAddress.keyPair.privateKey
if (!privateKey) {
throw new Error("Unable to retrieve keys")
}
const publicKeyUnit8Array = window.parent.Base58.decode(recipientPublicKey)
const convertedPrivateKey = ed2curve.convertSecretKey(privateKey)
const convertedPublicKey = ed2curve.convertPublicKey(publicKeyUnit8Array)
const sharedSecret = new Uint8Array(32)
nacl.lowlevel.crypto_scalarmult(sharedSecret, convertedPrivateKey, convertedPublicKey)
const chatEncryptionSeed = new window.parent.Sha256().process(sharedSecret).finish().result
const nonce = new Uint8Array(24);
window.crypto.getRandomValues(nonce);
const encryptedData = nacl.secretbox(uint8Array, nonce, chatEncryptionSeed)
const str = "qortalEncryptedData";
const strEncoder = new TextEncoder();
const strUint8Array = strEncoder.encode(str);
const combinedData = new Uint8Array(strUint8Array.length + nonce.length + encryptedData.length);
combinedData.set(strUint8Array);
combinedData.set(nonce, strUint8Array.length);
combinedData.set(encryptedData, strUint8Array.length + nonce.length);
const uint8arrayToData64 = uint8ArrayToBase64(combinedData)
return {
encryptedData: uint8arrayToData64,
recipientPublicKey
}
} catch (error) {
console.log({ error })
throw new Error("Error in encrypting data")
}
}

View File

@@ -0,0 +1,47 @@
// GET_USER_ACCOUNT action
export const GET_USER_ACCOUNT = 'GET_USER_ACCOUNT';
// LINK_TO_QDN_RESOURCE action
export const LINK_TO_QDN_RESOURCE = 'LINK_TO_QDN_RESOURCE';
// QDN_RESOURCE_DISPLAYED action
export const QDN_RESOURCE_DISPLAYED = 'QDN_RESOURCE_DISPLAYED';
// PUBLISH_QDN_RESOURCE action
export const PUBLISH_QDN_RESOURCE = 'PUBLISH_QDN_RESOURCE';
// SEND_CHAT_MESSAGE action
export const SEND_CHAT_MESSAGE = 'SEND_CHAT_MESSAGE';
// JOIN_GROUP action
export const JOIN_GROUP = 'JOIN_GROUP';
// DEPLOY_AT action
export const DEPLOY_AT = 'DEPLOY_AT';
// GET_WALLET_BALANCE action
export const GET_WALLET_BALANCE = 'GET_WALLET_BALANCE';
// SEND_COIN action
export const SEND_COIN = 'SEND_COIN';
// PUBLISH_MULTIPLE_QDN_RESOURCES
export const PUBLISH_MULTIPLE_QDN_RESOURCES = 'PUBLISH_MULTIPLE_QDN_RESOURCES'
// GET_LIST_ITEMS
export const GET_LIST_ITEMS = 'GET_LIST_ITEMS'
// ADD_LIST_ITEMS
export const ADD_LIST_ITEMS = 'ADD_LIST_ITEMS'
// DELETE_LIST_ITEM
export const DELETE_LIST_ITEM = 'DELETE_LIST_ITEM'
// ENCRYPT_DATA
export const ENCRYPT_DATA = 'ENCRYPT_DATA'
// DECRYPT_DATA
export const DECRYPT_DATA = 'DECRYPT_DATA'
// SAVE_FILE
export const SAVE_FILE = 'SAVE_FILE'

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,84 @@
import { LitElement, html, css } from 'lit'
import '@material/mwc-snackbar'
let queueElement
class SnackQueue extends LitElement {
static get properties() {
return {
busy: {
type: Boolean,
attribute: 'busy',
reflectToAttribute: true
},
currentSnack: {
type: Object,
attribute: 'current-snack',
reflectToAttribute: true
},
_queue: {
type: Array
},
_labelText: { type: String },
_stacked: { type: Boolean },
_leading: { type: Boolean },
_closeOnEscape: { type: Boolean },
_timeoutMs: { type: Number },
action: {},
_dismiss: {},
_dismissIcon: { type: String }
}
}
static get styles() {
return css``
}
constructor() {
super()
this._queue = []
this.busy = false
this._timeoutMs = 5000
}
render() {
return html`
<mwc-snackbar id="snack" labelText="${this._labelText}" ?stacked=${this._stacked} ?leading=${this._leading} ?closeOnEscape=${this._closeOnEscape} timeoutMs=${this._timeoutMs}>
${this._action}
${this._dismiss ? html`
<mwc-icon-button icon="${this._dismissIcon}" slot="dismiss"></mwc-icon-button>
` : ''}
</mwc-snackbar>
`
}
firstUpdated() {
this._snackbar = this.shadowRoot.getElementById('snack')
}
_shift() {
if (this.busy || this._queue.length === 0) return
const item = this._queue.shift()
this._labelText = item.labelText || ''
this._action = item.action || ''
this._dismiss = item.dismiss || false
this._dismissIcon = item.dismissIcon || 'close'
this._leading = !!item.leading
this._closeOnEscape = (item.closeOnEscape && item.closeOnEscape !== false) // JSON.parse maybe needs to be compared to 'false'...in which case no need for complex expression
this._timeoutMs = (item.timeoutMs >= 4000 && item.timeoutMs <= 10000) ? item.timeoutMs : 5000
this._snackbar.show()
}
add(item) {
this._queue.push(item)
this._shift()
}
}
window.customElements.define('snack-queue', SnackQueue)
const queueNode = document.createElement('snack-queue')
queueNode.id = 'queue-node'
queueNode.loadingMessage = ''
queueElement = document.body.appendChild(queueNode)
export default queueElement

View File

@@ -0,0 +1,87 @@
import { makeFormatter } from './utils';
const datetimes = new WeakMap();
export default class ExtendedTimeElement extends HTMLElement {
static get observedAttributes() {
return [
'datetime',
'day',
'format',
'lang',
'hour',
'minute',
'month',
'second',
'title',
'weekday',
'year',
'time-zone-name'
];
}
connectedCallback() {
const title = this.getFormattedTitle();
if (title && !this.hasAttribute('title')) {
this.setAttribute('title', title);
}
const text = this.getFormattedDate();
if (text) {
this.textContent = text;
}
}
attributeChangedCallback(attrName, oldValue, newValue) {
const oldTitle = this.getFormattedTitle();
if (attrName === 'datetime') {
const millis = Date.parse(newValue);
if (isNaN(millis)) {
datetimes.delete(this);
}
else {
datetimes.set(this, new Date(millis));
}
}
const title = this.getFormattedTitle();
const currentTitle = this.getAttribute('title');
if (attrName !== 'title' && title && (!currentTitle || currentTitle === oldTitle)) {
this.setAttribute('title', title);
}
const text = this.getFormattedDate();
if (text) {
this.textContent = text;
}
}
get date() {
return datetimes.get(this);
}
getFormattedTitle() {
const date = this.date;
if (!date)
return;
const formatter = titleFormatter();
if (formatter) {
return formatter.format(date);
}
else {
try {
return date.toLocaleString();
}
catch (e) {
if (e instanceof RangeError) {
return date.toString();
}
else {
throw e;
}
}
}
}
getFormattedDate() {
return;
}
}
const titleFormatter = makeFormatter({
day: 'numeric',
month: 'short',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
timeZoneName: 'short'
});

View File

@@ -0,0 +1,705 @@
const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];
function pad(num) {
return `0${num}`.slice(-2);
}
function strftime(time, formatString) {
const day = time.getDay();
const date = time.getDate();
const month = time.getMonth();
const year = time.getFullYear();
const hour = time.getHours();
const minute = time.getMinutes();
const second = time.getSeconds();
return formatString.replace(/%([%aAbBcdeHIlmMpPSwyYZz])/g, function (_arg) {
let match;
const modifier = _arg[1];
switch (modifier) {
case '%':
return '%';
case 'a':
return weekdays[day].slice(0, 3);
case 'A':
return weekdays[day];
case 'b':
return months[month].slice(0, 3);
case 'B':
return months[month];
case 'c':
return time.toString();
case 'd':
return pad(date);
case 'e':
return String(date);
case 'H':
return pad(hour);
case 'I':
return pad(strftime(time, '%l'));
case 'l':
if (hour === 0 || hour === 12) {
return String(12);
}
else {
return String((hour + 12) % 12);
}
case 'm':
return pad(month + 1);
case 'M':
return pad(minute);
case 'p':
if (hour > 11) {
return 'PM';
}
else {
return 'AM';
}
case 'P':
if (hour > 11) {
return 'pm';
}
else {
return 'am';
}
case 'S':
return pad(second);
case 'w':
return String(day);
case 'y':
return pad(year % 100);
case 'Y':
return String(year);
case 'Z':
match = time.toString().match(/\((\w+)\)$/);
return match ? match[1] : '';
case 'z':
match = time.toString().match(/\w([+-]\d\d\d\d) /);
return match ? match[1] : '';
}
return '';
});
}
function makeFormatter(options) {
let format;
return function () {
if (format)
return format;
if ('Intl' in window) {
try {
format = new Intl.DateTimeFormat(undefined, options);
return format;
}
catch (e) {
if (!(e instanceof RangeError)) {
throw e;
}
}
}
};
}
let dayFirst = null;
const dayFirstFormatter = makeFormatter({ day: 'numeric', month: 'short' });
function isDayFirst() {
if (dayFirst !== null) {
return dayFirst;
}
const formatter = dayFirstFormatter();
if (formatter) {
const output = formatter.format(new Date(0));
dayFirst = !!output.match(/^\d/);
return dayFirst;
}
else {
return false;
}
}
let yearSeparator = null;
const yearFormatter = makeFormatter({ day: 'numeric', month: 'short', year: 'numeric' });
function isYearSeparator() {
if (yearSeparator !== null) {
return yearSeparator;
}
const formatter = yearFormatter();
if (formatter) {
const output = formatter.format(new Date(0));
yearSeparator = !!output.match(/\d,/);
return yearSeparator;
}
else {
return true;
}
}
function isThisYear(date) {
const now = new Date();
return now.getUTCFullYear() === date.getUTCFullYear();
}
function makeRelativeFormat(locale, options) {
if ('Intl' in window && 'RelativeTimeFormat' in window.Intl) {
try {
return new Intl.RelativeTimeFormat(locale, options);
}
catch (e) {
if (!(e instanceof RangeError)) {
throw e;
}
}
}
}
function localeFromElement(el) {
const container = el.closest('[lang]');
if (container instanceof HTMLElement && container.lang) {
return container.lang;
}
return 'default';
}
const datetimes = new WeakMap();
class ExtendedTimeElement extends HTMLElement {
static get observedAttributes() {
return [
'datetime',
'day',
'format',
'lang',
'hour',
'minute',
'month',
'second',
'title',
'weekday',
'year',
'time-zone-name'
];
}
connectedCallback() {
const title = this.getFormattedTitle();
if (title && !this.hasAttribute('title')) {
this.setAttribute('title', title);
}
const text = this.getFormattedDate();
if (text) {
this.textContent = text;
}
}
attributeChangedCallback(attrName, oldValue, newValue) {
const oldTitle = this.getFormattedTitle();
if (attrName === 'datetime') {
const millis = Date.parse(newValue);
if (isNaN(millis)) {
datetimes.delete(this);
}
else {
datetimes.set(this, new Date(millis));
}
}
const title = this.getFormattedTitle();
const currentTitle = this.getAttribute('title');
if (attrName !== 'title' && title && (!currentTitle || currentTitle === oldTitle)) {
this.setAttribute('title', title);
}
const text = this.getFormattedDate();
if (text) {
this.textContent = text;
}
}
get date() {
return datetimes.get(this);
}
getFormattedTitle() {
const date = this.date;
if (!date)
return;
const formatter = titleFormatter();
if (formatter) {
return formatter.format(date);
}
else {
try {
return date.toLocaleString();
}
catch (e) {
if (e instanceof RangeError) {
return date.toString();
}
else {
throw e;
}
}
}
}
getFormattedDate() {
return;
}
}
const titleFormatter = makeFormatter({
day: 'numeric',
month: 'short',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
timeZoneName: 'short'
});
const formatters = new WeakMap();
class LocalTimeElement extends ExtendedTimeElement {
attributeChangedCallback(attrName, oldValue, newValue) {
if (attrName === 'hour' || attrName === 'minute' || attrName === 'second' || attrName === 'time-zone-name') {
formatters.delete(this);
}
super.attributeChangedCallback(attrName, oldValue, newValue);
}
getFormattedDate() {
const d = this.date;
if (!d)
return;
const date = formatDate(this, d) || '';
const time = formatTime(this, d) || '';
return `${date} ${time}`.trim();
}
}
function formatDate(el, date) {
const props = {
weekday: {
short: '%a',
long: '%A'
},
day: {
numeric: '%e',
'2-digit': '%d'
},
month: {
short: '%b',
long: '%B'
},
year: {
numeric: '%Y',
'2-digit': '%y'
}
};
let format = isDayFirst() ? 'weekday day month year' : 'weekday month day, year';
for (const prop in props) {
const value = props[prop][el.getAttribute(prop) || ''];
format = format.replace(prop, value || '');
}
format = format.replace(/(\s,)|(,\s$)/, '');
return strftime(date, format).replace(/\s+/, ' ').trim();
}
function formatTime(el, date) {
const options = {};
const hour = el.getAttribute('hour');
if (hour === 'numeric' || hour === '2-digit')
options.hour = hour;
const minute = el.getAttribute('minute');
if (minute === 'numeric' || minute === '2-digit')
options.minute = minute;
const second = el.getAttribute('second');
if (second === 'numeric' || second === '2-digit')
options.second = second;
const tz = el.getAttribute('time-zone-name');
if (tz === 'short' || tz === 'long')
options.timeZoneName = tz;
if (Object.keys(options).length === 0) {
return;
}
let factory = formatters.get(el);
if (!factory) {
factory = makeFormatter(options);
formatters.set(el, factory);
}
const formatter = factory();
if (formatter) {
return formatter.format(date);
}
else {
const timef = options.second ? '%H:%M:%S' : '%H:%M';
return strftime(date, timef);
}
}
if (!window.customElements.get('local-time')) {
window.LocalTimeElement = LocalTimeElement;
window.customElements.define('local-time', LocalTimeElement);
}
class RelativeTime {
constructor(date, locale) {
this.date = date;
this.locale = locale;
}
toString() {
const ago = this.timeElapsed();
if (ago) {
return ago;
}
else {
const ahead = this.timeAhead();
if (ahead) {
return ahead;
}
else {
return `on ${this.formatDate()}`;
}
}
}
timeElapsed() {
const ms = new Date().getTime() - this.date.getTime();
const sec = Math.round(ms / 1000);
const min = Math.round(sec / 60);
const hr = Math.round(min / 60);
const day = Math.round(hr / 24);
if (ms >= 0 && day < 30) {
return this.timeAgoFromMs(ms);
}
else {
return null;
}
}
timeAhead() {
const ms = this.date.getTime() - new Date().getTime();
const sec = Math.round(ms / 1000);
const min = Math.round(sec / 60);
const hr = Math.round(min / 60);
const day = Math.round(hr / 24);
if (ms >= 0 && day < 30) {
return this.timeUntil();
}
else {
return null;
}
}
timeAgo() {
const ms = new Date().getTime() - this.date.getTime();
return this.timeAgoFromMs(ms);
}
timeAgoFromMs(ms) {
const sec = Math.round(ms / 1000);
const min = Math.round(sec / 60);
const hr = Math.round(min / 60);
const day = Math.round(hr / 24);
const month = Math.round(day / 30);
const year = Math.round(month / 12);
if (ms < 0) {
return formatRelativeTime(this.locale, 0, 'second');
}
else if (sec < 10) {
return formatRelativeTime(this.locale, 0, 'second');
}
else if (sec < 45) {
return formatRelativeTime(this.locale, -sec, 'second');
}
else if (sec < 90) {
return formatRelativeTime(this.locale, -min, 'minute');
}
else if (min < 45) {
return formatRelativeTime(this.locale, -min, 'minute');
}
else if (min < 90) {
return formatRelativeTime(this.locale, -hr, 'hour');
}
else if (hr < 24) {
return formatRelativeTime(this.locale, -hr, 'hour');
}
else if (hr < 36) {
return formatRelativeTime(this.locale, -day, 'day');
}
else if (day < 30) {
return formatRelativeTime(this.locale, -day, 'day');
}
else if (month < 18) {
return formatRelativeTime(this.locale, -month, 'month');
}
else {
return formatRelativeTime(this.locale, -year, 'year');
}
}
microTimeAgo() {
const ms = new Date().getTime() - this.date.getTime();
const sec = Math.round(ms / 1000);
const min = Math.round(sec / 60);
const hr = Math.round(min / 60);
const day = Math.round(hr / 24);
const month = Math.round(day / 30);
const year = Math.round(month / 12);
if (min < 1) {
return '1m';
}
else if (min < 60) {
return `${min}m`;
}
else if (hr < 24) {
return `${hr}h`;
}
else if (day < 365) {
return `${day}d`;
}
else {
return `${year}y`;
}
}
timeUntil() {
const ms = this.date.getTime() - new Date().getTime();
return this.timeUntilFromMs(ms);
}
timeUntilFromMs(ms) {
const sec = Math.round(ms / 1000);
const min = Math.round(sec / 60);
const hr = Math.round(min / 60);
const day = Math.round(hr / 24);
const month = Math.round(day / 30);
const year = Math.round(month / 12);
if (month >= 18) {
return formatRelativeTime(this.locale, year, 'year');
}
else if (month >= 12) {
return formatRelativeTime(this.locale, year, 'year');
}
else if (day >= 45) {
return formatRelativeTime(this.locale, month, 'month');
}
else if (day >= 30) {
return formatRelativeTime(this.locale, month, 'month');
}
else if (hr >= 36) {
return formatRelativeTime(this.locale, day, 'day');
}
else if (hr >= 24) {
return formatRelativeTime(this.locale, day, 'day');
}
else if (min >= 90) {
return formatRelativeTime(this.locale, hr, 'hour');
}
else if (min >= 45) {
return formatRelativeTime(this.locale, hr, 'hour');
}
else if (sec >= 90) {
return formatRelativeTime(this.locale, min, 'minute');
}
else if (sec >= 45) {
return formatRelativeTime(this.locale, min, 'minute');
}
else if (sec >= 10) {
return formatRelativeTime(this.locale, sec, 'second');
}
else {
return formatRelativeTime(this.locale, 0, 'second');
}
}
microTimeUntil() {
const ms = this.date.getTime() - new Date().getTime();
const sec = Math.round(ms / 1000);
const min = Math.round(sec / 60);
const hr = Math.round(min / 60);
const day = Math.round(hr / 24);
const month = Math.round(day / 30);
const year = Math.round(month / 12);
if (day >= 365) {
return `${year}y`;
}
else if (hr >= 24) {
return `${day}d`;
}
else if (min >= 60) {
return `${hr}h`;
}
else if (min > 1) {
return `${min}m`;
}
else {
return '1m';
}
}
formatDate() {
let format = isDayFirst() ? '%e %b' : '%b %e';
if (!isThisYear(this.date)) {
format += isYearSeparator() ? ', %Y' : ' %Y';
}
return strftime(this.date, format);
}
formatTime() {
const formatter = timeFormatter();
if (formatter) {
return formatter.format(this.date);
}
else {
return strftime(this.date, '%l:%M%P');
}
}
}
function formatRelativeTime(locale, value, unit) {
const formatter = makeRelativeFormat(locale, { numeric: 'auto' });
if (formatter) {
return formatter.format(value, unit);
}
else {
return formatEnRelativeTime(value, unit);
}
}
function formatEnRelativeTime(value, unit) {
if (value === 0) {
switch (unit) {
case 'year':
case 'quarter':
case 'month':
case 'week':
return `this ${unit}`;
case 'day':
return 'today';
case 'hour':
case 'minute':
return `in 0 ${unit}s`;
case 'second':
return 'now';
}
}
else if (value === 1) {
switch (unit) {
case 'year':
case 'quarter':
case 'month':
case 'week':
return `next ${unit}`;
case 'day':
return 'tomorrow';
case 'hour':
case 'minute':
case 'second':
return `in 1 ${unit}`;
}
}
else if (value === -1) {
switch (unit) {
case 'year':
case 'quarter':
case 'month':
case 'week':
return `last ${unit}`;
case 'day':
return 'yesterday';
case 'hour':
case 'minute':
case 'second':
return `1 ${unit} ago`;
}
}
else if (value > 1) {
switch (unit) {
case 'year':
case 'quarter':
case 'month':
case 'week':
case 'day':
case 'hour':
case 'minute':
case 'second':
return `in ${value} ${unit}s`;
}
}
else if (value < -1) {
switch (unit) {
case 'year':
case 'quarter':
case 'month':
case 'week':
case 'day':
case 'hour':
case 'minute':
case 'second':
return `${-value} ${unit}s ago`;
}
}
throw new RangeError(`Invalid unit argument for format() '${unit}'`);
}
const timeFormatter = makeFormatter({ hour: 'numeric', minute: '2-digit' });
class RelativeTimeElement extends ExtendedTimeElement {
getFormattedDate() {
const date = this.date;
if (!date)
return;
return new RelativeTime(date, localeFromElement(this)).toString();
}
connectedCallback() {
nowElements.push(this);
if (!updateNowElementsId) {
updateNowElements();
updateNowElementsId = window.setInterval(updateNowElements, 60 * 1000);
}
super.connectedCallback();
}
disconnectedCallback() {
const ix = nowElements.indexOf(this);
if (ix !== -1) {
nowElements.splice(ix, 1);
}
if (!nowElements.length) {
if (updateNowElementsId) {
clearInterval(updateNowElementsId);
updateNowElementsId = null;
}
}
}
}
const nowElements = [];
let updateNowElementsId;
function updateNowElements() {
let time, i, len;
for (i = 0, len = nowElements.length; i < len; i++) {
time = nowElements[i];
time.textContent = time.getFormattedDate() || '';
}
}
if (!window.customElements.get('relative-time')) {
window.RelativeTimeElement = RelativeTimeElement;
window.customElements.define('relative-time', RelativeTimeElement);
}
class TimeAgoElement extends RelativeTimeElement {
getFormattedDate() {
const format = this.getAttribute('format');
const date = this.date;
if (!date)
return;
if (format === 'micro') {
return new RelativeTime(date, localeFromElement(this)).microTimeAgo();
}
else {
return new RelativeTime(date, localeFromElement(this)).timeAgo();
}
}
}
if (!window.customElements.get('time-ago')) {
window.TimeAgoElement = TimeAgoElement;
window.customElements.define('time-ago', TimeAgoElement);
}
class TimeUntilElement extends RelativeTimeElement {
getFormattedDate() {
const format = this.getAttribute('format');
const date = this.date;
if (!date)
return;
if (format === 'micro') {
return new RelativeTime(date, localeFromElement(this)).microTimeUntil();
}
else {
return new RelativeTime(date, localeFromElement(this)).timeUntil();
}
}
}
if (!window.customElements.get('time-until')) {
window.TimeUntilElement = TimeUntilElement;
window.customElements.define('time-until', TimeUntilElement);
}
export { LocalTimeElement, RelativeTimeElement, TimeAgoElement, TimeUntilElement };

View File

@@ -0,0 +1,81 @@
import { strftime, makeFormatter, isDayFirst } from './utils';
import ExtendedTimeElement from './extended-time-element';
const formatters = new WeakMap();
export default class LocalTimeElement extends ExtendedTimeElement {
attributeChangedCallback(attrName, oldValue, newValue) {
if (attrName === 'hour' || attrName === 'minute' || attrName === 'second' || attrName === 'time-zone-name') {
formatters.delete(this);
}
super.attributeChangedCallback(attrName, oldValue, newValue);
}
getFormattedDate() {
const d = this.date;
if (!d)
return;
const date = formatDate(this, d) || '';
const time = formatTime(this, d) || '';
return `${date} ${time}`.trim();
}
}
function formatDate(el, date) {
const props = {
weekday: {
short: '%a',
long: '%A'
},
day: {
numeric: '%e',
'2-digit': '%d'
},
month: {
short: '%b',
long: '%B'
},
year: {
numeric: '%Y',
'2-digit': '%y'
}
};
let format = isDayFirst() ? 'weekday day month year' : 'weekday month day, year';
for (const prop in props) {
const value = props[prop][el.getAttribute(prop) || ''];
format = format.replace(prop, value || '');
}
format = format.replace(/(\s,)|(,\s$)/, '');
return strftime(date, format).replace(/\s+/, ' ').trim();
}
function formatTime(el, date) {
const options = {};
const hour = el.getAttribute('hour');
if (hour === 'numeric' || hour === '2-digit')
options.hour = hour;
const minute = el.getAttribute('minute');
if (minute === 'numeric' || minute === '2-digit')
options.minute = minute;
const second = el.getAttribute('second');
if (second === 'numeric' || second === '2-digit')
options.second = second;
const tz = el.getAttribute('time-zone-name');
if (tz === 'short' || tz === 'long')
options.timeZoneName = tz;
if (Object.keys(options).length === 0) {
return;
}
let factory = formatters.get(el);
if (!factory) {
factory = makeFormatter(options);
formatters.set(el, factory);
}
const formatter = factory();
if (formatter) {
return formatter.format(date);
}
else {
const timef = options.second ? '%H:%M:%S' : '%H:%M';
return strftime(date, timef);
}
}
if (!window.customElements.get('local-time')) {
window.LocalTimeElement = LocalTimeElement;
window.customElements.define('local-time', LocalTimeElement);
}

View File

@@ -0,0 +1,44 @@
import RelativeTime from './relative-time';
import ExtendedTimeElement from './extended-time-element';
import { localeFromElement } from './utils';
export default class RelativeTimeElement extends ExtendedTimeElement {
getFormattedDate() {
const date = this.date;
if (!date)
return;
return new RelativeTime(date, localeFromElement(this)).toString();
}
connectedCallback() {
nowElements.push(this);
if (!updateNowElementsId) {
updateNowElements();
updateNowElementsId = window.setInterval(updateNowElements, 60 * 1000);
}
super.connectedCallback();
}
disconnectedCallback() {
const ix = nowElements.indexOf(this);
if (ix !== -1) {
nowElements.splice(ix, 1);
}
if (!nowElements.length) {
if (updateNowElementsId) {
clearInterval(updateNowElementsId);
updateNowElementsId = null;
}
}
}
}
const nowElements = [];
let updateNowElementsId;
function updateNowElements() {
let time, i, len;
for (i = 0, len = nowElements.length; i < len; i++) {
time = nowElements[i];
time.textContent = time.getFormattedDate() || '';
}
}
if (!window.customElements.get('relative-time')) {
window.RelativeTimeElement = RelativeTimeElement;
window.customElements.define('relative-time', RelativeTimeElement);
}

View File

@@ -0,0 +1,290 @@
import { strftime, makeFormatter, makeRelativeFormat, isDayFirst, isThisYear, isYearSeparator } from './utils';
export default class RelativeTime {
constructor(date, locale) {
this.date = date;
this.locale = locale;
}
toString() {
const ago = this.timeElapsed();
if (ago) {
return ago;
}
else {
const ahead = this.timeAhead();
if (ahead) {
return ahead;
}
else {
return `on ${this.formatDate()}`;
}
}
}
timeElapsed() {
const ms = new Date().getTime() - this.date.getTime();
const sec = Math.round(ms / 1000);
const min = Math.round(sec / 60);
const hr = Math.round(min / 60);
const day = Math.round(hr / 24);
if (ms >= 0 && day < 30) {
return this.timeAgoFromMs(ms);
}
else {
return null;
}
}
timeAhead() {
const ms = this.date.getTime() - new Date().getTime();
const sec = Math.round(ms / 1000);
const min = Math.round(sec / 60);
const hr = Math.round(min / 60);
const day = Math.round(hr / 24);
if (ms >= 0 && day < 30) {
return this.timeUntil();
}
else {
return null;
}
}
timeAgo() {
const ms = new Date().getTime() - this.date.getTime();
return this.timeAgoFromMs(ms);
}
timeAgoFromMs(ms) {
const sec = Math.round(ms / 1000);
const min = Math.round(sec / 60);
const hr = Math.round(min / 60);
const day = Math.round(hr / 24);
const month = Math.round(day / 30);
const year = Math.round(month / 12);
if (ms < 0) {
return formatRelativeTime(this.locale, 0, 'second');
}
else if (sec < 10) {
return formatRelativeTime(this.locale, 0, 'second');
}
else if (sec < 45) {
return formatRelativeTime(this.locale, -sec, 'second');
}
else if (sec < 90) {
return formatRelativeTime(this.locale, -min, 'minute');
}
else if (min < 45) {
return formatRelativeTime(this.locale, -min, 'minute');
}
else if (min < 90) {
return formatRelativeTime(this.locale, -hr, 'hour');
}
else if (hr < 24) {
return formatRelativeTime(this.locale, -hr, 'hour');
}
else if (hr < 36) {
return formatRelativeTime(this.locale, -day, 'day');
}
else if (day < 30) {
return formatRelativeTime(this.locale, -day, 'day');
}
else if (month < 18) {
return formatRelativeTime(this.locale, -month, 'month');
}
else {
return formatRelativeTime(this.locale, -year, 'year');
}
}
microTimeAgo() {
const ms = new Date().getTime() - this.date.getTime();
const sec = Math.round(ms / 1000);
const min = Math.round(sec / 60);
const hr = Math.round(min / 60);
const day = Math.round(hr / 24);
const month = Math.round(day / 30);
const year = Math.round(month / 12);
if (min < 1) {
return '1m';
}
else if (min < 60) {
return `${min}m`;
}
else if (hr < 24) {
return `${hr}h`;
}
else if (day < 365) {
return `${day}d`;
}
else {
return `${year}y`;
}
}
timeUntil() {
const ms = this.date.getTime() - new Date().getTime();
return this.timeUntilFromMs(ms);
}
timeUntilFromMs(ms) {
const sec = Math.round(ms / 1000);
const min = Math.round(sec / 60);
const hr = Math.round(min / 60);
const day = Math.round(hr / 24);
const month = Math.round(day / 30);
const year = Math.round(month / 12);
if (month >= 18) {
return formatRelativeTime(this.locale, year, 'year');
}
else if (month >= 12) {
return formatRelativeTime(this.locale, year, 'year');
}
else if (day >= 45) {
return formatRelativeTime(this.locale, month, 'month');
}
else if (day >= 30) {
return formatRelativeTime(this.locale, month, 'month');
}
else if (hr >= 36) {
return formatRelativeTime(this.locale, day, 'day');
}
else if (hr >= 24) {
return formatRelativeTime(this.locale, day, 'day');
}
else if (min >= 90) {
return formatRelativeTime(this.locale, hr, 'hour');
}
else if (min >= 45) {
return formatRelativeTime(this.locale, hr, 'hour');
}
else if (sec >= 90) {
return formatRelativeTime(this.locale, min, 'minute');
}
else if (sec >= 45) {
return formatRelativeTime(this.locale, min, 'minute');
}
else if (sec >= 10) {
return formatRelativeTime(this.locale, sec, 'second');
}
else {
return formatRelativeTime(this.locale, 0, 'second');
}
}
microTimeUntil() {
const ms = this.date.getTime() - new Date().getTime();
const sec = Math.round(ms / 1000);
const min = Math.round(sec / 60);
const hr = Math.round(min / 60);
const day = Math.round(hr / 24);
const month = Math.round(day / 30);
const year = Math.round(month / 12);
if (day >= 365) {
return `${year}y`;
}
else if (hr >= 24) {
return `${day}d`;
}
else if (min >= 60) {
return `${hr}h`;
}
else if (min > 1) {
return `${min}m`;
}
else {
return '1m';
}
}
formatDate() {
let format = isDayFirst() ? '%e %b' : '%b %e';
if (!isThisYear(this.date)) {
format += isYearSeparator() ? ', %Y' : ' %Y';
}
return strftime(this.date, format);
}
formatTime() {
const formatter = timeFormatter();
if (formatter) {
return formatter.format(this.date);
}
else {
return strftime(this.date, '%l:%M%P');
}
}
}
function formatRelativeTime(locale, value, unit) {
const formatter = makeRelativeFormat(locale, { numeric: 'auto' });
if (formatter) {
return formatter.format(value, unit);
}
else {
return formatEnRelativeTime(value, unit);
}
}
function formatEnRelativeTime(value, unit) {
if (value === 0) {
switch (unit) {
case 'year':
case 'quarter':
case 'month':
case 'week':
return `this ${unit}`;
case 'day':
return 'today';
case 'hour':
case 'minute':
return `in 0 ${unit}s`;
case 'second':
return 'now';
}
}
else if (value === 1) {
switch (unit) {
case 'year':
case 'quarter':
case 'month':
case 'week':
return `next ${unit}`;
case 'day':
return 'tomorrow';
case 'hour':
case 'minute':
case 'second':
return `in 1 ${unit}`;
}
}
else if (value === -1) {
switch (unit) {
case 'year':
case 'quarter':
case 'month':
case 'week':
return `last ${unit}`;
case 'day':
return 'yesterday';
case 'hour':
case 'minute':
case 'second':
return `1 ${unit} ago`;
}
}
else if (value > 1) {
switch (unit) {
case 'year':
case 'quarter':
case 'month':
case 'week':
case 'day':
case 'hour':
case 'minute':
case 'second':
return `in ${value} ${unit}s`;
}
}
else if (value < -1) {
switch (unit) {
case 'year':
case 'quarter':
case 'month':
case 'week':
case 'day':
case 'hour':
case 'minute':
case 'second':
return `${-value} ${unit}s ago`;
}
}
throw new RangeError(`Invalid unit argument for format() '${unit}'`);
}
const timeFormatter = makeFormatter({ hour: 'numeric', minute: '2-digit' });

View File

@@ -0,0 +1,21 @@
import RelativeTime from './relative-time';
import RelativeTimeElement from './relative-time-element';
import { localeFromElement } from './utils';
export default class TimeAgoElement extends RelativeTimeElement {
getFormattedDate() {
const format = this.getAttribute('format');
const date = this.date;
if (!date)
return;
if (format === 'micro') {
return new RelativeTime(date, localeFromElement(this)).microTimeAgo();
}
else {
return new RelativeTime(date, localeFromElement(this)).timeAgo();
}
}
}
if (!window.customElements.get('time-ago')) {
window.TimeAgoElement = TimeAgoElement;
window.customElements.define('time-ago', TimeAgoElement);
}

View File

@@ -0,0 +1,21 @@
import RelativeTime from './relative-time';
import RelativeTimeElement from './relative-time-element';
import { localeFromElement } from './utils';
export default class TimeUntilElement extends RelativeTimeElement {
getFormattedDate() {
const format = this.getAttribute('format');
const date = this.date;
if (!date)
return;
if (format === 'micro') {
return new RelativeTime(date, localeFromElement(this)).microTimeUntil();
}
else {
return new RelativeTime(date, localeFromElement(this)).timeUntil();
}
}
}
if (!window.customElements.get('time-until')) {
window.TimeUntilElement = TimeUntilElement;
window.customElements.define('time-until', TimeUntilElement);
}

View File

@@ -0,0 +1,166 @@
const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];
function pad(num) {
return `0${num}`.slice(-2);
}
export function strftime(time, formatString) {
const day = time.getDay();
const date = time.getDate();
const month = time.getMonth();
const year = time.getFullYear();
const hour = time.getHours();
const minute = time.getMinutes();
const second = time.getSeconds();
return formatString.replace(/%([%aAbBcdeHIlmMpPSwyYZz])/g, function (_arg) {
let match;
const modifier = _arg[1];
switch (modifier) {
case '%':
return '%';
case 'a':
return weekdays[day].slice(0, 3);
case 'A':
return weekdays[day];
case 'b':
return months[month].slice(0, 3);
case 'B':
return months[month];
case 'c':
return time.toString();
case 'd':
return pad(date);
case 'e':
return String(date);
case 'H':
return pad(hour);
case 'I':
return pad(strftime(time, '%l'));
case 'l':
if (hour === 0 || hour === 12) {
return String(12);
}
else {
return String((hour + 12) % 12);
}
case 'm':
return pad(month + 1);
case 'M':
return pad(minute);
case 'p':
if (hour > 11) {
return 'PM';
}
else {
return 'AM';
}
case 'P':
if (hour > 11) {
return 'pm';
}
else {
return 'am';
}
case 'S':
return pad(second);
case 'w':
return String(day);
case 'y':
return pad(year % 100);
case 'Y':
return String(year);
case 'Z':
match = time.toString().match(/\((\w+)\)$/);
return match ? match[1] : '';
case 'z':
match = time.toString().match(/\w([+-]\d\d\d\d) /);
return match ? match[1] : '';
}
return '';
});
}
export function makeFormatter(options) {
let format;
return function () {
if (format)
return format;
if ('Intl' in window) {
try {
format = new Intl.DateTimeFormat(undefined, options);
return format;
}
catch (e) {
if (!(e instanceof RangeError)) {
throw e;
}
}
}
};
}
let dayFirst = null;
const dayFirstFormatter = makeFormatter({ day: 'numeric', month: 'short' });
export function isDayFirst() {
if (dayFirst !== null) {
return dayFirst;
}
const formatter = dayFirstFormatter();
if (formatter) {
const output = formatter.format(new Date(0));
dayFirst = !!output.match(/^\d/);
return dayFirst;
}
else {
return false;
}
}
let yearSeparator = null;
const yearFormatter = makeFormatter({ day: 'numeric', month: 'short', year: 'numeric' });
export function isYearSeparator() {
if (yearSeparator !== null) {
return yearSeparator;
}
const formatter = yearFormatter();
if (formatter) {
const output = formatter.format(new Date(0));
yearSeparator = !!output.match(/\d,/);
return yearSeparator;
}
else {
return true;
}
}
export function isThisYear(date) {
const now = new Date();
return now.getUTCFullYear() === date.getUTCFullYear();
}
export function makeRelativeFormat(locale, options) {
if ('Intl' in window && 'RelativeTimeFormat' in window.Intl) {
try {
return new Intl.RelativeTimeFormat(locale, options);
}
catch (e) {
if (!(e instanceof RangeError)) {
throw e;
}
}
}
}
export function localeFromElement(el) {
const container = el.closest('[lang]');
if (container instanceof HTMLElement && container.lang) {
return container.lang;
}
return 'default';
}

View File

@@ -0,0 +1,13 @@
export const parentEpml = new Epml({
type: 'WINDOW',
source: window.parent
})
export const visiblePluginEpml = new Epml({
type: 'PROXY',
source: {
proxy: parentEpml,
target: 'visible-plugin',
id: 'core-plugin' // self id for responses, matches that in proxy.html
}
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,170 @@
import { LitElement, html, css } from 'lit'
import { Epml } from '../../../../epml.js'
import '@polymer/paper-spinner/paper-spinner-lite.js'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class GroupTransaction extends LitElement {
static get properties() {
return {
loading: { type: Boolean },
upTime: { type: String },
mintingAccounts: { type: Array },
peers: { type: Array },
addMintingAccountLoading: { type: Boolean },
removeMintingAccountLoading: { type: Boolean },
addPeerLoading: { type: Boolean },
confPeerLoading: { type: Boolean },
addMintingAccountKey: { type: String },
removeMintingAccountKey: { type: String },
addPeerMessage: { type: String },
confPeerMessage: { type: String },
addMintingAccountMessage: { type: String },
removeMintingAccountMessage: { type: String },
tempMintingAccount: { type: Object },
selectedAddress: { type: Object }
}
}
static get styles() {
return css`
* {
--mdc-theme-primary: rgb(3, 169, 244);
--paper-input-container-focus-color: var(--mdc-theme-primary);
}
paper-spinner-lite{
height: 24px;
width: 24px;
--paper-spinner-color: var(--mdc-theme-primary);
--paper-spinner-stroke-width: 2px;
}
#group-transaction-page {
background:#fff;
}
mwc-textfield {
width:100%;
}
.red {
--mdc-theme-primary: red;
}
.red-button {
--mdc-theme-primary: red;
--mdc-theme-on-primary: white;
}
mwc-button.red-button {
--mdc-theme-primary: red;
--mdc-theme-on-primary: white;
}
.group-transaction-card {
padding:12px 24px;
background:#fff;
border-radius:2px;
box-shadow: 11;
}
h2 {
margin:0;
}
h2, h3, h4, h5 {
color:#333;
font-weight: 400;
}
[hidden] {
display: hidden !important;
visibility: none !important;
}
.details {
display: flex;
font-size: 18px;
}
`
}
constructor() {
super()
this.upTime = ""
this.mintingAccounts = []
this.peers = []
this.addPeerLoading = false
this.confPeerLoading = false
this.addMintingAccountLoading = false
this.removeMintingAccountLoading = false
this.addMintingAccountKey = ''
this.removeMintingAccountKey = ''
this.addPeerMessage = ''
this.confPeerMessage = ''
this.addMintingAccountMessage = ''
this.removeMintingAccountMessage = ''
this.tempMintingAccount = {}
this.selectedAddress = {}
this.config = {
user: {
node: {
}
}
}
}
render() {
return html`
<div id="group-transaction-page">
<div class="group-transaction-card">
<h2>Group Transaction</h2>
<p>${this.addMintingAccountMessage}</p>
</div>
</div>
`
}
firstUpdated() {
const getGroupIdFromURL = () => {
let tempUrl = document.location.href
let decodeTempUrl = decodeURI(tempUrl)
let splitedUrl = decodeTempUrl.split('?')
let myGroupId = splitedUrl[1]
this.addMintingAccountMessage = myGroupId
}
getGroupIdFromURL()
let configLoaded = false
parentEpml.ready().then(() => {
parentEpml.subscribe('selected_address', async selectedAddress => {
this.selectedAddress = {}
selectedAddress = JSON.parse(selectedAddress)
if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
this.selectedAddress = selectedAddress
})
parentEpml.subscribe('config', c => {
if (!configLoaded) {
// setTimeout(getGroupIdFromURL, 1)
configLoaded = true
}
this.config = JSON.parse(c)
})
})
parentEpml.imReady()
}
isEmptyArray(arr) {
if (!arr) { return true }
return arr.length === 0
}
}
window.customElements.define('group-transaction', GroupTransaction)

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css">
<link rel="stylesheet" href="/font/switch-theme.css">
<script>
const checkBack = localStorage.getItem('qortalTheme')
if (checkBack === 'dark') {
newtheme = 'dark';
} else {
newtheme = 'light';
}
document.querySelector('html').setAttribute('theme', newtheme);
</script>
<style>
html {
--scrollbarBG: #a1a1a1;
--thumbBG: #6a6c75;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}
html,
body {
margin: 0;
font-family: "Roboto", sans-serif;
background: var(--plugback);
}
</style>
</head>
<body>
<group-transaction></group-transaction>
<script src="group-transaction.js"></script>
</body>
</html>

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css">
<link rel="stylesheet" href="/font/switch-theme.css">
<script>
const checkBack = localStorage.getItem('qortalTheme')
if (checkBack === 'dark') {
newtheme = 'dark';
} else {
newtheme = 'light';
}
document.querySelector('html').setAttribute('theme', newtheme);
</script>
<style>
html {
--scrollbarBG: #a1a1a1;
--thumbBG: #6a6c75;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}
html,
body {
margin: 0;
font-family: "Roboto", sans-serif;
background: var(--plugback);
}
</style>
</head>
<body>
<group-management></group-management>
<script src="group-management.js"></script>
</body>
</html>

View File

@@ -0,0 +1 @@
<h1>Core plugin</h1>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,179 @@
import { parentEpml } from './connect.js';
import './streams/streams.js';
let config = {};
let haveRegisteredNodeManagement = false;
parentEpml.ready().then(() => {
// pluginUrlsConf
let pluginUrlsConf = [
{
url: 'minting',
domain: 'core',
page: 'minting/index.html',
title: 'Minting Details',
icon: 'vaadin:info-circle',
menus: [],
parent: false,
},
{
url: 'become-minter',
domain: 'core',
page: 'become-minter/index.html',
title: 'Become a Minter',
icon: 'vaadin:info-circle',
menus: [],
parent: false,
},
{
url: 'sponsorship-list',
domain: 'core',
page: 'sponsorship-list/index.html',
title: 'Become a Minter',
icon: 'vaadin:info-circle',
menus: [],
parent: false,
},
{
url: 'wallet',
domain: 'core',
page: 'wallet/index.html',
title: 'Wallet',
icon: 'vaadin:wallet',
menus: [],
parent: false,
},
{
url: 'trade-portal',
domain: 'core',
page: 'trade-portal/index.html',
title: 'Trade Portal',
icon: 'vaadin:bullets',
menus: [],
parent: false,
},
{
url: 'trade-bot-portal',
domain: 'core',
page: 'trade-bot/index.html',
title: 'Auto Buy',
icon: 'vaadin:calc-book',
menus: [],
parent: false,
},
{
url: 'reward-share',
domain: 'core',
page: 'reward-share/index.html',
title: 'Reward Share',
icon: 'vaadin:share-square',
menus: [],
parent: false,
},
{
url: 'name-registration',
domain: 'core',
page: 'name-registration/index.html',
title: 'Name Registration',
icon: 'vaadin:user-check',
menus: [],
parent: false,
},
{
url: 'names-market',
domain: 'core',
page: 'names-market/index.html',
title: 'Names Market',
icon: 'vaadin:user-check',
menus: [],
parent: false,
},
{
url: 'websites',
domain: 'core',
page: 'qdn/index.html',
title: 'Websites',
icon: 'vaadin:desktop',
menus: [],
parent: false,
},
{
url: 'qapps',
domain: 'core',
page: 'q-app/index.html',
title: 'Q-Apps',
icon: 'vaadin:desktop',
menus: [],
parent: false,
},
{
url: 'data-management',
domain: 'core',
page: 'qdn/data-management/index.html',
title: 'Data Management',
icon: 'vaadin:database',
menus: [],
parent: false,
},
{
url: 'q-chat',
domain: 'core',
page: 'messaging/q-chat/index.html',
title: 'Q-Chat',
icon: 'vaadin:chat',
menus: [],
parent: false,
},
{
url: 'group-management',
domain: 'core',
page: 'group-management/index.html',
title: 'Group Management',
icon: 'vaadin:group',
menus: [],
parent: false,
},
{
url: 'puzzles',
domain: 'core',
page: 'puzzles/index.html',
title: 'Puzzles',
icon: 'vaadin:puzzle-piece',
menus: [],
parent: false,
},
];
const registerPlugins = (pluginInfo) => {
parentEpml.request('registerUrl', pluginInfo);
};
const checkNode =
window.parent.reduxStore.getState().app.nodeConfig.knownNodes[
window.parent.reduxStore.getState().app.nodeConfig.node
];
parentEpml.subscribe('config', (c) => {
config = JSON.parse(c);
// Only register node management if node management is enabled and it hasn't already been registered
if (!haveRegisteredNodeManagement && checkNode.enableManagement) {
haveRegisteredNodeManagement = true;
let nodeManagementConf = {
url: 'node-management',
domain: 'core',
page: 'node-management/index.html',
title: 'Node Management',
icon: 'vaadin:cloud',
menus: [],
parent: false,
};
let _pluginUrlsConf = [...pluginUrlsConf, nodeManagementConf];
registerPlugins(_pluginUrlsConf);
} else {
registerPlugins(pluginUrlsConf);
}
});
});

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,82 @@
import { LitElement, html, css } from 'lit'
class ChainMessaging extends LitElement {
static get properties() {
return {
loading: { type: Boolean },
theme: { type: String, reflect: true }
}
}
static get styles() {
return css`
* {
--mdc-theme-primary: rgb(3, 169, 244);
--paper-input-container-focus-color: var(--mdc-theme-primary);
}
paper-spinner-lite{
height: 24px;
width: 24px;
--paper-spinner-color: var(--mdc-theme-primary);
--paper-spinner-stroke-width: 2px;
}
#chain-messaging-page {
background: var(--white);
}
`
}
constructor() {
super()
this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light'
}
render() {
return html`
<div id="chain-messaging-page">
<h2 style="text-align: center; margin-top: 3rem; color: var(--black)">Coming Soon!</h2>
</div>
`
}
firstUpdated() {
this.changeTheme()
setInterval(() => {
this.changeTheme();
}, 100)
window.addEventListener("contextmenu", (event) => {
event.preventDefault();
});
window.addEventListener("click", () => {
});
window.onkeyup = (e) => {
if (e.keyCode === 27) {
}
}
}
changeTheme() {
const checkTheme = localStorage.getItem('qortalTheme')
if (checkTheme === 'dark') {
this.theme = 'dark';
} else {
this.theme = 'light';
}
document.querySelector('html').setAttribute('theme', this.theme);
}
isEmptyArray(arr) {
if (!arr) { return true }
return arr.length === 0
}
}
window.customElements.define('chain-messaging', ChainMessaging)

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css">
<link rel="stylesheet" href="/font/switch-theme.css">
<script>
const checkBack = localStorage.getItem('qortalTheme')
if (checkBack === 'dark') {
newtheme = 'dark';
} else {
newtheme = 'light';
}
document.querySelector('html').setAttribute('theme', newtheme);
</script>
<style>
html {
--scrollbarBG: #a1a1a1;
--thumbBG: #6a6c75;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}
html,
body {
margin: 0;
font-family: "Roboto", sans-serif;
background: var(--plugback);
}
</style>
</head>
<body>
<chain-messaging></chain-messaging>
<script src="chain-messaging.js"></script>
</body>
</html>

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css">
<link rel="stylesheet" href="/font/switch-theme.css">
<script>
const checkBack = localStorage.getItem('qortalTheme')
if (checkBack === 'dark') {
newtheme = 'dark';
} else {
newtheme = 'light';
}
document.querySelector('html').setAttribute('theme', newtheme);
</script>
<style>
html {
--scrollbarBG: #a1a1a1;
--thumbBG: #6a6c75;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}
html,
body {
margin: 0;
font-family: "Roboto", sans-serif;
background: var(--plugback);
}
</style>
</head>
<body>
<q-messaging></q-messaging>
<script src="messaging.js"></script>
</body>
</html>

View File

@@ -0,0 +1,244 @@
import { LitElement, html, css } from 'lit'
import { Epml } from '../../../epml.js'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class Messaging extends LitElement {
static get properties() {
return {
theme: { type: String, reflect: true }
}
}
static get styles() {
return css`
* {
--mdc-theme-primary: rgb(3, 169, 244);
--mdc-theme-surface: var(--white);
--mdc-dialog-content-ink-color: var(--black);
--paper-input-container-focus-color: var(--mdc-theme-primary);
--lumo-primary-text-color: rgb(0, 167, 245);
--lumo-primary-color-50pct: rgba(0, 167, 245, 0.5);
--lumo-primary-color-10pct: rgba(0, 167, 245, 0.1);
--lumo-primary-color: hsl(199, 100%, 48%);
--lumo-base-color: var(--white);
--lumo-body-text-color: var(--black);
}
#page {
background: var(--white);
padding: 12px 24px;
}
h2 {
margin:0;
font-weight: 400;
}
h3, h4, h5 {
color: var(--black);
font-weight: 400;
}
.major-title {
color: rgb(3, 169, 244);
margin-top: 1rem;
font-weight: 400;
font-size: 28px;
text-align: center;
}
.sub-title {
color: rgb(3, 169, 244);
margin-top: .5rem;
font-weight: 400;
text-align: center;
}
.sub-title:hover {
cursor: pointer;
}
.sub-url {
font-size: 19px;
text-decoration: underline;
}
.sub-url:hover {
cursor: pointer;
}
.divCard {
border: 1px solid var(--border);
padding: 1em;
box-shadow: 0 .3px 1px 0 rgba(0,0,0,0.14), 0 1px 1px -1px rgba(0,0,0,0.12), 0 1px 2px 0 rgba(0,0,0,0.20);
margin-bottom: 1.5rem;
}
p {
color: var(--black);
}
ul, ul li {
color: var(--black);
}
`
}
constructor() {
super()
this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light'
}
render() {
return html`
<div id="page">
<div class="divCard">
<h2 class="major-title">Welcome to the Qortal Messaging System!</h2>
<p style="font-size: 19px;">With this system, you are able to accomplish multiple types of messaging available to you in Qortal:</p>
<ul>
<li class="sub-url" @click=${() => this.getUrl('chain-messaging')}><strong>Chain Based Messaging</strong></li>
<li class="sub-url" @click=${() => this.getUrl('q-chat')}><strong>Q-Chat</strong></li>
</ul>
</div>
<div class="divCard">
<h3 class="sub-title" @click=${() => this.getUrl('chain-messaging')}>Chain-Based Messaging</h3>
<p style="font-size: 17px;">A long-term message that is stored <strong>ON CHAIN</strong>.
These messages <strong>are able</strong> to be <strong>sent to groups or individual accounts</strong>, and are essentially <strong>the 'e-mail' of Qortal</strong>.
Use these messages if you intend on the message being a <strong>PERMANENT message</strong> that stays when and where you send it.</p>
<ul>
<li style="font-size: 17px; padding: 10px;">There are no @ in Qortal Chain Messaging, only 'registered names'. As the registered names on the chain can only be registered ONCE. Therefore, there are NO DUPLICATES.</li>
<li style="font-size: 17px; padding: 10px;">Chain Messages LAST FOREVER</li>
<li style="font-size: 17px; padding: 10px;"><strong>Encyption is DEFAULT</strong>, unless chosen to be NOT encrypted. (also known as a 'public message' which are readable by an api call from anyone.)</li>
<li style="font-size: 17px; padding: 10px;">'Attachments' will be possible in the future, with the use of the Qortal Data System.</li>
<li style="font-size: 17px; padding: 10px;">Public Messages can be used for 'verification purposes' such as 'copyrights' or 'I did it first' notations. The terms 'copyright' and 'patent' are a thing of the past, if you did it first, put in a public message, and it is by default proven.</li>
</ul>
</div>
<div class="divCard">
<h3 class="sub-title" @click=${() => this.getUrl('q-chat')}>Q-Chat</h3>
<p style="font-size: 17px;">Is a custom chat system that is UNLIKE ANY OTHER in existence. It is the FIRST OF ITS KIND IN THE WORLD.
It is a real-time, blockchain-based chat system that utilizes a memory-based PoW (Proof Of Work) algorithm, to implement a specialized transaction that is 'temporary', on the Qortal blockchain.
Q-Chat messages will DISSAPEAR AFTER 24 HOURS and therefore are not a great choice for things you wish to be permanent.</p>
<ul>
<li style="font-size: 17px; padding: 10px;"><strong>In the future, there will be a 'pinning' system</strong>, that will allow you to convert, or send messages by default with, the ability to stay forever on the Qortal Blockchain. So that if you DO decide that you like a message enough to keep it forever, you may do so.</li>
<li style="font-size: 17px; padding: 10px;"><strong>Q-chat messages are encrypted</strong> (at the moment in June, 2020, Q-chat PM's are encrypted, but the group messages are base58 encoded, meaning they aren't plain text, but if you're smart you can decode them. However, IN THE NEAR FUTURE, ALL MESSAGES REGARDLESS OF GROUP OR PM WILL BE DEFAULT ENCRYPTED WITH PUB/PRIV KEY OF YOUR QORTAL ACCOUNT.</li>
<li style="font-size: 17px; padding: 10px;">Uses a UNIQUE memory-based PoW algorithm, to send messages FREE (no transaction fee)</li>
<li style="font-size: 17px; padding: 10px;">Text-based messaging for the future.</li>
</ul>
</div>
</div>
`
}
firstUpdated() {
this.changeTheme()
setInterval(() => {
this.changeTheme();
}, 100)
window.addEventListener("contextmenu", (event) => {
event.preventDefault();
this._textMenu(event)
});
window.addEventListener("click", () => {
parentEpml.request('closeCopyTextMenu', null)
});
window.onkeyup = (e) => {
if (e.keyCode === 27) {
parentEpml.request('closeCopyTextMenu', null)
}
}
let configLoaded = false
parentEpml.ready().then(() => {
parentEpml.subscribe('selected_address', async selectedAddress => {
this.selectedAddress = {}
selectedAddress = JSON.parse(selectedAddress)
if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
this.selectedAddress = selectedAddress
})
parentEpml.subscribe('config', c => {
if (!configLoaded) {
configLoaded = true
}
this.config = JSON.parse(c)
})
parentEpml.subscribe('copy_menu_switch', async value => {
if (value === 'false' && window.getSelection().toString().length !== 0) {
this.clearSelection()
}
})
})
parentEpml.imReady()
}
changeTheme() {
const checkTheme = localStorage.getItem('qortalTheme')
if (checkTheme === 'dark') {
this.theme = 'dark';
} else {
this.theme = 'light';
}
document.querySelector('html').setAttribute('theme', this.theme);
}
getUrl(pageId) {
this.onPageNavigation(`/app/${pageId}`)
}
onPageNavigation(pageUrl) {
parentEpml.request('setPageUrl', pageUrl)
}
_textMenu(event) {
const getSelectedText = () => {
var text = "";
if (typeof window.getSelection != "undefined") {
text = window.getSelection().toString();
} else if (typeof this.shadowRoot.selection != "undefined" && this.shadowRoot.selection.type == "Text") {
text = this.shadowRoot.selection.createRange().text;
}
return text;
}
const checkSelectedTextAndShowMenu = () => {
let selectedText = getSelectedText();
if (selectedText && typeof selectedText === 'string') {
let _eve = { pageX: event.pageX, pageY: event.pageY, clientX: event.clientX, clientY: event.clientY }
let textMenuObject = { selectedText: selectedText, eventObject: _eve, isFrame: true }
parentEpml.request('openCopyTextMenu', textMenuObject)
}
}
checkSelectedTextAndShowMenu()
}
clearSelection() {
window.getSelection().removeAllRanges()
window.parent.getSelection().removeAllRanges()
}
}
window.customElements.define('q-messaging', Messaging)

View File

@@ -0,0 +1,82 @@
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.chatBytes, e.data.path, e.data.difficulty)
postMessage(response)
})
const memory = new WebAssembly.Memory({ initial: 256, maximum: 256 })
const heap = new Uint8Array(memory.buffer)
const computePow = async (chatBytes, path, difficulty) => {
let response = null
await new Promise((resolve, reject)=> {
const _chatBytesArray = Object.keys(chatBytes).map(function (key) { return chatBytes[key]; });
const chatBytesArray = new Uint8Array(_chatBytesArray);
const chatBytesHash = new Sha256().process(chatBytesArray).finish().result;
const hashPtr = sbrk(32, heap);
const hashAry = new Uint8Array(memory.buffer, hashPtr, 32);
hashAry.set(chatBytesHash);
const workBufferLength = 8 * 1024 * 1024;
const workBufferPtr = sbrk(workBufferLength, heap);
const importObject = {
env: {
memory: memory
},
};
function loadWebAssembly(filename, imports) {
// Fetch the file and compile it
return fetch(filename)
.then(response => response.arrayBuffer())
.then(buffer => WebAssembly.compile(buffer))
.then(module => {
// Create the instance.
return new WebAssembly.Instance(module, importObject);
});
}
loadWebAssembly(path)
.then(wasmModule => {
response = {
nonce : wasmModule.exports.compute2(hashPtr, workBufferPtr, workBufferLength, difficulty),
chatBytesArray
}
resolve()
});
})
return response
}

View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css">
<link rel="stylesheet" href="/font/switch-theme.css">
<script>
const checkBack = localStorage.getItem('qortalTheme')
if (checkBack === 'dark') {
newtheme = 'dark';
} else {
newtheme = 'light';
}
document.querySelector('html').setAttribute('theme', newtheme);
</script>
<style>
html {
--scrollbarBG: #a1a1a1;
--thumbBG: #6a6c75;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}
html,
body {
margin: 0;
font-family: "Roboto", sans-serif;
background: var(--plugback);
overflow: hidden;
}
</style>
</head>
<body>
<q-chat></q-chat>
<script src="q-chat.js"></script>
</body>
</html>

View File

@@ -0,0 +1,479 @@
import { css } from 'lit'
export const qchatStyles = css`
* {
--mdc-theme-primary: rgb(3, 169, 244);
--mdc-theme-secondary: var(--mdc-theme-primary);
--paper-input-container-focus-color: var(--mdc-theme-primary);
--mdc-theme-surface: var(--white);
--mdc-dialog-content-ink-color: var(--black);
--lumo-primary-text-color: rgb(0, 167, 245);
--lumo-primary-color-50pct: rgba(0, 167, 245, 0.5);
--lumo-primary-color-10pct: rgba(0, 167, 245, 0.1);
--lumo-primary-color: hsl(199, 100%, 48%);
--lumo-base-color: var(--white);
--lumo-body-text-color: var(--black);
--_lumo-grid-border-color: var(--border);
--_lumo-grid-secondary-border-color: var(--border2);
--mdc-dialog-min-width: 750px;
}
paper-spinner-lite {
height: 24px;
width: 24px;
--paper-spinner-color: var(--mdc-theme-primary);
--paper-spinner-stroke-width: 2px;
}
*,
*:before,
*:after {
box-sizing: border-box;
}
ul {
list-style: none;
padding: 0;
}
.container {
margin: 0 auto;
width: 100%;
background: var(--white);
}
.people-list {
width: 20vw;
float: left;
height: 100vh;
overflow-y: hidden;
border-right: 3px #ddd solid;
}
.people-list .blockedusers {
z-index: 1;
position: absolute;
bottom: 0;
width: 20vw;
background: var(--white);
border-right: 3px #ddd solid;
display: flex;
justify-content: space-between;
gap: 15px;
flex-direction: column;
padding: 5px 30px 0 30px;
}
.groups-button-container {
position: relative;
}
.groups-button {
width: 100%;
background-color: rgb(116, 69, 240);
border: none;
color: white;
font-weight: bold;
font-family: 'Roboto';
letter-spacing: 0.8px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 5px;
gap: 10px;
padding: 5px 8px;
transition: all 0.1s ease-in-out;
}
.groups-button-notif {
position: absolute;
top: -10px;
right: -8px;
width: 25px;
border-radius: 50%;
height: 25px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
font-family: Montserrat, sans-serif;
font-size: 16px;
font-weight: bold;
color: black;
background-color: rgb(51, 213, 0);
user-select: none;
transition: all 0.3s ease-in-out 0s;
}
.groups-button-notif:hover {
cursor: auto;
box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;
}
.groups-button-notif:hover + .groups-button-notif-number {
display: block;
opacity: 1;
animation: fadeIn 0.6s;
}
@keyframes fadeIn {
from {
opacity: 0;
top: -10px;
}
to {
opacity: 1;
top: -60px;
}
}
.groups-button-notif-number {
position: absolute;
transform: translateX(-50%);
left: 50%;
width: 150px;
text-align: center;
border-radius: 3px;
padding: 5px 10px;
background-color: white;
color: black;
font-family: Roboto, sans-serif;
letter-spacing: 0.3px;
font-weight: 300;
display: none;
opacity: 0;
top: -60px;
box-shadow: rgb(216 216 216 / 25%) 0px 6px 12px -2px, rgb(0 0 0 / 30%) 0px 3px 7px -3px;
}
.groups-button:hover {
cursor: pointer;
filter: brightness(120%);
}
.people-list .search {
padding-top: 20px;
padding-left: 20px;
padding-right: 20px;
}
.center {
margin: 0;
position: absolute;
padding-top: 12px;
left: 50%;
-ms-transform: translateX(-50%);
transform: translateX(-50%);
}
.people-list .create-chat {
border-radius: 5px;
border: none;
display: inline-block;
padding: 14px;
color: #fff;
background: var(--tradehead);
width: 100%;
font-size: 15px;
text-align: center;
cursor: pointer;
}
.people-list .create-chat:hover {
opacity: .8;
box-shadow: 0 3px 5px rgba(0, 0, 0, .2);
}
.people-list ul {
padding: 0px 0px 60px 0px;
height: 85vh;
overflow-y: auto;
overflow-x: hidden;
}
.people-list ul::-webkit-scrollbar-track {
background-color: whitesmoke;
border-radius: 7px;
}
.people-list ul::-webkit-scrollbar {
width: 6px;
border-radius: 7px;
background-color: whitesmoke;
}
.people-list ul::-webkit-scrollbar-thumb {
background-color: rgb(180, 176, 176);
border-radius: 7px;
transition: all 0.3s ease-in-out;
}
.chat {
width: 80vw;
height: 100vh;
float: left;
background: var(--white);
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
color: #434651;
box-sizing: border-box;
}
.chat .new-message-bar {
display: flex;
flex: 0 1 auto;
align-items: center;
justify-content: space-between;
padding: 0px 25px;
font-size: 14px;
font-weight: 500;
top: 0;
position: absolute;
left: 20vw;
right: 0;
z-index: 5;
background: var(--tradehead);
color: var(--white);
border-radius: 0 0 8px 8px;
min-height: 25px;
transition: opacity .15s;
text-transform: capitalize;
opacity: .85;
cursor: pointer;
}
.chat .new-message-bar:hover {
opacity: .75;
transform: translateY(-1px);
box-shadow: 0 3px 7px rgba(0, 0, 0, .2);
}
.hide-new-message-bar {
display: none !important;
}
.chat .chat-history {
position: absolute;
top: 0;
right: 0;
bottom: 100%;
left: 20vw;
border-bottom: 2px solid var(--white);
overflow-y: hidden;
height: 100vh;
box-sizing: border-box;
}
.chat .chat-message {
padding: 10px;
height: 10%;
display: inline-block;
width: 100%;
background-color: #eee;
}
.chat .chat-message textarea {
width: 90%;
border: none;
font-size: 16px;
padding: 10px 20px;
border-radius: 5px;
resize: none;
}
.chat .chat-message button {
float: right;
color: #94c2ed;
font-size: 16px;
text-transform: uppercase;
border: none;
cursor: pointer;
font-weight: bold;
background: #f2f5f8;
padding: 10px;
margin-top: 4px;
margin-right: 4px;
}
.chat .chat-message button:hover {
color: #75b1e8;
}
.online,
.offline,
.me {
margin-right: 3px;
font-size: 10px;
}
.clearfix:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
.red {
--mdc-theme-primary: red;
}
h2 {
margin:0;
}
h2, h3, h4, h5 {
color: var(--black);
font-weight: 400;
}
[hidden] {
display: hidden !important;
visibility: none !important;
}
.details {
display: flex;
font-size: 18px;
}
.title {
font-weight:600;
font-size:12px;
line-height: 32px;
opacity: 0.66;
}
.textarea {
width: 100%;
border: none;
display: inline-block;
font-size: 16px;
padding: 10px 20px;
border-radius: 5px;
height: 120px;
resize: none;
background: #eee;
}
.dialog-container {
position: relative;
display: flex;
align-items: center;
flex-direction: column;
padding: 0 10px;
gap: 10px;
height: 100%;
}
.dialog-header {
color: var(--chat-bubble-msg-color);
}
.dialog-subheader {
color: var(--chat-bubble-msg-color);
}
.modal-button-row {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.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;
}
.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: all 0.3s ease-in-out;
background: none;
border-radius: 50%;
padding: 6px 3px;
font-size: 21px;
}
.search-icon:hover {
cursor: pointer;
background: #d7d7d75c;
}
.search-results-div {
position: absolute;
top: 25px;
right: 25px;
}
.user-verified {
position: absolute;
top: 0;
right: 5px;
display: flex;
align-items: center;
gap: 10px;
color: #04aa2e;
font-size: 13px;
}
`

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,984 @@
import { LitElement, html, css } from 'lit';
import { render } from 'lit/html.js';
import { passiveSupport } from 'passive-events-support/src/utils'
passiveSupport({
events: ['touchstart']
})
import { Epml } from '../../../../epml.js';
import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate';
import { qchatStyles } from './q-chat-css.src.js'
import WebWorker from 'web-worker:./computePowWorker.src.js';
import {repeat} from 'lit/directives/repeat.js';
registerTranslateConfig({
loader: lang => fetch(`/language/${lang}.json`).then(res => res.json())
})
import '../../components/ChatWelcomePage.js'
import '../../components/ChatHead.js'
import '../../components/ChatPage.js'
import '../../components/WrapperModal.js';
import '../../components/ChatSeachResults.js';
import snackbar from '../../components/snackbar.js'
import '@polymer/paper-spinner/paper-spinner-lite.js'
import '@material/mwc-button'
import '@material/mwc-dialog'
import '@material/mwc-icon'
import '@material/mwc-snackbar'
import '@vaadin/grid'
import StarterKit from '@tiptap/starter-kit'
import Underline from '@tiptap/extension-underline';
import Placeholder from '@tiptap/extension-placeholder'
import Highlight from '@tiptap/extension-highlight'
import { Editor, Extension } from '@tiptap/core'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class Chat extends LitElement {
static get properties() {
return {
selectedAddress: { type: Object },
chatTitle: { type: String },
chatHeads: { type: Array },
chatHeadsObj: { type: Object },
chatId: { type: String },
messages: { type: Array },
btnDisable: { type: Boolean },
isLoading: { type: Boolean },
balance: { type: Number },
theme: { type: String, reflect: true },
blockedUsers: { type: Array },
blockedUserList: { type: Array },
privateMessagePlaceholder: { type: String},
imageFile: { type: Object },
activeChatHeadUrl: { type: String },
openPrivateMessage: { type: Boolean },
userFound: { type: Array},
userFoundModalOpen: { type: Boolean },
userSelected: { type: Object },
editor: {type: Object},
groupInvites: { type: Array },
}
}
static styles = [qchatStyles]
constructor() {
super()
this.selectedAddress = {}
this.config = {
user: {
node: {
}
}
}
this.chatTitle = ""
this.chatHeads = []
this.chatHeadsObj = {}
this.chatId = ''
this.balance = 1
this.messages = []
this.btnDisable = false
this.isLoading = false
this.showNewMessageBar = this.showNewMessageBar.bind(this)
this.hideNewMessageBar = this.hideNewMessageBar.bind(this)
this.setOpenPrivateMessage = this.setOpenPrivateMessage.bind(this)
this._sendMessage = this._sendMessage.bind(this)
this.insertImage = this.insertImage.bind(this)
this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light'
this.blockedUsers = []
this.blockedUserList = []
this.privateMessagePlaceholder = ""
this.imageFile = null
this.activeChatHeadUrl = ''
this.openPrivateMessage = false
this.userFound = []
this.userFoundModalOpen = false
this.userSelected = {}
this.groupInvites = []
}
async setActiveChatHeadUrl(url) {
this.activeChatHeadUrl = ''
await this.updateComplete;
this.activeChatHeadUrl = url
}
resetChatEditor(){
this.editor.commands.setContent('')
}
async getUpdateCompleteTextEditor() {
await super.getUpdateComplete();
const marginElements = Array.from(this.shadowRoot.querySelectorAll('chat-text-editor'));
await Promise.all(marginElements.map(el => el.updateComplete));
const marginElements2 = Array.from(this.shadowRoot.querySelectorAll('wrapper-modal'));
await Promise.all(marginElements2.map(el => el.updateComplete));
return true;
}
async connectedCallback() {
super.connectedCallback();
await this.getUpdateCompleteTextEditor();
const elementChatId = this.shadowRoot.getElementById('messageBox').shadowRoot.getElementById('privateMessage')
this.editor = new Editor({
onUpdate: ()=> {
this.shadowRoot.getElementById('messageBox').getMessageSize(this.editor.getJSON())
},
element: elementChatId,
extensions: [
StarterKit,
Underline,
Highlight,
Placeholder.configure({
placeholder: 'Write something …',
}),
Extension.create({
addKeyboardShortcuts:()=> {
return {
'Enter': ()=> {
const chatTextEditor = this.shadowRoot.getElementById('messageBox')
chatTextEditor.sendMessageFunc({
})
return true
}
}
}})
]
})
this.unsubscribeStore = window.parent.reduxStore.subscribe(() => {
try {
if(window.parent.location && window.parent.location.search){
const queryString = window.parent.location.search;
const params = new URLSearchParams(queryString);
const chat = params.get("chat")
if(chat && chat !== this.activeChatHeadUrl){
let url = window.parent.location.href;
let newUrl = url.split("?")[0];
window.parent.history.pushState({}, "", newUrl);
this.setActiveChatHeadUrl(chat)
}
}
} catch (error) {
console.error(error)
}
});
}
disconnectedCallback() {
super.disconnectedCallback();
this.editor.destroy();
this.unsubscribeStore();
}
updatePlaceholder(editor, text){
editor.extensionManager.extensions.forEach((extension) => {
if (extension.name === "placeholder") {
extension.options["placeholder"] = text
editor.commands.focus('end')
}
})
}
render() {
return html`
<div class="container clearfix">
<div class="people-list" id="people-list">
<div class="search">
<div class="create-chat" @click=${() => {
this.openPrivateMessage = true;
}}>${translate("chatpage.cchange1")}
</div>
</div>
<ul class="list">
${this.isEmptyArray(this.chatHeads) ? this.renderLoadingText() : this.renderChatHead(this.chatHeads)}
</ul>
<div class="blockedusers">
<!-- <div class="groups-button-container">
<button
@click=${() => {
this.redirectToGroups();
}}
class="groups-button">
<mwc-icon>groups</mwc-icon>
${translate("sidemenu.groupmanagement")}
</button>
${this.groupInvites.length > 0 ? (
html`
<div class="groups-button-notif">
${this.groupInvites.length}
</div>
<div class="groups-button-notif-number">
${this.groupInvites.length} ${translate("chatpage.cchange60")}
</div>
`
) : null}
</div> -->
<mwc-button
raised
label="${translate("chatpage.cchange3")}"
icon="person_off"
@click=${() => this.shadowRoot.querySelector('#blockedUserDialog').show()}>
</mwc-button>
</div>
</div>
<div class="chat">
<div id="newMessageBar" class="new-message-bar hide-new-message-bar clearfix" @click=${() => this.scrollToBottom()}>
<span style="flex: 1;">${translate("chatpage.cchange4")}</span>
<span>${translate("chatpage.cchange5")} <mwc-icon style="font-size: 16px; vertical-align: bottom;">keyboard_arrow_down</mwc-icon></span>
</div>
<div class="chat-history">
${this.activeChatHeadUrl ? html`${this.renderChatPage()}` : html`${this.renderChatWelcomePage()}`}
</div>
</div>
<!-- Start Chatting Dialog -->
<wrapper-modal
.onClickFunc=${() => {
this.resetChatEditor();
this.openPrivateMessage = false;
this.shadowRoot.getElementById('sendTo').value = "";
this.userFoundModalOpen = false;
this.userFound = [];
} }
style=${this.openPrivateMessage ? "visibility:visible;z-index:50" : "visibility: hidden;z-index:-100;position: relative"}>
<div style=${"position: relative"}>
<div class="dialog-container">
<div class="dialog-header" style="text-align: center">
<h1>${translate("chatpage.cchange1")}</h1>
<hr>
</div>
<p class="dialog-subheader">${translate("chatpage.cchange6")}</p>
<div class="search-field">
<input
type="text"
class="name-input"
?disabled=${this.isLoading}
id="sendTo"
placeholder="${translate("chatpage.cchange7")}"
value=${this.userSelected.name ? this.userSelected.name: ''}
@keypress=${() => {
this.userSelected = {};
this.requestUpdate();
}}
/>
${this.userSelected.name ? (
html`
<div class="user-verified">
<p >${translate("chatpage.cchange38")}</p>
<vaadin-icon icon="vaadin:check-circle-o" slot="icon"></vaadin-icon>
</div>
`
) : (
html`
<vaadin-icon
@click=${this.userSearch}
slot="icon"
icon="vaadin:open-book"
class="search-icon">
</vaadin-icon>
`
)}
</div>
<chat-text-editor
iframeId="privateMessage"
?hasGlobalEvents=${false}
placeholder="${translate("chatpage.cchange8")}"
.imageFile=${this.imageFile}
._sendMessage=${this._sendMessage}
.insertImage=${this.insertImage}
?isLoading=${this.isLoading}
.isLoadingMessages=${false}
id="messageBox"
.editor=${this.editor}
.updatePlaceholder=${(editor, value)=> this.updatePlaceholder(editor, value)}
>
</chat-text-editor>
<div class="modal-button-row">
<button
class="modal-button-red"
@click=${() => {
this.resetChatEditor();
this.openPrivateMessage = false;
}}
?disabled="${this.isLoading}"
>
${translate("chatpage.cchange33")}
</button>
<button
class="modal-button"
@click=${()=> {
const chatTextEditor = this.shadowRoot.getElementById('messageBox')
chatTextEditor.sendMessageFunc({
})
}}
?disabled="${this.isLoading}">
${this.isLoading === false
? this.renderSendText()
: html`
<paper-spinner-lite active></paper-spinner-lite>
`}
</button>
</div>
</div>
<div class="search-results-div">
<chat-search-results
.onClickFunc=${(result) => {
this.userSelected = result;
this.userFound = [];
this.userFoundModalOpen = false;
}}
.closeFunc=${() => {
this.userFoundModalOpen = false;
this.userFound = [];
}}
.searchResults=${this.userFound}
?isOpen=${this.userFoundModalOpen}
?loading=${this.isLoading}>
</chat-search-results>
</div>
</div>
</wrapper-modal>
<!-- Blocked User Dialog -->
<mwc-dialog id="blockedUserDialog">
<div style="text-align:center">
<h1>${translate("chatpage.cchange10")}</h1>
<hr>
<br>
</div>
<vaadin-grid theme="compact" id="blockedGrid" ?hidden="${this.isEmptyArray(this.blockedUserList)}" aria-label="Blocked List" .items="${this.blockedUserList}" all-rows-visible>
<vaadin-grid-column auto-width header="${translate("chatpage.cchange11")}" .renderer=${(root, column, data) => {
if (data.item.name === "No registered name") {
render(html`${translate("chatpage.cchange15")}`, root);
} else {
render(html`${data.item.name}`, root);
}
}}>
</vaadin-grid-column>
<vaadin-grid-column auto-width header="${translate("chatpage.cchange12")}" path="owner"></vaadin-grid-column>
<vaadin-grid-column width="10rem" flex-grow="0" header="${translate("chatpage.cchange13")}" .renderer=${(root, column, data) => {
render(html`${this.renderUnblockButton(data.item)}`, root);
}}>
</vaadin-grid-column>
</vaadin-grid>
${this.isEmptyArray(this.blockedUserList) ? html`
<span style="color: var(--black); text-align: center;">${translate("chatpage.cchange14")}</span>
`: ''}
<mwc-button
slot="primaryAction"
dialogAction="cancel"
class="red"
>
${translate("general.close")}
</mwc-button>
</mwc-dialog>
</div>
`
}
async firstUpdated() {
this.changeLanguage();
this.changeTheme();
this.getChatBlockedList();
this.getLocalBlockedList();
// await this.getPendingGroupInvites();
const getBlockedUsers = async () => {
let blockedUsers = await parentEpml.request('apiCall', {
url: `/lists/blockedAddresses?apiKey=${this.getApiKey()}`
})
this.blockedUsers = blockedUsers
setTimeout(getBlockedUsers, 60000)
}
const stopKeyEventPropagation = (e) => {
e.stopPropagation();
return false;
}
const nameInput = this.shadowRoot.getElementById('sendTo');
nameInput.addEventListener('keydown', stopKeyEventPropagation);
this.shadowRoot.getElementById('messageBox').addEventListener('keydown', stopKeyEventPropagation);
const runFunctionsAfterPageLoad = () => {
// Functions to exec after render while waiting for page info...
// getDataFromURL()
try {
let key = `${window.parent.reduxStore.getState().app.selectedAddress.address.substr(0, 10)}_chat-heads`
let localChatHeads = localStorage.getItem(key)
this.setChatHeads(JSON.parse(localChatHeads))
} catch (e) {
// TODO: Could add error handling in case there's a weird one... (-_-)
return
}
// Clear Interval...
if (this.selectedAddress.address !== undefined) {
clearInterval(runFunctionsAfterPageLoadInterval)
return
}
}
let runFunctionsAfterPageLoadInterval = setInterval(runFunctionsAfterPageLoad, 100);
window.addEventListener("contextmenu", (event) => {
event.preventDefault()
this._textMenu(event)
})
window.addEventListener("click", () => {
parentEpml.request('closeCopyTextMenu', null)
parentEpml.request('closeFramePasteMenu', null)
})
window.addEventListener('storage', () => {
const checkLanguage = localStorage.getItem('qortalLanguage')
const checkTheme = localStorage.getItem('qortalTheme')
use(checkLanguage)
if (checkTheme === 'dark') {
this.theme = 'dark'
} else {
this.theme = 'light'
}
document.querySelector('html').setAttribute('theme', this.theme)
})
window.onkeyup = (e) => {
if (e.keyCode === 27) {
parentEpml.request('closeCopyTextMenu', null)
parentEpml.request('closeFramePasteMenu', null)
}
}
let configLoaded = false
parentEpml.ready().then(() => {
parentEpml.subscribe('selected_address', async selectedAddress => {
this.selectedAddress = {}
selectedAddress = JSON.parse(selectedAddress)
if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
this.selectedAddress = selectedAddress
})
parentEpml.subscribe('config', c => {
if (!configLoaded) {
setTimeout(getBlockedUsers, 1)
configLoaded = true
}
this.config = JSON.parse(c)
})
parentEpml.subscribe('chat_heads', chatHeads => {
chatHeads = JSON.parse(chatHeads)
this.getChatHeadFromState(chatHeads)
})
parentEpml.request('apiCall', {
url: `/addresses/balance/${window.parent.reduxStore.getState().app.selectedAddress.address}`
}).then(res => {
this.balance = res
})
parentEpml.subscribe('copy_menu_switch', async value => {
if (value === 'false' && window.getSelection().toString().length !== 0) {
this.clearSelection()
}
})
})
parentEpml.imReady()
}
setOpenPrivateMessage(props) {
this.openPrivateMessage = props.open;
this.shadowRoot.getElementById("sendTo").value = props.name
}
async userSearch() {
const nameValue = this.shadowRoot.getElementById('sendTo').value;
if(!nameValue) {
this.userFound = [];
this.userFoundModalOpen = true;
return;
}
try {
const result = await parentEpml.request('apiCall', {
type: 'api',
url: `/names/${nameValue}`
})
if (result.error === 401) {
this.userFound = [];
} else {
this.userFound = [
...this.userFound,
result,
];
}
this.userFoundModalOpen = true;
} catch (error) {
let err4string = get("chatpage.cchange35");
parentEpml.request('showSnackBar', `${err4string}`)
}
}
redirectToGroups() {
window.location.href = `../../group-management/index.html`
}
async _sendMessage(outSideMsg, msg) {
this.isLoading = true;
const trimmedMessage = msg
if (/^\s*$/.test(trimmedMessage)) {
this.isLoading = false;
} else {
const messageObject = {
messageText: trimmedMessage,
images: [''],
repliedTo: '',
version: 3
}
const stringifyMessageObject = JSON.stringify(messageObject)
this.sendMessage(stringifyMessageObject);
}
}
async sendMessage(messageText) {
this.isLoading = true;
const _recipient = this.shadowRoot.getElementById('sendTo').value;
let recipient;
const validateName = async (receiverName) => {
let myRes;
try {
let myNameRes = await parentEpml.request('apiCall', {
type: 'api',
url: `/names/${receiverName}`
});
if (myNameRes.error === 401) {
myRes = false;
} else {
myRes = myNameRes;
}
return myRes;
} catch (error) {
return "";
}
};
const myNameRes = await validateName(_recipient);
if (!myNameRes) {
recipient = _recipient;
} else {
recipient = myNameRes.owner;
};
const getAddressPublicKey = async () => {
let isEncrypted;
let _publicKey;
let addressPublicKey = await parentEpml.request('apiCall', {
type: 'api',
url: `/addresses/publickey/${recipient}`
})
if (addressPublicKey.error === 102) {
_publicKey = false;
let err4string = get("chatpage.cchange19");
parentEpml.request('showSnackBar', `${err4string}`);
this.isLoading = false;
} else if (addressPublicKey !== false) {
isEncrypted = 1;
_publicKey = addressPublicKey;
sendMessageRequest(isEncrypted, _publicKey);
} else {
let err4string = get("chatpage.cchange39");
parentEpml.request('showSnackBar', `${err4string}`);
this.isLoading = false;
}
};
let _reference = new Uint8Array(64);
window.crypto.getRandomValues(_reference);
let reference = window.parent.Base58.encode(_reference);
const sendMessageRequest = async (isEncrypted, _publicKey) => {
let chatResponse = await parentEpml.request('chat', {
type: 18,
nonce: this.selectedAddress.nonce,
params: {
timestamp: Date.now(),
recipient: recipient,
recipientPublicKey: _publicKey,
hasChatReference: 0,
message: messageText,
lastReference: reference,
proofOfWorkNonce: 0,
isEncrypted: 1,
isText: 1
}
});
_computePow(chatResponse);
};
const _computePow = async (chatBytes) => {
const difficulty = this.balance < 4 ? 18 : 8
const path = window.parent.location.origin + '/memory-pow/memory-pow.wasm.full';
const worker = new WebWorker();
let nonce = null;
let chatBytesArray = null;
await new Promise((res, rej) => {
worker.postMessage({chatBytes, path, difficulty});
worker.onmessage = e => {
worker.terminate();
chatBytesArray = e.data.chatBytesArray;
nonce = e.data.nonce;
res();
}
});
let _response = await parentEpml.request('sign_chat', {
nonce: this.selectedAddress.nonce,
chatBytesArray: chatBytesArray,
chatNonce: nonce
});
getSendChatResponse(_response);
};
const getSendChatResponse = (response) => {
if (response === true) {
this.setActiveChatHeadUrl(`direct/${recipient}`);
this.shadowRoot.getElementById('sendTo').value = "";
this.openPrivateMessage = false;
this.resetChatEditor();
} else if (response.error) {
parentEpml.request('showSnackBar', response.message);
} else {
let err2string = get("chatpage.cchange21");
parentEpml.request('showSnackBar', `${err2string}`);
}
this.isLoading = false;
};
// Exec..
getAddressPublicKey();
}
insertImage(file) {
if (file.type.includes('image')) {
this.imageFile = file;
return;
}
parentEpml.request('showSnackBar', get("chatpage.cchange28"));
}
renderLoadingText() {
return html`${translate("chatpage.cchange2")}`
}
renderSendText() {
return html`${translate("chatpage.cchange9")}`
}
relMessages() {
setTimeout(() => {
window.location.href = window.location.href.split( '#' )[0]
}, 500)
}
getLocalBlockedList() {
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 blockedAddressesUrl = `${nodeUrl}/lists/blockedAddresses?apiKey=${this.getApiKey()}`
localStorage.removeItem("MessageBlockedAddresses")
var hidelist = []
fetch(blockedAddressesUrl).then(response => {
return response.json()
}).then(data => {
data.map(item => {
hidelist.push(item)
})
localStorage.setItem("MessageBlockedAddresses", JSON.stringify(hidelist))
this.blockedUserList = hidelist
})
}
getChatBlockedList() {
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 blockedAddressesUrl = `${nodeUrl}/lists/blockedAddresses?apiKey=${this.getApiKey()}`
const err1string = 'No registered name'
localStorage.removeItem("ChatBlockedAddresses")
var obj = [];
fetch(blockedAddressesUrl).then(response => {
return response.json()
}).then(data => {
return data.map(item => {
const noName = {
name: err1string,
owner: item
}
fetch(`${nodeUrl}/names/address/${item}?limit=0&reverse=true`).then(res => {
return res.json()
}).then(jsonRes => {
if(jsonRes.length) {
jsonRes.map (item => {
obj.push(item)
})
} else {
obj.push(noName)
}
localStorage.setItem("ChatBlockedAddresses", JSON.stringify(obj))
this.blockedUserList = JSON.parse(localStorage.getItem("ChatBlockedAddresses") || "[]")
})
})
})
}
async getPendingGroupInvites() {
const myAddress = window.parent.reduxStore.getState().app.selectedAddress.address
try {
let pendingGroupInvites = await parentEpml.request('apiCall', {
url: `/groups/invites/${myAddress}`
})
this.groupInvites = pendingGroupInvites;
} catch (error) {
let err4string = get("chatpage.cchange61");
parentEpml.request('showSnackBar', `${err4string}`)
}
}
async unblockUser(websiteObj) {
let owner = websiteObj.owner
let items = [
owner
]
let ownersJsonString = JSON.stringify({ "items": items })
let ret = await parentEpml.request('apiCall', {
url: `/lists/blockedAddresses?apiKey=${this.getApiKey()}`,
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: `${ownersJsonString}`
})
if (ret === true) {
this.blockedUsers = this.blockedUsers.filter(item => item != owner)
this.getChatBlockedList()
this.blockedUserList = JSON.parse(localStorage.getItem("ChatBlockedAddresses") || "[]")
let err2string = get("chatpage.cchange16")
snackbar.add({
labelText: `${err2string}`,
dismiss: true
})
this.relMessages()
}
else {
let err3string = get("chatpage.cchange17")
snackbar.add({
labelText: `${err3string}`,
dismiss: true
})
}
return ret
}
renderUnblockButton(websiteObj) {
return html`<mwc-button dense unelevated label="${translate("chatpage.cchange18")}" icon="person_remove" @click="${() => this.unblockUser(websiteObj)}"></mwc-button>`
}
changeTheme() {
const checkTheme = localStorage.getItem('qortalTheme')
if (checkTheme === 'dark') {
this.theme = 'dark';
} else {
this.theme = 'light';
}
document.querySelector('html').setAttribute('theme', this.theme);
}
changeLanguage() {
const checkLanguage = localStorage.getItem('qortalLanguage')
if (checkLanguage === null || checkLanguage.length === 0) {
localStorage.setItem('qortalLanguage', 'us')
use('us')
} else {
use(checkLanguage)
}
}
renderChatWelcomePage() {
return html`
<chat-welcome-page
myAddress=${JSON.stringify(this.selectedAddress)}
.setOpenPrivateMessage=${(val) => this.setOpenPrivateMessage(val)}>
</chat-welcome-page>`
}
renderChatHead(chatHeadArr) {
return chatHeadArr.map(eachChatHead => {
return html`<chat-head activeChatHeadUrl=${this.activeChatHeadUrl} .setActiveChatHeadUrl=${(val)=> this.setActiveChatHeadUrl(val)} chatInfo=${JSON.stringify(eachChatHead)}></chat-head>`
})
}
renderChatPage() {
// Check for the chat ID from and render chat messages
// Else render Welcome to Q-CHat
// TODO: DONE: Do the above in the ChatPage
return html`
<chat-page
.chatHeads=${this.chatHeads}
.hideNewMessageBar=${this.hideNewMessageBar}
.showNewMessageBar=${this.showNewMessageBar}
myAddress=${window.parent.reduxStore.getState().app.selectedAddress.address}
chatId=${this.activeChatHeadUrl}
.setOpenPrivateMessage=${(val) => this.setOpenPrivateMessage(val)}
.setActiveChatHeadUrl=${(val)=> this.setActiveChatHeadUrl(val)}>
</chat-page>
`
}
setChatHeads(chatObj) {
const chatObjGroups = Array.isArray(chatObj.groups) ? chatObj.groups : [];
const chatObjDirect = Array.isArray(chatObj.direct) ? chatObj.direct : [];
let groupList = chatObjGroups.map(group => group.groupId === 0 ? { groupId: group.groupId, url: `group/${group.groupId}`, groupName: "Qortal General Chat", timestamp: group.timestamp === undefined ? 2 : group.timestamp, sender: group.sender } : { ...group, timestamp: group.timestamp === undefined ? 1 : group.timestamp, url: `group/${group.groupId}` })
let directList = chatObjDirect.map(dc => {
return { ...dc, url: `direct/${dc.address}` }
})
const compareNames = (a, b) => {
return a.groupName.localeCompare(b.groupName)
}
groupList.sort(compareNames)
let chatHeadMasterList = [...groupList, ...directList]
const compareArgs = (a, b) => {
return b.timestamp - a.timestamp
}
this.chatHeads = chatHeadMasterList.sort(compareArgs)
}
getChatHeadFromState(chatObj) {
if (chatObj === undefined) {
return
} else {
this.chatHeadsObj = chatObj
this.setChatHeads(chatObj)
}
}
_textMenu(event) {
const getSelectedText = () => {
var text = "";
if (typeof window.getSelection != "undefined") {
text = window.getSelection().toString();
} else if (typeof this.shadowRoot.selection != "undefined" && this.shadowRoot.selection.type == "Text") {
text = this.shadowRoot.selection.createRange().text;
}
return text;
}
const checkSelectedTextAndShowMenu = () => {
let selectedText = getSelectedText();
if (selectedText && typeof selectedText === 'string') {
let _eve = { pageX: event.pageX, pageY: event.pageY, clientX: event.clientX, clientY: event.clientY }
let textMenuObject = { selectedText: selectedText, eventObject: _eve, isFrame: true }
parentEpml.request('openCopyTextMenu', textMenuObject)
}
}
checkSelectedTextAndShowMenu()
}
_textArea(e) {
if (e.keyCode === 13 && !e.shiftKey) this._sendMessage()
}
clearSelection() {
window.getSelection().removeAllRanges()
window.parent.getSelection().removeAllRanges()
}
onPageNavigation(pageUrl) {
parentEpml.request('setPageUrl', pageUrl)
}
isEmptyArray(arr) {
if (!arr) { return true }
return arr.length === 0
}
getApiKey() {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
let apiKey = myNode.apiKey;
return apiKey;
}
scrollToBottom() {
const viewElement = this.shadowRoot.querySelector('chat-page').shadowRoot.querySelector('chat-scroller').shadowRoot.getElementById('viewElement');
viewElement.scroll({ top: viewElement.scrollHeight, left: 0, behavior: 'smooth' })
}
showNewMessageBar() {
this.shadowRoot.getElementById('newMessageBar').classList.remove('hide-new-message-bar')
}
hideNewMessageBar() {
this.shadowRoot.getElementById('newMessageBar').classList.add('hide-new-message-bar')
}
}
window.customElements.define('q-chat', Chat)

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css">
<link rel="stylesheet" href="/font/switch-theme.css">
<script>
const checkBack = localStorage.getItem('qortalTheme')
if (checkBack === 'dark') {
newtheme = 'dark';
} else {
newtheme = 'light';
}
document.querySelector('html').setAttribute('theme', newtheme);
</script>
<style>
html {
--scrollbarBG: #a1a1a1;
--thumbBG: #6a6c75;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}
html,
body {
margin: 0;
font-family: "Roboto", sans-serif;
background: var(--plugback);
}
</style>
</head>
<body>
<minting-info></minting-info>
<script src="minting-info.js"></script>
</body>
</html>

View File

@@ -0,0 +1,877 @@
import { LitElement, html, css } from 'lit'
import { render } from 'lit/html.js'
import { Epml } from '../../../epml.js'
import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate'
registerTranslateConfig({
loader: lang => fetch(`/language/${lang}.json`).then(res => res.json())
})
import '@material/mwc-icon'
import '@material/mwc-button'
import '@material/mwc-dialog'
import '@material/mwc-textfield'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class MintingInfo extends LitElement {
static get properties() {
return {
selectedAddress: { type: Object },
loading: { type: Boolean },
adminInfo: { type: Array },
nodeInfo: { type: Array },
sampleBlock: { type: Array },
addressInfo: { type: Array },
addressLevel: { type: Array },
theme: { type: String, reflect: true },
tier4Online: { type: Number }
}
}
static get styles() {
return css`
* {
--mdc-theme-surface: var(--white);
--mdc-dialog-content-ink-color: var(--black);
}
@keyframes moveInBottom {
0% {
opacity: 0;
transform: translateY(30px);
}
100% {
opacity: 1;
transform: translate(0);
}
}
[hidden] {
display: hidden !important;
visibility: none !important;
}
h4 {
font-weight:600;
font-size:20px;
line-height: 28px;
color: var(--black);
}
.header-title {
display: block;
overflow: hidden;
font-size: 40px;
color: var(--black);
font-weight: 400;
text-align: center;
white-space: pre-wrap;
overflow-wrap: break-word;
word-break: break-word;
cursor: inherit;
margin-top: 2rem;
}
.level-black {
font-size: 32px;
color: var(--black);
font-weight: 400;
text-align: center;
margin-top: 2rem;
}
.level-blue {
display: inline-block;
font-size: 32px;
color: #03a9f4;
font-weight: 400;
text-align: center;
margin-top: 2rem;
}
.sub-main {
position: relative;
text-align: center;
width: 100%;
}
.center-box {
position: absolute;
width: 100%;
top: 50%;
left: 50%;
transform: translate(-50%, 0%);
text-align: center;
}
.content-box {
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px 25px;
text-align: center;
display: inline-block;
min-width: 250px;
margin-left: 10px;
margin-bottom: 5px;
}
.help-icon {
color: #f44336;
}
.details {
display: flex;
font-size: 18px;
}
.title {
font-weight:600;
font-size:20px;
line-height: 28px;
opacity: 0.66;
color: #03a9f4;
}
.sub-title {
font-weight:600;
font-size:20px;
line-height: 24px;
opacity: 0.66;
}
.input {
width: 90%;
border: none;
display: inline-block;
font-size: 20px;
padding: 10px 20px;
border-radius: 5px;
resize: none;
background: #eee;
}
.textarea {
width: 90%;
border: none;
display: inline-block;
font-size: 16px;
padding: 10px 20px;
border-radius: 5px;
height: 120px;
resize: none;
background: #eee;
}
.not-minter {
display: inline-block;
font-size: 32px;
color: #03a9f4;
font-weight: 400;
margin-top: 2rem;
}
.blue {
color: #03a9f4;
}
.black {
color: var(--black);
}
.red {
color: #f44336;
}
.red-button {
--mdc-theme-primary: red;
--mdc-theme-on-primary: white;
}
mwc-button.red-button {
--mdc-theme-primary: red;
--mdc-theme-on-primary: white;
}
`
}
constructor() {
super()
this.selectedAddress = window.parent.reduxStore.getState().app.selectedAddress.address
this.adminInfo = []
this.nodeInfo = []
this.sampleBlock = []
this.addressInfo = []
this.addressLevel = []
this.tier4Online = 0
this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light'
}
render() {
if (this.renderMintingPage() === "false") {
return html`
<div>
<div>
<span class="header-title">${translate("mintingpage.mchange1")}</span>
<hr style="color: #eee; border-radius: 80%; margin-bottom: 2rem;">
</div>
<div class="sub-main">
<div class="center-box">
<div>
<span class="level-black">${translate("mintingpage.mchange2")}</span>
<hr style="width: 50%; color: #eee; border-radius: 80%; margin-bottom: 2rem;">
</div><br>
<div class="content-box">
<span class="title">${translate("mintingpage.mchange3")}</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._averageBlockTime()} ${translate("mintingpage.mchange25")}</h4>
</div>
<div class="content-box">
<span class="title">${translate("mintingpage.mchange4")}</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._timeCalc()} ${translate("mintingpage.mchange26")}</h4>
</div>
<div class="content-box">
<span class="title">${translate("mintingpage.mchange5")}</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._dayReward()} QORT</h4>
</div><br>
</div>
</div>
</div>
`} else {
return html`
<div>
<div>
<span class="header-title">${translate("mintingpage.mchange1")}</span>
<hr style="color: #eee; border-radius: 80%; margin-bottom: 2rem;">
</div>
<div class="sub-main">
<div class="center-box">
<div>
<span class="level-black">${translate("mintingpage.mchange2")}</span>
<hr style="width: 50%; color: #eee; border-radius: 80%; margin-bottom: 2rem;">
</div><br>
<div class="content-box">
<span class="title">${translate("mintingpage.mchange3")}</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._averageBlockTime()} ${translate("mintingpage.mchange25")}</h4>
</div>
<div class="content-box">
<span class="title">${translate("mintingpage.mchange4")}</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._timeCalc()} ${translate("mintingpage.mchange26")}</h4>
</div>
<div class="content-box">
<span class="title">${translate("mintingpage.mchange5")}</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._dayReward()} QORT</h4>
</div><br><br><br>
<div>
<span class="level-black">${this.renderMintingHelp()}</span>
<hr style="width: 50%;color: #eee; border-radius: 80%; margin-bottom: 2rem;">
</div><br>
<div class="content-box">
<span class="title">${translate("mintingpage.mchange15")}</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4><span class=${this.cssMinting}>${this._mintingStatus()}</span></h4>
</div>
<div class="content-box">
<span class="title">${translate("mintingpage.mchange16")}</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${translate("mintingpage.mchange27")} ${this.addressInfo.level}</h4>
</div>
<div class="content-box">
<span class="title">${translate("mintingpage.mchange17")}</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._levelUpBlocks()} ${translate("mintingpage.mchange26")}</h4>
</div><br>
<div>
<span class="level-black">${translate("mintingpage.mchange18")} <div class="level-blue">${this._levelUp()}</div> ${translate("mintingpage.mchange38")} <div class="level-blue">${this._levelUpDays()}</div> ${translate("mintingpage.mchange29")} !</span>
</div><br><br><br>
<div>
<span class="level-black">${translate("mintingpage.mchange19")}</span>
<hr style="width: 50%; color: #eee; border-radius: 80%; margin-bottom: 2rem;">
</div><br>
<div class="content-box">
<span class="title">${translate("mintingpage.mchange20")}</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._currentTier()}</h4>
</div>
<div class="content-box">
<span class="title">${translate("mintingpage.mchange21")}</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._countLevels()} ${translate("mintingpage.mchange30")}</h4>
</div>
<div class="content-box">
<span class="title">${translate("mintingpage.mchange22")}</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._tierPercent()} %</h4>
</div>
<div class="content-box">
<span class="title">${translate("mintingpage.mchange23")}</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._countReward()} QORT</h4>
</div>
<div class="content-box">
<span class="title">${translate("mintingpage.mchange24")}</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._countRewardDay()} QORT</h4>
</div>
</div>
</div>
<!-- Become A Minter Dialog -->
<mwc-dialog id="becomeMinterDialog">
<div style="text-align:center">
<h2>${translate("mintingpage.mchange32")}</h2>
<hr>
</div>
<div>
<h3>${translate("mintingpage.mchange33")}</h3><br />
${translate("mintingpage.mchange34")}
</div>
<div>
<h3>${translate("mintingpage.mchange35")}</h3><br />
${translate("mintingpage.mchange36")}
<br />
${translate("mintingpage.mchange37")}
</div>
<mwc-button slot="primaryAction" dialogAction="cancel" class="red-button">${translate("general.close")}</mwc-button>
</mwc-dialog>
</div>
`}
}
async firstUpdated() {
this.changeTheme()
this.changeLanguage()
await this.getAddressLevel()
const getAdminInfo = () => {
parentEpml.request("apiCall", { url: `/admin/info` }).then((res) => {
setTimeout(() => { this.adminInfo = res; }, 1)
})
setTimeout(getAdminInfo, 30000)
};
const getNodeInfo = () => {
parentEpml.request("apiCall", { url: `/admin/status` }).then((res) => {
this.nodeInfo = res
// Now look up the sample block
getSampleBlock()
})
setTimeout(getNodeInfo, 30000)
};
const getSampleBlock = () => {
let callBlock = parseFloat(this.nodeInfo.height) - 1440
parentEpml.request("apiCall", { url: `/blocks/byheight/${callBlock}` }).then((res) => {
setTimeout(() => { this.sampleBlock = res }, 1)
})
}
const getAddressInfo = () => {
parentEpml.request('apiCall', { url: `/addresses/${window.parent.reduxStore.getState().app.selectedAddress.address}` }).then((res) => {
setTimeout(() => { this.addressInfo = res }, 1)
})
setTimeout(getAddressInfo, 30000)
}
window.addEventListener('storage', () => {
const checkLanguage = localStorage.getItem('qortalLanguage')
const checkTheme = localStorage.getItem('qortalTheme')
use(checkLanguage)
if (checkTheme === 'dark') {
this.theme = 'dark'
} else {
this.theme = 'light'
}
document.querySelector('html').setAttribute('theme', this.theme)
})
let configLoaded = false;
parentEpml.ready().then(() => {
parentEpml.subscribe('selected_address', async selectedAddress => {
this.selectedAddress = {}
selectedAddress = JSON.parse(selectedAddress)
if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
this.selectedAddress = selectedAddress
})
})
parentEpml.ready().then(() => {
parentEpml.subscribe("config", async c => {
if (!configLoaded) {
setTimeout(getAdminInfo, 1)
setTimeout(getNodeInfo, 1)
setTimeout(getAddressInfo, 1)
setInterval(this.getAddressLevel, 30000)
configLoaded = true
}
this.config = JSON.parse(c)
})
parentEpml.subscribe('copy_menu_switch', async value => {
if (value === 'false' && window.getSelection().toString().length !== 0) this.clearSelection()
})
})
parentEpml.imReady()
}
async getAddressLevel() {
const callLevels = await parentEpml.request('apiCall', {
url: `/addresses/online/levels`
})
this.addressLevel = callLevels
this.tier4Online = parseFloat(this.addressLevel[7].count) + parseFloat(this.addressLevel[8].count)
}
changeTheme() {
const checkTheme = localStorage.getItem('qortalTheme')
if (checkTheme === 'dark') {
this.theme = 'dark'
} else {
this.theme = 'light'
}
document.querySelector('html').setAttribute('theme', this.theme)
}
changeLanguage() {
const checkLanguage = localStorage.getItem('qortalLanguage')
if (checkLanguage === null || checkLanguage.length === 0) {
localStorage.setItem('qortalLanguage', 'us')
use('us')
} else {
use(checkLanguage)
}
}
renderMintingPage() {
if (this.addressInfo.error === 124) {
return "false"
} else {
return "true"
}
}
_averageBlockTime() {
let avgBlockString = (this.adminInfo.currentTimestamp - this.sampleBlock.timestamp).toString();
let averageTimeString = ((avgBlockString / 1000) / 1440).toFixed(2)
let averageBlockTimeString = (averageTimeString).toString()
return "" + averageBlockTimeString
}
_timeCalc() {
let timeString = (this.adminInfo.currentTimestamp - this.sampleBlock.timestamp).toString()
let averageString = ((timeString / 1000) / 1440).toFixed(2)
let averageBlockDay = (86400 / averageString).toFixed(2)
let averageBlockDayString = (averageBlockDay).toString()
return "" + averageBlockDayString
}
_dayReward() {
let rewardString = (this._timeCalc() * this._blockReward()).toFixed(2)
let rewardDayString = (rewardString).toString()
return "" + rewardDayString
}
_mintingStatus() {
if (this.nodeInfo.isMintingPossible === true && this.nodeInfo.isSynchronizing === true) {
this.cssMinting = "blue"
return html`${translate("appinfo.minting")}`
} else if (this.nodeInfo.isMintingPossible === true && this.nodeInfo.isSynchronizing === false) {
this.cssMinting = "blue"
return html`${translate("appinfo.minting")}`
} else if (this.nodeInfo.isMintingPossible === false && this.nodeInfo.isSynchronizing === true) {
this.cssMinting = "red"
return html`(${translate("appinfo.synchronizing")}... ${this.nodeStatus.syncPercent !== undefined ? this.nodeStatus.syncPercent + '%' : ''})`
} else if (this.nodeInfo.isMintingPossible === false && this.nodeInfo.isSynchronizing === false) {
this.cssMinting = "red"
return html`${translate("mintingpage.mchange9")}`
} else {
return "No Status"
}
}
renderMintingHelp() {
if (this._mintingStatus() === "Not Minting") {
return html`${translate("mintingpage.mchange9")} <div class="level-blue">==></div> ${translate("mintingpage.mchange7")}<br><mwc-button class="red-button" @click=${() => this.shadowRoot.querySelector("#becomeMinterDialog").show()}><mwc-icon class="help-icon">help_outline</mwc-icon>&nbsp;${translate("mintingpage.mchange31")}</mwc-button>`
} else {
return html`${translate("mintingpage.mchange6")}`
}
}
_levelUpDays() {
let countDays = ((this._blocksNeed() - (this.addressInfo.blocksMinted + this.addressInfo.blocksMintedAdjustment)) / this._timeCalc()).toFixed(2)
let countString = (countDays).toString()
return "" + countString
}
_levelUpBlocks() {
let countBlocksString = (this._blocksNeed() - (this.addressInfo.blocksMinted + this.addressInfo.blocksMintedAdjustment)).toString()
return "" + countBlocksString
}
_blocksNeed() {
if (this.addressInfo.level === 0) {
return "7200"
} else if (this.addressInfo.level === 1) {
return "72000"
} else if (this.addressInfo.level === 2) {
return "201600"
} else if (this.addressInfo.level === 3) {
return "374400"
} else if (this.addressInfo.level === 4) {
return "618400"
} else if (this.addressInfo.level === 5) {
return "964000"
} else if (this.addressInfo.level === 6) {
return "1482400"
} else if (this.addressInfo.level === 7) {
return "2173600"
} else if (this.addressInfo.level === 8) {
return "3037600"
} else if (this.addressInfo.level === 9) {
return "4074400"
}
}
_levelUp() {
if (this.addressInfo.level === 0) {
return "1"
} else if (this.addressInfo.level === 1) {
return "2"
} else if (this.addressInfo.level === 2) {
return "3"
} else if (this.addressInfo.level === 3) {
return "4"
} else if (this.addressInfo.level === 4) {
return "5"
} else if (this.addressInfo.level === 5) {
return "6"
} else if (this.addressInfo.level === 6) {
return "7"
} else if (this.addressInfo.level === 7) {
return "8"
} else if (this.addressInfo.level === 8) {
return "9"
} else if (this.addressInfo.level === 9) {
return "10"
}
}
_currentTier() {
if (this.addressInfo.level === 0) {
return html`${translate("mintingpage.mchange28")} 0 (${translate("mintingpage.mchange27")} 0)`
} else if (this.addressInfo.level === 1) {
return html`${translate("mintingpage.mchange28")} 1 (${translate("mintingpage.mchange27")} 1 + 2)`
} else if (this.addressInfo.level === 2) {
return html`${translate("mintingpage.mchange28")} 1 (${translate("mintingpage.mchange27")} 1 + 2)`
} else if (this.addressInfo.level === 3) {
return html`${translate("mintingpage.mchange28")} 2 (${translate("mintingpage.mchange27")} 3 + 4)`
} else if (this.addressInfo.level === 4) {
return html`${translate("mintingpage.mchange28")} 2 (${translate("mintingpage.mchange27")} 3 + 4)`
} else if (this.addressInfo.level === 5) {
return html`${translate("mintingpage.mchange28")} 3 (${translate("mintingpage.mchange27")} 5 + 6)`
} else if (this.addressInfo.level === 6) {
return html`${translate("mintingpage.mchange28")} 3 (${translate("mintingpage.mchange27")} 5 + 6)`
} else if (this.addressInfo.level === 7) {
return html`${translate("mintingpage.mchange28")} 4 (${translate("mintingpage.mchange27")} 7 + 8)`
} else if (this.addressInfo.level === 8) {
return html`${translate("mintingpage.mchange28")} 4 (${translate("mintingpage.mchange27")} 7 + 8)`
} else if (this.addressInfo.level === 9) {
return html`${translate("mintingpage.mchange28")} 5 (${translate("mintingpage.mchange27")} 9 + 10)`
} else if (this.addressInfo.level === 10) {
return html`${translate("mintingpage.mchange28")} 5 (${translate("mintingpage.mchange27")} 9 + 10)`
}
}
_tierPercent() {
if (this.addressInfo.level === 0) {
return "0"
} else if (this.addressInfo.level === 1) {
return "6"
} else if (this.addressInfo.level === 2) {
return "6"
} else if (this.addressInfo.level === 3) {
return "13"
} else if (this.addressInfo.level === 4) {
return "13"
} else if (this.addressInfo.level === 5) {
if (this.tier4Online < 30) {
return "45"
} else {
return "19"
}
} else if (this.addressInfo.level === 6) {
if (this.tier4Online < 30) {
return "45"
} else {
return "19"
}
} else if (this.addressInfo.level === 7) {
if (this.tier4Online < 30) {
return "45"
} else {
return "26"
}
} else if (this.addressInfo.level === 8) {
if (this.tier4Online < 30) {
return "45"
} else {
return "26"
}
} else if (this.addressInfo.level === 9) {
return "32"
} else if (this.addressInfo.level === 10) {
return "32"
}
}
_blockReward() {
if (this.nodeInfo.height < 259201) {
return "5.00"
} else if (this.nodeInfo.height < 518401) {
return "4.75"
} else if (this.nodeInfo.height < 777601) {
return "4.50"
} else if (this.nodeInfo.height < 1036801) {
return "4.25"
} else if (this.nodeInfo.height < 1296001) {
return "4.00"
} else if (this.nodeInfo.height < 1555201) {
return "3.75"
} else if (this.nodeInfo.height < 1814401) {
return "3.50"
} else if (this.nodeInfo.height < 2073601) {
return "3.25"
} else if (this.nodeInfo.height < 2332801) {
return "3.00"
} else if (this.nodeInfo.height < 2592001) {
return "2.75"
} else if (this.nodeInfo.height < 2851201) {
return "2.50"
} else if (this.nodeInfo.height < 3110401) {
return "2.25"
} else {
return "2.00"
}
}
_countLevels() {
if (this.addressInfo.level === 0) {
let countTier0 = (this.addressLevel[0].count).toString()
return "" + countTier0
} else if (this.addressInfo.level === 1) {
let countTier10 = (this.addressLevel[1].count + this.addressLevel[2].count).toString()
return "" + countTier10
} else if (this.addressInfo.level === 2) {
let countTier11 = (this.addressLevel[1].count + this.addressLevel[2].count).toString()
return "" + countTier11
} else if (this.addressInfo.level === 3) {
let countTier20 = (this.addressLevel[3].count + this.addressLevel[4].count).toString()
return "" + countTier20
} else if (this.addressInfo.level === 4) {
let countTier21 = (this.addressLevel[3].count + this.addressLevel[4].count).toString()
return "" + countTier21
} else if (this.addressInfo.level === 5) {
if (this.tier4Online < 30) {
let countTier30 = (this.addressLevel[5].count + this.addressLevel[6].count + this.addressLevel[7].count + this.addressLevel[8].count).toString()
return "" + countTier30
} else {
let countTier30 = (this.addressLevel[5].count + this.addressLevel[6].count).toString()
return "" + countTier30
}
} else if (this.addressInfo.level === 6) {
if (this.tier4Online < 30) {
let countTier31 = (this.addressLevel[5].count + this.addressLevel[6].count + this.addressLevel[7].count + this.addressLevel[8].count).toString()
return "" + countTier31
} else {
let countTier31 = (this.addressLevel[5].count + this.addressLevel[6].count).toString()
return "" + countTier31
}
} else if (this.addressInfo.level === 7) {
if (this.tier4Online < 30) {
let countTier40 = (this.addressLevel[5].count + this.addressLevel[6].count + this.addressLevel[7].count + this.addressLevel[8].count).toString()
return "" + countTier40
} else {
let countTier40 = (this.addressLevel[7].count + this.addressLevel[8].count).toString()
return "" + countTier40
}
} else if (this.addressInfo.level === 8) {
if (this.tier4Online < 30) {
let countTier40 = (this.addressLevel[5].count + this.addressLevel[6].count + this.addressLevel[7].count + this.addressLevel[8].count).toString()
return "" + countTier40
} else {
let countTier41 = (this.addressLevel[7].count + this.addressLevel[8].count).toString()
return "" + countTier41
}
} else if (this.addressInfo.level === 9) {
let countTier50 = (this.addressLevel[9].count + this.addressLevel[10].count).toString()
return "" + countTier50
} else if (this.addressInfo.level === 10) {
let countTier51 = (this.addressLevel[9].count + this.addressLevel[10].count).toString()
return "" + countTier51
}
}
_countReward() {
if (this.addressInfo.level === 0) {
return "0"
} else if (this.addressInfo.level === 1) {
let countReward10 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[1].count + this.addressLevel[2].count)).toFixed(8)
let countReward11 = (countReward10).toString()
return "" + countReward11
} else if (this.addressInfo.level === 2) {
let countReward20 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[1].count + this.addressLevel[2].count)).toFixed(8)
let countReward21 = (countReward20).toString()
return "" + countReward21
} else if (this.addressInfo.level === 3) {
let countReward30 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[3].count + this.addressLevel[4].count)).toFixed(8)
let countReward31 = (countReward30).toString()
return "" + countReward31;
} else if (this.addressInfo.level === 4) {
let countReward40 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[3].count + this.addressLevel[4].count)).toFixed(8)
let countReward41 = (countReward40).toString();
return "" + countReward41;
} else if (this.addressInfo.level === 5) {
if (this.tier4Online< 30) {
let countReward50 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[5].count + this.addressLevel[6].count + this.addressLevel[7].count + this.addressLevel[8].count)).toFixed(8)
let countReward51 = (countReward50).toString();
return "" + countReward51;
} else {
let countReward50 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[5].count + this.addressLevel[6].count)).toFixed(8)
let countReward51 = (countReward50).toString();
return "" + countReward51;
}
} else if (this.addressInfo.level === 6) {
if (this.tier4Online < 30) {
let countReward60 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[5].count + this.addressLevel[6].count + this.addressLevel[7].count + this.addressLevel[8].count)).toFixed(8)
let countReward61 = (countReward60).toString()
return "" + countReward61
} else {
let countReward60 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[5].count + this.addressLevel[6].count)).toFixed(8)
let countReward61 = (countReward60).toString()
return "" + countReward61
}
} else if (this.addressInfo.level === 7) {
if (this.tier4Online < 30) {
let countReward70 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[5].count + this.addressLevel[6].count + this.addressLevel[7].count + this.addressLevel[8].count)).toFixed(8)
let countReward71 = (countReward70).toString()
return "" + countReward71
} else {
let countReward70 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[7].count + this.addressLevel[8].count)).toFixed(8)
let countReward71 = (countReward70).toString()
return "" + countReward71
}
} else if (this.addressInfo.level === 8) {
if (this.tier4Online < 30) {
let countReward80 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[5].count + this.addressLevel[6].count + this.addressLevel[7].count + this.addressLevel[8].count)).toFixed(8)
let countReward81 = (countReward80).toString()
return "" + countReward81
} else {
let countReward80 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[7].count + this.addressLevel[8].count)).toFixed(8)
let countReward81 = (countReward80).toString()
return "" + countReward81
}
} else if (this.addressInfo.level === 9) {
let countReward90 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[9].count + this.addressLevel[10].count)).toFixed(8)
let countReward91 = (countReward90).toString()
return "" + countReward91
} else if (this.addressInfo.level === 10) {
let countReward100 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[9].count + this.addressLevel[10].count)).toFixed(8)
let countReward101 = (countReward100).toString()
return "" + countReward101
}
}
_countRewardDay() {
if (this.addressInfo.level === 0) {
return "0"
} else if (this.addressInfo.level === 1) {
let countRewardDay10 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[1].count + this.addressLevel[2].count) * this._timeCalc()).toFixed(8)
let countRewardDay11 = (countRewardDay10).toString()
return "" + countRewardDay11
} else if (this.addressInfo.level === 2) {
let countRewardDay20 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[1].count + this.addressLevel[2].count) * this._timeCalc()).toFixed(8)
let countRewardDay21 = (countRewardDay20).toString()
return "" + countRewardDay21
} else if (this.addressInfo.level === 3) {
let countRewardDay30 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[3].count + this.addressLevel[4].count) * this._timeCalc()).toFixed(8)
let countRewardDay31 = (countRewardDay30).toString()
return "" + countRewardDay31
} else if (this.addressInfo.level === 4) {
let countRewardDay40 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[3].count + this.addressLevel[4].count) * this._timeCalc()).toFixed(8)
let countRewardDay41 = (countRewardDay40).toString()
return "" + countRewardDay41
} else if (this.addressInfo.level === 5) {
if (this.tier4Online < 30) {
let countRewardDay50 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[5].count + this.addressLevel[6].count + this.addressLevel[7].count + this.addressLevel[8].count) * this._timeCalc()).toFixed(8)
let countRewardDay51 = (countRewardDay50).toString()
return "" + countRewardDay51
} else {
let countRewardDay50 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[5].count + this.addressLevel[6].count) * this._timeCalc()).toFixed(8)
let countRewardDay51 = (countRewardDay50).toString()
return "" + countRewardDay51
}
} else if (this.addressInfo.level === 6) {
if (this.tier4Online < 30) {
let countRewardDay60 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[5].count + this.addressLevel[6].count + this.addressLevel[7].count + this.addressLevel[8].count) * this._timeCalc()).toFixed(8)
let countRewardDay61 = (countRewardDay60).toString()
return "" + countRewardDay61
} else {
let countRewardDay60 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[5].count + this.addressLevel[6].count) * this._timeCalc()).toFixed(8)
let countRewardDay61 = (countRewardDay60).toString()
return "" + countRewardDay61
}
} else if (this.addressInfo.level === 7) {
if (this.tier4Online < 30) {
let countRewardDay70 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[5].count + this.addressLevel[6].count + this.addressLevel[7].count + this.addressLevel[8].count) * this._timeCalc()).toFixed(8)
let countRewardDay71 = (countRewardDay70).toString()
return "" + countRewardDay71
} else {
let countRewardDay70 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[7].count + this.addressLevel[8].count) * this._timeCalc()).toFixed(8)
let countRewardDay71 = (countRewardDay70).toString()
return "" + countRewardDay71
}
} else if (this.addressInfo.level === 8) {
if (this.tier4Online < 30) {
let countRewardDay80 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[5].count + this.addressLevel[6].count + this.addressLevel[7].count + this.addressLevel[8].count) * this._timeCalc()).toFixed(8)
let countRewardDay81 = (countRewardDay80).toString()
return "" + countRewardDay81
} else {
let countRewardDay80 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[7].count + this.addressLevel[8].count) * this._timeCalc()).toFixed(8)
let countRewardDay81 = (countRewardDay80).toString()
return "" + countRewardDay81
}
} else if (this.addressInfo.level === 9) {
let countRewardDay90 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[9].count + this.addressLevel[10].count) * this._timeCalc()).toFixed(8)
let countRewardDay91 = (countRewardDay90).toString()
return "" + countRewardDay91
} else if (this.addressInfo.level === 10) {
let countRewardDay100 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[9].count + this.addressLevel[10].count) * this._timeCalc()).toFixed(8)
let countRewardDay101 = (countRewardDay100).toString()
return "" + countRewardDay101
}
}
clearSelection() {
window.getSelection().removeAllRanges()
window.parent.getSelection().removeAllRanges()
}
isEmptyArray(arr) {
if (!arr) return true
return arr.length === 0
}
}
window.customElements.define('minting-info', MintingInfo)

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css">
<link rel="stylesheet" href="/font/switch-theme.css">
<script>
const checkBack = localStorage.getItem('qortalTheme')
if (checkBack === 'dark') {
newtheme = 'dark';
} else {
newtheme = 'light';
}
document.querySelector('html').setAttribute('theme', newtheme);
</script>
<style>
html {
--scrollbarBG: #a1a1a1;
--thumbBG: #6a6c75;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}
html,
body {
margin: 0;
font-family: "Roboto", sans-serif;
background: var(--plugback);
}
</style>
</head>
<body>
<name-registration></name-registration>
<script src="name-registration.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css">
<link rel="stylesheet" href="/font/switch-theme.css">
<script>
const checkBack = localStorage.getItem('qortalTheme')
if (checkBack === 'dark') {
newtheme = 'dark';
} else {
newtheme = 'light';
}
document.querySelector('html').setAttribute('theme', newtheme);
</script>
<style>
html {
--scrollbarBG: #a1a1a1;
--thumbBG: #6a6c75;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}
html,
body {
margin: 0;
font-family: "Roboto", sans-serif;
background: var(--plugback);
}
</style>
</head>
<body>
<names-market></names-market>
<script src="names-market.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css">
<link rel="stylesheet" href="/font/switch-theme.css">
<script>
const checkBack = localStorage.getItem('qortalTheme')
if (checkBack === 'dark') {
newtheme = 'dark';
} else {
newtheme = 'light';
}
document.querySelector('html').setAttribute('theme', newtheme);
</script>
<style>
html {
--scrollbarBG: #a1a1a1;
--thumbBG: #6a6c75;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}
html,
body {
margin: 0;
font-family: "Roboto", sans-serif;
background: var(--plugback);
}
</style>
</head>
<body>
<node-management></node-management>
<script src="node-management.js"></script>
</body>
</html>

View File

@@ -0,0 +1,636 @@
import { LitElement, html, css } from 'lit'
import { render } from 'lit/html.js'
import { Epml } from '../../../epml.js'
import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate'
registerTranslateConfig({
loader: lang => fetch(`/language/${lang}.json`).then(res => res.json())
})
import '@polymer/paper-spinner/paper-spinner-lite.js'
import '@material/mwc-icon'
import '@material/mwc-textfield'
import '@material/mwc-button'
import '@material/mwc-dialog'
import '@vaadin/grid'
const parentEpml = new Epml({ type: "WINDOW", source: window.parent })
class NodeManagement extends LitElement {
static get properties() {
return {
loading: { type: Boolean },
upTime: { type: String },
mintingAccounts: { type: Array },
peers: { type: Array },
addMintingAccountLoading: { type: Boolean },
removeMintingAccountLoading: { type: Boolean },
addPeerLoading: { type: Boolean },
confPeerLoading: { type: Boolean },
addMintingAccountKey: { type: String },
removeMintingAccountKey: { type: String },
addPeerMessage: { type: String },
confPeerMessage: { type: String },
addMintingAccountMessage: { type: String },
removeMintingAccountMessage: { type: String },
tempMintingAccount: { type: Object },
nodeConfig: { type: Object },
nodeDomain: { type: String },
myElementId: { type: String },
theme: { type: String, reflect: true }
}
}
static get styles() {
return css`
* {
--mdc-theme-primary: rgb(3, 169, 244);
--paper-input-container-focus-color: var(--mdc-theme-primary);
--mdc-theme-surface: var(--white);
--mdc-dialog-content-ink-color: var(--black);
--lumo-primary-text-color: rgb(0, 167, 245);
--lumo-primary-color-50pct: rgba(0, 167, 245, 0.5);
--lumo-primary-color-10pct: rgba(0, 167, 245, 0.1);
--lumo-primary-color: hsl(199, 100%, 48%);
--lumo-base-color: var(--white);
--lumo-body-text-color: var(--black);
--lumo-secondary-text-color: var(--sectxt);
--lumo-contrast-60pct: var(--vdicon);
--_lumo-grid-border-color: var(--border);
--_lumo-grid-secondary-border-color: var(--border2);
}
paper-spinner-lite {
height: 24px;
width: 24px;
--paper-spinner-color: var(--mdc-theme-primary);
--paper-spinner-stroke-width: 2px;
}
#node-management-page {
background: var(--white);
}
mwc-textfield {
width: 100%;
}
.red {
--mdc-theme-primary: #F44336;
}
.red-button {
--mdc-theme-primary: red;
--mdc-theme-on-primary: white;
}
mwc-button.red-button {
--mdc-theme-primary: red;
--mdc-theme-on-primary: white;
}
.node-card {
padding: 12px 24px;
background: var(--white);
border-radius: 2px;
box-shadow: 11;
}
h2 {
margin: 0;
}
h2,
h3,
h4,
h5 {
color: var(--black);
font-weight: 400;
}
.sblack {
color: var(--black);
}
[hidden] {
display: hidden !important;
visibility: none !important;
}
.details {
display: flex;
font-size: 18px;
}
`;
}
constructor() {
super()
this.upTime = ""
this.mintingAccounts = []
this.peers = []
this.addPeerLoading = false
this.confPeerLoading = false
this.addMintingAccountLoading = false
this.removeMintingAccountLoading = false
this.addMintingAccountKey = ""
this.addPeerMessage = ""
this.confPeerMessage = ""
this.addMintingAccountMessage = ""
this.tempMintingAccount = {}
this.config = {
user: {
node: {},
},
};
this.nodeConfig = {}
this.nodeDomain = ""
this.myElementId = ''
this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light'
}
render() {
return html`
<div id="node-management-page">
<div class="node-card">
<h2>${translate("nodepage.nchange1")} ${this.nodeDomain}</h2>
<mwc-button style="float:right;" class="red" ?hidden="${(this.upTime === "offline")}" @click=${() => this.stopNode()}><mwc-icon>dangerous</mwc-icon>&nbsp;${translate("nodepage.nchange31")}</mwc-button>
<span class="sblack"><br />${translate("nodepage.nchange2")} ${this.upTime}</span>
<br /><br />
<div id="minting">
<div style="min-height:48px; display: flex; padding-bottom: 6px;">
<h3 style="margin: 0; flex: 1; padding-top: 8px; display: inline;">${translate("nodepage.nchange3")}</h3>
<mwc-button
style="float:right;"
@click=${() => this.shadowRoot.querySelector("#addMintingAccountDialog").show()}
>
<mwc-icon>add</mwc-icon>
${translate("nodepage.nchange4")}
</mwc-button>
</div>
<!-- Add Minting Account Dialog -->
<mwc-dialog id="addMintingAccountDialog" scrimClickAction="${this.addMintingAccountLoading ? "" : "close"}">
<div>${translate("nodepage.nchange5")}</div>
<br />
<mwc-textfield
?disabled="${this.addMintingAccountLoading}"
label="${translate("nodepage.nchange6")}"
id="addMintingAccountKey"
>
</mwc-textfield>
<div style="text-align:right; height:36px;" ?hidden=${this.addMintingAccountMessage === ""}>
<span ?hidden="${this.addMintingAccountLoading}">
${this.addMintingAccountMessage} &nbsp;
</span>
<span ?hidden="${!this.addMintingAccountLoading}">
<!-- loading message -->
${translate("nodepage.nchange7")} &nbsp;
<paper-spinner-lite
style="margin-top:12px;"
?active="${this.addMintingAccountLoading}"
alt="Adding minting account"
>
</paper-spinner-lite>
</span>
</div>
<mwc-button
?disabled="${this.addMintingAccountLoading}"
slot="primaryAction"
@click=${this.addMintingAccount}
>
${translate("nodepage.nchange8")}
</mwc-button>
<mwc-button
?disabled="${this.addMintingAccountLoading}"
slot="secondaryAction"
dialogAction="cancel"
class="red"
>
${translate("general.close")}
</mwc-button>
</mwc-dialog>
<vaadin-grid theme="large" id="mintingAccountsGrid" ?hidden="${this.isEmptyArray(this.mintingAccounts)}" .items="${this.mintingAccounts}" aria-label="Minting Accounts" all-rows-visible>
<vaadin-grid-column auto-width header="${translate("nodepage.nchange9")}" path="mintingAccount"></vaadin-grid-column>
<vaadin-grid-column auto-width header="${translate("nodepage.nchange10")}" path="recipientAccount"></vaadin-grid-column>
<vaadin-grid-column width="12em" header="${translate("nodepage.nchange11")}" .renderer=${(root, column, data) => {
render(html`<mwc-button class="red" ?disabled=${this.removeMintingAccountLoading} @click=${() => this.removeMintingAccount(data.item.publicKey)}><mwc-icon>create</mwc-icon>&nbsp;${translate("nodepage.nchange12")}</mwc-button>`, root)
}}></vaadin-grid-column>
</vaadin-grid>
${this.isEmptyArray(this.mintingAccounts) ? html`<span style="color: var(--black);">${translate("nodepage.nchange13")}</span>` : ""}
</div>
<br />
<div id="peers">
<div style="min-height: 48px; display: flex; padding-bottom: 6px;">
<h3 style="margin: 0; flex: 1; padding-top: 8px; display: inline;">
<span>${translate("nodepage.nchange14")}</span>
<span>(${this.peers.length})</span>
</h3>
<mwc-button @click=${() => this.shadowRoot.querySelector("#addPeerDialog").show()}><mwc-icon>add</mwc-icon>&nbsp;${translate("nodepage.nchange15")}</mwc-button>
</div>
<mwc-dialog id="addPeerDialog" scrimClickAction="${this.addPeerLoading ? "" : "close"}">
<div>${translate("nodepage.nchange16")}</div>
<br />
<mwc-textfield ?disabled="${this.addPeerLoading}" label="${translate("nodepage.nchange17")}" id="addPeerAddress" ></mwc-textfield>
<div style="text-align:right; height:36px;" ?hidden=${this.addPeerMessage === ""}>
<span ?hidden="${this.addPeerLoading}"> ${this.addPeerMessage} &nbsp;</span>
<span ?hidden="${!this.addPeerLoading}">
<paper-spinner-lite
style="margin-top:12px;"
?active="${this.addPeerLoading}"
alt="Adding minting account"
>
</paper-spinner-lite>
</span>
</div>
<mwc-button
?disabled="${this.addPeerLoading}"
@click="${this.addPeer}"
slot="primaryAction"
>
${translate("nodepage.nchange8")}
</mwc-button>
<mwc-button
slot="secondaryAction"
dialogAction="cancel"
?disabled="${this.addPeerLoading}"
class="red"
>
${translate("general.close")}
</mwc-button>
</mwc-dialog>
<vaadin-grid theme="large" id="peersGrid" ?hidden="${this.isEmptyArray(this.peers)}" .items="${this.peers}" aria-label="Peers" all-rows-visible>
<vaadin-grid-column header="${translate("nodepage.nchange18")}" path="address"></vaadin-grid-column>
<vaadin-grid-column header="${translate("nodepage.nchange19")}" path="lastHeight"></vaadin-grid-column>
<vaadin-grid-column header="${translate("nodepage.nchange20")}" path="version"></vaadin-grid-column>
<vaadin-grid-column header="${translate("nodepage.nchange21")}" path="age"></vaadin-grid-column>
<vaadin-grid-column width="12em" header="${translate("nodepage.nchange22")}" .renderer=${(root, column, data) => {
render(html`<mwc-button class="red" @click=${() => this.removePeer(data.item.address, data.index)}><mwc-icon>delete</mwc-icon>&nbsp;${translate("nodepage.nchange12")}</mwc-button><mwc-button class="green" @click=${() => this.forceSyncPeer(data.item.address, data.index)}>&nbsp;${translate("nodepage.nchange23")}</mwc-button>`, root)
}}></vaadin-grid-column>
</vaadin-grid>
${this.isEmptyArray(this.peers) ? html`<span style="color: var(--black);">${translate("nodepage.nchange24")}</span>` : ""}
</div>
<br />
</div>
</div>
`;
}
firstUpdated() {
this.changeTheme()
this.changeLanguage()
// Call updateMintingAccounts
this.updateMintingAccounts()
window.addEventListener('contextmenu', (event) => {
event.preventDefault()
this.isTextMenuOpen = true
this._textMenu(event)
})
window.addEventListener('click', () => {
if (this.isTextMenuOpen) {
parentEpml.request('closeCopyTextMenu', null)
}
})
window.addEventListener('storage', () => {
const checkLanguage = localStorage.getItem('qortalLanguage')
const checkTheme = localStorage.getItem('qortalTheme')
use(checkLanguage)
if (checkTheme === 'dark') {
this.theme = 'dark'
} else {
this.theme = 'light'
}
document.querySelector('html').setAttribute('theme', this.theme)
})
window.onkeyup = (e) => {
if (e.keyCode === 27) {
parentEpml.request('closeCopyTextMenu', null)
}
}
this.shadowRoot.getElementById('addMintingAccountKey').addEventListener('contextmenu', (event) => {
const getSelectedText = () => {
var text = ''
if (typeof window.getSelection != 'undefined') {
text = window.getSelection().toString()
} else if (typeof this.shadowRoot.selection != 'undefined' && this.shadowRoot.selection.type == 'Text') {
text = this.shadowRoot.selection.createRange().text
}
return text
}
const checkSelectedTextAndShowMenu = () => {
let selectedText = getSelectedText()
if (selectedText && typeof selectedText === 'string') {
} else {
this.myElementId = ''
this.pasteMenu(event, 'addMintingAccountKey')
this.myElementId = this.shadowRoot.getElementById('addMintingAccountKey')
this.isPasteMenuOpen = true
event.preventDefault()
event.stopPropagation()
}
}
checkSelectedTextAndShowMenu()
})
// Calculate HH MM SS from Milliseconds...
const convertMsToTime = (milliseconds) => {
let day, hour, minute, seconds;
seconds = Math.floor(milliseconds / 1000);
minute = Math.floor(seconds / 60);
seconds = seconds % 60;
hour = Math.floor(minute / 60);
minute = minute % 60;
day = Math.floor(hour / 24);
hour = hour % 24;
if (isNaN(day)) {
return "offline";
}
return day + "d " + hour + "h " + minute + "m";
};
const getNodeUpTime = () => {
parentEpml
.request("apiCall", {
url: `/admin/uptime`,
})
.then((res) => {
this.upTime = "";
setTimeout(() => {
this.upTime = convertMsToTime(res);
}, 1);
});
setTimeout(getNodeUpTime, this.config.user.nodeSettings.pingInterval);
};
const updatePeers = () => {
parentEpml
.request("apiCall", {
url: `/peers`,
})
.then((res) => {
setTimeout(() => {
this.peers = res;
}, 1);
});
setTimeout(updatePeers, this.config.user.nodeSettings.pingInterval);
};
const getNodeConfig = () => {
parentEpml.request("getNodeConfig").then((res) => {
setTimeout(() => {
this.nodeConfig = res;
}, 1);
let myNode = window.parent.reduxStore.getState().app.nodeConfig
.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
this.nodeDomain = myNode.domain + ":" + myNode.port;
});
setTimeout(getNodeConfig, 1000);
};
let configLoaded = false
parentEpml.ready().then(() => {
parentEpml.subscribe("config", async c => {
if (!configLoaded) {
setTimeout(getNodeUpTime, 1);
setTimeout(updatePeers, 1);
setTimeout(this.updateMintingAccounts, 1);
setTimeout(getNodeConfig, 1);
configLoaded = true;
}
this.config = JSON.parse(c);
})
parentEpml.subscribe('copy_menu_switch', async (value) => {
if (value === 'false' && this.isTextMenuOpen === true) {
this.clearSelection()
this.isTextMenuOpen = false
}
})
parentEpml.subscribe('frame_paste_menu_switch', async res => {
res = JSON.parse(res)
if (res.isOpen === false && this.isPasteMenuOpen === true) {
this.pasteToTextBox(this.myElementId)
this.isPasteMenuOpen = false
}
})
})
parentEpml.imReady()
}
changeTheme() {
const checkTheme = localStorage.getItem('qortalTheme')
if (checkTheme === 'dark') {
this.theme = 'dark';
} else {
this.theme = 'light';
}
document.querySelector('html').setAttribute('theme', this.theme);
}
changeLanguage() {
const checkLanguage = localStorage.getItem('qortalLanguage')
if (checkLanguage === null || checkLanguage.length === 0) {
localStorage.setItem('qortalLanguage', 'us')
use('us')
} else {
use(checkLanguage)
}
}
renderErr1Text() {
return html`${translate("nodepage.nchange27")}`
}
renderErr2Text() {
return html`${translate("nodepage.nchange28")}`
}
forceSyncPeer(peerAddress, rowIndex) {
parentEpml
.request("apiCall", {
url: `/admin/forcesync?apiKey=${this.getApiKey()}`,
method: "POST",
body: peerAddress,
})
.then((res) => {
let err3string = get("nodepage.nchange25")
parentEpml.request('showSnackBar', `${err3string}` + peerAddress);
});
}
removePeer(peerAddress, rowIndex) {
parentEpml
.request("apiCall", {
url: `/peers?apiKey=${this.getApiKey()}`,
method: "DELETE",
body: peerAddress,
})
.then((res) => {
let err4string = get("nodepage.nchange26")
parentEpml.request('showSnackBar', `${err4string}` + peerAddress);
this.peers.splice(rowIndex, 1);
});
}
stopNode() {
parentEpml
.request("apiCall", {
url: `/admin/stop?apiKey=${this.getApiKey()}`,
method: "GET"
})
.then((res) => {
let err7string = get("nodepage.nchange32")
parentEpml.request('showSnackBar', `${err7string}`);
});
}
onPageNavigation(pageUrl) {
parentEpml.request("setPageUrl", pageUrl);
}
addPeer(e) {
this.addPeerLoading = true;
const addPeerAddress = this.shadowRoot.querySelector("#addPeerAddress")
.value;
parentEpml
.request("apiCall", {
url: `/peers?apiKey=${this.getApiKey()}`,
method: "POST",
body: addPeerAddress,
})
.then((res) => {
this.addPeerMessage = res.message;
this.addPeerLoading = false;
});
}
addMintingAccount(e) {
this.addMintingAccountLoading = true;
this.addMintingAccountMessage = "Loading...";
this.addMintingAccountKey = this.shadowRoot.querySelector(
"#addMintingAccountKey"
).value;
parentEpml
.request("apiCall", {
url: `/admin/mintingaccounts?apiKey=${this.getApiKey()}`,
method: "POST",
body: this.addMintingAccountKey,
})
.then((res) => {
if (res === true) {
this.updateMintingAccounts();
this.addMintingAccountKey = "";
this.addMintingAccountMessage = this.renderErr1Text();
this.addMintingAccountLoading = false;
} else {
this.addMintingAccountKey = "";
this.addMintingAccountMessage = this.renderErr2Text(); // Corrected an error here thanks to crow (-_-)
this.addMintingAccountLoading = false;
}
});
}
updateMintingAccounts() {
parentEpml.request("apiCall", {
url: `/admin/mintingaccounts`,
}).then((res) => {
setTimeout(() => this.mintingAccounts = res, 1);
});
}
pasteToTextBox(elementId) {
window.focus()
navigator.clipboard.readText().then((clipboardText) => {
elementId.value += clipboardText
elementId.focus()
})
}
pasteMenu(event, elementId) {
let eventObject = { pageX: event.pageX, pageY: event.pageY, clientX: event.clientX, clientY: event.clientY, elementId }
parentEpml.request('openFramePasteMenu', eventObject)
}
_textMenu(event) {
const getSelectedText = () => {
var text = ''
if (typeof window.getSelection != 'undefined') {
text = window.getSelection().toString()
} else if (typeof this.shadowRoot.selection != 'undefined' && this.shadowRoot.selection.type == 'Text') {
text = this.shadowRoot.selection.createRange().text
}
return text
}
const checkSelectedTextAndShowMenu = () => {
let selectedText = getSelectedText()
if (selectedText && typeof selectedText === 'string') {
let _eve = { pageX: event.pageX, pageY: event.pageY, clientX: event.clientX, clientY: event.clientY }
let textMenuObject = { selectedText: selectedText, eventObject: _eve, isFrame: true }
parentEpml.request('openCopyTextMenu', textMenuObject)
}
}
checkSelectedTextAndShowMenu()
}
removeMintingAccount(publicKey) {
this.removeMintingAccountLoading = true;
parentEpml.request("apiCall", {
url: `/admin/mintingaccounts?apiKey=${this.getApiKey()}`,
method: "DELETE",
body: publicKey,
}).then((res) => {
if (res === true) {
this.updateMintingAccounts();
this.removeMintingAccountLoading = false;
let err5string = get("nodepage.nchange29")
parentEpml.request('showSnackBar', `${err5string}`);
} else {
this.removeMintingAccountLoading = false;
let err6string = get("nodepage.nchange30")
parentEpml.request('showSnackBar', `${err6string}`);
}
});
}
getApiKey() {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
let apiKey = myNode.apiKey;
return apiKey;
}
clearSelection() {
window.getSelection().removeAllRanges()
window.parent.getSelection().removeAllRanges()
}
isEmptyArray(arr) {
if (!arr) return true;
return arr.length === 0;
}
}
window.customElements.define("node-management", NodeManagement);

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css">
<link rel="stylesheet" href="/font/switch-theme.css">
<script>
const checkBack = localStorage.getItem('qortalTheme')
if (checkBack === 'dark') {
newtheme = 'dark';
} else {
newtheme = 'light';
}
document.querySelector('html').setAttribute('theme', newtheme);
</script>
<style>
html {
--scrollbarBG: #a1a1a1;
--thumbBG: #6a6c75;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}
html,
body {
margin: 0;
font-family: "Roboto", sans-serif;
background: var(--plugback);
}
</style>
</head>
<body>
<puzzles-info></puzzles-info>
<script src="puzzles.js"></script>
</body>
</html>

View File

@@ -0,0 +1,539 @@
import { LitElement, html, css } from 'lit'
import { render } from 'lit/html.js'
import { Epml } from '../../../epml.js'
import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate'
registerTranslateConfig({
loader: lang => fetch(`/language/${lang}.json`).then(res => res.json())
})
// Not sure if these are imported in the proper way:
import nacl from '../../../../crypto/api/deps/nacl-fast.js'
import Base58 from '../../../../crypto/api/deps/Base58.js'
import publicKeyToAddress from '../../../../crypto/api/wallet/publicKeyToAddress.js'
import '@material/mwc-icon'
import '@material/mwc-button'
import '@material/mwc-textfield'
import '@material/mwc-dialog'
import '@material/mwc-slider'
import '@polymer/paper-spinner/paper-spinner-lite.js'
import '@vaadin/grid'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
const DEFAULT_FEE = 0.001
const PAYMENT_TX_TYPE = 2
class Puzzles extends LitElement {
static get properties() {
return {
loading: { type: Boolean },
invalid: { type: Boolean },
puzzles: { type: Array },
solved: { type: Object },
selectedAddress: { type: Object },
selectedPuzzle: { type: Object },
error: { type: Boolean },
message: { type: String },
theme: { type: String, reflect: true }
}
}
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);
--paper-input-container-focus-color: var(--mdc-theme-primary);
--lumo-primary-text-color: rgb(0, 167, 245);
--lumo-primary-color-50pct: rgba(0, 167, 245, 0.5);
--lumo-primary-color-10pct: rgba(0, 167, 245, 0.1);
--lumo-primary-color: hsl(199, 100%, 48%);
--lumo-base-color: var(--white);
--lumo-body-text-color: var(--black);
--lumo-secondary-text-color: var(--sectxt);
--lumo-contrast-60pct: var(--vdicon);
--_lumo-grid-border-color: var(--border);
--_lumo-grid-secondary-border-color: var(--border2);
}
#puzzle-page {
background: var(--white);
padding: 12px 24px;
}
h2 {
margin:0;
}
h2, h3, h4, h5 {
color: var(--black);
font-weight: 400;
}
.red {
--mdc-theme-primary: #F44336;
}
.clue {
font-family: "Lucida Console", "Courier New", monospace;
font-size: smaller;
}
.divCard {
border: 1px solid var(--border);
padding: 1em;
box-shadow: 0 .3px 1px 0 rgba(0,0,0,0.14), 0 1px 1px -1px rgba(0,0,0,0.12), 0 1px 2px 0 rgba(0,0,0,0.20);
margin-bottom: 2em;
}
`
}
constructor() {
super()
this.loading = false
this.invalid = true
this.puzzles = []
this.solved = {}
this.selectedAddress = {}
this.selectedPuzzle = {}
this.error = false
this.message = ''
this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light'
}
render() {
return html`
<div id="puzzle-page">
<div style="min-height:48px; display: flex; padding-bottom: 6px;">
<h3 style="margin: 0; flex: 1; padding-top: 8px; display: inline;">${translate("puzzlepage.pchange1")}</h3>
</div>
<div class="divCard">
<vaadin-grid theme="compact, wrap-cell-content" id="puzzlesGrid" ?hidden="${this.isEmptyArray(this.puzzles)}" .items="${this.puzzles}" aria-label="Puzzles" all-rows-visible>
<vaadin-grid-column width="20em" header="${translate("puzzlepage.pchange2")}" .renderer=${(root, column, data) => {
if (data.item.isSolved) {
render(html`<span style="font-size: smaller;">${translate("puzzlepage.pchange3")}<br>${data.item.winner}</span>`, root)
} else {
render(html`<span>${data.item.reward} QORT</span>`, root)
}
}}>
</vaadin-grid-column>
<vaadin-grid-column width="16em" header="${translate("puzzlepage.pchange4")}" path="name"></vaadin-grid-column>
<vaadin-grid-column width="40%" header="${translate("puzzlepage.pchange5")}" path="description"></vaadin-grid-column>
<vaadin-grid-column width="24em" header="${translate("puzzlepage.pchange6")}" .renderer=${(root, column, data) => {
render(html`<span class="clue">${data.item.clue}</span>`, root)
}}></vaadin-grid-column>
<vaadin-grid-column width="10em" header="${translate("puzzlepage.pchange7")}" .renderer=${(root, column, data) => {
if (data.item.isSolved) {
render(html``, root)
} else {
render(html`<mwc-button @click=${() => this.guessPuzzle(data.item)}><mwc-icon>queue</mwc-icon>&nbsp;${translate("puzzlepage.pchange8")}</mwc-button>`, root)
}
}}></vaadin-grid-column>
</vaadin-grid>
<mwc-dialog id="puzzleGuessDialog" scrimClickAction="${this.loading ? '' : 'close'}">
<div>${translate("puzzlepage.pchange9")} ${this.selectedPuzzle.reward} QORT:</div>
<br>
<div id="puzzleGuessName">${translate("puzzlepage.pchange4")}: ${this.selectedPuzzle.name}</div>
<div id="puzzleGuessDescription">${translate("puzzlepage.pchange5")}: ${this.selectedPuzzle.description}</div>
<div id="puzzleGuessClue" ?hidden=${!this.selectedPuzzle.clue}>Clue: <span class="clue">${this.selectedPuzzle.clue}</span></div>
<br>
<div id="puzzleGuessInputHint" style="font-size: smaller;">${translate("puzzlepage.pchange10")} <b>${translate("puzzlepage.pchange11")}</b> ${translate("puzzlepage.pchange12")}</div>
<mwc-textfield style="width:100%" ?disabled="${this.loading}" label="${translate("puzzlepage.pchange13")}" id="puzzleGuess" pattern="[1-9A-HJ-NP-Za-km-z]{43,44}" style="font-family: monospace;" maxLength="44" charCounter="true" autoValidate="true"></mwc-textfield>
<div style="text-align:right; height:36px;">
<span ?hidden="${!this.loading}">
<!-- loading message -->
${translate("puzzlepage.pchange14")} &nbsp;
<paper-spinner-lite
style="margin-top:12px;"
?active="${this.loading}"
alt="Checking puzzle guess"
>
</paper-spinner-lite>
</span>
<span ?hidden=${this.message === ''} style="${this.error ? 'color:red;' : ''}">
${this.message}
</span>
</div>
<mwc-button
?disabled="${this.loading || this.invalid}"
slot="primaryAction"
@click=${this.submitPuzzleGuess}
>
${translate("puzzlepage.pchange15")}
</mwc-button>
<mwc-button
?disabled="${this.loading}"
slot="secondaryAction"
dialogAction="cancel"
class="red"
>
${translate("general.close")}
</mwc-button>
</mwc-dialog>
</div>
`
}
firstUpdated() {
this.changeTheme()
this.changeLanguage()
window.addEventListener("contextmenu", (event) => {
event.preventDefault();
this._textMenu(event)
})
window.addEventListener("click", () => {
parentEpml.request('closeCopyTextMenu', null)
})
window.addEventListener('storage', () => {
const checkLanguage = localStorage.getItem('qortalLanguage')
const checkTheme = localStorage.getItem('qortalTheme')
use(checkLanguage)
if (checkTheme === 'dark') {
this.theme = 'dark'
} else {
this.theme = 'light'
}
document.querySelector('html').setAttribute('theme', this.theme)
})
window.onkeyup = (e) => {
if (e.keyCode === 27) {
parentEpml.request('closeCopyTextMenu', null)
}
}
const textBox = this.shadowRoot.getElementById("puzzleGuess")
// keep track of input validity so we can enabled/disable submit button
textBox.validityTransform = (newValue, nativeValidity) => {
this.invalid = !nativeValidity.valid
return nativeValidity
}
const getPuzzleGroupMembers = async () => {
return await parentEpml.request('apiCall', {
url: `/groups/members/165`
})
}
const getBalance = async (address) => {
return await parentEpml.request('apiCall', {
url: `/addresses/balance/${address}`
})
}
const getName = async (memberAddress) => {
let _names = await parentEpml.request('apiCall', {
url: `/names/address/${memberAddress}`
})
if (_names.length === 0) return "";
return _names[0].name
}
const getNameInfo = async (name) => {
// We have to explicitly encode '#' to stop them being interpreted as in-page references
name = name.replaceAll('#', '%23')
return await parentEpml.request('apiCall', {
url: `/names/${name}`
})
}
const getFirstOutgoingPayment = async (sender) => {
let _payments = await parentEpml.request('apiCall', {
url: `/transactions/search?confirmationStatus=CONFIRMED&limit=20&txType=PAYMENT&address=${sender}`
})
return _payments.find(payment => payment.creatorAddress === sender)
}
const updatePuzzles = async () => {
let _puzzleGroupMembers = await getPuzzleGroupMembers()
let _puzzles = []
await Promise.all(_puzzleGroupMembers.members
.sort((a, b) => b.joined - a.joined)
.map(async (member) => {
let _puzzleAddress = member.member
if (member.isAdmin) return
// Already solved? No need to refresh info
if (this.solved[_puzzleAddress]) {
_puzzles.push(this.solved[_puzzleAddress])
return
}
let _name = await getName(_puzzleAddress)
// No name???
if (_name === "") return
let _reward = await getBalance(_puzzleAddress)
let _isSolved = _reward < 1.0;
let _nameInfo = await getNameInfo(_name)
let _nameData = JSON.parse(_nameInfo.data)
let _puzzle = {
reward: _reward,
address: _puzzleAddress,
name: _name,
description: _nameData.description,
isSolved: _isSolved
}
if (_nameData.clue)
_puzzle.clue = _nameData.clue;
if (_isSolved) {
// Info on winner
let _payment = await getFirstOutgoingPayment(_puzzleAddress)
_puzzle.winner = _payment.recipient
// Does winner have a name?
let _winnerName = await getName(_puzzle.winner)
if (_winnerName) _puzzle.winner = _winnerName
// Add to 'solved' map to prevent info refresh as it'll never change
this.solved[_puzzleAddress] = _puzzle
}
_puzzles.push(_puzzle);
}))
this.puzzles = _puzzles;
setTimeout(updatePuzzles, 20000)
}
let configLoaded = false
parentEpml.ready().then(() => {
parentEpml.subscribe('selected_address', async selectedAddress => {
this.selectedAddress = {}
selectedAddress = JSON.parse(selectedAddress)
if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
this.selectedAddress = selectedAddress
})
parentEpml.subscribe('config', c => {
if (!configLoaded) {
setTimeout(updatePuzzles, 1)
configLoaded = true
}
this.config = JSON.parse(c)
})
parentEpml.subscribe('copy_menu_switch', async value => {
if (value === 'false' && window.getSelection().toString().length !== 0) {
this.clearSelection()
}
})
parentEpml.subscribe('frame_paste_menu_switch', async res => {
res = JSON.parse(res)
if (res.isOpen === false && this.isPasteMenuOpen === true) {
this.pasteToTextBox(textBox)
this.isPasteMenuOpen = false
}
})
})
parentEpml.imReady()
textBox.addEventListener('contextmenu', (event) => {
const getSelectedText = () => {
var text = "";
if (typeof window.getSelection != "undefined") {
text = window.getSelection().toString();
} else if (typeof this.shadowRoot.selection != "undefined" && this.shadowRoot.selection.type == "Text") {
text = this.shadowRoot.selection.createRange().text;
}
return text;
}
const checkSelectedTextAndShowMenu = () => {
let selectedText = getSelectedText();
if (selectedText && typeof selectedText === 'string') {
// ...
} else {
this.pasteMenu(event)
this.isPasteMenuOpen = true
// Prevent Default and Stop Event Bubbling
event.preventDefault()
event.stopPropagation()
}
}
checkSelectedTextAndShowMenu()
})
}
changeTheme() {
const checkTheme = localStorage.getItem('qortalTheme')
if (checkTheme === 'dark') {
this.theme = 'dark';
} else {
this.theme = 'light';
}
document.querySelector('html').setAttribute('theme', this.theme);
}
changeLanguage() {
const checkLanguage = localStorage.getItem('qortalLanguage')
if (checkLanguage === null || checkLanguage.length === 0) {
localStorage.setItem('qortalLanguage', 'us')
use('us')
} else {
use(checkLanguage)
}
}
renderErr1Text() {
return html`${translate("puzzlepage.pchange16")}`
}
renderErr2Text() {
return html`${translate("puzzlepage.pchange17")}`
}
async guessPuzzle(puzzle) {
this.selectedPuzzle = puzzle
this.shadowRoot.getElementById("puzzleGuess").value = ''
this.shadowRoot.getElementById("puzzleGuess").checkValidity()
this.message = ''
this.invalid = true
this.shadowRoot.querySelector('#puzzleGuessDialog').show()
}
async submitPuzzleGuess(e) {
this.loading = true
this.error = false
// Check for valid guess
const guess = this.shadowRoot.getElementById("puzzleGuess").value
let _rawGuess = Base58.decode(guess)
let _keyPair = nacl.sign.keyPair.fromSeed(_rawGuess)
let _guessAddress = publicKeyToAddress(_keyPair.publicKey)
if (_guessAddress !== this.selectedPuzzle.address) {
this.error = true
this.message = this.renderErr1Text()
this.loading = false
return
}
// Get Last Ref
const getLastRef = async (address) => {
let myRef = await parentEpml.request('apiCall', {
url: `/addresses/lastreference/${address}`
})
return myRef
}
let lastRef = await getLastRef(_guessAddress)
let amount = this.selectedPuzzle.reward - DEFAULT_FEE;
let recipientAddress = this.selectedAddress.address
let txnParams = {
recipient: recipientAddress,
amount: amount,
lastReference: lastRef,
fee: DEFAULT_FEE
}
// Mostly copied from qortal-ui-core/src/plugins/routes.js
let txnResponse = await parentEpml.request('standaloneTransaction', {
type: 2,
keyPair: {
publicKey: _keyPair.publicKey,
privateKey: _keyPair.secretKey
},
params: txnParams
})
if (txnResponse.success) {
this.message = this.renderErr2Text()
} else {
this.error = true
if (txnResponse.data) {
this.message = "Error while claiming reward: " + txnResponse.data.message
} else {
this.message = "Error while claiming reward: " + txnResponse.message
}
}
this.loading = false
}
pasteToTextBox(textBox) {
// Return focus to the window
window.focus()
navigator.clipboard.readText().then(clipboardText => {
textBox.value += clipboardText
textBox.focus()
});
}
pasteMenu(event) {
let eventObject = { pageX: event.pageX, pageY: event.pageY, clientX: event.clientX, clientY: event.clientY }
parentEpml.request('openFramePasteMenu', eventObject)
}
_textMenu(event) {
const getSelectedText = () => {
var text = "";
if (typeof window.getSelection != "undefined") {
text = window.getSelection().toString();
} else if (typeof this.shadowRoot.selection != "undefined" && this.shadowRoot.selection.type == "Text") {
text = this.shadowRoot.selection.createRange().text;
}
return text;
}
const checkSelectedTextAndShowMenu = () => {
let selectedText = getSelectedText();
if (selectedText && typeof selectedText === 'string') {
let _eve = { pageX: event.pageX, pageY: event.pageY, clientX: event.clientX, clientY: event.clientY }
let textMenuObject = { selectedText: selectedText, eventObject: _eve, isFrame: true }
parentEpml.request('openCopyTextMenu', textMenuObject)
}
}
checkSelectedTextAndShowMenu()
}
isEmptyArray(arr) {
if (!arr) { return true }
return arr.length === 0
}
clearSelection() {
window.getSelection().removeAllRanges()
window.parent.getSelection().removeAllRanges()
}
}
window.customElements.define('puzzles-info', Puzzles)

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css">
<link rel="stylesheet" href="/font/switch-theme.css">
<script>
const checkBack = localStorage.getItem('qortalTheme')
if (checkBack === 'dark') {
newtheme = 'dark';
} else {
newtheme = 'light';
}
document.querySelector('html').setAttribute('theme', newtheme);
</script>
<style>
html {
--scrollbarBG: #a1a1a1;
--thumbBG: #6a6c75;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}
html,
body {
margin: 0;
font-family: "Roboto", sans-serif;
background: var(--plugback);
}
</style>
</head>
<body>
<q-apps></q-apps>
<script src="q-apps.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,82 @@
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.chatBytes, e.data.path, e.data.difficulty)
postMessage(response)
})
const memory = new WebAssembly.Memory({ initial: 256, maximum: 256 })
const heap = new Uint8Array(memory.buffer)
const computePow = async (chatBytes, path, difficulty) => {
let response = null
await new Promise((resolve, reject)=> {
const _chatBytesArray = Object.keys(chatBytes).map(function (key) { return chatBytes[key]; });
const chatBytesArray = new Uint8Array(_chatBytesArray);
const chatBytesHash = new Sha256().process(chatBytesArray).finish().result;
const hashPtr = sbrk(32, heap);
const hashAry = new Uint8Array(memory.buffer, hashPtr, 32);
hashAry.set(chatBytesHash);
const workBufferLength = 8 * 1024 * 1024;
const workBufferPtr = sbrk(workBufferLength, heap);
const importObject = {
env: {
memory: memory
},
};
function loadWebAssembly(filename, imports) {
// Fetch the file and compile it
return fetch(filename)
.then(response => response.arrayBuffer())
.then(buffer => WebAssembly.compile(buffer))
.then(module => {
// Create the instance.
return new WebAssembly.Instance(module, importObject);
});
}
loadWebAssembly(path)
.then(wasmModule => {
response = {
nonce : wasmModule.exports.compute2(hashPtr, workBufferPtr, workBufferLength, difficulty),
chatBytesArray
}
resolve()
});
})
return response
}

View File

@@ -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
}

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css">
<link rel="stylesheet" href="/font/switch-theme.css">
<script>
const checkBack = localStorage.getItem('qortalTheme')
if (checkBack === 'dark') {
newtheme = 'dark';
} else {
newtheme = 'light';
}
document.querySelector('html').setAttribute('theme', newtheme);
</script>
<style>
html {
--scrollbarBG: #a1a1a1;
--thumbBG: #6a6c75;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}
html,
body {
margin: 0;
font-family: "Roboto", sans-serif;
background: var(--plugback);
}
</style>
</head>
<body>
<web-browser></web-browser>
<script src="browser.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css">
<link rel="stylesheet" href="/font/switch-theme.css">
<script>
const checkBack = localStorage.getItem('qortalTheme')
if (checkBack === 'dark') {
newtheme = 'dark';
} else {
newtheme = 'light';
}
document.querySelector('html').setAttribute('theme', newtheme);
</script>
<style>
html {
--scrollbarBG: #a1a1a1;
--thumbBG: #6a6c75;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}
html,
body {
margin: 0;
font-family: "Roboto", sans-serif;
background: var(--plugback);
}
</style>
</head>
<body>
<data-management></data-management>
<script src="data-management.js"></script>
</body>
</html>

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css">
<link rel="stylesheet" href="/font/switch-theme.css">
<script>
const checkBack = localStorage.getItem('qortalTheme')
if (checkBack === 'dark') {
newtheme = 'dark';
} else {
newtheme = 'light';
}
document.querySelector('html').setAttribute('theme', newtheme);
</script>
<style>
html {
--scrollbarBG: #a1a1a1;
--thumbBG: #6a6c75;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}
html,
body {
margin: 0;
font-family: "Roboto", sans-serif;
background: var(--plugback);
}
</style>
</head>
<body>
<websites-list></websites-list>
<script src="websites.js"></script>
</body>
</html>

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css">
<link rel="stylesheet" href="/font/switch-theme.css">
<script>
const checkBack = localStorage.getItem('qortalTheme')
if (checkBack === 'dark') {
newtheme = 'dark';
} else {
newtheme = 'light';
}
document.querySelector('html').setAttribute('theme', newtheme);
</script>
<style>
html {
--scrollbarBG: #a1a1a1;
--thumbBG: #6a6c75;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}
html,
body {
margin: 0;
font-family: "Roboto", sans-serif;
background: var(--plugback);
}
</style>
</head>
<body>
<publish-data></publish-data>
<script src="publish.js"></script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More