4
1
mirror of https://github.com/Qortal/qortal-ui.git synced 2025-02-11 17:55:51 +00:00

Merge pull request #197 from Philreact/feature/q-chat-groups

Feature/q chat groups
This commit is contained in:
QuickMythril 2023-10-03 15:44:15 -04:00 committed by GitHub
commit 6d9984476b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 12926 additions and 3889 deletions

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
/node_modules

View File

@ -3,6 +3,7 @@
"browser": true,
"es2021": true
},
"plugins": ["lit", "wc"],
"extends": ["eslint:recommended", "plugin:lit/recommended", "plugin:wc/recommended"],
"parserOptions": {
"ecmaVersion": 12,

View File

@ -200,6 +200,8 @@
"snack3": "Successfully added and saved custom node",
"snack4": "Nodes successfully saved as",
"snack5": "Nodes successfully imported",
"snack6": "Successfully removed custom node",
"snack7": "Successfully edited custom node",
"exp1": "Export Private Master Key",
"exp2": "Export Master Key",
"exp3": "Export",
@ -519,7 +521,8 @@
"nchange44": "To the new name",
"nchange45": "On pressing confirm, the name update request will be sent!",
"nchange46": "Name Sale History",
"nchange47": "Name Update Successful!"
"nchange47": "Name Update Successful!",
"nchange48": "Warning! If you update your name, you will forfeit the resources associated with the original name. In other words, you will lose ownership of the content under the original name in the QDN. Proceed with caution!"
},
"websitespage": {
"schange1": "Browse Websites",
@ -829,7 +832,14 @@
"cchange80": "This image has been deleted",
"cchange81": "This image type is not supported",
"cchange82": "This attachment has been deleted",
"cchange90": "No messages"
"cchange90": "No messages",
"cchange91": "Sending...",
"cchange92": "Unread messages below",
"cchange93": "Image copied to clipboard",
"cchange94": "loaded",
"cchange95": "Only my resources",
"cchange96": "Open Group Management",
"cchange97": "Join group link copied to clipboard"
},
"welcomepage": {
"wcchange1": "Welcome to Q-Chat",
@ -1161,5 +1171,10 @@
"lot11": "There are no open lotteries!",
"lot12": "There are no finished lotteries!",
"lot13": "Players"
},
"notifications": {
"notify1": "Confirming transaction",
"notify2": "Transaction confirmed",
"explanation": "Your transaction is getting confirmed. To track its progress, click on the bell icon."
}
}

View File

@ -42,6 +42,8 @@ import '../functional-components/side-menu.js'
import '../functional-components/side-menu-item.js'
import './start-minting.js'
import './notification-view/notification-bell.js'
import './notification-view/notification-bell-general.js'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
@ -556,7 +558,10 @@ class AppView extends connect(store)(LitElement) {
<img src="${this.config.coin.logo}" style="height:32px; padding-left:12px;">
</span>
</div>
<div style="display:flex;align-items:center;gap:20px">
<notification-bell></notification-bell>
<notification-bell-general></notification-bell-general>
</div>
<div style="display: inline;">
<span>
<img src="/img/${translate("selectmenu.languageflag")}-flag-round-icon-32.png" style="width: 32px; height: 32px; padding-top: 4px;">

View File

@ -15,7 +15,7 @@ import './login-section.js'
import '../qort-theme-toggle.js'
import settings from '../../functional-components/settings-page.js'
import { addAutoLoadImageChat, removeAutoLoadImageChat, addChatLastSeen, allowQAPPAutoAuth, removeQAPPAutoAuth, removeQAPPAutoLists, allowQAPPAutoLists, addTabInfo, setTabNotifications, setNewTab } from '../../redux/app/app-actions.js'
import { addAutoLoadImageChat, removeAutoLoadImageChat, addChatLastSeen, allowQAPPAutoAuth, removeQAPPAutoAuth, removeQAPPAutoLists, allowQAPPAutoLists, addTabInfo, setTabNotifications, setNewTab, setNewNotification, setSideEffectAction } from '../../redux/app/app-actions.js'
window.reduxStore = store
window.reduxAction = {
@ -28,7 +28,9 @@ window.reduxAction = {
removeQAPPAutoLists: removeQAPPAutoLists,
addTabInfo: addTabInfo,
setTabNotifications: setTabNotifications,
setNewTab: setNewTab
setNewTab: setNewTab,
setNewNotification: setNewNotification,
setSideEffectAction: setSideEffectAction
}
const animationDuration = 0.7 // Seconds

View File

@ -0,0 +1,527 @@
import { LitElement, html, css } from 'lit';
import { connect } from 'pwa-helpers';
import '@vaadin/item';
import '@vaadin/list-box';
import '@polymer/paper-icon-button/paper-icon-button.js';
import '@polymer/iron-icons/iron-icons.js';
import { store } from '../../store.js';
import { setNewNotification, setNewTab } from '../../redux/app/app-actions.js';
import { routes } from '../../plugins/routes.js';
import '@material/mwc-icon';
import { translate, get } from 'lit-translate';
import { repeat } from 'lit/directives/repeat.js';
import config from '../../notifications/config.js';
import '../../../../plugins/plugins/core/components/TimeAgo.js';
import './popover.js';
class NotificationBellGeneral extends connect(store)(LitElement) {
static properties = {
notifications: { type: Array },
showNotifications: { type: Boolean },
notificationCount: { type: Boolean },
theme: { type: String, reflect: true },
notifications: { type: Array },
currentNotification: { type: Object },
};
constructor() {
super();
this.notifications = [];
this.showNotifications = false;
this.notificationCount = false;
this.initialFetch = false;
this.theme = localStorage.getItem('qortalTheme')
? localStorage.getItem('qortalTheme')
: 'light';
this.currentNotification = null;
}
firstUpdated() {
try {
let value = JSON.parse(localStorage.getItem('isFirstTimeUser'));
if (!value && value !== false) {
value = true;
}
this.isFirstTimeUser = value;
} catch (error) {}
}
async stateChanged(state) {
if (state.app.newNotification) {
const newNotification = state.app.newNotification;
this.notifications = [newNotification, ...this.notifications];
store.dispatch(setNewNotification(null));
if (this.isFirstTimeUser) {
const target = this.shadowRoot.getElementById(
'popover-notification'
);
const popover =
this.shadowRoot.querySelector('popover-component');
if (popover) {
popover.openPopover(target);
}
localStorage.setItem('isFirstTimeUser', JSON.stringify(false));
this.isFirstTimeUser = false;
}
}
}
handleBlur() {
setTimeout(() => {
if (!this.shadowRoot.contains(document.activeElement)) {
this.showNotifications = false;
}
}, 0);
}
changeStatus(signature, statusTx) {
const copyNotifications = [...this.notifications];
const findNotification = this.notifications.findIndex(
(notification) => notification.reference.signature === signature
);
if (findNotification !== -1) {
copyNotifications[findNotification] = {
...copyNotifications[findNotification],
status: statusTx,
};
this.notifications = copyNotifications;
}
}
render() {
const hasOngoing = this.notifications.find(
(notification) => notification.status !== 'confirmed'
);
return html`
<div class="layout">
<popover-component
for="popover-notification"
message=${get('notifications.explanation')}
></popover-component>
<div
id="popover-notification"
@click=${() => this._toggleNotifications()}
>
${hasOngoing
? html`
<mwc-icon style="color: green;cursor:pointer"
>notifications</mwc-icon
>
`
: html`
<mwc-icon
style="color: var(--black); cursor:pointer"
>notifications</mwc-icon
>
`}
</div>
${hasOngoing
? html`
<span
class="count"
style="cursor:pointer"
@click=${() => this._toggleNotifications()}
>
<mwc-icon
style="color: var(--black);font-size:18px"
>pending</mwc-icon
>
</span>
`
: ''}
<div
id="notification-panel"
class="popover-panel"
style="visibility:${this.showNotifications
? 'visibile'
: 'hidden'}"
tabindex="0"
@blur=${this.handleBlur}
>
<div class="notifications-list">
${repeat(
this.notifications,
(notification) => notification.reference.signature, // key function
(notification) => html`
<notification-item-tx
.changeStatus=${(val1, val2) =>
this.changeStatus(val1, val2)}
status=${notification.status}
timestamp=${notification.timestamp}
type=${notification.type}
signature=${notification.reference
.signature}
></notification-item-tx>
`
)}
</div>
</div>
</div>
`;
}
_toggleNotifications() {
if (this.notifications.length === 0) return;
this.showNotifications = !this.showNotifications;
if (this.showNotifications) {
requestAnimationFrame(() => {
this.shadowRoot.getElementById('notification-panel').focus();
});
}
}
static styles = css`
.layout {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
margin-right: 20px;
}
.count {
position: absolute;
top: -5px;
right: -5px;
font-size: 12px;
background-color: red;
color: white;
border-radius: 50%;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.nocount {
display: none;
}
.popover-panel {
position: absolute;
width: 200px;
padding: 10px;
background-color: var(--white);
border: 1px solid var(--black);
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
top: 40px;
max-height: 350px;
overflow: auto;
scrollbar-width: thin;
scrollbar-color: #6a6c75 #a1a1a1;
}
.popover-panel::-webkit-scrollbar {
width: 11px;
}
.popover-panel::-webkit-scrollbar-track {
background: #a1a1a1;
}
.popover-panel::-webkit-scrollbar-thumb {
background-color: #6a6c75;
border-radius: 6px;
border: 3px solid #a1a1a1;
}
.notifications-list {
display: flex;
flex-direction: column;
}
.notification-item {
padding: 5px;
border-bottom: 1px solid;
display: flex;
justify-content: space-between;
cursor: pointer;
transition: 0.2s all;
}
.notification-item:hover {
background: var(--nav-color-hover);
}
p {
font-size: 14px;
color: var(--black);
margin: 0px;
padding: 0px;
}
`;
}
customElements.define('notification-bell-general', NotificationBellGeneral);
class NotificationItemTx extends connect(store)(LitElement) {
static properties = {
status: { type: String },
type: { type: String },
timestamp: { type: Number },
signature: { type: String },
changeStatus: { attribute: false },
};
constructor() {
super();
this.nodeUrl = this.getNodeUrl();
this.myNode = this.getMyNode();
}
getNodeUrl() {
const myNode =
store.getState().app.nodeConfig.knownNodes[
window.parent.reduxStore.getState().app.nodeConfig.node
];
const nodeUrl =
myNode.protocol + '://' + myNode.domain + ':' + myNode.port;
return nodeUrl;
}
getMyNode() {
const myNode =
store.getState().app.nodeConfig.knownNodes[
window.parent.reduxStore.getState().app.nodeConfig.node
];
return myNode;
}
async getStatus() {
let interval = null;
let stop = false;
const getAnswer = async () => {
const getTx = async (minterAddr) => {
const url = `${this.nodeUrl}/transactions/signature/${this.signature}`;
const res = await fetch(url);
const data = await res.json();
return data;
};
if (!stop) {
stop = true;
try {
const txTransaction = await getTx();
if (!txTransaction.error && txTransaction.signature && txTransaction.blockHeight) {
clearInterval(interval);
this.changeStatus(this.signature, 'confirmed');
}
} catch (error) {}
stop = false;
}
};
interval = setInterval(getAnswer, 20000);
}
firstUpdated() {
this.getStatus();
}
render() {
return html`
<div class="notification-item" @click=${() => {}}>
<div>
<p style="margin-bottom:10px; font-weight:bold">
${translate('transpage.tchange1')}
</p>
</div>
<div>
<p style="margin-bottom:5px">
${translate('walletpage.wchange35')}: ${this.type}
</p>
<p style="margin-bottom:5px">
${translate('tubespage.schange28')}:
${this.status === 'confirming'
? translate('notifications.notify1')
: translate('notifications.notify2')}
</p>
${this.status !== 'confirmed'
? html`
<div class="centered">
<div class="loader">Loading...</div>
</div>
`
: ''}
<div
style="display:flex;justify-content:space-between;align-items:center"
>
<message-time
timestamp=${this.timestamp}
style="color:red;font-size:12px"
></message-time>
${this.status === 'confirmed'
? html`
<mwc-icon style="color: green;"
>done</mwc-icon
>
`
: ''}
</div>
</div>
</div>
`;
}
_toggleNotifications() {
if (this.notifications.length === 0) return;
this.showNotifications = !this.showNotifications;
}
static styles = css`
.centered {
display: flex;
justify-content: center;
align-items: center;
}
.layout {
width: 100px;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.count {
position: absolute;
top: -5px;
right: -5px;
font-size: 12px;
background-color: red;
color: white;
border-radius: 50%;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.nocount {
display: none;
}
.popover-panel {
position: absolute;
width: 200px;
padding: 10px;
background-color: var(--white);
border: 1px solid var(--black);
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
top: 40px;
max-height: 350px;
overflow: auto;
scrollbar-width: thin;
scrollbar-color: #6a6c75 #a1a1a1;
}
.popover-panel::-webkit-scrollbar {
width: 11px;
}
.popover-panel::-webkit-scrollbar-track {
background: #a1a1a1;
}
.popover-panel::-webkit-scrollbar-thumb {
background-color: #6a6c75;
border-radius: 6px;
border: 3px solid #a1a1a1;
}
.notifications-list {
display: flex;
flex-direction: column;
}
.notification-item {
padding: 5px;
border-bottom: 1px solid;
display: flex;
flex-direction: column;
cursor: default;
}
.notification-item:hover {
background: var(--nav-color-hover);
}
p {
font-size: 14px;
color: var(--black);
margin: 0px;
padding: 0px;
}
.loader,
.loader:before,
.loader:after {
border-radius: 50%;
width: 10px;
height: 10px;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation: load7 1.8s infinite ease-in-out;
animation: load7 1.8s infinite ease-in-out;
}
.loader {
color: var(--black);
font-size: 5px;
margin-bottom: 20px;
position: relative;
text-indent: -9999em;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
.loader:before,
.loader:after {
content: '';
position: absolute;
top: 0;
}
.loader:before {
left: -3.5em;
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
.loader:after {
left: 3.5em;
}
@-webkit-keyframes load7 {
0%,
80%,
100% {
box-shadow: 0 2.5em 0 -1.3em;
}
40% {
box-shadow: 0 2.5em 0 0;
}
}
@keyframes load7 {
0%,
80%,
100% {
box-shadow: 0 2.5em 0 -1.3em;
}
40% {
box-shadow: 0 2.5em 0 0;
}
}
`;
}
customElements.define('notification-item-tx', NotificationItemTx);

View File

@ -158,7 +158,7 @@ class NotificationBell extends connect(store)(LitElement) {
url: `qdn/browser/index.html${query}`,
id: 'q-mail-notification',
myPlugObj: {
"url": "qapps",
"url": "myapp",
"domain": "core",
"page": `qdn/browser/index.html${query}`,
"title": "Q-Mail",
@ -196,7 +196,7 @@ class NotificationBell extends connect(store)(LitElement) {
url: `qdn/browser/index.html${query}`,
id: 'q-mail-notification',
myPlugObj: {
"url": "qapps",
"url": "myapp",
"domain": "core",
"page": `qdn/browser/index.html${query}`,
"title": "Q-Mail",
@ -210,7 +210,6 @@ class NotificationBell extends connect(store)(LitElement) {
static styles = css`
.layout {
width: 100px;
display: flex;
flex-direction: column;
align-items: center;
@ -220,7 +219,7 @@ class NotificationBell extends connect(store)(LitElement) {
.count {
position: absolute;
top: 2px;
right: 32px;
right: 0px;
font-size: 12px;
background-color: red;
color: white;

View File

@ -0,0 +1,74 @@
// popover-component.js
import { LitElement, html, css } from 'lit';
import { createPopper } from '@popperjs/core';
import '@material/mwc-icon'
export class PopoverComponent extends LitElement {
static styles = css`
:host {
display: none;
position: absolute;
background-color: var(--white);
border: 1px solid #ddd;
padding: 8px;
z-index: 10;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
color: var(--black);
max-width: 250px;
}
.close-icon {
cursor: pointer;
float: right;
margin-left: 10px;
color: var(--black)
}
`;
static properties = {
for: { type: String, reflect: true },
message: { type: String }
};
constructor() {
super();
this.message = '';
}
firstUpdated() {
// We'll defer the popper attachment to the openPopover() method to ensure target availability
}
attachToTarget(target) {
console.log({target})
if (!this.popperInstance && target) {
this.popperInstance = createPopper(target, this, {
placement: 'bottom',
strategy: 'fixed'
});
}
}
openPopover(target) {
this.attachToTarget(target);
this.style.display = 'block';
}
closePopover() {
this.style.display = 'none';
if (this.popperInstance) {
this.popperInstance.destroy();
this.popperInstance = null;
}
this.requestUpdate();
}
render() {
return html`
<span class="close-icon" @click="${this.closePopover}"><mwc-icon style="color: var(--black)">close</mwc-icon></span>
<div><mwc-icon style="color: var(--black)">info</mwc-icon> ${this.message}</div>
`;
}
}
customElements.define('popover-component', PopoverComponent);

View File

@ -97,13 +97,13 @@ class ShowPlugin extends connect(store)(LitElement) {
.hideIframe {
display: none;
position: absolute;
zIndex: -10;
z-Index: -10;
}
.showIframe {
display: block;
display: flex;
position: relative;
zIndex: 1;
z-Index: 1;
}
.tabs {
@ -135,7 +135,7 @@ class ShowPlugin extends connect(store)(LitElement) {
min-width: 110px;
max-width: 220px;
overflow: hidden;
zIndex: 2;
z-index: 2;
}
.tabCard {
@ -171,7 +171,7 @@ class ShowPlugin extends connect(store)(LitElement) {
border-left: 1px solid var(--black);
border-right: 1px solid var(--black);
border-bottom: 1px solid var(--white);
zIndex: 1;
z-index: 1;
}
.close {
@ -432,7 +432,9 @@ class ShowPlugin extends connect(store)(LitElement) {
<div
id="tab-${tab.id}"
class="tab ${this.currentTab === index ? 'active' : ''}"
@click="${() => this.currentTab = index}"
@click="${() => {
this.currentTab = index
}}"
>
<div id="icon-${tab.id}" class="${this.currentTab === index ? "iconActive" : "iconInactive"}">
<mwc-icon>${icon}</mwc-icon>
@ -441,10 +443,13 @@ class ShowPlugin extends connect(store)(LitElement) {
${count ? html`
<span class="tabTitle ml-30">${title}</span>
<span class="count ml-5">${count}</span>
<span class="show ml-25"><mwc-icon class="close" @click=${() => {this.removeTab(index, tab.id)}}>close</mwc-icon></span>
<span class="show ml-25"><mwc-icon class="close" @click=${(event) => {
event.stopPropagation(); this.removeTab(index, tab.id)
}}>close</mwc-icon></span>
` : html`
<span class="tabTitle ml-30">${title}</span>
<span class="show ml-25"><mwc-icon class="close" @click=${() => {this.removeTab(index, tab.id)}}>close</mwc-icon></span>
<span class="show ml-25"><mwc-icon class="close" @click=${(event) => {event.stopPropagation(); this.removeTab(index, tab.id)}}>close</mwc-icon></span>
`}
</div>
</div>
@ -460,6 +465,7 @@ class ShowPlugin extends connect(store)(LitElement) {
id: this.uid.rnd()
})
this.currentTab = lengthOfTabs
}}
>+</button>
</div>
@ -667,10 +673,10 @@ class ShowPlugin extends connect(store)(LitElement) {
let iconId = ''
this.tabs = this.tabs.filter((tab, tIndex) => tIndex !== index)
this.currentTab = this.tabs.length - 1;
const tabD = this.tabs.length - 1
const plugObj = this.tabs[tabD].url
theId = this.tabs[tabD].id
tabId = 'tab-' + theId
frameId = 'frame-' + theId
@ -819,7 +825,14 @@ class ShowPlugin extends connect(store)(LitElement) {
if (state.app.newTab) {
const newTab = state.app.newTab
if (!this.tabs.find((tab) => tab.id === newTab.id)) {
if(newTab.openExisting && this.tabs.find((tab)=> tab.url === newTab.url)){
const findIndex = this.tabs.findIndex((tab) => tab.url === newTab.url)
if (findIndex !== -1) {
this.currentTab = findIndex
}
store.dispatch(setNewTab(null))
} else if (!this.tabs.find((tab) => tab.id === newTab.id)) {
this.addTab(newTab)
this.currentTab = this.tabs.length - 1
store.dispatch(setNewTab(null))
@ -893,7 +906,7 @@ class NavBar extends connect(store)(LitElement) {
flex-flow: column;
align-items: center;
padding: 20px;
height: 100vh;
height: calc(100vh - 120px);
overflow-y: auto;
}
@ -901,7 +914,7 @@ class NavBar extends connect(store)(LitElement) {
display: flex;
justify-content: space-between;
align-items: center;
background-color: color: var(--white);
background-color: var(--white);
padding: 10px 20px;
max-width: 750px;
width: 80%;
@ -2276,10 +2289,10 @@ class NavBar extends connect(store)(LitElement) {
if (service === "APP") {
this.changePage({
"url": "qapp",
"url": "myapp",
"domain": "core",
"page": `qdn/browser/index.html${query}`,
"title": "Q-App",
"title": name || "Q-App",
"icon": "vaadin:external-browser",
"mwcicon": "open_in_browser",
"menus": [],
@ -2287,10 +2300,10 @@ class NavBar extends connect(store)(LitElement) {
})
} else if (service === "WEBSITE") {
this.changePage({
"url": "websites",
"url": "myapp",
"domain": "core",
"page": `qdn/browser/index.html${query}`,
"title": "Website",
"title": name || "Website",
"icon": "vaadin:desktop",
"mwcicon": "desktop_mac",
"menus": [],

View File

@ -1,34 +1,46 @@
import { LitElement, html, css } from 'lit'
import { connect } from 'pwa-helpers'
import { store } from '../store.js'
import { doAddNode, doSetNode, doLoadNodeConfig } from '../redux/app/app-actions.js'
import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate'
import snackbar from './snackbar.js'
import '../components/language-selector.js'
import '../custom-elements/frag-file-input.js'
import FileSaver from 'file-saver'
import { LitElement, html, css } from 'lit';
import { connect } from 'pwa-helpers';
import { store } from '../store.js';
import {
doAddNode,
doSetNode,
doLoadNodeConfig,
doRemoveNode,
doEditNode,
} from '../redux/app/app-actions.js';
import {
use,
get,
translate,
translateUnsafeHTML,
registerTranslateConfig,
} from 'lit-translate';
import snackbar from './snackbar.js';
import '../components/language-selector.js';
import '../custom-elements/frag-file-input.js';
import FileSaver from 'file-saver';
import '@material/mwc-dialog'
import '@material/mwc-button'
import '@material/mwc-select'
import '@material/mwc-textfield'
import '@material/mwc-icon'
import '@material/mwc-list/mwc-list-item.js'
import '@material/mwc-dialog';
import '@material/mwc-button';
import '@material/mwc-select';
import '@material/mwc-textfield';
import '@material/mwc-icon';
import '@material/mwc-list/mwc-list-item.js';
registerTranslateConfig({
loader: lang => fetch(`/language/${lang}.json`).then(res => res.json())
})
loader: (lang) => fetch(`/language/${lang}.json`).then((res) => res.json()),
});
const checkLanguage = localStorage.getItem('qortalLanguage')
const checkLanguage = localStorage.getItem('qortalLanguage');
if (checkLanguage === null || checkLanguage.length === 0) {
localStorage.setItem('qortalLanguage', 'us')
use('us')
localStorage.setItem('qortalLanguage', 'us');
use('us');
} else {
use(checkLanguage)
use(checkLanguage);
}
let settingsDialog
let settingsDialog;
class SettingsPage extends connect(store)(LitElement) {
static get properties() {
@ -36,8 +48,10 @@ class SettingsPage extends connect(store)(LitElement) {
lastSelected: { type: Number },
nodeConfig: { type: Object },
theme: { type: String, reflect: true },
nodeIndex: { type: Number }
}
nodeIndex: { type: Number },
isBeingEdited: { type: Boolean },
dropdownOpen: { type: Boolean },
};
}
static get styles() {
@ -51,6 +65,7 @@ class SettingsPage extends connect(store)(LitElement) {
--mdc-dialog-min-width: 300px;
--mdc-dialog-max-width: 650px;
--mdc-dialog-max-height: 700px;
--mdc-list-item-text-width: 100%;
}
#main {
@ -83,7 +98,9 @@ class SettingsPage extends connect(store)(LitElement) {
.buttongreen {
color: #03c851;
}
.buttonBlue {
color: #03a9f4;
}
.floatleft {
float: left;
}
@ -91,49 +108,248 @@ class SettingsPage extends connect(store)(LitElement) {
.floatright {
float: right;
}
`
.list-parent {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
#customSelect {
position: relative;
border: 1px solid #ccc;
cursor: pointer;
background: var(--plugback);
}
#customSelect .selected {
padding: 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
#customSelect ul {
position: absolute;
top: 100%;
left: 0;
list-style: none;
margin: 0;
padding: 0;
border: 1px solid #ccc;
display: none;
background: var(--plugback);
width: 100%;
box-sizing: border-box;
z-index: 10;
}
#customSelect ul.open {
display: block;
}
#customSelect ul li {
padding: 10px;
transition: 0.2s all;
}
#customSelect ul li:hover {
background-color: var(--graylight);
}
.selected-left-side{
display: flex;
align-items: center;
}
`;
}
constructor() {
super()
this.nodeConfig = {}
this.nodeIndex = localStorage.getItem('mySelectedNode')
this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light'
super();
this.nodeConfig = {};
this.nodeIndex = localStorage.getItem('mySelectedNode');
this.theme = localStorage.getItem('qortalTheme')
? localStorage.getItem('qortalTheme')
: 'light';
this.isBeingEdited = false;
this.isBeingEditedIndex = null;
this.dropdownOpen = false;
}
render() {
return html`
<mwc-dialog id="settingsDialog" opened=false>
<mwc-dialog id="settingsDialog" opened="false">
<div style="display: inline; text-align: center;">
<h1>${translate("settings.settings")}</h1>
<hr>
<h1>${translate('settings.settings')}</h1>
<hr />
</div>
<br>
<div style="min-height: 250px; min-width: 500px; box-sizing: border-box; position: relative;">
<mwc-select icon="link" id="nodeSelect" label="${translate("settings.nodeurl")}" index="${this.nodeIndex}" @selected="${(e) => this.nodeSelected(e)}" style="min-width: 130px; max-width:100%; width:100%;">
${this.nodeConfig.knownNodes.map((n, index) => html`
<mwc-list-item value="${index}">
<span class="name">${n.name}</span>
<span>${n.protocol + '://' + n.domain + ':' + n.port}</span>
</mwc-list-item>
`)}
</mwc-select>
<p style="margin-top: 30px; text-align: center;">${translate("settings.nodehint")}</p>
<br />
<div
style="min-height: 250px; min-width: 500px; box-sizing: border-box; position: relative;"
>
<div id="customSelect" @click="${this.toggleDropdown}" @blur="${
this.handleBlur
}" tabindex="0">
<div class="selected">
<div class="selected-left-side">
<mwc-icon style="margin-right: 10px"
>link</mwc-icon
>
${
this.selectedItem
? html`
<div>
<span class="name"
>${this.selectedItem
.name}</span
>
<span
>${this.selectedItem
.protocol +
'://' +
this.selectedItem.domain +
':' +
this.selectedItem
.port}</span
>
</div>
`
: 'Please select an option'
}
</div>
<mwc-icon
>expand_more</mwc-icon
>
</div>
<ul class="${this.dropdownOpen ? 'open' : ''}">
${this.nodeConfig.knownNodes.map(
(n, index) => html`
<li
@click="${(e) =>
this.handleSelection(e, n, index)}"
>
<div class="list-parent">
<div>
<span class="name"
>${n.name}</span
>
<span
>${n.protocol +
'://' +
n.domain +
':' +
n.port}</span
>
</div>
<div>
<mwc-button
outlined
@click="${(e) => {
e.stopPropagation();
const currentValues =
this.nodeConfig
.knownNodes[
index
];
const dialog =
this.shadowRoot.querySelector(
'#addNodeDialog'
);
// Set the value for mwc-textfield elements
dialog.querySelector(
'#nameInput'
).value =
currentValues.name;
dialog.querySelector(
'#domainInput'
).value =
currentValues.domain;
dialog.querySelector(
'#portInput'
).value =
currentValues.port;
// Set the selected value for mwc-select
const protocolList =
dialog.querySelector(
'#protocolList'
);
const desiredProtocol =
currentValues.protocol;
protocolList.value =
desiredProtocol;
this.isBeingEdited = true;
this.isBeingEditedIndex =
index;
this.shadowRoot
.querySelector(
'#addNodeDialog'
)
.show();
}}"
><mwc-icon class="buttonBlue"
>edit</mwc-icon
></mwc-button
>
<mwc-button
outlined
@click="${(e) =>
this.removeNode(
e,
index
)}"
><mwc-icon class="buttonred"
>remove</mwc-icon
></mwc-button
>
</div>
</div>
</li>
`
)}
</ul>
</div>
<p style="margin-top: 30px; text-align: center;">
${translate('settings.nodehint')}
</p>
<center>
<mwc-button outlined @click="${() => this.shadowRoot.querySelector('#addNodeDialog').show()}"><mwc-icon class="buttongreen">add</mwc-icon>${translate("settings.addcustomnode")}</mwc-button>
<mwc-button
outlined
@click="${() => this.shadowRoot.querySelector('#addNodeDialog').show()}"
><mwc-icon class="buttongreen">add</mwc-icon
>${translate('settings.addcustomnode')}</mwc-button
>
</center>
<center>
<mwc-button outlined @click="${() => this.removeList()}"><mwc-icon class="buttonred">remove</mwc-icon>${translate("settings.deletecustomnode")}</mwc-button>
<mwc-button outlined @click="${() => this.removeList()}"
><mwc-icon class="buttonred">remove</mwc-icon
>${translate('settings.deletecustomnode')}</mwc-button
>
</center>
<br>
<div class="floatleft">${this.renderExportNodesListButton()}</div><div class="floatright">${this.renderImportNodesListButton()}</div>
<br><br>
<br />
<div class="floatleft">
${this.renderExportNodesListButton()}
</div>
<div style="min-height:100px; min-width: 300px; box-sizing: border-box; position: relative;">
<hr><br>
<div class="floatright">
${this.renderImportNodesListButton()}
</div>
<br /><br />
</div>
<div
style="min-height:100px; min-width: 300px; box-sizing: border-box; position: relative;"
>
<hr />
<br />
<center>
<div id="main">
<mwc-icon class="globe">language</mwc-icon>&nbsp;<language-selector></language-selector>
<mwc-icon class="globe">language</mwc-icon
>&nbsp;<language-selector></language-selector>
</div>
</center>
</div>
@ -142,278 +358,419 @@ class SettingsPage extends connect(store)(LitElement) {
dialogAction="close"
class="red"
>
${translate("general.close")}
${translate('general.close')}
</mwc-button>
</mwc-dialog>
<mwc-dialog id="addNodeDialog">
<div style="text-align: center;">
<h2>${translate("settings.addcustomnode")}</h2>
<hr>
<h2>${translate('settings.addcustomnode')}</h2>
<hr />
</div>
<br>
<mwc-textfield id="nameInput" style="width:100%;" label="${translate("login.name")}"></mwc-textfield>
<br>
<mwc-select id="protocolList" style="width:100%;" label="${translate("settings.protocol")}">
<br />
<mwc-textfield
id="nameInput"
style="width:100%;"
label="${translate('login.name')}"
></mwc-textfield>
<br />
<mwc-select
id="protocolList"
style="width:100%;"
label="${translate('settings.protocol')}"
>
<mwc-list-item value="http">http</mwc-list-item>
<mwc-list-item value="https">https</mwc-list-item>
</mwc-select>
<br>
<mwc-textfield id="domainInput" style="width:100%;" label="${translate("settings.domain")}"></mwc-textfield>
<mwc-textfield id="portInput" style="width:100%;" label="${translate("settings.port")}"></mwc-textfield>
<br />
<mwc-textfield
id="domainInput"
style="width:100%;"
label="${translate('settings.domain')}"
></mwc-textfield>
<mwc-textfield
id="portInput"
style="width:100%;"
label="${translate('settings.port')}"
></mwc-textfield>
<mwc-button
slot="secondaryAction"
dialogAction="close"
class="red"
>
${translate("general.close")}
${translate('general.close')}
</mwc-button>
<mwc-button
slot="primaryAction"
@click="${this.addNode}"
>
${translate("settings.addandsave")}
<mwc-button slot="primaryAction" @click="${this.addNode}">
${translate('settings.addandsave')}
</mwc-button>
</mwc-dialog>
<mwc-dialog id="importQortalNodesListDialog">
<div style="text-align:center">
<h2>${translate("settings.import")}</h2>
<hr>
<br>
<h2>${translate('settings.import')}</h2>
<hr />
<br />
</div>
<div style="min-height: 150px; min-width: 500px; box-sizing: border-box; position: relative;">
<frag-file-input accept=".nodes" @file-read-success="${(e) => this.importQortalNodesList(e.detail.result)}"></frag-file-input>
<h4 style="color: #F44336; text-align: center;">${translate("walletpage.wchange56")}</h4>
<h5 style="text-align: center;">${translate("settings.warning")}</h5>
<div
style="min-height: 150px; min-width: 500px; box-sizing: border-box; position: relative;"
>
<frag-file-input
accept=".nodes"handleBlur(event) {
if (!this.shadowRoot.querySelector("#customSelect").contains(event.relatedTarget)) {
this.dropdownOpen = false;
}
}
@file-read-success="${(e) => this.importQortalNodesList(e.detail.result)}"
></frag-file-input>
<h4 style="color: #F44336; text-align: center;">
${translate('walletpage.wchange56')}
</h4>
<h5 style="text-align: center;">
${translate('settings.warning')}
</h5>
</div>
<mwc-button
slot="primaryAction"
dialogAction="cancel"
class="red"
>
${translate("general.close")}
${translate('general.close')}
</mwc-button>
</mwc-dialog>
`
`;
}
firstUpdated() {
const checkNode = localStorage.getItem('mySelectedNode')
const checkNode = localStorage.getItem('mySelectedNode');
if (checkNode === null || checkNode.length === 0) {
localStorage.setItem('mySelectedNode', 0)
localStorage.setItem('mySelectedNode', 0);
} else {
}
}
toggleDropdown() {
this.dropdownOpen = !this.dropdownOpen;
}
handleBlur(event) {
if (
!this.shadowRoot
.querySelector('#customSelect')
.contains(event.relatedTarget)
) {
this.dropdownOpen = false;
}
}
focusOnCustomSelect() {
const customSelect = this.shadowRoot.querySelector('#customSelect');
if (customSelect) {
customSelect.focus();
}
}
handleSelection(event, node, index) {
event.stopPropagation();
this.selectedItem = node;
this.dropdownOpen = false;
this.requestUpdate();
this.nodeSelected(index);
}
show() {
this.shadowRoot.getElementById('settingsDialog').show()
this.shadowRoot.getElementById('settingsDialog').show();
}
close() {
this.shadowRoot.getElementById('settingsDialog').close()
this.shadowRoot.getElementById('settingsDialog').close();
}
removeList() {
localStorage.removeItem("myQortalNodes")
localStorage.removeItem('myQortalNodes');
const obj1 = {
name: 'Local Node',
protocol: 'http',
domain: '127.0.0.1',
port: 12391,
enableManagement: true
}
enableManagement: true,
};
const obj2 = {
name: 'Local Testnet',
protocol: 'http',
domain: '127.0.0.1',
port: 62391,
enableManagement: true
}
enableManagement: true,
};
var renewNodes = [];
renewNodes.push(obj1,obj2)
localStorage.setItem('myQortalNodes', JSON.stringify(renewNodes))
renewNodes.push(obj1, obj2);
localStorage.setItem('myQortalNodes', JSON.stringify(renewNodes));
let snack1string = get("settings.snack1")
let snack1string = get('settings.snack1');
snackbar.add({
labelText: `${snack1string}`,
dismiss: true
})
dismiss: true,
});
localStorage.removeItem('mySelectedNode')
localStorage.setItem('mySelectedNode', 0)
localStorage.removeItem('mySelectedNode');
localStorage.setItem('mySelectedNode', 0);
store.dispatch(doLoadNodeConfig())
store.dispatch(doLoadNodeConfig());
}
nodeSelected(e) {
const selectedNodeIndex = this.shadowRoot.getElementById('nodeSelect').value
const selectedNode = this.nodeConfig.knownNodes[selectedNodeIndex]
const selectedNodeUrl = `${selectedNode.protocol + '://' + selectedNode.domain + ':' + selectedNode.port}`
nodeSelected(selectedNodeIndex) {
const selectedNode = this.nodeConfig.knownNodes[selectedNodeIndex];
const selectedNodeUrl = `${
selectedNode.protocol +
'://' +
selectedNode.domain +
':' +
selectedNode.port
}`;
const index = parseInt(selectedNodeIndex)
if (isNaN(index)) return
const index = parseInt(selectedNodeIndex);
if (isNaN(index)) return;
store.dispatch(doSetNode(selectedNodeIndex))
store.dispatch(doSetNode(selectedNodeIndex));
localStorage.removeItem('mySelectedNode')
localStorage.setItem('mySelectedNode', selectedNodeIndex)
localStorage.removeItem('mySelectedNode');
localStorage.setItem('mySelectedNode', selectedNodeIndex);
let snack2string = get("settings.snack2")
let snack2string = get('settings.snack2');
snackbar.add({
labelText: `${snack2string} : ${selectedNodeUrl}`,
dismiss: true
})
dismiss: true,
});
this.shadowRoot.querySelector('#settingsDialog').close()
// this.shadowRoot.querySelector('#settingsDialog').close();
}
addNode() {
const nameInput = this.shadowRoot.getElementById('nameInput').value
const protocolList = this.shadowRoot.getElementById('protocolList').value
const domainInput = this.shadowRoot.getElementById('domainInput').value
const portInput = this.shadowRoot.getElementById('portInput').value
addNode(e) {
e.stopPropagation();
if (this.isBeingEdited) {
this.editNode(this.isBeingEditedIndex);
return;
}
const nameInput = this.shadowRoot.getElementById('nameInput').value;
const protocolList =
this.shadowRoot.getElementById('protocolList').value;
const domainInput = this.shadowRoot.getElementById('domainInput').value;
const portInput = this.shadowRoot.getElementById('portInput').value;
if (protocolList.length >= 4 && domainInput.length >= 3 && portInput.length >= 2) {
if (
protocolList.length >= 4 &&
domainInput.length >= 3 &&
portInput.length >= 2
) {
const nodeObject = {
name: nameInput,
protocol: protocolList,
domain: domainInput,
port: portInput,
enableManagement: true
}
enableManagement: true,
};
store.dispatch(doAddNode(nodeObject))
store.dispatch(doAddNode(nodeObject));
const haveNodes = JSON.parse(localStorage.getItem('myQortalNodes'))
const haveNodes = JSON.parse(localStorage.getItem('myQortalNodes'));
if (haveNodes === null || haveNodes.length === 0) {
var savedNodes = [];
savedNodes.push(nodeObject);
localStorage.setItem('myQortalNodes', JSON.stringify(savedNodes))
localStorage.setItem(
'myQortalNodes',
JSON.stringify(savedNodes)
);
let snack3string = get("settings.snack3")
let snack3string = get('settings.snack3');
snackbar.add({
labelText: `${snack3string}`,
dismiss: true
})
dismiss: true,
});
this.shadowRoot.getElementById('nameInput').value = ''
this.shadowRoot.getElementById('protocolList').value = ''
this.shadowRoot.getElementById('domainInput').value = ''
this.shadowRoot.getElementById('portInput').value = ''
this.shadowRoot.querySelector('#addNodeDialog').close()
this.shadowRoot.getElementById('nameInput').value = '';
this.shadowRoot.getElementById('protocolList').value = '';
this.shadowRoot.getElementById('domainInput').value = '';
this.shadowRoot.getElementById('portInput').value = '';
this.shadowRoot.querySelector('#addNodeDialog').close();
} else {
var stored = JSON.parse(localStorage.getItem('myQortalNodes'));
stored.push(nodeObject);
localStorage.setItem('myQortalNodes', JSON.stringify(stored));
let snack3string = get("settings.snack3")
let snack3string = get('settings.snack3');
snackbar.add({
labelText: `${snack3string}`,
dismiss: true
})
dismiss: true,
});
this.shadowRoot.getElementById('nameInput').value = ''
this.shadowRoot.getElementById('protocolList').value = ''
this.shadowRoot.getElementById('domainInput').value = ''
this.shadowRoot.getElementById('portInput').value = ''
this.shadowRoot.getElementById('nameInput').value = '';
this.shadowRoot.getElementById('protocolList').value = '';
this.shadowRoot.getElementById('domainInput').value = '';
this.shadowRoot.getElementById('portInput').value = '';
this.shadowRoot.querySelector('#addNodeDialog').close()
this.shadowRoot.querySelector('#addNodeDialog').close();
}
}
}
removeNode(event, index) {
event.stopPropagation();
let stored = JSON.parse(localStorage.getItem('myQortalNodes'));
stored.splice(index, 1);
localStorage.setItem('myQortalNodes', JSON.stringify(stored));
store.dispatch(doRemoveNode(index));
let snack3string = get('settings.snack6');
snackbar.add({
labelText: `${snack3string}`,
dismiss: true,
});
this.shadowRoot.querySelector('#addNodeDialog').close();
}
editNode(index) {
const nameInput = this.shadowRoot.getElementById('nameInput').value;
const protocolList =
this.shadowRoot.getElementById('protocolList').value;
const domainInput = this.shadowRoot.getElementById('domainInput').value;
const portInput = this.shadowRoot.getElementById('portInput').value;
if (
protocolList.length >= 4 &&
domainInput.length >= 3 &&
portInput.length >= 2
) {
const nodeObject = {
name: nameInput,
protocol: protocolList,
domain: domainInput,
port: portInput,
enableManagement: true,
};
let stored = JSON.parse(localStorage.getItem('myQortalNodes'));
const copyStored = [...stored];
copyStored[index] = nodeObject;
localStorage.setItem('myQortalNodes', JSON.stringify(copyStored));
store.dispatch(doEditNode(index, nodeObject));
let snack3string = get('settings.snack7');
snackbar.add({
labelText: `${snack3string}`,
dismiss: true,
});
this.shadowRoot.getElementById('nameInput').value = '';
this.shadowRoot.getElementById('protocolList').value = '';
this.shadowRoot.getElementById('domainInput').value = '';
this.shadowRoot.getElementById('portInput').value = '';
this.isBeingEdited = false;
this.isBeingEditedIndex = null;
this.shadowRoot.querySelector('#addNodeDialog').close();
}
}
openImportNodesDialog() {
this.shadowRoot.querySelector("#importQortalNodesListDialog").show()
this.shadowRoot.querySelector('#importQortalNodesListDialog').show();
}
closeImportNodesDialog() {
this.shadowRoot.querySelector("#importQortalNodesListDialog").close()
this.shadowRoot.querySelector('#importQortalNodesListDialog').close();
}
renderExportNodesListButton() {
return html`
<mwc-button dense unelevated label="${translate("settings.export")}" @click="${() => this.exportQortalNodesList()}"></mwc-button>
`
<mwc-button
dense
unelevated
label="${translate('settings.export')}"
@click="${() => this.exportQortalNodesList()}"
></mwc-button>
`;
}
exportQortalNodesList() {
let nodelist = ""
const qortalNodesList = JSON.stringify(localStorage.getItem("myQortalNodes"))
const qortalNodesListSave = JSON.parse((qortalNodesList) || "[]")
const blob = new Blob([qortalNodesListSave], { type: 'text/plain;charset=utf-8' })
nodelist = "qortal.nodes"
this.saveFileToDisk(blob, nodelist)
let nodelist = '';
const qortalNodesList = JSON.stringify(
localStorage.getItem('myQortalNodes')
);
const qortalNodesListSave = JSON.parse(qortalNodesList || '[]');
const blob = new Blob([qortalNodesListSave], {
type: 'text/plain;charset=utf-8',
});
nodelist = 'qortal.nodes';
this.saveFileToDisk(blob, nodelist);
}
async saveFileToDisk(blob, fileName) {
try {
const fileHandle = await self.showSaveFilePicker({
suggestedName: fileName,
types: [{
description: "File",
}]
})
types: [
{
description: 'File',
},
],
});
const writeFile = async (fileHandle, contents) => {
const writable = await fileHandle.createWritable()
await writable.write(contents)
await writable.close()
}
writeFile(fileHandle, blob).then(() => console.log("FILE SAVED"))
let snack4string = get("settings.snack4")
const writable = await fileHandle.createWritable();
await writable.write(contents);
await writable.close();
};
writeFile(fileHandle, blob).then(() => console.log('FILE SAVED'));
let snack4string = get('settings.snack4');
snackbar.add({
labelText: `${snack4string} qortal.nodes`,
dismiss: true
})
dismiss: true,
});
} catch (error) {
if (error.name === 'AbortError') {
return
return;
}
FileSaver.saveAs(blob, fileName)
FileSaver.saveAs(blob, fileName);
}
}
renderImportNodesListButton() {
return html`
<mwc-button dense unelevated label="${translate("settings.import")}" @click="${() => this.openImportNodesDialog()}"></mwc-button>
`
<mwc-button
dense
unelevated
label="${translate('settings.import')}"
@click="${() => this.openImportNodesDialog()}"
></mwc-button>
`;
}
async importQortalNodesList(file) {
localStorage.removeItem("myQortalNodes")
const newItems = JSON.parse((file) || "[]")
localStorage.setItem("myQortalNodes", JSON.stringify(newItems))
this.shadowRoot.querySelector('#importQortalNodesListDialog').close()
localStorage.removeItem('myQortalNodes');
const newItems = JSON.parse(file || '[]');
localStorage.setItem('myQortalNodes', JSON.stringify(newItems));
this.shadowRoot.querySelector('#importQortalNodesListDialog').close();
let snack5string = get("settings.snack5")
let snack5string = get('settings.snack5');
snackbar.add({
labelText: `${snack5string}`,
dismiss: true
})
dismiss: true,
});
localStorage.removeItem('mySelectedNode')
localStorage.setItem('mySelectedNode', 0)
localStorage.removeItem('mySelectedNode');
localStorage.setItem('mySelectedNode', 0);
store.dispatch(doLoadNodeConfig())
store.dispatch(doLoadNodeConfig());
}
stateChanged(state) {
this.config = state.config
this.nodeConfig = state.app.nodeConfig
this.config = state.config;
this.nodeConfig = state.app.nodeConfig;
}
}
window.customElements.define('settings-page', SettingsPage)
window.customElements.define('settings-page', SettingsPage);
const settings = document.createElement('settings-page')
settingsDialog = document.body.appendChild(settings)
const settings = document.createElement('settings-page');
settingsDialog = document.body.appendChild(settings);
export default settingsDialog
export default settingsDialog;

View File

@ -8,6 +8,7 @@ const APP_INFO_STATE = 'app_info_state'
const CHAT_HEADS_STREAM_NAME = 'chat_heads'
const NODE_CONFIG_STREAM_NAME = 'node_config'
const CHAT_LAST_SEEN = 'chat_last_seen'
const SIDE_EFFECT_ACTION = 'side_effect_action'
export const loggedInStream = new EpmlStream(LOGIN_STREAM_NAME, () => store.getState().app.loggedIn)
export const configStream = new EpmlStream(CONFIG_STREAM_NAME, () => store.getState().config)
@ -16,6 +17,8 @@ export const appInfoStateStream = new EpmlStream(APP_INFO_STATE, () => store.get
export const chatHeadsStateStream = new EpmlStream(CHAT_HEADS_STREAM_NAME, () => store.getState().app.chatHeads)
export const nodeConfigStream = new EpmlStream(NODE_CONFIG_STREAM_NAME, () => store.getState().app.nodeConfig)
export const chatLastSeenStream = new EpmlStream(CHAT_LAST_SEEN, () => store.getState().app.chatLastSeen)
export const sideEffectActionStream = new EpmlStream(SIDE_EFFECT_ACTION, () => store.getState().app.sideEffectAction)
let oldState = {
@ -56,6 +59,10 @@ store.subscribe(() => {
if (oldState.app.appInfo !== state.app.appInfo) {
appInfoStateStream.emit(state.app.appInfo)
}
if (oldState.app.sideEffectAction !== state.app.sideEffectAction) {
sideEffectActionStream.emit(state.app.sideEffectAction)
}
oldState = state
})

View File

@ -1,5 +1,5 @@
// Core App Actions here...
import { UPDATE_BLOCK_INFO, UPDATE_NODE_STATUS, UPDATE_NODE_INFO, CHAT_HEADS, ACCOUNT_INFO, ADD_AUTO_LOAD_IMAGES_CHAT, REMOVE_AUTO_LOAD_IMAGES_CHAT, ALLOW_QAPP_AUTO_AUTH, REMOVE_QAPP_AUTO_AUTH, SET_CHAT_LAST_SEEN, ADD_CHAT_LAST_SEEN, ALLOW_QAPP_AUTO_LISTS, REMOVE_QAPP_AUTO_LISTS, SET_NEW_TAB, ADD_TAB_INFO, SET_TAB_NOTIFICATIONS, IS_OPEN_DEV_DIALOG } from '../app-action-types.js'
import { UPDATE_BLOCK_INFO, UPDATE_NODE_STATUS, UPDATE_NODE_INFO, CHAT_HEADS, ACCOUNT_INFO, ADD_AUTO_LOAD_IMAGES_CHAT, REMOVE_AUTO_LOAD_IMAGES_CHAT, ALLOW_QAPP_AUTO_AUTH, REMOVE_QAPP_AUTO_AUTH, SET_CHAT_LAST_SEEN, ADD_CHAT_LAST_SEEN, ALLOW_QAPP_AUTO_LISTS, REMOVE_QAPP_AUTO_LISTS, SET_NEW_TAB, ADD_TAB_INFO, SET_TAB_NOTIFICATIONS, IS_OPEN_DEV_DIALOG, SET_NEW_NOTIFICATION, SET_SIDE_EFFECT } from '../app-action-types.js'
export const doUpdateBlockInfo = (blockObj) => {
return (dispatch, getState) => {
@ -126,6 +126,12 @@ export const setNewTab = (payload) => {
payload
}
}
export const setNewNotification = (payload) => {
return {
type: SET_NEW_NOTIFICATION,
payload
}
}
export const setIsOpenDevDialog = (payload)=> {
return {
type: IS_OPEN_DEV_DIALOG,
@ -145,3 +151,10 @@ export const setTabNotifications = (payload) => {
payload
}
}
export const setSideEffectAction = (payload)=> {
return {
type: SET_SIDE_EFFECT,
payload
}
}

View File

@ -1,5 +1,5 @@
// Node Config Actions here...
import { LOAD_NODE_CONFIG, SET_NODE, ADD_NODE } from '../app-action-types.js'
import { LOAD_NODE_CONFIG, SET_NODE, ADD_NODE, REMOVE_NODE, EDIT_NODE } from '../app-action-types.js'
import { UI_VERSION } from '../version.js'
const nodeConfigUrl = '/getConfig'
@ -72,6 +72,16 @@ export const doAddNode = (nodeObject) => {
return dispatch(addNode(nodeObject))
}
}
export const doRemoveNode = (index) => {
return (dispatch, getState) => {
return dispatch(removeNode(index))
}
}
export const doEditNode = (index, nodeObject) => {
return (dispatch, getState) => {
return dispatch(editNode({index, nodeObject}))
}
}
const addNode = (payload) => {
return {
@ -80,6 +90,18 @@ const addNode = (payload) => {
}
}
const editNode = (payload) => {
return {
type: EDIT_NODE,
payload
}
}
const removeNode = (payload) => {
return {
type: REMOVE_NODE,
payload
}
}
const obj1 = {
name: 'Local Node',
protocol: 'http',

View File

@ -12,6 +12,8 @@ export const UPDATE_NODE_STATUS = 'UPDATE_NODE_STATUS'
export const UPDATE_NODE_INFO = 'UPDATE_NODE_INFO'
export const SET_NODE = 'SET_NODE'
export const ADD_NODE = 'ADD_NODE'
export const EDIT_NODE = 'EDIT_NODE'
export const REMOVE_NODE = 'REMOVE_NODE'
export const LOAD_NODE_CONFIG = 'LOAD_NODE_CONFIG'
export const PAGE_URL = 'PAGE_URL'
export const CHAT_HEADS = 'CHAT_HEADS'
@ -29,3 +31,5 @@ export const SET_NEW_TAB = 'SET_NEW_TAB'
export const ADD_TAB_INFO = 'ADD_TAB_INFO'
export const SET_TAB_NOTIFICATIONS = 'SET_TAB_NOTIFICATIONS'
export const IS_OPEN_DEV_DIALOG = 'IS_OPEN_DEV_DIALOG'
export const SET_NEW_NOTIFICATION = 'SET_NEW_NOTIFICATION'
export const SET_SIDE_EFFECT= 'SET_SIDE_EFFECT'

View File

@ -1,9 +1,9 @@
// Loading state, login state, isNavDrawOpen state etc. None of this needs to be saved to localstorage.
import { loadStateFromLocalStorage, saveStateToLocalStorage } from '../../localStorageHelpers.js'
import { LOG_IN, LOG_OUT, NETWORK_CONNECTION_STATUS, INIT_WORKERS, ADD_PLUGIN_URL, ADD_PLUGIN, ADD_NEW_PLUGIN_URL, NAVIGATE, SELECT_ADDRESS, ACCOUNT_INFO, CHAT_HEADS, UPDATE_BLOCK_INFO, UPDATE_NODE_STATUS, UPDATE_NODE_INFO, LOAD_NODE_CONFIG, SET_NODE, ADD_NODE, PAGE_URL, ADD_AUTO_LOAD_IMAGES_CHAT, REMOVE_AUTO_LOAD_IMAGES_CHAT, ALLOW_QAPP_AUTO_AUTH, REMOVE_QAPP_AUTO_AUTH, SET_CHAT_LAST_SEEN, ADD_CHAT_LAST_SEEN, ALLOW_QAPP_AUTO_LISTS, REMOVE_QAPP_AUTO_LISTS, SET_NEW_TAB, ADD_TAB_INFO, SET_TAB_NOTIFICATIONS, IS_OPEN_DEV_DIALOG } from './app-action-types.js'
import { LOG_IN, LOG_OUT, NETWORK_CONNECTION_STATUS, INIT_WORKERS, ADD_PLUGIN_URL, ADD_PLUGIN, ADD_NEW_PLUGIN_URL, NAVIGATE, SELECT_ADDRESS, ACCOUNT_INFO, CHAT_HEADS, UPDATE_BLOCK_INFO, UPDATE_NODE_STATUS, UPDATE_NODE_INFO, LOAD_NODE_CONFIG, SET_NODE, ADD_NODE, PAGE_URL, ADD_AUTO_LOAD_IMAGES_CHAT, REMOVE_AUTO_LOAD_IMAGES_CHAT, ALLOW_QAPP_AUTO_AUTH, REMOVE_QAPP_AUTO_AUTH, SET_CHAT_LAST_SEEN, ADD_CHAT_LAST_SEEN, ALLOW_QAPP_AUTO_LISTS, REMOVE_QAPP_AUTO_LISTS, SET_NEW_TAB, ADD_TAB_INFO, SET_TAB_NOTIFICATIONS, IS_OPEN_DEV_DIALOG, REMOVE_NODE, EDIT_NODE, SET_NEW_NOTIFICATION, SET_SIDE_EFFECT } from './app-action-types.js'
import { initWorkersReducer } from './reducers/init-workers.js'
import { loginReducer } from './reducers/login-reducer.js'
import { setNode, addNode } from './reducers/manage-node.js'
import { setNode, addNode, removeNode, editNode } from './reducers/manage-node.js'
import localForage from "localforage";
const chatLastSeen = localForage.createInstance({
name: "chat-last-seen",
@ -50,7 +50,9 @@ const INITIAL_STATE = {
chatLastSeen: [],
newTab: null,
tabInfo: {},
isOpenDevDialog: false
isOpenDevDialog: false,
newNotification: null,
sideEffectAction: null
}
export default (state = INITIAL_STATE, action) => {
@ -120,6 +122,10 @@ export default (state = INITIAL_STATE, action) => {
return setNode(state, action)
case ADD_NODE:
return addNode(state, action)
case EDIT_NODE:
return editNode(state, action)
case REMOVE_NODE:
return removeNode(state, action)
case PAGE_URL:
return {
...state,
@ -273,6 +279,19 @@ export default (state = INITIAL_STATE, action) => {
}
}
case SET_NEW_NOTIFICATION: {
return {
...state,
newNotification: action.payload
}
}
case SET_SIDE_EFFECT: {
return {
...state,
sideEffectAction: action.payload
}
}
default:
return state
}

View File

@ -20,3 +20,26 @@ export const addNode = (state, action) => {
}
}
}
export const editNode = (state, action) => {
const copyKnownNodes = [...state.nodeConfig.knownNodes]
copyKnownNodes[action.payload.index] = action.payload.nodeObject
return {
...state,
nodeConfig: {
...state.nodeConfig,
knownNodes: copyKnownNodes
}
}
}
export const removeNode = (state, action) => {
const copyKnownNodes = [...state.nodeConfig.knownNodes]
copyKnownNodes.splice(action.payload, 1);
return {
...state,
nodeConfig: {
...state.nodeConfig,
knownNodes: copyKnownNodes
}
}
}

954
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,18 +27,31 @@
"build-electron": "electron-builder build --publish never",
"deploy-electron": "electron-builder build --win --publish never",
"release": "NODE_ENV=production electron-builder build --publish never",
"publish": "electron-builder -p always"
"publish": "electron-builder -p always",
"lint": "eslint './**/*.{js,mjs}'"
},
"dependencies": {
"@hapi/hapi": "21.3.2",
"@hapi/inert": "7.1.0",
"@lit-labs/motion": "1.0.4",
"@popperjs/core": "^2.11.8",
"@tiptap/core": "2.0.4",
"@tiptap/extension-highlight": "2.0.4",
"@tiptap/extension-image": "2.0.4",
"@tiptap/extension-placeholder": "2.0.4",
"@tiptap/extension-underline": "2.0.4",
"@tiptap/html": "2.0.4",
"@tiptap/pm": "2.0.4",
"@tiptap/starter-kit": "2.0.4",
"asmcrypto.js": "2.3.2",
"bcryptjs": "2.4.3",
"buffer": "6.0.3",
"compressorjs": "1.2.1",
"crypto-js": "4.1.1",
"electron-log": "4.4.8",
"electron-updater": "6.1.4",
"electron-dl": "3.5.0",
"electron-log": "4.4.8",
"electron-store": "8.1.0",
"electron-updater": "6.1.4",
"emoji-picker-js": "https://github.com/Qortal/emoji-picker-js",
"extract-zip": "2.0.1",
"jssha": "3.3.1",
@ -57,40 +70,9 @@
"prosemirror-transform": "1.7.5",
"prosemirror-view": "1.31.7",
"sass": "1.66.1",
"short-unique-id": "5.0.2",
"@hapi/hapi": "21.3.2",
"@hapi/inert": "7.1.0",
"@lit-labs/motion": "1.0.4",
"@tiptap/pm": "2.0.4",
"@tiptap/core": "2.0.4",
"@tiptap/extension-highlight": "2.0.4",
"@tiptap/extension-image": "2.0.4",
"@tiptap/extension-placeholder": "2.0.4",
"@tiptap/extension-underline": "2.0.4",
"@tiptap/html": "2.0.4",
"@tiptap/starter-kit": "2.0.4"
"short-unique-id": "5.0.2"
},
"devDependencies": {
"axios": "1.5.0",
"electron": "26.2.0",
"electron-builder": "24.6.4",
"electron-packager": "17.1.2",
"epml": "0.3.3",
"file-saver": "2.0.5",
"highcharts": "11.1.0",
"html-escaper": "3.0.3",
"is-electron": "2.2.2",
"lit": "2.8.0",
"lit-translate": "2.0.1",
"pwa-helpers": "0.9.1",
"passive-events-support": "1.1.0",
"redux": "4.2.1",
"redux-thunk": "2.4.2",
"rollup": "3.29.0",
"rollup-plugin-node-globals": "1.4.0",
"rollup-plugin-progress": "1.1.2",
"rollup-plugin-scss": "3.0.0",
"shelljs": "0.8.5",
"@babel/core": "7.22.17",
"@material/mwc-button": "0.27.0",
"@material/mwc-checkbox": "0.27.0",
@ -140,7 +122,30 @@
"@vaadin/icons": "24.1.7",
"@vaadin/password-field": "24.1.7",
"@vaadin/tooltip": "24.1.7",
"@zip.js/zip.js": "2.7.29"
"@zip.js/zip.js": "2.7.29",
"axios": "1.5.0",
"electron": "26.2.0",
"electron-builder": "24.6.4",
"electron-packager": "17.1.2",
"epml": "0.3.3",
"eslint": "^8.50.0",
"eslint-plugin-lit": "^1.9.1",
"eslint-plugin-wc": "^2.0.3",
"file-saver": "2.0.5",
"highcharts": "11.1.0",
"html-escaper": "3.0.3",
"is-electron": "2.2.2",
"lit": "2.8.0",
"lit-translate": "2.0.1",
"passive-events-support": "1.1.0",
"pwa-helpers": "0.9.1",
"redux": "4.2.1",
"redux-thunk": "2.4.2",
"rollup": "3.29.0",
"rollup-plugin-node-globals": "1.4.0",
"rollup-plugin-progress": "1.1.2",
"rollup-plugin-scss": "3.0.0",
"shelljs": "0.8.5"
},
"engines": {
"node": ">=18.16.1"

View File

@ -0,0 +1,348 @@
import { LitElement, html, css } from 'lit';
import { Epml } from '../../../epml';
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';
import './ChatImage';
import './ReusableImage';
import {
get
} from 'lit-translate';
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent });
class ChatGroupsManager 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 },
chatId: { type: String },
_chatId: { type: String },
isReceipient: { type: Boolean },
groups: { type: Array },
viewImage: { type: Boolean },
autoView: {type: Boolean},
onlyMyImages: {type: Boolean},
repost: {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.getGroups = this.getGroups.bind(this);
this.viewElement = '';
this.downObserverElement = '';
this.sendMoneyLoading = false;
this.btnDisable = false;
this.errorMessage = '';
this.successMessage = '';
this.groups = [];
this.viewImage = false;
this.myName =
window.parent.reduxStore.getState().app.accountInfo.names[0].name;
this.myAddress =
window.parent.reduxStore.getState().app.selectedAddress.address;
this.autoView =false
this.onlyMyImages = true
}
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;
align-items: center;
}
.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);
}
.message-myBg {
background-color: var(--chat-bubble-myBg) !important;
margin-bottom: 15px;
border-radius: 5px;
padding: 5px;
}
.message-data-name {
user-select: none;
color: #03a9f4;
margin-bottom: 5px;
}
.message-user-info {
display: flex;
justify-content: space-between;
width: 100%;
gap: 10px;
}
.hideImg {
visibility: hidden;
}
.checkbox-row {
position: relative;
display: flex;
align-items: center;
align-content: center;
font-family: Montserrat, sans-serif;
font-weight: 600;
color: var(--black);
padding-left: 5px;
}
`;
}
async getGroups() {
try {
let endpoint = `/groups`
const groups = await parentEpml.request('apiCall', {
type: 'api',
url: endpoint,
});
let list = groups
this.groups = list
} catch (error) {
console.log(error);
}
}
firstUpdated() {
// this.viewElement = this.shadowRoot.getElementById('viewElement');
// this.downObserverElement =
// this.shadowRoot.getElementById('downObserver');
// this.elementObserver();
this.getGroups()
}
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.images.length < 20) {
return;
}
this.getMoreImages();
}
}
selectAuto(e) {
if (e.target.checked) {
this.autoView = false
} else {
this.autoView = true
}
}
selectMyImages(e) {
if (e.target.checked) {
this.onlyMyImages = false
} else {
this.onlyMyImages = true
}
}
render() {
console.log('this.groups', this.groups)
return html`
<div class="container">
<div class="close-row" style="margin-top: 15px">
<mwc-icon @click=${()=> {
this.getGroups()
}} style="color: var(--black); cursor:pointer;">refresh</mwc-icon>
</div>
<div class="checkbox-row">
<label for="authButton" id="authButtonLabel" style="color: var(--black);">
${get('chatpage.cchange69')}
</label>
<mwc-checkbox style="margin-right: -15px;" id="authButton" @click=${(e) => this.selectAuto(e)} ?checked=${this.autoView}></mwc-checkbox>
</div>
<div class="checkbox-row">
<label for="authButton" id="authButtonLabel" style="color: var(--black);">
${get('chatpage.cchange95')}
</label>
<mwc-checkbox style="margin-right: -15px;" id="authButton" @click=${(e) => this.selectMyImages(e)} ?checked=${this.onlyMyImages}></mwc-checkbox>
</div>
<div id="viewElement" class="container-body">
<div id='downObserver'></div>
</div>
</div>
</div>
`;
}
}
customElements.define('chat-groups-manager', ChatGroupsManager);

View File

@ -0,0 +1,98 @@
import { LitElement, html, css } from 'lit';
import {
translate,
} from 'lit-translate';
import '@material/mwc-menu';
import '@material/mwc-list/mwc-list-item.js';
import '@material/mwc-dialog'
import './ChatGroupManager'
export class ChatGroupsModal extends LitElement {
static get properties() {
return {
openDialogGroupsModal: { type: Boolean },
setOpenDialogGroupsModal: { attribute: false}
};
}
static get styles() {
return css`
* {
--mdc-theme-text-primary-on-background: var(--black);
--mdc-dialog-max-width: 85vw;
--mdc-dialog-max-height: 95vh;
}
.imageContainer {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
@-webkit-keyframes loadingAnimation {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes loadingAnimation {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
`;
}
constructor() {
super();
this.openDialogGroupsModal = false
}
firstUpdated() {
}
render() {
console.log('hello')
return html`
<mwc-dialog
id="showDialogGroupsModal"
?open=${this.openDialogGroupsModal}
@closed=${() => {
this.setOpenDialogGroupsModal(false)
}}>
<div class="dialog-header"></div>
<div class="dialog-container ">
<chat-groups-manager></chat-groups-manager>
</div>
<mwc-button
slot="primaryAction"
dialogAction="cancel"
class="red"
@click=${() => {
this.setOpenDialogGroupsModal(false)
}}
>
${translate('general.close')}
</mwc-button>
</mwc-dialog>
`;
}
}
customElements.define('chat-groups-modal', ChatGroupsModal);

View File

@ -0,0 +1,353 @@
import { LitElement, html, css } from 'lit';
import {
get,
translate,
} from 'lit-translate';
import axios from 'axios'
import { RequestQueueWithPromise } from '../../utils/queue';
import '@material/mwc-menu';
import '@material/mwc-list/mwc-list-item.js'
import { Epml } from '../../../epml';
const requestQueue = new RequestQueueWithPromise(5);
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
export class ChatImage extends LitElement {
static get properties() {
return {
resource: { type: Object },
isReady: { type: Boolean},
status: {type: Object},
setOpenDialogImage: { attribute: false}
};
}
static get styles() {
return css`
* {
--mdc-theme-text-primary-on-background: var(--black);
}
img {
max-width:45vh;
max-height:40vh;
border-radius: 5px;
cursor: pointer;
position: relative;
}
.smallLoading,
.smallLoading:after {
border-radius: 50%;
width: 2px;
height: 2px;
}
.smallLoading {
border-width: 0.8em;
border-style: solid;
border-color: rgba(3, 169, 244, 0.2) rgba(3, 169, 244, 0.2)
rgba(3, 169, 244, 0.2) rgb(3, 169, 244);
font-size: 30px;
position: relative;
text-indent: -9999em;
transform: translateZ(0px);
animation: 1.1s linear 0s infinite normal none running loadingAnimation;
}
.defaultSize {
width: 45vh;
height: 40vh;
}
mwc-menu {
position: absolute;
}
@-webkit-keyframes loadingAnimation {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes loadingAnimation {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
`;
}
constructor() {
super();
this.resource = {
identifier: "",
name: "",
service: ""
}
this.status = {
status: ''
}
this.url = ""
this.isReady = false
this.nodeUrl = this.getNodeUrl()
this.myNode = this.getMyNode()
this.hasCalledWhenDownloaded = false
this.isFetching = false
this.observer = new IntersectionObserver(entries => {
for (const entry of entries) {
if (entry.isIntersecting && this.status.status !== 'READY') {
this._fetchImage();
// Stop observing after the image has started loading
this.observer.unobserve(this);
}
}
});
}
getNodeUrl(){
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]
const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port
return nodeUrl
}
getMyNode(){
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]
return myNode
}
getApiKey() {
const myNode =
window.parent.reduxStore.getState().app.nodeConfig.knownNodes[
window.parent.reduxStore.getState().app.nodeConfig.node
];
let apiKey = myNode.apiKey;
return apiKey;
}
async fetchResource() {
try {
if(this.isFetching) return
this.isFetching = true
await axios.get(`${this.nodeUrl}/arbitrary/resource/properties/${this.resource.service}/${this.resource.name}/${this.resource.identifier}?apiKey=${this.myNode.apiKey}`)
this.isFetching = false
} catch (error) {
this.isFetching = false
}
}
async fetchVideoUrl() {
this.fetchResource()
this.url = `${this.nodeUrl}/arbitrary/${this.resource.service}/${this.resource.name}/${this.resource.identifier}?async=true&apiKey=${this.myNode.apiKey}`
}
async fetchStatus(){
let isCalling = false
let percentLoaded = 0
let timer = 24
const response = await axios.get(`${this.nodeUrl}/arbitrary/resource/status/${this.resource.service}/${this.resource.name}/${this.resource.identifier}?apiKey=${this.myNode.apiKey}`)
if(response && response.data && response.data.status === 'READY'){
this.status = response.data
return
}
const intervalId = setInterval(async () => {
if (isCalling) return
isCalling = true
const data = await requestQueue.enqueue(() => {
return axios.get(`${this.nodeUrl}/arbitrary/resource/status/${this.resource.service}/${this.resource.name}/${this.resource.identifier}?apiKey=${this.myNode.apiKey}`)
});
const res = data.data
isCalling = false
if (res.localChunkCount) {
if (res.percentLoaded) {
if (
res.percentLoaded === percentLoaded &&
res.percentLoaded !== 100
) {
timer = timer - 5
} else {
timer = 24
}
if (timer < 0) {
timer = 24
isCalling = true
this.status = {
...res,
status: 'REFETCHING'
}
setTimeout(() => {
isCalling = false
this.fetchResource()
}, 25000)
return
}
percentLoaded = res.percentLoaded
}
this.status = res
if(this.status.status === 'DOWNLOADED'){
this.fetchResource()
}
}
// check if progress is 100% and clear interval if true
if (res.status === 'READY') {
clearInterval(intervalId)
this.status = res
this.isReady = true
}
}, 5000) // 1 second interval
}
async _fetchImage() {
try {
this.fetchVideoUrl({
name: this.resource.name,
service: this.resource.service,
identifier: this.resource.identifier
})
this.fetchStatus()
} catch (error) { /* empty */ }
}
firstUpdated(){
this.observer.observe(this);
}
shouldUpdate(changedProperties) {
if (changedProperties.has('setOpenDialogImage') && changedProperties.size === 1) {
return false;
}
return true
}
showContextMenu(e) {
e.preventDefault();
e.stopPropagation();
const contextMenu = this.shadowRoot.getElementById('contextMenu');
const containerRect = e.currentTarget.getBoundingClientRect();
// Adjusting the positions
const adjustedX = e.clientX - containerRect.left;
const adjustedY = e.clientY - containerRect.top;
contextMenu.style.top = `${adjustedY}px`;
contextMenu.style.left = `${adjustedX}px`;
contextMenu.open = true;
}
async handleCopy(e) {
e.stopPropagation();
const image = this.shadowRoot.querySelector('img');
// Create a canvas and draw the image on it.
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
// Convert canvas image to blob
canvas.toBlob(blob => {
try {
const clipboardItem = new ClipboardItem({ 'image/png': blob });
navigator.clipboard.write([clipboardItem]).then(() => {
const msg = get("chatpage.cchange93")
parentEpml.request('showSnackBar', msg)
console.log('Image copied to clipboard');
}).catch(err => {
console.error('Failed to copy image: ', err);
});
} catch (error) {
console.error('Error copying the image: ', error);
}
}, 'image/png');
}
handleMenuBlur() {
setTimeout(() => {
if (!this.isMenuItemClicked) {
const contextMenu = this.shadowRoot.getElementById('contextMenu');
contextMenu.open = false;
}
}, 100);
}
render() {
return html`
<div
class=${[
`image-container`,
this.status.status !== 'READY'
? 'defaultSize'
: '',
this.status.status !== 'READY'
? 'hideImg'
: '',
].join(' ')}
>
${
this.status.status !== 'READY'
? html`
<div
style="display:flex;flex-direction:column;width:100%;height:100%;justify-content:center;align-items:center;position:absolute;"
>
<div
class=${`smallLoading`}
></div>
<p style="color: var(--black)">${`${Math.round(this.status.percentLoaded || 0
).toFixed(0)}% `}${translate('chatpage.cchange94')}</p>
</div>
`
: ''
}
${this.status.status === 'READY' ? html`
<div class="customContextMenuDiv" @contextmenu="${this.showContextMenu}" style="position:relative">
<img crossOrigin="anonymous" @click=${()=> this.setOpenDialogImage(true)} src=${this.url} />
<mwc-menu id="contextMenu" @blur="${this.handleMenuBlur}">
<mwc-list-item @click="${this.handleCopy}">Copy</mwc-list-item>
</mwc-menu>
</div>
` : ''}
</div>
`
}
}
customElements.define('chat-image', ChatImage);

View File

@ -79,11 +79,7 @@ class ChatModals extends LitElement {
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()

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,670 @@
import { LitElement, html, css } from 'lit';
import { Epml } from '../../../epml';
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';
import './ChatImage';
import './ReusableImage';
import {
get,
translate,
} from 'lit-translate';
import { generateIdFromAddresses } from '../../utils/id-generation';
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent });
class ChatRightPanelResources 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 },
chatId: { type: String },
_chatId: { type: String },
isReceipient: { type: Boolean },
images: { type: Array },
viewImage: { type: Boolean },
autoView: {type: Boolean},
onlyMyImages: {type: Boolean},
repost: {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.getMoreImages = this.getMoreImages.bind(this);
this.viewElement = '';
this.downObserverElement = '';
this.sendMoneyLoading = false;
this.btnDisable = false;
this.errorMessage = '';
this.successMessage = '';
this.images = [];
this.viewImage = false;
this.myName =
window.parent.reduxStore.getState().app.accountInfo.names[0].name;
this.myAddress =
window.parent.reduxStore.getState().app.selectedAddress.address;
this.autoView =false
this.onlyMyImages = true
}
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;
align-items: center;
}
.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);
}
.message-myBg {
background-color: var(--chat-bubble-myBg) !important;
margin-bottom: 15px;
border-radius: 5px;
padding: 5px;
}
.message-data-name {
user-select: none;
color: #03a9f4;
margin-bottom: 5px;
}
.message-user-info {
display: flex;
justify-content: space-between;
width: 100%;
gap: 10px;
}
.hideImg {
visibility: hidden;
}
.checkbox-row {
position: relative;
display: flex;
align-items: center;
align-content: center;
font-family: Montserrat, sans-serif;
font-weight: 600;
color: var(--black);
padding-left: 5px;
}
`;
}
async getMoreImages(reset) {
try {
if(reset){
this.images = []
}
const groupPart = this.isReceipient
? `direct_${generateIdFromAddresses(this._chatId, this.myAddress)}`
: `group_${this._chatId}`;
let offset = reset ? 0 : this.images.length;
let endpoint = `/arbitrary/resources/search?service=QCHAT_IMAGE&identifier=qchat_${groupPart}&reverse=true&limit=20&reverse=true&offset=${offset}`
if(this.onlyMyImages){
endpoint = endpoint + `&name=${this.myName}`
}
const qchatImages = await parentEpml.request('apiCall', {
type: 'api',
url: endpoint,
});
let list = []
if(reset){
list = qchatImages
} else {
list = [...this.images, ...qchatImages]
}
this.images = list
} catch (error) {
console.log(error);
}
}
firstUpdated() {
this.viewElement = this.shadowRoot.getElementById('viewElement');
this.downObserverElement =
this.shadowRoot.getElementById('downObserver');
this.elementObserver();
}
async updated(changedProperties) {
if (changedProperties && changedProperties.has('_chatId')) {
this.images = [];
this.getMoreImages(true);
}
if (changedProperties && changedProperties.has('onlyMyImages')) {
this.getMoreImages(true)
}
}
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.images.length < 20) {
return;
}
this.getMoreImages();
}
}
selectAuto(e) {
if (e.target.checked) {
this.autoView = false
} else {
this.autoView = true
}
}
selectMyImages(e) {
if (e.target.checked) {
this.onlyMyImages = false
} else {
this.onlyMyImages = true
}
}
render() {
return html`
<div class="container">
<div class="close-row" style="margin-top: 15px">
<mwc-icon @click=${()=> {
this.getMoreImages(true)
}} style="color: var(--black); cursor:pointer;">refresh</mwc-icon>
<vaadin-icon class="top-bar-icon" @click=${() =>
this.toggle(
false
)} style="margin: 0px 10px" icon="vaadin:close" slot="icon"></vaadin-icon>
</div>
<div class="checkbox-row">
<label for="authButton" id="authButtonLabel" style="color: var(--black);">
${get('chatpage.cchange69')}
</label>
<mwc-checkbox style="margin-right: -15px;" id="authButton" @click=${(e) => this.selectAuto(e)} ?checked=${this.autoView}></mwc-checkbox>
</div>
<div class="checkbox-row">
<label for="authButton" id="authButtonLabel" style="color: var(--black);">
${get('chatpage.cchange95')}
</label>
<mwc-checkbox style="margin-right: -15px;" id="authButton" @click=${(e) => this.selectMyImages(e)} ?checked=${this.onlyMyImages}></mwc-checkbox>
</div>
<div id="viewElement" class="container-body">
${this.images.map((image) => {
return html`<image-parent .repost=${this.repost} .image=${image} ?autoView=${this.autoView}></image-parent>`;
})}
<div id='downObserver'></div>
</div>
</div>
</div>
`;
}
}
customElements.define('chat-right-panel-resources', ChatRightPanelResources);
class ImageParent 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 },
chatId: { type: String },
_chatId: { type: String },
isReceipient: { type: Boolean },
images: { type: Array },
viewImage: { type: Boolean },
image: { type: Object },
autoView: {type: Boolean},
repost: {attribute: false},
isImgLoaded: {type: Boolean}
};
}
constructor() {
super();
this.leaveGroupObj = {};
this.leaveFee = 0.001;
this.error = false;
this.chatHeads = [];
this.groupAdmin = [];
this.groupMembers = [];
this.viewElement = '';
this.sendMoneyLoading = false;
this.btnDisable = false;
this.errorMessage = '';
this.successMessage = '';
this.isImgLoaded = false
this.images = [];
this.viewImage = false;
this.myName =
window.parent.reduxStore.getState().app.accountInfo.names[0].name;
}
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;
gap: 20px;
align-items: center;
}
.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);
}
.message-myBg {
background-color: var(--chat-bubble-myBg) !important;
margin-bottom: 15px;
border-radius: 5px;
padding: 5px;
}
.message-data-name {
user-select: none;
color: #03a9f4;
margin-bottom: 5px;
}
.message-user-info {
display: flex;
justify-content: space-between;
width: 100%;
gap: 10px;
}
.hideImg {
visibility: hidden;
}
.image-container {
display: flex;
}
.repost-btn {
margin-top: 4px;
max-height: 28px;
padding: 5px 5px;
font-size: 14px;
background-color: #03a9f4;
color: white;
border: 1px solid transparent;
border-radius: 3px;
cursor: pointer;
}
`;
}
firstUpdated() {}
async updated(changedProperties) {
if (changedProperties && changedProperties.has('chatId')) {
// const autoSeeChatList =
// window.parent.reduxStore.getState().app.autoLoadImageChats;
// if (autoSeeChatList.includes(this.chatId)) {
// this.viewImage = true;
// }
}
}
onLoad(){
this.isImgLoaded = true
this.requestUpdate()
}
render() {
return html`
${!this.autoView && !this.viewImage && this.myName !== this.image.name
? html`
<div class="message-myBg">
<div class="message-user-info">
<span class="message-data-name">
${this.image.name}
</span>
</div>
<div
@click=${() => {
this.viewImage = true;
}}
class=${[`image-container`].join(' ')}
style="height: 200px"
>
<div
style="display:flex;width:100%;height:100%;justify-content:center;align-items:center;cursor:pointer;color:var(--black);"
>
${translate('chatpage.cchange40')}
</div>
</div>
</div>
`
: html``}
${this.autoView || this.viewImage || this.myName === this.image.name
? html`
<div class="message-myBg">
<div class="message-user-info">
<span class="message-data-name">
${this.image.name}
</span>
</div>
<reusable-image
.resource=${{
name: this.image.name,
service: this.image.service,
identifier: this.image.identifier,
}}
.onLoad=${()=> this.onLoad()}
></reusable-image>
${this.isImgLoaded ? html`
<div class="actions-parent">
<button class="repost-btn" @click=${()=> this.repost(this.image)}>repost</button>
</div>
` : ''}
</div>
`
: ''}
`;
}
}
customElements.define('image-parent', ImageParent);

View File

@ -43,6 +43,10 @@ export const chatStyles = css`
margin: 0;
padding: 20px 17px;
}
.message-sending {
opacity: 0.5;
cursor: progress;
}
.chat-list {
overflow-y: auto;
@ -256,7 +260,6 @@ export const chatStyles = css`
.message-parent {
padding: 3px;
background: rgba(245, 245, 245, 0);
transition: all 0.1s ease-in-out;
}
.message-parent:hover {
@ -364,7 +367,6 @@ export const chatStyles = css`
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;
@ -410,7 +412,6 @@ export const chatStyles = css`
width: 150px;
height: 32px;
padding: 3px 8px;
box-shadow: rgba(77, 77, 82, 0.2) 0px 7px 29px 0px;
}
.block-user:hover {
@ -753,6 +754,17 @@ export const chatStyles = css`
visibility: visible;
}
.unread-divider {
width: 100%;
padding: 5px;
color: var(--black);
border-bottom: 1px solid var(--black);
display: flex;
justify-content: center;
border-radius: 2px;
margin-top: 5px;
}
.blink-bg{
border-radius: 8px;
animation: blinkingBackground 3s;

File diff suppressed because it is too large Load Diff

View File

@ -36,7 +36,8 @@ class ChatTextEditor extends LitElement {
isEnabledChatEnter: {type: Boolean},
openGifModal: { type: Boolean },
setOpenGifModal: { attribute: false },
chatId: {type: String}
chatId: {type: String},
messageQueue: {type: Array}
}
}
@ -170,7 +171,7 @@ class ChatTextEditor extends LitElement {
padding: 0px 10px;
height: 100%;
display: flex;
align-items: center;
align-items: safe center;
}
.element::-webkit-scrollbar-track {
background-color: whitesmoke;
@ -376,6 +377,8 @@ mwc-checkbox::shadow .mdc-checkbox::after, mwc-checkbox::shadow .mdc-checkbox::b
this.userName = window.parent.reduxStore.getState().app.accountInfo.names[0]
this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light'
this.editor = null
this.messageQueue = []
}
render() {
@ -499,7 +502,11 @@ mwc-checkbox::shadow .mdc-checkbox::after, mwc-checkbox::shadow .mdc-checkbox::b
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>
<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>
@ -515,7 +522,7 @@ mwc-checkbox::shadow .mdc-checkbox::after, mwc-checkbox::shadow .mdc-checkbox::b
icon="vaadin:check"
slot="icon"
@click=${() => {
this.sendMessageFunc();
this.sendMessageFunc(this.messageQueue);
}}
>
</vaadin-icon>
@ -538,7 +545,7 @@ mwc-checkbox::shadow .mdc-checkbox::after, mwc-checkbox::shadow .mdc-checkbox::b
alt="send-icon"
class="send-icon"
@click=${() => {
this.sendMessageFunc();
this.sendMessageFunc(this.messageQueue);
}}
/>
` :
@ -569,6 +576,29 @@ mwc-checkbox::shadow .mdc-checkbox::after, mwc-checkbox::shadow .mdc-checkbox::b
}
}
async handlePasteEvent(e) {
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
});
this.insertFile(file);
}
}
}
async firstUpdated() {
window.addEventListener('storage', () => {
const checkTheme = localStorage.getItem('qortalTheme');
@ -648,7 +678,7 @@ mwc-checkbox::shadow .mdc-checkbox::after, mwc-checkbox::shadow .mdc-checkbox::b
return;
}
this.chatMessageSize = 0;
this._sendMessage(props, this.editor.getJSON());
this._sendMessage(props, this.editor.getJSON(), this.messageQueue);
}
getMessageSize(message){

View File

@ -304,11 +304,7 @@ class ChatWelcomePage extends LitElement {
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()

View File

@ -1,9 +1,14 @@
import { LitElement, html, css } from 'lit'
import { render } from 'lit/html.js'
import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate'
import { LitElement, html, css } from 'lit';
import { render } from 'lit/html.js';
import {
use,
get,
translate,
translateUnsafeHTML,
registerTranslateConfig,
} from 'lit-translate';
export class ImageComponent extends LitElement {
static get properties() {
return {
class: { type: String },
@ -13,8 +18,8 @@ attempts: { type: Number },
maxAttempts: { type: Number },
error: { type: Boolean },
sendMessage: { attribute: false },
setOpenGifModal: { attribute: false }
}
setOpenGifModal: { attribute: false },
};
}
static get styles() {
@ -38,66 +43,68 @@ 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;
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
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
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
if (this.attempts > this.maxAttempts) return;
await new Promise((res) => {
setTimeout(() => {
res()
}, 1000)
})
res();
}, 1000);
});
try {
const response = await fetch(this.gif.url)
const data = await response.json()
const response = await fetch(this.gif.url);
const data = await response.json();
if (data.ok) {
this.error = false
this.error = false;
this.gif = {
...this.gif,
url: data.src
url: data.src,
};
this.requestUpdate();
} else if (!data.ok || data.error) {
this.error = true
this.error = true;
} else {
this.error = false
this.error = false;
}
} catch (error) {
this.error = true
console.error(error)
this._fetchImage()
this.error = true;
console.error(error);
this._fetchImage();
}
}
render() {
if (this.error && this.attempts <= this.maxAttempts) {
setTimeout(() => {
this._fetchImage()
}, 1000)
this._fetchImage();
}, 1000);
}
return html`
${this.gif && !this.error
? html`
<img
return html` ${this.gif && !this.error
? html` <img
class=${this.class}
src=${this.gif.url}
alt=${this.alt}
@ -107,20 +114,20 @@ alt=${this.alt}
identifier: this.gif.identifier,
name: this.gif.name,
filePath: this.gif.filePath,
service: "GIF_REPOSITORY"
})
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>
: 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>
`
}`
<p class="gif-error-msg">${translate('gifs.gchange16')}</p>
`}`;
}
}
customElements.define('image-component', ImageComponent)
customElements.define('image-component', ImageComponent);

View File

@ -4,14 +4,17 @@ import { Epml } from '../../../epml.js'
import snackbar from './snackbar.js'
import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate'
import '@polymer/paper-tooltip/paper-tooltip.js'
import { RequestQueue } from '../../utils/queue.js'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
const queue = new RequestQueue(3);
class LevelFounder extends LitElement {
static get properties() {
return {
checkleveladdress: { type: String },
selectedAddress: { type: String },
config: { type: Object },
memberInfo: { type: Array }
}
@ -39,7 +42,7 @@ class LevelFounder extends LitElement {
}
h2, h3, h4, h5 {
color:# var(--black);
color: var(--black);
font-weight: 400;
}
@ -88,7 +91,6 @@ class LevelFounder extends LitElement {
constructor() {
super()
this.memberInfo = []
this.selectedAddress = window.parent.reduxStore.getState().app.selectedAddress.address
}
render() {
@ -101,25 +103,20 @@ class LevelFounder extends LitElement {
}
firstUpdated() {
this.checkAddressInfo()
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()
queue.push(() => this.checkAddressInfo());
}
async checkAddressInfo() {
try {
let toCheck = this.checkleveladdress
const memberInfo = await parentEpml.request('apiCall', {
url: `/addresses/${toCheck}`
})
this.memberInfo = memberInfo
} catch (error) {
console.error(error)
}
}
renderFounder() {

View File

@ -290,11 +290,7 @@ class NameMenu extends LitElement {
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()
}

View File

@ -0,0 +1,332 @@
import { LitElement, html, css } from 'lit';
import {
translate,
} from 'lit-translate';
import axios from 'axios';
import { RequestQueueWithPromise } from '../../utils/queue';
import '@material/mwc-menu';
import '@material/mwc-list/mwc-list-item.js';
import '@material/mwc-dialog'
const requestQueue = new RequestQueueWithPromise(5);
const requestQueue2 = new RequestQueueWithPromise(5);
export class ResuableImage extends LitElement {
static get properties() {
return {
resource: { type: Object },
isReady: { type: Boolean },
status: { type: Object },
missingData: {type: Boolean},
openDialogImage: { type: Boolean },
onLoad: {attribute: false}
};
}
static get styles() {
return css`
* {
--mdc-theme-text-primary-on-background: var(--black);
--mdc-dialog-max-width: 85vw;
--mdc-dialog-max-height: 95vh;
}
img {
width: 100%;
height: auto;
object-fit: contain;
border-radius: 5px;
position: relative;
}
.smallLoading,
.smallLoading:after {
border-radius: 50%;
width: 2px;
height: 2px;
}
.smallLoading {
border-width: 0.8em;
border-style: solid;
border-color: rgba(3, 169, 244, 0.2) rgba(3, 169, 244, 0.2)
rgba(3, 169, 244, 0.2) rgb(3, 169, 244);
font-size: 30px;
position: relative;
text-indent: -9999em;
transform: translateZ(0px);
animation: 1.1s linear 0s infinite normal none running
loadingAnimation;
}
.imageContainer {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
@-webkit-keyframes loadingAnimation {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes loadingAnimation {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
`;
}
constructor() {
super();
this.resource = {
identifier: '',
name: '',
service: '',
};
this.status = {
status: '',
};
this.url = '';
this.isReady = false;
this.nodeUrl = this.getNodeUrl();
this.myNode = this.getMyNode();
this.hasCalledWhenDownloaded = false;
this.isFetching = false;
this.missingData = false
this.openDialogImage = false
this.observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting && this.status.status !== 'READY') {
this._fetchImage();
// Stop observing after the image has started loading
this.observer.unobserve(this);
}
}
});
}
getNodeUrl() {
const myNode =
window.parent.reduxStore.getState().app.nodeConfig.knownNodes[
window.parent.reduxStore.getState().app.nodeConfig.node
];
const nodeUrl =
myNode.protocol + '://' + myNode.domain + ':' + myNode.port;
return nodeUrl;
}
getMyNode() {
const myNode =
window.parent.reduxStore.getState().app.nodeConfig.knownNodes[
window.parent.reduxStore.getState().app.nodeConfig.node
];
return myNode;
}
getApiKey() {
const myNode =
window.parent.reduxStore.getState().app.nodeConfig.knownNodes[
window.parent.reduxStore.getState().app.nodeConfig.node
];
let apiKey = myNode.apiKey;
return apiKey;
}
async fetchResource() {
try {
if (this.isFetching) return;
this.isFetching = true;
await requestQueue2.enqueue(() => {
return axios.get(
`${this.nodeUrl}/arbitrary/resource/properties/${this.resource.service}/${this.resource.name}/${this.resource.identifier}?apiKey=${this.myNode.apiKey}`
);
});
this.isFetching = false;
} catch (error) {
this.isFetching = false;
}
}
async fetchVideoUrl() {
this.fetchResource();
this.url = `${this.nodeUrl}/arbitrary/${this.resource.service}/${this.resource.name}/${this.resource.identifier}?async=true&apiKey=${this.myNode.apiKey}`;
}
async fetchStatus() {
let isCalling = false;
let percentLoaded = 0;
let timer = 24;
const response = await axios.get(
`${this.nodeUrl}/arbitrary/resource/status/${this.resource.service}/${this.resource.name}/${this.resource.identifier}?apiKey=${this.myNode.apiKey}`
);
if (response && response.data && response.data.status === 'READY') {
this.status = response.data;
this.onLoad()
return;
}
const intervalId = setInterval(async () => {
if (isCalling) return;
isCalling = true;
const data = await requestQueue.enqueue(() => {
return axios.get(
`${this.nodeUrl}/arbitrary/resource/status/${this.resource.service}/${this.resource.name}/${this.resource.identifier}?apiKey=${this.myNode.apiKey}`
);
});
const res = data.data;
isCalling = false;
if (res.localChunkCount) {
if (res.percentLoaded) {
if (
res.percentLoaded === percentLoaded &&
res.percentLoaded !== 100
) {
timer = timer - 5;
} else {
timer = 24;
}
if (timer < 0) {
timer = 24;
isCalling = true;
this.status = {
...res,
status: 'REFETCHING',
};
setTimeout(() => {
isCalling = false;
this.fetchResource();
}, 25000);
return;
}
percentLoaded = res.percentLoaded;
}
this.status = res;
if (this.status.status === 'DOWNLOADED') {
this.fetchResource();
}
}
// check if progress is 100% and clear interval if true
if (res.status === 'READY') {
this.onLoad()
clearInterval(intervalId);
this.status = res;
this.isReady = true;
}
if(res.status === 'MISSING_DATA'){
this.status = res
this.missingData = true
clearInterval(intervalId)
}
}, 5000); // 1 second interval
}
async _fetchImage() {
try {
this.fetchVideoUrl({
name: this.resource.name,
service: this.resource.service,
identifier: this.resource.identifier,
});
this.fetchStatus();
} catch (error) { /* empty */ }
}
firstUpdated() {
this.observer.observe(this);
}
showContextMenu(e) {
e.preventDefault();
e.stopPropagation();
const contextMenu = this.shadowRoot.getElementById('contextMenu');
const containerRect = e.currentTarget.getBoundingClientRect();
// Adjusting the positions
const adjustedX = e.clientX - containerRect.left;
const adjustedY = e.clientY - containerRect.top;
contextMenu.style.top = `${adjustedY}px`;
contextMenu.style.left = `${adjustedX}px`;
contextMenu.open = true;
}
render() {
return html`
<div>
${this.status.status !== 'READY'
? html`
<div
style="display:flex;flex-direction:column;width:100%;height:100%;justify-content:center;align-items:center;"
>
<div class=${`smallLoading`}></div>
<p style="color: var(--black)">
${`${Math.round(
this.status.percentLoaded || 0
).toFixed(0)}% `}${translate(
'chatpage.cchange94'
)}
</p>
</div>
`
: ''}
${this.status.status === 'READY'
? html`
<div style="position:relative; cursor:pointer" @click=${()=> {
this.openDialogImage = true;
}}>
<img crossorigin="anonymous" src=${this.url} />
</div>
`
: ''}
</div>
<mwc-dialog
id="showDialogPublicKey"
?open=${this.openDialogImage}
@closed=${() => {
this.openDialogImage = false;
}}>
<div class="dialog-header"></div>
<div class="dialog-container imageContainer">
${this.openDialogImage ? html`
<img src=${this.url} style="height: auto; max-height: 80vh; width: auto; max-width: 80vw; object-fit: contain; border-radius: 5px;"/>
` : ''}
</div>
<mwc-button
slot="primaryAction"
dialogAction="cancel"
class="red"
@click=${() => {
this.openDialogImage = false;
}}
>
${translate('general.close')}
</mwc-button>
</mwc-dialog>
`;
}
}
customElements.define('reusable-image', ResuableImage);

View File

@ -95,168 +95,141 @@ export class TipUser extends LitElement {
}
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
const amount = this.shadowRoot.getElementById("amountInput").value;
const recipient = this.userName;
this.sendMoneyLoading = true;
this.btnDisable = true;
// Helper function to reset loading and button state
const resetState = () => {
this.sendMoneyLoading = false;
this.btnDisable = false;
}
if (parseFloat(amount) + parseFloat(0.011) > parseFloat(this.walletBalance)) {
this.sendMoneyLoading = false
this.btnDisable = false
let snack1string = get("chatpage.cchange51")
parentEpml.request('showSnackBar', `${snack1string}`)
return false
resetState();
const 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
resetState();
const 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
resetState();
const snack3string = get("chatpage.cchange53");
parentEpml.request('showSnackBar', `${snack3string}`);
return false;
}
const validateName = async (receiverName) => {
let myRes
let myNameRes = await parentEpml.request('apiCall', {
const myNameRes = await parentEpml.request('apiCall', {
type: 'api',
url: `/names/${receiverName}`,
})
if (myNameRes.error === 401) {
myRes = false
} else {
myRes = myNameRes
}
return myRes
}
url: `/names/${receiverName}`
});
return myNameRes.error === 401 ? false : myNameRes;
};
const validateAddress = async (receiverAddress) => {
let myAddress = await window.parent.validateAddress(receiverAddress)
return myAddress
}
const validateReceiver = async (recipient) => {
let lastRef = await this.getLastRef()
let theFee = await this.getSendQortFee()
let isAddress
try {
isAddress = await validateAddress(recipient)
} catch (err) {
isAddress = false
}
if (isAddress) {
let myTransaction = await makeTransactionRequest(recipient, lastRef, theFee)
getTxnRequestResponse(myTransaction)
} else {
let myNameRes = await validateName(recipient)
if (myNameRes !== false) {
let myNameAddress = myNameRes.owner
let myTransaction = await makeTransactionRequest(myNameAddress, lastRef, theFee)
getTxnRequestResponse(myTransaction)
} else {
console.error(this.renderReceiverText())
this.errorMessage = this.renderReceiverText()
this.sendMoneyLoading = false
this.btnDisable = false
}
}
}
return await window.parent.validateAddress(receiverAddress);
};
const getName = async (recipient) => {
try {
const getNames = await parentEpml.request("apiCall", {
type: "api",
url: `/names/address/${recipient}`,
url: `/names/address/${recipient}`
});
if (getNames?.length > 0 ) {
return getNames[0].name
} else {
return ''
}
return getNames?.length > 0 ? getNames[0].name : '';
} catch (error) {
return ""
}
return "";
}
};
const makeTransactionRequest = async (receiver, lastRef, theFee) => {
let myReceiver = receiver
let mylastRef = lastRef
let myFee = theFee
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', {
const makeTransactionRequest = async (receiver, lastRef) => {
const dialogAmount = get("transactions.amount");
const dialogAddress = get("login.address");
const dialogName = get("login.name");
const dialogTo = get("transactions.to");
const recipientName = await getName(receiver);
return await parentEpml.request('transaction', {
type: 2,
nonce: this.myAddress.nonce,
params: {
recipient: myReceiver,
recipient: receiver,
recipientName: recipientName,
amount: amount,
lastReference: mylastRef,
fee: myFee,
dialogamount: dialogamount,
dialogto: dialogto,
lastReference: lastRef,
fee: this.qortPaymentFee,
dialogAmount,
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)
this.errorMessage = txnResponse.message;
resetState();
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
this.shadowRoot.getElementById('amountInput').value = '';
this.errorMessage = '';
this.successMessage = this.renderSuccessText();
resetState();
setTimeout(() => {
this.setOpenTipUser(false)
this.successMessage = ""
}, 3000)
this.setOpenTipUser(false);
this.successMessage = "";
}, 3000);
} else {
this.errorMessage = txnResponse.data.message
this.sendMoneyLoading = false
this.btnDisable = false
throw new Error(txnResponse)
this.errorMessage = txnResponse.data.message;
resetState();
throw new Error(txnResponse);
}
};
const validateReceiver = async (recipient) => {
let lastRef = await this.getLastRef();
let isAddress;
try {
isAddress = await validateAddress(recipient);
} catch (err) {
isAddress = false;
}
if (isAddress) {
const myTransaction = await makeTransactionRequest(recipient, lastRef);
getTxnRequestResponse(myTransaction);
} else {
const myNameRes = await validateName(recipient);
if (myNameRes !== false) {
const myTransaction = await makeTransactionRequest(myNameRes.owner, lastRef);
getTxnRequestResponse(myTransaction);
} else {
this.errorMessage = this.renderReceiverText();
resetState();
}
}
validateReceiver(recipient)
};
await validateReceiver(recipient);
}
render() {
return html`
<div class="tip-user-header">

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
self.addEventListener('message', async e => {
const response = e.data.list.sort(function (a, b) {
return a.timestamp
- b.timestamp
})
postMessage(response)
})

View File

@ -1743,6 +1743,12 @@ class GroupManagement extends LitElement {
})
return joinedG
}
const getGroupInfo = async (groupId) => {
let joinedG = await parentEpml.request('apiCall', {
url: `/groups/${groupId}`
})
return joinedG
}
const getGroupInvites = async () => {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]
@ -1839,6 +1845,25 @@ class GroupManagement extends LitElement {
if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
this.selectedAddress = selectedAddress
})
parentEpml.subscribe('side_effect_action', async sideEffectActionParam => {
const sideEffectAction = JSON.parse(sideEffectActionParam)
if(sideEffectAction && sideEffectAction.type === 'openJoinGroupModal'){
const res = await getGroupInfo(sideEffectAction.data)
if(res && res.groupId){
if(res.isOpen){
this.joinGroup(res)
} else {
let snackbarstring = get("managegroup.mg45")
parentEpml.request('showSnackBar', `${snackbarstring}`)
}
}
window.parent.reduxStore.dispatch(
window.parent.reduxAction.setSideEffectAction(null)
);
}
})
parentEpml.subscribe('config', c => {
if (!configLoaded) {
setTimeout(getOpen_JoinedGroups, 1)
@ -2849,6 +2874,17 @@ class GroupManagement extends LitElement {
}
}
setTxNotification(tx){
window.parent.reduxStore.dispatch(
window.parent.reduxAction.setNewNotification({
type: 'JOIN_GROUP',
status: 'confirming',
reference: tx,
timestamp: Date.now()
})
);
}
async _joinGroup(groupId, groupName) {
this.resetDefaultSettings()
const joinFeeInput = this.joinFee
@ -2885,7 +2921,8 @@ class GroupManagement extends LitElement {
lastReference: lastRef,
groupdialog1: groupdialog1,
groupdialog2: groupdialog2
}
},
apiVersion: 2
})
return myTxnrequest
}
@ -2897,6 +2934,12 @@ class GroupManagement extends LitElement {
throw new Error(txnResponse)
} else if (txnResponse.success === true && !txnResponse.data.error) {
this.message = this.renderErr8Text()
this.setTxNotification({
groupName,
groupId,
timestamp: Date.now(),
...(txnResponse.data || {})
})
this.error = false
} else {
this.error = true

File diff suppressed because one or more lines are too long

View File

@ -157,6 +157,10 @@ export const qchatStyles = css`
padding-top: 20px;
padding-left: 20px;
padding-right: 20px;
display: flex;
align-items: center;
gap: 10px;
justify-content: space-between;
}
.center {
@ -172,13 +176,13 @@ export const qchatStyles = css`
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;
display: flex;
flex: 0;
}
.people-list .create-chat:hover {

View File

@ -1,10 +1,9 @@
import { LitElement, html, css } from 'lit'
import { LitElement, html } from 'lit'
import { render } from 'lit/html.js'
import { passiveSupport } from 'passive-events-support/src/utils'
import { Epml } from '../../../../epml.js'
import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate'
import { get, translate } from 'lit-translate'
import { qchatStyles } from './q-chat-css.src.js'
import { repeat } from 'lit/directives/repeat.js'
import { Editor, Extension } from '@tiptap/core'
import isElectron from 'is-electron'
import WebWorker from 'web-worker:./computePowWorker.src.js'
@ -13,19 +12,22 @@ import Underline from '@tiptap/extension-underline';
import Placeholder from '@tiptap/extension-placeholder'
import Highlight from '@tiptap/extension-highlight'
import snackbar from '../../components/snackbar.js'
import ShortUniqueId from 'short-unique-id';
import '../../components/ChatWelcomePage.js'
import '../../components/ChatHead.js'
import '../../components/ChatPage.js'
import '../../components/WrapperModal.js'
import '../../components/ChatSearchResults.js'
import '../../components/ChatGroupsModal.js'
import '@material/mwc-button'
import '@material/mwc-dialog'
import '@material/mwc-icon'
import '@material/mwc-snackbar'
import '@polymer/paper-spinner/paper-spinner-lite.js'
import '@vaadin/grid'
import '@vaadin/tooltip';
passiveSupport({ events: ['touchstart'] })
@ -53,15 +55,20 @@ class Chat extends LitElement {
userFoundModalOpen: { type: Boolean },
userSelected: { type: Object },
editor: {type: Object},
groupInvites: { type: Array }
groupInvites: { type: Array },
loggedInUserName: {type: String},
loggedInUserAddress: {type: String},
openDialogGroupsModal: {type: Boolean}
}
}
static styles = [qchatStyles]
static get styles() {
return [qchatStyles];
}
constructor() {
super()
this.selectedAddress = {}
this.selectedAddress = window.parent.reduxStore.getState().app.selectedAddress
this.config = {
user: {
node: {
@ -92,12 +99,15 @@ class Chat extends LitElement {
this.userFoundModalOpen = false
this.userSelected = {}
this.groupInvites = []
this.loggedInUserName = ""
this.openDialogGroupsModal = false
this.uid = new ShortUniqueId();
}
async setActiveChatHeadUrl(url) {
this.activeChatHeadUrl = ''
await this.updateComplete;
this.activeChatHeadUrl = url
this.requestUpdate()
}
resetChatEditor(){
@ -146,6 +156,8 @@ class Chat extends LitElement {
this.unsubscribeStore = window.parent.reduxStore.subscribe(() => {
try {
const currentState = window.parent.reduxStore.getState();
if(window.parent.location && window.parent.location.search) {
const queryString = window.parent.location.search
const params = new URLSearchParams(queryString)
@ -157,8 +169,13 @@ class Chat extends LitElement {
this.setActiveChatHeadUrl(chat)
}
}
} catch (error) {
if(currentState.app.accountInfo && currentState.app.accountInfo.names && currentState.app.accountInfo.names.length > 0 && this.loggedInUserName !== currentState.app.accountInfo.names[0].name){
this.loggedInUserName = currentState.app.accountInfo.names[0].name
}
if(currentState.app.accountInfo && currentState.app.accountInfo.addressInfo && currentState.app.accountInfo.addressInfo.address && this.loggedInUserAddress !== currentState.app.accountInfo.addressInfo.address){
this.loggedInUserAddress = currentState.app.accountInfo.addressInfo.address
}
} catch (error) { /* empty */ }
})
}
@ -178,44 +195,74 @@ class Chat extends LitElement {
})
}
setOpenDialogGroupsModal(val){
this.openDialogGroupsModal = val
}
openTabToGroupManagement(){
window.parent.reduxStore.dispatch(
window.parent.reduxAction.setNewTab({
url: `group-management`,
id: this.uid.rnd(),
myPlugObj: {
"url": "group-management",
"domain": "core",
"page": "group-management/index.html",
"title": "Group Management",
"icon": "vaadin:group",
"mwcicon": "group",
"pluginNumber": "plugin-fJZNpyLGTl",
"menus": [],
"parent": false
},
openExisting: true
})
);
}
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 id="openPrivateMessage" class="create-chat" @click=${() => { this.openPrivateMessage = true }}>
<mwc-icon style="color: var(--black);">edit_square</mwc-icon>
<vaadin-tooltip
for="openPrivateMessage"
position="top"
hover-delay=${200}
hide-delay=${1}
text=${get('chatpage.cchange1')}>
</vaadin-tooltip>
</div>
<div style="display:flex; align-items:center;gap:10px">
<div id="goToGroup" class="create-chat" @click=${() => { this.openTabToGroupManagement() }}>
<mwc-icon style="color: var(--black);">group_add</mwc-icon>
<vaadin-tooltip
for="goToGroup"
position="top"
hover-delay=${200}
hide-delay=${1}
text=${get('chatpage.cchange96')}>
</vaadin-tooltip>
</div>
<div id="blockedUsers" class="create-chat" @click=${() => { this.shadowRoot.querySelector('#blockedUserDialog').show() }}>
<mwc-icon style="color: var(--black);">person_off</mwc-icon>
<vaadin-tooltip
for="blockedUsers"
position="top"
hover-delay=${200}
hide-delay=${1}
text=${get('chatpage.cchange3')}>
</vaadin-tooltip>
</div>
</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()}>
@ -432,23 +479,32 @@ class Chat extends LitElement {
document.querySelector('html').setAttribute('theme', this.theme)
})
if (!isElectron()) {
} else {
if (!isElectron()) { /* empty */ } else {
window.addEventListener('contextmenu', (event) => {
event.preventDefault()
window.parent.electronAPI.showMyMenu()
})
// Check if the clicked element has the class
let target = event.target;
while (target !== null) {
if (target.classList && target.classList.contains('customContextMenuDiv')) {
// Your custom context menu logic
this.showContextMenu(event);
return;
}
target = target.parentNode;
}
// If it doesn't, show the default Electron context menu
event.preventDefault();
window.parent.electronAPI.showMyMenu();
});
}
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)
@ -464,6 +520,7 @@ class Chat extends LitElement {
url: `/addresses/balance/${window.parent.reduxStore.getState().app.selectedAddress.address}`
}).then(res => {
this.balance = res
this.requestUpdate()
})
})
parentEpml.imReady()
@ -473,9 +530,10 @@ class Chat extends LitElement {
}, 60000)
}
clearConsole() {
if (!isElectron()) {
} else {
if (!isElectron()) { /* empty */ } else {
console.clear()
window.parent.electronAPI.clearCache()
}
@ -565,7 +623,7 @@ class Chat extends LitElement {
recipient = _recipient
} else {
recipient = myNameRes.owner
};
}
const getAddressPublicKey = async () => {
let isEncrypted;
@ -620,7 +678,7 @@ class Chat extends LitElement {
const worker = new WebWorker()
let nonce = null
let chatBytesArray = null;
await new Promise((res, rej) => {
await new Promise((res) => {
worker.postMessage({chatBytes, path, difficulty})
worker.onmessage = e => {
worker.terminate()
@ -669,7 +727,7 @@ class Chat extends LitElement {
}
renderLoadingText() {
return html`${translate("chatpage.cchange2")}`
return html`<div style="width:100%;display:flex;justify-content:center"> <paper-spinner-lite active></paper-spinner-lite></div>`
}
renderSendText() {
@ -833,6 +891,9 @@ class Chat extends LitElement {
chatId=${this.activeChatHeadUrl}
.setOpenPrivateMessage=${(val) => this.setOpenPrivateMessage(val)}
.setActiveChatHeadUrl=${(val)=> this.setActiveChatHeadUrl(val)}
balance=${this.balance}
loggedInUserName=${this.loggedInUserName}
loggedInUserAddress=${this.loggedInUserAddress}
>
</chat-page>
`
@ -897,8 +958,16 @@ class Chat extends LitElement {
scrollToBottom() {
const viewElement = this.shadowRoot.querySelector('chat-page').shadowRoot.querySelector('chat-scroller').shadowRoot.getElementById('viewElement')
const chatScrollerElement = this.shadowRoot.querySelector('chat-page').shadowRoot.querySelector('chat-scroller')
if (chatScrollerElement && chatScrollerElement.disableAddingNewMessages) {
const chatPageElement = this.shadowRoot.querySelector('chat-page')
if(chatPageElement && chatPageElement.getLastestMessages)
chatPageElement.getLastestMessages()
} else {
viewElement.scroll({ top: viewElement.scrollHeight, left: 0, behavior: 'smooth' })
}
}
showNewMessageBar() {
this.shadowRoot.getElementById('newMessageBar').classList.remove('hide-new-message-bar')

View File

@ -21,6 +21,7 @@ import '@vaadin/icon'
import '@vaadin/icons'
import '@vaadin/grid'
import '@vaadin/text-field'
import { warningModal } from '../../utils/warning-modal.js'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
@ -753,7 +754,16 @@ class NameRegistration extends LitElement {
return html`<mwc-button class="warning" @click=${() => this.openUpdateNameDialog(nameObj)}><mwc-icon>update</mwc-icon>&nbsp;${translate("publishpage.pchange2")} ${translate("login.name")}</mwc-button>`
}
openUpdateNameDialog(nameObj) {
async openUpdateNameDialog(nameObj) {
const res = await warningModal.showModalAndWaitPublish(
{
message: get('registernamepage.nchange48')
}
);
if (res.action !== 'accept'){
this.closeUpdateNameDialog()
return
}
this.toUpdateName = ''
this.shadowRoot.getElementById("oldNameInput").value = ''
this.shadowRoot.getElementById("newNameInput").value = ''

View File

@ -4,6 +4,7 @@ import { Epml } from '../../../epml.js'
import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate'
import { columnBodyRenderer, gridRowDetailsRenderer } from '@vaadin/grid/lit.js'
import isElectron from 'is-electron'
import '@polymer/paper-spinner/paper-spinner-lite.js'
registerTranslateConfig({
loader: lang => fetch(`/language/${lang}.json`).then(res => res.json())
@ -42,7 +43,9 @@ class QApps extends LitElement {
blockedResources: { type: Array },
textStatus: { type: String },
textProgress: { type: String },
theme: { type: String, reflect: true }
theme: { type: String, reflect: true },
hasInitiallyFetched: {type:Boolean},
isLoading: {type: Boolean}
}
}
@ -369,6 +372,7 @@ class QApps extends LitElement {
this.blockedNames = []
this.relayMode = null
this.isLoading = false
this.hasInitiallyFetched = false
this.btnDisabled = false
this.searchName = ''
this.searchResources = []
@ -421,13 +425,14 @@ class QApps extends LitElement {
</vaadin-grid-column>
</vaadin-grid>
<div id="pages"></div>
${this.pageRes == null ? html`
Loading...
${this.isLoading ? html`
<div style="width:100%;display:flex;justify-content:center;position: fixed;top: 50%;left: 50%;transform: translate(-50%, -50%)"> <paper-spinner-lite active></paper-spinner-lite></div>
` : ''}
${this.isEmptyArray(this.pageRes) ? html`
${this.isEmptyArray(this.pageRes) && this.hasInitiallyFetched ? html`
<span style="color: var(--black);">${translate("appspage.schange10")}</span>
` : ''}
${this.renderRelayModeText()}<br>
</div>
<div id="tab-followed-content">
<div style="min-height:48px; display: flex; padding-bottom: 6px; margin: 2px;">
@ -738,11 +743,16 @@ class QApps extends LitElement {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]
const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port
let jsonOffsetUrl = `${nodeUrl}/arbitrary/resources?service=APP&default=true&limit=20&offset=${offset}&reverse=false&includestatus=true&includemetadata=true&excludeblocked=true`
this.isLoading = true
const jsonOffsetRes = await fetch(jsonOffsetUrl)
const jsonOffsetData = await jsonOffsetRes.json()
this.pageRes = jsonOffsetData
if(!this.hasInitiallyFetched){
this.hasInitiallyFetched = true
}
this.isLoading = false
}
async updateItemsFromPage(page) {

View File

@ -276,7 +276,7 @@ class PublishData extends LitElement {
<p>
<mwc-select id="category" label="${translate("publishpage.pchange7")}" index="0" style="min-width: 130px; max-width:100%; width:100%;">
${this.categories.map((c, index) => html`
<mwc-list-item value="${c.id}">${c.name}</mwc-list-item>
<mwc-list-item style="color:var(--black)" value="${c.id}">${c.name}</mwc-list-item>
`)}
</mwc-select>
</p>
@ -300,7 +300,9 @@ class PublishData extends LitElement {
<p style="color: green; word-break: break-word;">${this.successMessage}</p>
${this.loading ? html` <paper-progress indeterminate style="width:100%; margin:4px;"></paper-progress> ` : ''}
<div class="buttons">
<mwc-button ?disabled=${this.btnDisable} style="width:49%;" raised icon="science" @click=${(e) => this.shadowRoot.querySelector('#publishWithFeeDialog').close()}> ${translate("appspage.schange40")}</mwc-button>
<mwc-button ?disabled=${this.btnDisable} style="width:49%;" raised icon="science" @click=${(e) => {
this.doPublish(e, true, false)}
}> ${translate("appspage.schange40")}</mwc-button>
<mwc-button ?disabled=${this.btnDisable} style="width:49%;" raised icon="send" @click=${(e) => {
this.doPublish(e, false, true)
}}> ${translate("publishpage.pchange11")}</mwc-button>

View File

@ -0,0 +1,14 @@
export function simpleHash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) - hash + str.charCodeAt(i);
hash = hash & hash; // Convert to 32bit integer
}
return hash.toString();
}
export function generateIdFromAddresses(address1, address2) {
// Sort addresses lexicographically and concatenate
const sortedAddresses = [address1, address2].sort().join('');
return simpleHash(sortedAddresses);
}

View File

@ -0,0 +1,71 @@
export class RequestQueue {
constructor(maxConcurrent = 5) {
this.queue = [];
this.maxConcurrent = maxConcurrent;
this.currentConcurrent = 0;
}
push(request) {
return new Promise((resolve, reject) => {
this.queue.push({
request,
resolve,
reject,
});
this.checkQueue();
});
}
checkQueue() {
if (this.queue.length === 0 || this.currentConcurrent >= this.maxConcurrent) return;
const { request, resolve, reject } = this.queue.shift();
this.currentConcurrent++;
request()
.then(resolve)
.catch(reject)
.finally(() => {
this.currentConcurrent--;
this.checkQueue();
});
}
}
export class RequestQueueWithPromise {
constructor(maxConcurrent = 5) {
this.queue = [];
this.maxConcurrent = maxConcurrent;
this.currentlyProcessing = 0;
}
// Add a request to the queue and return a promise
enqueue(request) {
return new Promise((resolve, reject) => {
// Push the request and its resolve and reject callbacks to the queue
this.queue.push({ request, resolve, reject });
this.process();
});
}
// Process requests in the queue
async process() {
while (this.queue.length > 0 && this.currentlyProcessing < this.maxConcurrent) {
this.currentlyProcessing++;
const { request, resolve, reject } = this.queue.shift();
try {
const response = await request();
resolve(response);
} catch (error) {
reject(error);
} finally {
this.currentlyProcessing--;
this.process();
}
}
}
}

View File

@ -1,117 +1,251 @@
// export const replaceMessagesEdited = async ({
// decodedMessages,
// parentEpml,
// isReceipient,
// decodeMessageFunc,
// _publicKey,
// addToUpdateMessageHashmap
// }) => {
// const findNewMessages = decodedMessages.map(async (msg) => {
// let msgItem = null
// try {
// let msgQuery = `&involving=${msg.recipient}&involving=${msg.sender}`
// if (!isReceipient) {
// msgQuery = `&txGroupId=${msg.txGroupId}`
// }
// const response = await parentEpml.request("apiCall", {
// type: "api",
// url: `/chat/messages?chatreference=${msg.signature}&reverse=true${msgQuery}&limit=1&sender=${msg.sender}&encoding=BASE64`,
// })
// if (response && Array.isArray(response) && response.length !== 0) {
// let responseItem = { ...response[0] }
// const decodeResponseItem = decodeMessageFunc(responseItem, isReceipient, _publicKey)
// delete decodeResponseItem.timestamp
// msgItem = {
// ...msg,
// ...decodeResponseItem,
// senderName: msg.senderName,
// sender: msg.sender,
// editedTimestamp: response[0].timestamp,
// originalSignature: msg.signature
// }
// }
// } catch (error) {
// }
// return msgItem
// })
// const updateMessages = await Promise.all(findNewMessages)
// const filterOutNull = updateMessages.filter((item)=> item !== 'null' && item !== null)
// const findNewMessages2 = filterOutNull.map(async (msg) => {
// let parsedMessageObj = msg
// try {
// parsedMessageObj = JSON.parse(msg.decodedMessage)
// } catch (error) {
// return msg
// }
// let msgItem = msg
// try {
// let msgQuery = `&involving=${msg.recipient}&involving=${msg.sender}`
// if (!isReceipient) {
// msgQuery = `&txGroupId=${msg.txGroupId}`
// }
// if (parsedMessageObj.repliedTo) {
// let originalReply
// if(+parsedMessageObj.version > 2){
// originalReply = await parentEpml.request("apiCall", {
// type: "api",
// url: `/chat/message/${parsedMessageObj.repliedTo}?encoding=BASE64`,
// })
// }
// if(+parsedMessageObj.version < 3){
// originalReply = await parentEpml.request("apiCall", {
// type: "api",
// url: `/chat/messages?reference=${parsedMessageObj.repliedTo}&reverse=true${msgQuery}&encoding=BASE64`,
// })
// }
// const originalReplyMessage = originalReply.timestamp ? originalReply : originalReply.length !== 0 ? originalReply[0] : null
// const response = await parentEpml.request("apiCall", {
// type: "api",
// url: `/chat/messages?chatreference=${parsedMessageObj.repliedTo}&reverse=true${msgQuery}&limit=1&sender=${originalReplyMessage.sender}&encoding=BASE64`,
// })
// if (
// originalReplyMessage &&
// response &&
// Array.isArray(response) &&
// response.length !== 0
// ) {
// const decodeOriginalReply = decodeMessageFunc(originalReplyMessage, isReceipient, _publicKey)
// const decodeUpdatedReply = decodeMessageFunc(response[0], isReceipient, _publicKey)
// const formattedRepliedToData = {
// ...decodeUpdatedReply,
// senderName: decodeOriginalReply.senderName,
// sender: decodeOriginalReply.sender,
// }
// msgItem = {
// ...msg,
// repliedToData: formattedRepliedToData,
// }
// } else {
// if (
// originalReplyMessage
// ) {
// msgItem = {
// ...msg,
// repliedToData: decodeMessageFunc(originalReplyMessage, isReceipient, _publicKey),
// }
// }
// }
// }
// } catch (error) {
// }
// return msgItem
// })
// const updateMessages2 = await Promise.all(findNewMessages2)
// console.log({updateMessages2})
// updateMessages2.forEach((item)=> {
// addToUpdateMessageHashmap(item.originalSignature, item)
// })
// return updateMessages2
// }
export const replaceMessagesEdited = async ({
decodedMessages,
parentEpml,
isReceipient,
decodeMessageFunc,
_publicKey
_publicKey,
addToUpdateMessageHashmap
}) => {
const findNewMessages = decodedMessages.map(async (msg) => {
let msgItem = msg
try {
let msgQuery = `&involving=${msg.recipient}&involving=${msg.sender}`
if (!isReceipient) {
msgQuery = `&txGroupId=${msg.txGroupId}`
const MAX_CONCURRENT_REQUESTS = 5; // Maximum number of concurrent requests
const executeWithConcurrencyLimit = async (array, asyncFn) => {
const results = [];
const concurrencyPool = [];
for (const item of array) {
const promise = asyncFn(item);
concurrencyPool.push(promise);
if (concurrencyPool.length >= MAX_CONCURRENT_REQUESTS) {
results.push(...await Promise.all(concurrencyPool));
concurrencyPool.length = 0; // Clear the concurrency pool
}
const response = await parentEpml.request("apiCall", {
}
if (concurrencyPool.length > 0) {
results.push(...await Promise.all(concurrencyPool));
}
return results;
};
const findUpdatedMessage = async (msg) => {
let msgItem = { ...msg };
try {
let msgQuery = `&involving=${msg.recipient}&involving=${msg.sender}`;
if (!isReceipient) {
msgQuery = `&txGroupId=${msg.txGroupId}`;
}
// Find new messages first
const newMsgResponse = await parentEpml.request("apiCall", {
type: "api",
url: `/chat/messages?chatreference=${msg.signature}&reverse=true${msgQuery}&limit=1&sender=${msg.sender}&encoding=BASE64`,
})
});
if (response && Array.isArray(response) && response.length !== 0) {
let responseItem = { ...response[0] }
const decodeResponseItem = decodeMessageFunc(responseItem, isReceipient, _publicKey)
delete decodeResponseItem.timestamp
if (Array.isArray(newMsgResponse) && newMsgResponse.length > 0) {
const decodeResponseItem = decodeMessageFunc(newMsgResponse[0], isReceipient, _publicKey);
delete decodeResponseItem.timestamp;
msgItem = {
...msg,
...msgItem,
...decodeResponseItem,
senderName: msg.senderName,
sender: msg.sender,
editedTimestamp: response[0].timestamp,
}
}
} catch (error) {
editedTimestamp: newMsgResponse[0].timestamp,
originalSignature: msg.signature
};
}
return msgItem
})
const updateMessages = await Promise.all(findNewMessages)
const findNewMessages2 = updateMessages.map(async (msg) => {
let parsedMessageObj = msg
// Then check and find replies in the same iteration
let parsedMessageObj;
try {
parsedMessageObj = JSON.parse(msg.decodedMessage)
parsedMessageObj = JSON.parse(msg.decodedMessage);
} catch (error) {
return msg
}
let msgItem = msg
try {
let msgQuery = `&involving=${msg.recipient}&involving=${msg.sender}`
if (!isReceipient) {
msgQuery = `&txGroupId=${msg.txGroupId}`
// If parsing fails, return the msgItem as is
return msgItem;
}
if (parsedMessageObj.repliedTo) {
let originalReply
let originalReply;
if(+parsedMessageObj.version > 2){
originalReply = await parentEpml.request("apiCall", {
type: "api",
url: `/chat/message/${parsedMessageObj.repliedTo}?encoding=BASE64`,
})
}
if(+parsedMessageObj.version < 3){
});
} else {
originalReply = await parentEpml.request("apiCall", {
type: "api",
url: `/chat/messages?reference=${parsedMessageObj.repliedTo}&reverse=true${msgQuery}&encoding=BASE64`,
})
});
}
const originalReplyMessage = originalReply.timestamp ? originalReply : originalReply.length > 0 ? originalReply[0] : null;
const originalReplyMessage = originalReply.timestamp ? originalReply : originalReply.length !== 0 ? originalReply[0] : null
const response = await parentEpml.request("apiCall", {
const replyResponse = await parentEpml.request("apiCall", {
type: "api",
url: `/chat/messages?chatreference=${parsedMessageObj.repliedTo}&reverse=true${msgQuery}&limit=1&sender=${originalReplyMessage.sender}&encoding=BASE64`,
})
});
if (
originalReplyMessage &&
response &&
Array.isArray(response) &&
response.length !== 0
Array.isArray(replyResponse) &&
replyResponse.length !== 0
) {
const decodeOriginalReply = decodeMessageFunc(originalReplyMessage, isReceipient, _publicKey)
const decodeOriginalReply = decodeMessageFunc(originalReplyMessage, isReceipient, _publicKey);
const decodeUpdatedReply = decodeMessageFunc(replyResponse[0], isReceipient, _publicKey);
const decodeUpdatedReply = decodeMessageFunc(response[0], isReceipient, _publicKey)
const formattedRepliedToData = {
msgItem.repliedToData = {
...decodeUpdatedReply,
senderName: decodeOriginalReply.senderName,
sender: decodeOriginalReply.sender,
};
} else if (originalReplyMessage) {
msgItem.repliedToData = decodeMessageFunc(originalReplyMessage, isReceipient, _publicKey);
}
msgItem = {
...msg,
repliedToData: formattedRepliedToData,
}
} else {
if (
originalReplyMessage
) {
msgItem = {
...msg,
repliedToData: decodeMessageFunc(originalReplyMessage, isReceipient, _publicKey),
}
}
}
}
} catch (error) {
// Handle or log the error gracefully
console.error(error);
}
return msgItem
})
const updateMessages2 = await Promise.all(findNewMessages2)
return msgItem;
};
return updateMessages2
}
const sortedMessages = decodedMessages.sort((a, b) => b.timestamp - a.timestamp);
// Execute the functions with concurrency limit
const updatedMessages = await executeWithConcurrencyLimit(sortedMessages, findUpdatedMessage);
addToUpdateMessageHashmap(updatedMessages);
return updatedMessages;
};

View File

@ -0,0 +1,252 @@
import { get } from 'lit-translate';
export class WarningModal {
constructor() {
this.initializeStyles();
}
async showModalAndWaitPublish(data) {
return new Promise((resolve) => {
const modal = this.createModal(data);
document.body.appendChild(modal);
this.addModalEventListeners(modal, resolve);
});
}
createModal(data) {
const modal = document.createElement('div');
modal.id = "backdrop";
modal.classList.add("backdrop");
modal.innerHTML = `
<div class="modal my-modal-class">
<div class="modal-content">
<div class="modal-body">
<div class="modal-subcontainer">
<div class="checkbox-row">
<p style="font-size: 16px;overflow-wrap: anywhere;" class="modal-paragraph">${data.message}
</div>
</div>
</div>
<div class="modal-buttons">
<button id="cancel-button">${get("general.close")}</button>
<button id="ok-button">${get("general.continue")}</button>
</div>
</div>
</div>
`;
return modal;
}
addModalEventListeners(modal, resolve) {
// Event listener for the 'OK' button
const okButton = modal.querySelector('#ok-button');
okButton.addEventListener('click', () => {
const userData = { isWithFee: true };
if (modal.parentNode === document.body) {
document.body.removeChild(modal);
}
resolve({ action: 'accept', userData });
});
// Prevent modal content from closing the modal
const modalContent = modal.querySelector('.modal-content');
modalContent.addEventListener('click', e => {
e.stopPropagation();
});
// Event listeners for backdrop and 'Cancel' button
const backdropClick = document.getElementById('backdrop');
backdropClick.addEventListener('click', () => {
if (modal.parentNode === document.body) {
document.body.removeChild(modal);
}
resolve({ action: 'reject' });
});
const cancelButton = modal.querySelector('#cancel-button');
cancelButton.addEventListener('click', () => {
if (modal.parentNode === document.body) {
document.body.removeChild(modal);
}
resolve({ action: 'reject' });
});
}
initializeStyles() {
const styles = `
* {
--mdc-theme-primary: rgb(3, 169, 244);
--mdc-theme-secondary: var(--mdc-theme-primary);
--paper-input-container-focus-color: var(--mdc-theme-primary);
--mdc-checkbox-unchecked-color: var(--black);
--mdc-theme-on-surface: var(--black);
--mdc-checkbox-disabled-color: var(--black);
--mdc-checkbox-ink-color: var(--black);
}
.backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgb(186 186 186 / 26%);
overflow: hidden;
animation: backdrop_blur cubic-bezier(0.22, 1, 0.36, 1) 0.1s forwards;
z-index: 1000000;
}
@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;
}
}
.modal {
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
animation: 0.1s cubic-bezier(0.22, 1, 0.36, 1) 0s 1 normal forwards running modal_transition;
z-index: 1000001;
}
@keyframes modal_transition {
0% {
visibility: hidden;
opacity: 0;
}
100% {
visibility: visible;
opacity: 1;
}
}
.modal-content {
background-color: var(--white);
border-radius: 10px;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
max-width: 650px;
min-width: 300px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.modal-body {
padding: 25px;
}
.modal-subcontainer {
color: var(--black);
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.modal-subcontainer-error {
color: var(--black);
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.modal-paragraph-error {
font-family: Roboto, sans-serif;
font-size: 20px;
letter-spacing: 0.3px;
font-weight: 700;
color: var(--black);
margin: 0;
}
.modal-paragraph {
font-family: Roboto, sans-serif;
font-size: 18px;
letter-spacing: 0.3px;
font-weight: 300;
color: var(--black);
margin: 0;
word-wrap: break-word;
overflow-wrap: break-word;
}
.capitalize-first {
text-transform: capitalize;
}
.checkbox-row {
display: flex;
align-items: center;
font-family: Montserrat, sans-serif;
font-weight: 600;
color: var(--black);
}
.modal-buttons {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.modal-buttons button {
background-color: #4caf50;
border: none;
color: #fff;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.2s;
}
.modal-buttons button:hover {
background-color: #3e8e41;
}
#cancel-button {
background-color: #f44336;
}
#cancel-button:hover {
background-color: #d32f2f;
}
`;
const styleSheet = new CSSStyleSheet();
styleSheet.replaceSync(styles);
document.adoptedStyleSheets = [styleSheet];
}
static getInstance() {
if (!WarningModal.instance) {
WarningModal.instance = new WarningModal();
}
return WarningModal.instance;
}
}
export const warningModal = WarningModal.getInstance();