Merge pull request #90 from PhillipLangMartinez/feature/replies-lookup

New chat features
This commit is contained in:
AlphaX-Projects 2023-01-21 17:02:08 +01:00 committed by GitHub
commit 5411a3ae28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 11092 additions and 2204 deletions

14
.eslintrc.json Normal file
View File

@ -0,0 +1,14 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": ["eslint:recommended", "plugin:lit/recommended", "plugin:wc/recommended"],
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"rules": {
"no-mixed-spaces-and-tabs": 0
}
}

2
.gitignore vendored
View File

@ -5,7 +5,7 @@ yarn.lock
qortal-ui-plugins/plugins/core/**/*.js
!*.src.js
qortal-ui-core/src/redux/app/version.js
!qortal-ui-plugins/plugins/core/components/*.js
!qortal-ui-plugins/plugins/core/components/**/*.js
# Node modules
node_modules/

BIN
img/badges/level-0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
img/badges/level-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
img/badges/level-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
img/badges/level-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
img/badges/level-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
img/badges/level-5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
img/chain.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

View File

@ -0,0 +1,4 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
<path d="M4.02 42l41.98-18-41.98-18-.02 14 30 4-30 4z" fill="#03a9f4"/>
<path d="M0 0h48v48h-48z" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 253 B

BIN
img/qortal-chat-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -19,12 +19,12 @@
"install_link:all": "(cd qortal-ui-core && yarn install && yarn link) && (cd qortal-ui-plugins && yarn install && yarn link) && (cd qortal-ui-crypto && yarn install && yarn link) && (yarn link qortal-ui-core && yarn link qortal-ui-plugins && yarn link qortal-ui-crypto)",
"dev": "node server.js",
"prebuild": "node -p \"'export const UI_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > qortal-ui-core/src/redux/app/version.js",
"build-dev": "node build.js",
"build": "NODE_ENV=production node build.js",
"server": "NODE_ENV=production node server.js",
"watch": "node watch.js",
"watch-inline": "node watch-inline.js",
"start-electron": "NODE_ENV=production electron .",
"build-dev": "node --max-old-space-size=8192 build.js",
"build": "NODE_ENV=production node --max-old-space-size=8192 build.js",
"server": "NODE_ENV=production node --max-old-space-size=8192 server.js",
"watch": "node --max-old-space-size=8192 watch.js",
"watch-inline": "node --max-old-space-size=8192 watch-inline.js",
"start-electron": "NODE_ENV=production electron --js-flags=--max-old-space-size=8192 .",
"build-electron": "electron-builder build --publish never",
"deploy-electron": "electron-builder build --win --publish never",
"release": "NODE_ENV=production electron-builder build --publish never",
@ -40,7 +40,8 @@
"electron": "22.0.2",
"electron-builder": "23.6.0",
"electron-packager": "17.1.1",
"@electron/notarize": "1.2.3",
"eslint-plugin-lit": "1.8.0",
"eslint-plugin-wc": "1.4.0",
"shelljs": "0.8.5"
},
"engines": {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -2,19 +2,57 @@
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(MaterialIcons-Regular.eot); /* For IE6-8 */
src: url(MaterialIcons-Regular.eot);
/* For IE6-8 */
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url(MaterialIcons-Regular.woff2) format('woff2'),
url(MaterialIcons-Regular.woff) format('woff'),
url(MaterialIcons-Regular.ttf) format('truetype');
local('MaterialIcons-Regular'),
url(MaterialIcons-Regular.woff2) format('woff2'),
url(MaterialIcons-Regular.woff) format('woff'),
url(MaterialIcons-Regular.ttf) format('truetype');
}
@font-face {
font-family: 'Material Symbols Outlined';
font-style: normal;
src: local('MaterialSymbolsOutlined'),
url(MaterialSymbolsOutlined.ttf) format('truetype'),
url(MaterialSymbolsOutlined.woff2) format('woff2')
}
@font-face {
font-family: 'Montserrat';
src: local('Montserrat'),
local('Montserrat'),
url(Montserrat.ttf) format('truetype');
}
@font-face {
font-family: 'Raleway';
src: local('Raleway'),
local('Raleway'),
url(Raleway.ttf) format('truetype');
}
@font-face {
font-family: 'KoHo';
src: local('KoHo'),
local('KoHo'),
url(KoHo.ttf) format('truetype');
}
@font-face {
font-family: 'Livvic';
src: local('Livvic'),
local('Livvic'),
url(Livvic.ttf) format('truetype');
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px; /* Preferred icon size */
font-size: 24px;
/* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
@ -34,3 +72,17 @@
/* Support for IE. */
font-feature-settings: 'liga';
}
.material-symbols-outlined {
font-family: 'Material Symbols Outlined';
font-weight: normal;
font-style: normal;
font-size: 24px; /* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
}

View File

@ -7,6 +7,17 @@ html {
--border: #d0d6de;
--border2: #dde2e8;
--copybutton: #707584;
--chat-group: #080808;
--chat-bubble: #9f9f9f0a;
--chat-bubble-bg: #e6e6e6;
--chat-bubble-msg-color: #080808;
--reaction-bubble-outline: #6b6969;
--chat-menu-bg: #ffffff;
--chat-menu-outline: #dad9d9;
--chat-menu-icon: #3b3b3c;
--chat-menu-icon-hover: #dad9d9;
--block-user-bg-hover: #dad9d9;
--paperclip-icon: #494949;
--sectxt: #576374;
--vdicon: #707b8a;
--tradehead: #6a6c75;
@ -17,11 +28,12 @@ html {
--relaynodetxt: #646464;
--menuhover: #eeeeee;
--menuactive: #ebebeb;
--mainmenutext:#080808;
--mainmenutexthover:#080808;
--menuactivergb: 235, 235, 235;
--mainmenutext: #080808;
--mainmenutexthover: #080808;
--switchbackground: #666666;
--switchborder: #333333;
--sidetopbar: #ffffff;
--sidetopbar: #ffffff;
--nav-selected-color: #dddddd;
--nav-selected-color-text: #333333;
--nav-color-active: #d1d1d1;
@ -32,6 +44,13 @@ html {
--nav-border-selected-color: #03a9f4;
--error: #d50000;
--background: url("/img/qortal_background_light_.jpg");
--chatHeadBg: #ebebeb;
--chatHeadBgActive: #ebebeb;
--chatHeadText: #080808;
--chatHeadTextActive: #080808;
--lightChatHeadHover: #1e1f201a;
--group-header: #929292;
--group-drop-shadow: rgb(17 17 26 / 10%) 0px 1px 0px;
}
html[theme="dark"] {
@ -43,6 +62,17 @@ html[theme="dark"] {
--border: #0b305e;
--border2: #0b305e;
--copybutton: #d0d6de;
--chat-group: #ffffff;
--chat-bubble: #9694941a;
--chat-bubble-bg: #2d3749;
--chat-bubble-msg-color: #ffffff;
--reaction-bubble-outline: #ffffff;
--chat-menu-bg: #32394c;
--chat-menu-outline: #32394c;
--chat-menu-icon: #ffffff;
--chat-menu-icon-hover: #a49a9a36;
--block-user-bg-hover: #121a2f;
--paperclip-icon: #d0c9c9;
--sectxt: #bbc3cd;
--vdicon: #d0d6de;
--tradehead: #008fd5;
@ -53,11 +83,12 @@ html[theme="dark"] {
--relaynodetxt: #d4d4d4;
--menuhover: #008fd5;
--menuactive: #008fd5;
--mainmenutext:#008fd5;
--mainmenutexthover:#0f1a2e;
--menuactivergb: 0, 143, 213;
--mainmenutext: #008fd5;
--mainmenutexthover: #0f1a2e;
--switchbackground: #eeeeee;
--switchborder: #03a9f4;
--sidetopbar: #070d19;
--sidetopbar: #070d19;
--nav-selected-color: #0f1a2e;
--nav-selected-color-text: #76c8f5;
--nav-color-active: #d1d1d1;
@ -68,4 +99,11 @@ html[theme="dark"] {
--nav-border-selected-color: #76c8f5;
--error: #d50000;
--background: url("/img/qortal_background_dark_.jpg");
}
--chatHeadBg: #008fd5;
--chatHeadBgActive: #0f1a2e;
--chatHeadText: #ffffff;
--chatHeadTextActive: #ffffff;
--lightChatHeadHover: #e0e1e31a;
--group-header: #c8c8c8;
--group-drop-shadow: rgb(191 191 191 / 32%) 0px 1px 0px
}

File diff suppressed because it is too large Load Diff

View File

@ -76,7 +76,8 @@
"rollup": "3.10.0",
"rollup-plugin-node-globals": "1.4.0",
"rollup-plugin-progress": "1.1.2",
"rollup-plugin-scss": "3.0.0"
"rollup-plugin-scss": "3.0.0",
"rollup-plugin-web-worker-loader": "1.6.1"
},
"engines": {
"node": ">=16.17.1"

View File

@ -3,6 +3,8 @@ import { connect } from 'pwa-helpers'
import { store } from '../store.js'
import { doPageUrl } from '../redux/app/app-actions.js'
import { translate, translateUnsafeHTML } from 'lit-translate'
import WebWorker from 'web-worker:./computePowWorker.js';
import { routes } from '../plugins/routes.js';
import '@material/mwc-icon'
import '@material/mwc-button'
@ -94,6 +96,8 @@ class AppInfo extends connect(store)(LitElement) {
this.nodeStatus = {}
this.pageUrl = ''
this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light'
this.publicKeyisOnChainConfirmation = false
this.interval
}
render() {
@ -108,10 +112,114 @@ class AppInfo extends connect(store)(LitElement) {
`
}
async confirmPublicKeyOnChain(address) {
const _computePow2 = async (chatBytes) => {
const difficulty = 14;
const path = window.parent.location.origin + '/memory-pow/memory-pow.wasm.full'
const worker = new WebWorker();
let nonce = null
let chatBytesArray = null
await new Promise((res, rej) => {
worker.postMessage({chatBytes, path, difficulty});
worker.onmessage = e => {
worker.terminate()
chatBytesArray = e.data.chatBytesArray
nonce = e.data.nonce
res()
}
})
let _response = await routes.sign_chat({
data: {
nonce: store.getState().app.selectedAddress.nonce,
chatBytesArray: chatBytesArray,
chatNonce: nonce
},
});
return _response
};
let stop = false
const checkPublicKey = async () => {
if (!stop) {
stop = true;
try {
if(this.publicKeyisOnChainConfirmation){
clearInterval(this.interval)
return
}
const myNode = store.getState().app.nodeConfig.knownNodes[store.getState().app.nodeConfig.node];
const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port;
const url = `${nodeUrl}/addresses/publickey/${address}`;
const res = await fetch(url)
let data = ''
try {
data = await res.text();
} catch (error) {
data = {
error: 'error'
}
}
if(data === 'false' && this.nodeInfo.isSynchronizing !== true){
let _reference = new Uint8Array(64);
window.crypto.getRandomValues(_reference);
let reference = window.parent.Base58.encode(_reference);
const chatRes = await routes.chat({
data: {
type: 19,
nonce: store.getState().app.selectedAddress.nonce,
params: {
lastReference: reference,
proofOfWorkNonce: 0,
fee: 0,
timestamp: Date.now(),
},
disableModal: true
},
disableModal: true,
});
try {
const powRes = await _computePow2(chatRes)
if(powRes === true){
clearInterval(this.interval)
this.publicKeyisOnChainConfirmation = true
}
} catch (error) {
console.error(error)
}
}
if (!data.error && data !== 'false' && data) {
clearInterval(this.interval)
this.publicKeyisOnChainConfirmation = true
}
} catch (error) {
}
stop = false
}
};
this.interval = setInterval(checkPublicKey, 5000);
}
firstUpdated() {
this.getNodeInfo()
this.getCoreInfo()
try {
this.confirmPublicKeyOnChain(store.getState().app.selectedAddress.address)
} catch (error) {
console.error(error)
}
setInterval(() => {
this.getNodeInfo()
this.getCoreInfo()

View File

@ -142,8 +142,6 @@ class AppView extends connect(store)(LitElement) {
app-drawer {
box-shadow: var(--shadow-2);
background: var(--sidetopbar);
--app-drawer-scrim-background: rgba(0,0,0,0);
}
app-header {
@ -154,6 +152,8 @@ class AppView extends connect(store)(LitElement) {
background: var(--sidetopbar);
color: var(--black);
border-top: var(--border);
height: 48px;
padding: 3px;
}
paper-progress {
@ -183,24 +183,26 @@ class AppView extends connect(store)(LitElement) {
background: var(--sidetopbar);
}
.sideBarMenu{
.sideBarMenu {
overflow-y: auto;
flex: 1 1;
}
#sideBar::-webkit-scrollbar {
width: 7px;
background-color: transparent;
.sideBarMenu::-webkit-scrollbar-track {
background-color: whitesmoke;
border-radius: 7px;
}
#sideBar::-webkit-scrollbar-track {
background-color: transparent;
.sideBarMenu::-webkit-scrollbar {
width: 6px;
border-radius: 7px;
background-color: whitesmoke;
}
#sideBar::-webkit-scrollbar-thumb {
background-color: #333;
border-radius: 6px;
border: 3px solid #333;
.sideBarMenu::-webkit-scrollbar-thumb {
background-color: rgb(180, 176, 176);
border-radius: 7px;
transition: all 0.3s ease-in-out;
}
#balanceheader {
@ -323,6 +325,11 @@ class AppView extends connect(store)(LitElement) {
0%,100% { opacity: 0; }
50% { opacity: 10; }
}
.sideBarMenu::-webkit-scrollbar-thumb:hover {
background-color: rgb(148, 146, 146);
cursor: pointer;
}
`
]
}

View File

@ -0,0 +1,82 @@
import { Sha256 } from 'asmcrypto.js'
function sbrk(size, heap){
let brk = 512 * 1024 // stack top
let old = brk
brk += size
if (brk > heap.length)
throw new Error('heap exhausted')
return old
}
self.addEventListener('message', async e => {
const response = await computePow(e.data.chatBytes, e.data.path, e.data.difficulty)
postMessage(response)
})
const memory = new WebAssembly.Memory({ initial: 256, maximum: 256 })
const heap = new Uint8Array(memory.buffer)
const computePow = async (chatBytes, path, difficulty) => {
let response = null
await new Promise((resolve, reject)=> {
const _chatBytesArray = Object.keys(chatBytes).map(function (key) { return chatBytes[key]; });
const chatBytesArray = new Uint8Array(_chatBytesArray);
const chatBytesHash = new Sha256().process(chatBytesArray).finish().result;
const hashPtr = sbrk(32, heap);
const hashAry = new Uint8Array(memory.buffer, hashPtr, 32);
hashAry.set(chatBytesHash);
const workBufferLength = 8 * 1024 * 1024;
const workBufferPtr = sbrk(workBufferLength, heap);
const importObject = {
env: {
memory: memory
},
};
function loadWebAssembly(filename, imports) {
// Fetch the file and compile it
return fetch(filename)
.then(response => response.arrayBuffer())
.then(buffer => WebAssembly.compile(buffer))
.then(module => {
// Create the instance.
return new WebAssembly.Instance(module, importObject);
});
}
loadWebAssembly(path)
.then(wasmModule => {
response = {
nonce : wasmModule.exports.compute2(hashPtr, workBufferPtr, workBufferLength, difficulty),
chatBytesArray
}
resolve()
});
})
return response
}

View File

@ -7,6 +7,17 @@ html {
--border: #d0d6de;
--border2: #dde2e8;
--copybutton: #707584;
--chat-group: #080808;
--chat-bubble: #9f9f9f0a;
--chat-bubble-bg: #e6e6e6;
--chat-bubble-msg-color: #080808;
--reaction-bubble-outline: #6b6969;
--chat-menu-bg: #ffffff;
--chat-menu-outline: #dad9d9;
--chat-menu-icon: #3b3b3c;
--chat-menu-icon-hover: #dad9d9;
--block-user-bg-hover: #dad9d9;
--paperclip-icon: #494949;
--sectxt: #576374;
--vdicon: #707b8a;
--tradehead: #6a6c75;
@ -17,11 +28,11 @@ html {
--relaynodetxt: #646464;
--menuhover: #eeeeee;
--menuactive: #ebebeb;
--mainmenutext:#080808;
--mainmenutexthover:#080808;
--mainmenutext: #080808;
--mainmenutexthover: #080808;
--switchbackground: #666666;
--switchborder: #333333;
--sidetopbar: #ffffff;
--sidetopbar: #ffffff;
--nav-selected-color: #dddddd;
--nav-selected-color-text: #333333;
--nav-color-active: #d1d1d1;
@ -31,6 +42,12 @@ html {
--nav-border-color: #eeeeee;
--nav-border-selected-color: #03a9f4;
--background: url("/img/qortal_background_light_.jpg");
--chatHeadBg: #ebebeb;
--chatHeadBgActive: #ebebeb;
--chatHeadText: #080808;
--chatHeadTextActive: #080808;
--group-header: #929292;
--group-drop-shadow: rgb(17 17 26 / 10%) 0px 1px 0px;
}
html[theme="dark"] {
@ -42,6 +59,17 @@ html[theme="dark"] {
--border: #0b305e;
--border2: #0b305e;
--copybutton: #d0d6de;
--chat-group: #ffffff;
--chat-bubble: #9694941a;
--chat-bubble-bg: #2d3749;
--chat-bubble-msg-color: #ffffff;
--reaction-bubble-outline: #ffffff;
--chat-menu-bg: #32394c;
--chat-menu-outline: #32394c;
--chat-menu-icon: #ffffff;
--chat-menu-icon-hover: #a49a9a36;
--block-user-bg-hover: #121a2f;
--paperclip-icon: #d0c9c9;
--sectxt: #bbc3cd;
--vdicon: #d0d6de;
--tradehead: #008fd5;
@ -52,11 +80,11 @@ html[theme="dark"] {
--relaynodetxt: #d4d4d4;
--menuhover: #008fd5;
--menuactive: #008fd5;
--mainmenutext:#008fd5;
--mainmenutexthover:#0f1a2e;
--mainmenutext: #008fd5;
--mainmenutexthover: #0f1a2e;
--switchbackground: #eeeeee;
--switchborder: #03a9f4;
--sidetopbar: #070d19;
--sidetopbar: #070d19;
--nav-selected-color: #0f1a2e;
--nav-selected-color-text: #76c8f5;
--nav-color-active: #d1d1d1;
@ -66,4 +94,10 @@ html[theme="dark"] {
--nav-border-color: #0b305e;
--nav-border-selected-color: #76c8f5;
--background: url("/img/qortal_background_dark_.jpg");
}
--chatHeadBg: #008fd5;
--chatHeadBgActive: #0f1a2e;
--chatHeadText: #ffffff;
--chatHeadTextActive: #ffffff;
--group-header: #c8c8c8;
--group-drop-shadow: rgb(191 191 191 / 32%) 0px 1px 0px
}

View File

@ -7,6 +7,8 @@ const commonjs = require('@rollup/plugin-commonjs')
const alias = require('@rollup/plugin-alias')
const terser = require('@rollup/plugin-terser');
const scss = require('rollup-plugin-scss')
const webWorkerLoader = require('rollup-plugin-web-worker-loader');
const generateES5BuildConfig = require('./generateES5BuildConfig')
@ -61,6 +63,7 @@ const generateBuildConfig = ({ elementComponents, functionalComponents, otherOut
commonjs(),
globals(),
progress(),
webWorkerLoader(),
scss({
output: options.sassOutputDir
}),

View File

@ -5,6 +5,7 @@ const commonjs = require('@rollup/plugin-commonjs');
const progress = require('rollup-plugin-progress');
const terser = require('@rollup/plugin-terser');
const alias = require('@rollup/plugin-alias');
const webWorkerLoader = require('rollup-plugin-web-worker-loader');
const path = require('path');
@ -37,6 +38,7 @@ const generateRollupConfig = (file, { outputDir, aliases }) => {
}),
commonjs(),
progress(),
webWorkerLoader(),
babel.babel({
babelHelpers: 'bundled',
exclude: 'node_modules/**'

View File

@ -1,5 +1,6 @@
'use strict'
import ChatBase from './chat/ChatBase.js'
"use strict";
import ChatBase from "./chat/ChatBase.js"
import { QORT_DECIMALS } from "../constants.js"
export default class PublicizeTransaction extends ChatBase {
constructor() {
@ -8,16 +9,19 @@ export default class PublicizeTransaction extends ChatBase {
this.fee = 0
}
set proofOfWorkNonce(proofOfWorkNonce) {
this._proofOfWorkNonce = this.constructor.utils.int32ToBytes(proofOfWorkNonce)
}
get params() {
const params = super.params
params.push(
this._proofOfWorkNonce,
this._feeBytes
)
return params
}
set proofOfWorkNonce(proofOfWorkNonce) {
this._proofOfWorkNonce = this.constructor.utils.int32ToBytes(proofOfWorkNonce)
}
set fee(fee) {
this._fee = fee * QORT_DECIMALS
this._feeBytes = this.constructor.utils.int64ToBytes(this._fee)
}
get params() {
const params = super.params;
params.push(
this._proofOfWorkNonce,
this._feeBytes
)
return params;
}
}

View File

@ -0,0 +1,72 @@
'use strict';
import TransactionBase from '../TransactionBase.js'
import Base58 from '../../deps/Base58.js'
import { store } from '../../../api.js'
import { QORT_DECIMALS } from "../../constants.js"
export default class UpdateGroupTransaction extends TransactionBase {
constructor() {
super()
this.type = 23
}
render(html) {
const conf = store.getState().config
return html`
Are you sure to update this group ?
<div style="background: #eee; padding: 8px; margin: 8px 0; border-radius: 5px;">
</div>
On pressing confirm, the group details will be updated!
`
}
set fee(fee) {
this._fee = fee * QORT_DECIMALS
this._feeBytes = this.constructor.utils.int64ToBytes(this._fee)
}
set newOwner(newOwner) {
this._newOwner = newOwner instanceof Uint8Array ? newOwner : this.constructor.Base58.decode(newOwner)
}
set newIsOpen(newIsOpen) {
this._rGroupType = new Uint8Array(1)
this._rGroupType[0] = newIsOpen
}
set newDescription(newDescription) {
this._rGroupDescBytes = this.constructor.utils.stringtoUTF8Array(newDescription.toLocaleLowerCase())
this._rGroupDescLength = this.constructor.utils.int32ToBytes(this._rGroupDescBytes.length)
}
set newApprovalThreshold(newApprovalThreshold) {
this._rGroupApprovalThreshold = new Uint8Array(1)
this._rGroupApprovalThreshold[0] = newApprovalThreshold;
}
set newMinimumBlockDelay(newMinimumBlockDelay) {
this._rGroupMinimumBlockDelayBytes = this.constructor.utils.int32ToBytes(newMinimumBlockDelay)
}
set newMaximumBlockDelay(newMaximumBlockDelay) {
this._rGroupMaximumBlockDelayBytes = this.constructor.utils.int32ToBytes(newMaximumBlockDelay)
}
set _groupId(_groupId){
this._groupBytes = this.constructor.utils.int32ToBytes(_groupId)
}
get params() {
const params = super.params
params.push(
this._groupBytes,
this._newOwner,
this._rGroupDescLength,
this._rGroupDescBytes,
this._rGroupType,
this._rGroupApprovalThreshold,
this._rGroupMinimumBlockDelayBytes,
this._rGroupMaximumBlockDelayBytes,
this._feeBytes
)
console.log('verify params', params)
return params
}
}

View File

@ -16,6 +16,7 @@ import GroupKickTransaction from './groups/GroupKickTransaction.js'
import GroupInviteTransaction from './groups/GroupInviteTransaction.js'
import CancelGroupInviteTransaction from './groups/CancelGroupInviteTransaction.js'
import JoinGroupTransaction from './groups/JoinGroupTransaction.js'
import UpdateGroupTransaction from './groups/UpdateGroupTransaction.js'
import LeaveGroupTransaction from './groups/LeaveGroupTransaction.js'
import RewardShareTransaction from './reward-share/RewardShareTransaction.js'
import RemoveRewardShareTransaction from './reward-share/RemoveRewardShareTransaction.js'

View File

@ -8,6 +8,8 @@ const commonjs = require('@rollup/plugin-commonjs');
const alias = require('@rollup/plugin-alias');
const terser = require('@rollup/plugin-terser');
const babel = require('@rollup/plugin-babel');
const webWorkerLoader = require('rollup-plugin-web-worker-loader');
const aliases = {};
@ -40,6 +42,7 @@ const generateRollupConfig = (inputFile, outputFile) => {
commonjs(),
globals(),
progress(),
webWorkerLoader(),
babel.babel({
babelHelpers: 'bundled',
exclude: 'node_modules/**',

View File

@ -17,9 +17,31 @@
"author": "QORTAL <admin@qortal.org>",
"license": "GPL-3.0",
"dependencies": {
"@lit-labs/motion": "1.0.3",
"@material/mwc-list": "0.27.0",
"@material/mwc-select": "0.27.0",
"emoji-picker-js": "https://github.com/Qortal/emoji-picker-js"
"@tiptap/core": "2.0.0-beta.209",
"@tiptap/extension-image": "2.0.0-beta.209",
"@tiptap/extension-placeholder": "2.0.0-beta.209",
"@tiptap/extension-underline": "2.0.0-beta.209",
"@tiptap/extension-highlight": "2.0.0-beta.209",
"@tiptap/html": "2.0.0-beta.209",
"@tiptap/starter-kit": "2.0.0-beta.209",
"asmcrypto.js": "2.3.2",
"compressorjs": "1.1.1",
"emoji-picker-js": "https://github.com/Qortal/emoji-picker-js",
"prosemirror-commands": "1.5.0",
"prosemirror-dropcursor": "1.6.1",
"prosemirror-gapcursor": "1.3.1",
"prosemirror-history": "1.3.0",
"prosemirror-keymap": "1.2.0",
"prosemirror-model": "1.18.3",
"prosemirror-schema-list": "1.2.2",
"prosemirror-state": "1.4.2",
"prosemirror-transform": "1.7.0",
"prosemirror-view": "1.29.1",
"localforage": "1.10.0",
"short-unique-id": "4.4.4"
},
"devDependencies": {
"@babel/core": "7.20.12",
@ -60,7 +82,8 @@
"lit-translate": "2.0.1",
"rollup": "3.10.0",
"rollup-plugin-node-globals": "1.4.0",
"rollup-plugin-progress": "1.1.2"
"rollup-plugin-progress": "1.1.2",
"rollup-plugin-web-worker-loader": "1.6.1"
},
"engines": {
"node": ">=16.17.1"

View File

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

View File

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

View File

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

View File

@ -13,14 +13,18 @@ class ChatHead extends LitElement {
config: { type: Object },
chatInfo: { type: Object },
iconName: { type: String },
activeChatHeadUrl: { type: String }
activeChatHeadUrl: { type: String },
isImageLoaded: { type: Boolean },
setActiveChatHeadUrl: {attribute: false}
}
}
static get styles() {
return css`
li {
padding: 10px 2px 20px 5px;
width: 100%;
padding: 7px 5px 7px 5px;
cursor: pointer;
width: 100%;
}
@ -37,7 +41,7 @@ class ChatHead extends LitElement {
.img-icon {
float: left;
font-size:40px;
color: var(--black);
color: var(--chat-group);
}
.about {
@ -76,14 +80,54 @@ class ChatHead extends LitElement {
this.chatInfo = {}
this.iconName = ''
this.activeChatHeadUrl = ''
this.isImageLoaded = false
this.imageFetches = 0
}
createImage(imageUrl) {
const imageHTMLRes = new Image();
imageHTMLRes.src = imageUrl;
imageHTMLRes.style= "width:40px; height:40px; float: left; border-radius:50%";
imageHTMLRes.onclick= () => {
this.openDialogImage = true;
}
imageHTMLRes.onload = () => {
this.isImageLoaded = true;
}
imageHTMLRes.onerror = () => {
if (this.imageFetches < 4) {
setTimeout(() => {
this.imageFetches = this.imageFetches + 1;
imageHTMLRes.src = imageUrl;
}, 500);
} else {
this.isImageLoaded = false
}
};
return imageHTMLRes;
}
render() {
let avatarImg = '';
let backupAvatarImg = ''
if(this.chatInfo.name){
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port;
const avatarUrl = `${nodeUrl}/arbitrary/THUMBNAIL/${this.chatInfo.name}/qortal_avatar?async=true&apiKey=${myNode.apiKey}`;
avatarImg= this.createImage(avatarUrl)
}
return html`
<li @click=${() => this.getUrl(this.chatInfo.url)} class="clearfix ${this.activeChatHeadUrl === this.chatInfo.url ? 'active' : ''}">
<mwc-icon class="img-icon">account_circle</mwc-icon>
${this.isImageLoaded ? html`${avatarImg}` : html`` }
${!this.isImageLoaded && !this.chatInfo.name && !this.chatInfo.groupName ? html`<mwc-icon class="img-icon">account_circle</mwc-icon>` : html`` }
${!this.isImageLoaded && this.chatInfo.name ? html`<div style="width:40px; height:40px; float: left; border-radius:50%; background: ${this.activeChatHeadUrl === this.chatInfo.url ? 'var(--chatHeadBgActive)' : 'var(--chatHeadBg)' }; color: ${this.activeChatHeadUrl === this.chatInfo.url ? 'var(--chatHeadTextActive)' : 'var(--chatHeadText)' }; font-weight:bold; display: flex; justify-content: center; align-items: center; text-transform: capitalize">${this.chatInfo.name.charAt(0)}</div>`: ''}
${!this.isImageLoaded && this.chatInfo.groupName ? html`<div style="width:40px; height:40px; float: left; border-radius:50%; background: ${this.activeChatHeadUrl === this.chatInfo.url ? 'var(--chatHeadBgActive)' : 'var(--chatHeadBg)' }; color: ${this.activeChatHeadUrl === this.chatInfo.url ? 'var(--chatHeadTextActive)' : 'var(--chatHeadText)' }; font-weight:bold; display: flex; justify-content: center; align-items: center; text-transform: capitalize">${this.chatInfo.groupName.charAt(0)}</div>`: ''}
<div class="about">
<div class="name"><span style="float:left; padding-left: 8px; color: var(--black);">${this.chatInfo.groupName ? this.chatInfo.groupName : this.chatInfo.name !== undefined ? this.chatInfo.name : this.chatInfo.address.substr(0, 15)} </span> <mwc-icon style="float:right; padding: 0 1rem; color: var(--black);">${this.chatInfo.groupId !== undefined ? 'lock_open' : 'lock'}</mwc-icon> </div>
<div class="name"><span style="float:left; padding-left: 8px; color: var(--chat-group);">${this.chatInfo.groupName ? this.chatInfo.groupName : this.chatInfo.name !== undefined ? this.chatInfo.name : this.chatInfo.address.substr(0, 15)} </span> <mwc-icon style="float:right; padding: 0 1rem; color: var(--chat-group);">${this.chatInfo.groupId !== undefined ? 'lock_open' : 'lock'}</mwc-icon> </div>
</div>
</li>
`
@ -108,8 +152,19 @@ class ChatHead extends LitElement {
parentEpml.imReady()
}
shouldUpdate(changedProperties) {
if(changedProperties.has('activeChatHeadUrl')){
return true
}
if(changedProperties.has('chatInfo')){
return true
}
return false
}
getUrl(chatUrl) {
this.onPageNavigation(`/app/q-chat/${chatUrl}`)
this.setActiveChatHeadUrl(chatUrl)
}
onPageNavigation(pageUrl) {

View File

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

View File

@ -92,11 +92,11 @@ class ChatModals extends LitElement {
// Send Private Message
_sendMessage() {
this.isLoading = true
this.isLoading = true;
const recipient = this.shadowRoot.getElementById('sendTo').value
const messageBox = this.shadowRoot.getElementById('messageBox')
const messageText = messageBox.value
const recipient = this.shadowRoot.getElementById('sendTo').value;
const messageBox = this.shadowRoot.getElementById('messageBox');
const messageText = messageBox.value;
if (recipient.length === 0) {
this.isLoading = false
@ -105,22 +105,21 @@ class ChatModals extends LitElement {
} else {
this.sendMessage()
}
}
};
async sendMessage() {
this.isLoading = true
const _recipient = this.shadowRoot.getElementById('sendTo').value
const messageBox = this.shadowRoot.getElementById('messageBox')
const messageText = messageBox.value
let recipient
this.isLoading = true;
const _recipient = this.shadowRoot.getElementById('sendTo').value;
const messageBox = this.shadowRoot.getElementById('messageBox');
const messageText = messageBox.value;
let recipient;
const validateName = async (receiverName) => {
let myRes
let myRes;
let myNameRes = await parentEpml.request('apiCall', {
type: 'api',
url: `/names/${receiverName}`
})
});
if (myNameRes.error === 401) {
myRes = false
@ -128,7 +127,7 @@ class ChatModals extends LitElement {
myRes = myNameRes
}
return myRes
return myRes;
}
const myNameRes = await validateName(_recipient)
@ -139,7 +138,6 @@ class ChatModals extends LitElement {
recipient = myNameRes.owner
}
let _reference = new Uint8Array(64);
window.crypto.getRandomValues(_reference);
@ -175,7 +173,13 @@ class ChatModals extends LitElement {
};
const sendMessageRequest = async (isEncrypted, _publicKey) => {
const messageObject = {
messageText,
images: [''],
repliedTo: '',
version: 1
}
const stringifyMessageObject = JSON.stringify(messageObject)
let chatResponse = await parentEpml.request('chat', {
type: 18,
nonce: this.selectedAddress.nonce,
@ -184,7 +188,7 @@ class ChatModals extends LitElement {
recipient: recipient,
recipientPublicKey: _publicKey,
hasChatReference: 0,
message: messageText,
message: stringifyMessageObject,
lastReference: reference,
proofOfWorkNonce: 0,
isEncrypted: isEncrypted,
@ -361,7 +365,10 @@ class ChatModals extends LitElement {
<p style='margin-bottom:0;'>
<textarea class='textarea' @keydown=${(e) => this._textArea(e)} ?disabled=${this.isLoading} id='messageBox' placeholder='${translate('welcomepage.wcchange5')}' rows='1'></textarea>
</p>
<mwc-button ?disabled='${this.isLoading}' slot='primaryAction' @click=${this._sendMessage}>${translate('welcomepage.wcchange6')}
<mwc-button ?disabled='${this.isLoading}' slot='primaryAction' @click=${() => {
this._sendMessage();
}
}>${translate('welcomepage.wcchange6')}
</mwc-button>
<mwc-button
?disabled='${this.isLoading}'

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,306 @@
import { LitElement, html, css } from "lit";
import { render } from "lit/html.js";
import { get, translate } from "lit-translate";
import { Epml } from "../../../epml";
import { getUserNameFromAddress } from "../../utils/getUserNameFromAddress";
import snackbar from "./snackbar.js";
import "@material/mwc-button";
import "@material/mwc-dialog";
import "@polymer/paper-spinner/paper-spinner-lite.js";
import '@polymer/paper-progress/paper-progress.js';
import "@material/mwc-icon";
import '@vaadin/button';
import "./WrapperModal";
import "./TipUser"
import "./UserInfo/UserInfo";
class ChatRightPanel extends LitElement {
static get properties() {
return {
leaveGroupObj: { type: Object },
error: { type: Boolean },
chatHeads: { type: Array },
groupAdmin: { attribute: false },
groupMembers: { attribute: false },
selectedHead: { type: Object },
toggle: { attribute: false },
getMoreMembers:{ attribute: false },
setOpenPrivateMessage: { attribute: false },
userName: { type: String },
walletBalance: { type: Number },
sendMoneyLoading: { type: Boolean },
btnDisable: { type: Boolean },
errorMessage: { type: String },
successMessage: { type: String },
setOpenTipUser: { attribute: false },
setOpenUserInfo: { attribute: false },
setUserName: { attribute: false },
}
}
constructor() {
super()
this.leaveGroupObj = {}
this.leaveFee = 0.001
this.error = false
this.chatHeads = []
this.groupAdmin = []
this.groupMembers = []
this.observerHandler = this.observerHandler.bind(this)
this.viewElement = ''
this.downObserverElement = ''
this.myAddress = window.parent.reduxStore.getState().app.selectedAddress.address
this.sendMoneyLoading = false
this.btnDisable = false
this.errorMessage = ""
this.successMessage = ""
}
static get styles() {
return css`
.top-bar-icon {
cursor: pointer;
height: 18px;
width: 18px;
transition: 0.2s all;
}
.top-bar-icon:hover {
color: var(--black);
}
.modal-button {
font-family: Roboto, sans-serif;
font-size: 16px;
color: var(--mdc-theme-primary);
background-color: transparent;
padding: 8px 10px;
border-radius: 5px;
border: none;
transition: all 0.3s ease-in-out;
}
.close-row {
width: 100%;
display: flex;
justify-content: flex-end;
height: 50px;
flex:0
}
.container-body {
width: 100%;
display: flex;
flex-direction: column;
flex-grow: 1;
overflow:auto;
margin-top: 5px;
padding: 0px 6px;
box-sizing: border-box;
}
.container-body::-webkit-scrollbar-track {
background-color: whitesmoke;
border-radius: 7px;
}
.container-body::-webkit-scrollbar {
width: 6px;
border-radius: 7px;
background-color: whitesmoke;
}
.container-body::-webkit-scrollbar-thumb {
background-color: rgb(180, 176, 176);
border-radius: 7px;
transition: all 0.3s ease-in-out;
}
.container-body::-webkit-scrollbar-thumb:hover {
background-color: rgb(148, 146, 146);
cursor: pointer;
}
p {
color: var(--black);
margin: 0px;
padding: 0px;
word-break: break-all;
}
.container {
display: flex;
width: 100%;
flex-direction: column;
height: 100%;
}
.chat-right-panel-label {
font-family: Montserrat, sans-serif;
color: var(--group-header);
padding: 5px;
font-size: 13px;
user-select: none;
}
.group-info {
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 10px;
}
.group-name {
font-family: Raleway, sans-serif;
font-size: 20px;
color: var(--chat-bubble-msg-color);
text-align: center;
user-select: none;
}
.group-description {
font-family: Roboto, sans-serif;
color: var(--chat-bubble-msg-color);
letter-spacing: 0.3px;
font-weight: 300;
font-size: 14px;
margin-top: 15px;
word-break: break-word;
user-select: none;
}
.group-subheader {
font-family: Montserrat, sans-serif;
font-size: 14px;
color: var(--chat-bubble-msg-color);
}
.group-data {
font-family: Roboto, sans-serif;
letter-spacing: 0.3px;
font-weight: 300;
font-size: 14px;
color: var(--chat-bubble-msg-color);
}
`
}
firstUpdated() {
this.viewElement = this.shadowRoot.getElementById('viewElement');
this.downObserverElement = this.shadowRoot.getElementById('downObserver');
this.elementObserver();
}
async updated(changedProperties) {
if (changedProperties && changedProperties.has('selectedHead')) {
if (this.selectedHead !== {}) {
const userName = await getUserNameFromAddress(this.selectedHead.address);
this.userName = userName;
}
}
}
elementObserver() {
const options = {
root: this.viewElement,
rootMargin: '0px',
threshold: 1
}
// identify an element to observe
const elementToObserve = this.downObserverElement;
// passing it a callback function
const observer = new IntersectionObserver(this.observerHandler, options);
// call `observe()` on that MutationObserver instance,
// passing it the element to observe, and the options object
observer.observe(elementToObserve);
}
observerHandler(entries) {
if (!entries[0].isIntersecting) {
return
} else {
if(this.groupMembers.length < 20){
return
}
console.log('this.leaveGroupObjp', this.leaveGroupObj)
this.getMoreMembers(this.leaveGroupObj.groupId)
}
}
render() {
const owner = this.groupAdmin.filter((admin)=> admin.address === this.leaveGroupObj.owner)
return html`
<div class="container">
<div class="close-row" style="margin-top: 15px">
<vaadin-icon class="top-bar-icon" @click=${()=> this.toggle(false)} style="margin: 0px 10px" icon="vaadin:close" slot="icon"></vaadin-icon>
</div>
<div id="viewElement" class="container-body">
<p class="group-name">${this.leaveGroupObj && this.leaveGroupObj.groupName}</p>
<div class="group-info">
<p class="group-description">${this.leaveGroupObj && this.leaveGroupObj.description}</p>
<p class="group-subheader">Members: <span class="group-data">${this.leaveGroupObj && this.leaveGroupObj.memberCount}</span></p>
<p class="group-subheader">Date created : <span class="group-data">${new Date(this.leaveGroupObj.created).toLocaleDateString("en-US")}</span></p>
</div>
<br />
<p class="chat-right-panel-label">GROUP OWNER</p>
${owner.map((item) => {
return html`<chat-side-nav-heads
activeChatHeadUrl=""
.setActiveChatHeadUrl=${(val) => {
if (val.address === this.myAddress) return;
console.log({ val });
this.selectedHead = val;
this.setOpenUserInfo(true);
this.setUserName({
sender: val.address,
senderName: val.name ? val.name : ""
});
}}
chatInfo=${JSON.stringify(item)}
></chat-side-nav-heads>`
})}
<p class="chat-right-panel-label">ADMINS</p>
${this.groupAdmin.map((item) => {
return html`<chat-side-nav-heads
activeChatHeadUrl=""
.setActiveChatHeadUrl=${(val) => {
if (val.address === this.myAddress) return;
console.log({ val });
this.selectedHead = val;
this.setOpenUserInfo(true);
this.setUserName({
sender: val.address,
senderName: val.name ? val.name : ""
});
}}
chatInfo=${JSON.stringify(item)}
></chat-side-nav-heads>`
})}
<p class="chat-right-panel-label">MEMBERS</p>
${this.groupMembers.map((item) => {
return html`<chat-side-nav-heads
activeChatHeadUrl=""
.setActiveChatHeadUrl=${(val) => {
if (val.address === this.myAddress) return;
console.log({ val });
this.selectedHead = val;
this.setOpenUserInfo(true);
this.setUserName({
sender: val.address,
senderName: val.name ? val.name : ""
});
}}
chatInfo=${JSON.stringify(item)}
></chat-side-nav-heads>`
})}
<div id='downObserver'></div>
</div>
</div>
</div>
`
}
}
customElements.define("chat-right-panel", ChatRightPanel)

View File

@ -15,6 +15,12 @@ export const chatStyles = css`
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
--mdc-theme-primary: rgb(3, 169, 244);
--mdc-theme-secondary: var(--mdc-theme-primary);
--mdc-dialog-max-width: 85vw;
--mdc-dialog-max-height: 95vh;
}
* :focus-visible {
outline: none;
}
*::-webkit-scrollbar-track {
@ -35,110 +41,214 @@ export const chatStyles = css`
ul {
list-style: none;
margin: 0;
padding: 20px;
}
.last-message-ref {
position: fixed;
font-size: 20px;
right: 40px;
bottom: 100px;
width: 50;
height: 50;
z-index: 5;
opacity: 0;
color: black;
background-color: white;
border-radius: 50%;
transition: all 0.1s ease-in-out;
}
.last-message-ref:hover {
cursor: pointer;
transform: scale(1.1);
padding: 20px 17px;
}
.chat-list {
overflow-y: auto;
overflow-x: hidden;
height: 92vh;
height: 100%;
box-sizing: border-box;
}
.message-data {
width: 92%;
margin-bottom: 15px;
margin-left: 50px;
margin-left: 55px;
}
.message-data-name {
color: var(--black);
user-select: none;
color: #03a9f4;
margin-bottom: 5px;
}
.forwarded-text {
user-select: none;
color: #03a9f4;
margin-bottom: 5px;
}
.message-data-forward {
user-select: none;
color: var(--mainmenutext);
margin-bottom: 5px;
font-size: 12px;
}
.message-data-my-name {
color: #cf21e8;
text-shadow: 0 0 3px #cf21e8;
}
.message-data-time {
color: #a8aab1;
color: #888888;
font-size: 13px;
padding-left: 6px;
padding-bottom: 4px;
user-select: none;
display: flex;
width: 100%;
padding-top: 2px;
}
.message-data-level {
color: #03a9f4;
.message-data-time-hidden {
visibility: hidden;
transition: all 0.1s ease-in-out;
color: #888888;
font-size: 13px;
padding-left: 8px;
padding-bottom: 4px;
user-select: none;
display: flex;
width: 100%;
padding-top: 2px;
}
.message-user-info {
display: flex;
justify-content: space-between;
width: 100%;
gap: 10px;
}
.chat-bubble-container {
display:flex;
gap: 7px;
}
.message-container {
position: relative;
}
.message-subcontainer1 {
position: relative;
display: flex;
align-items: flex-end;
}
.message-subcontainer2 {
position: relative;
display: flex;
background-color: var(--chat-bubble-bg);
flex-grow: 0;
flex-direction: column;
align-items: flex-start;
justify-content: center;
border-radius: 5px;
padding: 12px 15px 4px 15px;
width: fit-content;
min-width: 150px;
}
.message-triangle {
position: relative;
}
.message-triangle:after {
content: "";
position: absolute;
bottom: 0px;
left: -9px;
width: 0;
height: 0;
border-style: solid;
border-width: 0px 0px 7px 9px;
border-color: transparent transparent var(--chat-bubble-bg) transparent;
}
.message-reactions {
background-color: transparent;
width: calc(100% - 54px);
margin-left: 54px;
}
.original-message {
position: relative;
display: flex;
flex-direction: column;
color: var(--chat-bubble-msg-color);
line-height: 19px;
user-select: text;
font-size: 15px;
width: 90%;
border-radius: 5px;
padding: 8px 5px 8px 25px;
margin-bottom: 10px;
cursor: pointer;
}
.original-message:before {
content: "";
position: absolute;
top: 5px;
left: 10px;
height: 75%;
width: 2.6px;
background-color: var(--mdc-theme-primary);
}
.original-message-sender {
margin: 0 0 5px 0;
color: var(--mdc-theme-primary);
cursor: pointer;
}
.replied-message {
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 300px;
max-height: 40px;
}
.replied-message p {
margin: 0px;
padding: 0px;
}
.message {
color: black;
padding: 12px 10px;
display: flex;
flex-direction: column;
color: var(--chat-bubble-msg-color);
line-height: 19px;
white-space: pre-line;
word-wrap: break-word;
overflow-wrap: anywhere;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
font-size: 16px;
border-radius: 7px;
margin-bottom: 20px;
width: 90%;
width: 100%;
position: relative;
}
.message:after {
bottom: 100%;
left: 93%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
white-space: pre-line;
word-wrap: break-word;
pointer-events: none;
border-bottom-color: #ddd;
border-width: 10px;
margin-left: -10px;
.message-data-avatar {
margin: 0px 10px 0px 3px;
width: 42px;
height: 42px;
float: left;
}
.message-parent {
padding: 3px;
background: rgba(245, 245, 245, 0);
transition: all 0.1s ease-in-out;
}
.message-parent:hover {
background: var(--chat-bubble);
border-radius: 8px;
}
.message-parent:hover .chat-hover {
display: block;
}
.message-parent:hover .message{
filter:brightness(0.90);
.message-parent:hover .message-data-time-hidden {
visibility: visible;
}
.chat-hover {
display: none;
position: absolute;
top: -38px;
left: 88.2%;
top: -25px;
right: 5px;
}
.emoji {
@ -148,27 +258,7 @@ export const chatStyles = css`
vertical-align: bottom;
object-fit: contain;
}
.my-message {
background: #d1d1d1;
border: 2px solid #eeeeee;
}
.my-message:after {
border-bottom-color: #d1d1d1;
left: 7%;
}
.other-message {
background: #f1f1f1;
border: 2px solid #dedede;
}
.other-message:after {
border-bottom-color: #f1f1f1;
left: 7%;
}
.align-left {
text-align: left;
}
@ -202,25 +292,29 @@ export const chatStyles = css`
display: flex;
flex-direction: row;
align-items: center;
gap: 5px;
background-color: white;
border: 1px solid #dad9d9;
background-color: var(--chat-menu-bg);
border: 1px solid var(--chat-menu-outline);
border-radius: 5px;
height:100%;
width: 100px;
position: relative;
}
.container:focus-visible {
outline: none;
}
.menu-icon {
width: 100%;
padding: 5px;
padding: 5px 7px;
display: flex;
align-items: center;
font-size: 13px;
color: var(--chat-menu-icon);
}
.menu-icon:hover {
background-color: #dad9d9;
border-radius: 5px;
background-color: var(--chat-menu-icon-hover);
transition: all 0.1s ease-in-out;
cursor: pointer;
}
@ -231,11 +325,12 @@ export const chatStyles = css`
.tooltip:before {
content: attr(data-text);
display: none;
position: absolute;
top: -47px;
left: 50%;
transform: translateX(-50%);
width: 90px;
width: auto;
padding: 10px;
border-radius: 10px;
background:#fff;
@ -244,7 +339,8 @@ export const chatStyles = css`
box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px;
font-size: 12px;
z-index: 5;
display: none;
white-space: nowrap;
overflow: hidden;
}
.tooltip:hover:before {
@ -269,17 +365,299 @@ export const chatStyles = css`
.block-user-container {
display: block;
position: absolute;
left: -48px;
left: -5px;
}
.block-user {
justify-content: space-between;
width: 100%;
padding: 5px 7px;
display: flex;
align-items: center;
font-size: 13px;
color: var(--chat-menu-icon);
justify-content: space-evenly;
border: 1px solid rgb(218, 217, 217);
border-radius: 5px;
background-color: white;
width: 100%;
background-color: var(--chat-menu-bg);
width: 150px;
height: 32px;
padding: 3px 8px;
box-shadow: rgba(77, 77, 82, 0.2) 0px 7px 29px 0px;
}
.block-user:hover {
cursor:pointer;
background-color: var(--block-user-bg-hover);
transition: all 0.1s ease-in-out 0s;
}
.reactions-bg {
background-color: #d5d5d5;
border-radius: 10px;
padding: 5px;
color: black;
margin-right: 10px;
transition: all 0.1s ease-in-out;
border: 0.5px solid transparent;
cursor: pointer;
}
.reactions-bg:hover {
border: 0.5px solid var(--reaction-bubble-outline);
}
.image-container {
display: flex;
}
.message-data-level {
height: 21px;
width: 21px;
overflow: hidden;
}
.defaultSize {
width: 45vh;
height: 40vh;
}
.image-deleted-msg {
font-family: Roboto, sans-serif;
font-size: 14px;
font-style: italic;
color: var(--chat-bubble-msg-color);
margin: 0;
padding-top: 10px;
}
.image-delete-icon {
margin-left: 5px;
height: 20px;
cursor: pointer;
visibility: hidden;
transition: .2s all;
opacity: 0.8;
color: rgb(228, 222, 222);
padding-left: 7px;
}
.image-delete-icon:hover {
opacity: 1;
}
.message-parent:hover .image-delete-icon {
visibility: visible;
}
.imageContainer {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.spinnerContainer {
display: flex;
width: 100%;
justify-content: center
}
.delete-image-msg {
font-family: Livvic, sans-serif;
font-size: 20px;
color: var(--chat-bubble-msg-color);
letter-spacing: 0.3px;
font-weight: 300;
text-align: center;
}
.modal-button-row {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.modal-button {
font-family: Roboto, sans-serif;
font-size: 16px;
color: var(--mdc-theme-primary);
background-color: transparent;
padding: 8px 10px;
border-radius: 5px;
border: none;
transition: all 0.3s ease-in-out;
}
.modal-button-red {
font-family: Roboto, sans-serif;
font-size: 16px;
color: #F44336;
background-color: transparent;
padding: 8px 10px;
border-radius: 5px;
border: none;
transition: all 0.3s ease-in-out;
}
.modal-button-red:hover {
cursor: pointer;
background-color: #f4433663;
}
.modal-button:hover {
cursor: pointer;
background-color: #03a8f475;
}
#messageContent p {
margin: 0px;
padding: 0px;
}
#messageContent p mark {
background-color: #ffe066;
border-radius: 0.25em;
box-decoration-break: clone;
padding: 0.125em 0;
}
#messageContent > * + * {
outline: none;
}
#messageContent ul,
ol {
padding: 0 1rem;
}
#messageContent h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
#messageContent code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
#messageContent pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
white-space: pre-wrap;
}
#messageContent pre code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
#messageContent img {
width: 1.7em;
height: 1.5em;
margin: 0px;
}
#messageContent blockquote {
padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
}
#messageContent hr {
border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1);
margin: 2rem 0;
}
.replied-message p {
margin: 0px;
padding: 0px;
}
.replied-message > * + * {
margin-top: 0.75em;
outline: none;
}
.replied-message ul,
ol {
padding: 0 1rem;
}
.replied-message h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
.replied-message code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
.replied-message pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
white-space: pre-wrap;
margin: 0px;
}
.replied-message pre code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
.replied-message img {
width: 1.7em;
height: 1.5em;
margin: 0px;
}
.replied-message blockquote {
padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
}
.replied-message hr {
border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1);
margin: 2rem 0;
}
.edited-message-style {
font-family: "Work Sans", sans-serif;
font-style: italic;
font-size: 13px;
visibility: visible;
}
.blink-bg{
border-radius: 8px;
animation: blinkingBackground 3s;
}
@keyframes blinkingBackground{
0% { background-color: rgba(var(--menuactivergb), 1)}
100% { background-color:rgba(var(--menuactivergb), 0)}
}
`

View File

@ -5,26 +5,54 @@ import { translate, get } from 'lit-translate';
import {unsafeHTML} from 'lit/directives/unsafe-html.js';
import { chatStyles } from './ChatScroller-css.js'
import { Epml } from "../../../epml";
import { cropAddress } from "../../utils/cropAddress";
import './LevelFounder.js';
import './NameMenu.js';
import './ChatModals.js';
import './WrapperModal';
import "./UserInfo/UserInfo";
import '@vaadin/icons';
import '@vaadin/icon';
import '@material/mwc-button';
import '@material/mwc-dialog';
import '@material/mwc-icon';
import { EmojiPicker } from 'emoji-picker-js';
import { generateHTML } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import Underline from '@tiptap/extension-underline';
import Highlight from '@tiptap/extension-highlight'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
let toggledMessage = {}
class ChatScroller extends LitElement {
static get properties() {
return {
getNewMessage: { attribute: false },
getOldMessage: { attribute: false },
emojiPicker: { attribute: false },
escapeHTML: { attribute: false },
initialMessages: { type: Array }, // First set of messages to load.. 15 messages max ( props )
messages: { type: Array },
hideMessages: { type: Array }
hideMessages: { type: Array },
setRepliedToMessageObj: { attribute: false },
setEditedMessageObj: { attribute: false },
sendMessage: { attribute: false },
sendMessageForward: { attribute: false },
showLastMessageRefScroller: { attribute: false },
emojiPicker: { attribute: false },
isLoadingMessages: { type: Boolean},
setIsLoadingMessages: { attribute: false },
chatId: { type: String },
setForwardProperties: { attribute: false },
setOpenPrivateMessage: { attribute: false },
setOpenUserInfo: { attribute: false },
setOpenTipUser: { attribute: false },
setUserName: { attribute: false },
setSelectedHead: { attribute: false },
openTipUser: { type: Boolean },
openUserInfo: { type: Boolean },
userName: { type: String },
selectedHead: { type: Object },
goToRepliedMessage: { attribute: false },
getOldMessageAfter: {attribute: false}
}
}
@ -36,61 +64,165 @@ class ChatScroller extends LitElement {
this._upObserverhandler = this._upObserverhandler.bind(this)
this._downObserverHandler = this._downObserverHandler.bind(this)
this.myAddress = window.parent.reduxStore.getState().app.selectedAddress.address
this.hideMessages = JSON.parse(localStorage.getItem("MessageBlockedAddresses") || "[]")
this.hideMessages = JSON.parse(localStorage.getItem("MessageBlockedAddresses") || "[]")
this.openTipUser = false;
this.openUserInfo = false;
}
render() {
let formattedMessages = this.messages.reduce((messageArray, message, index) => {
const lastGroupedMessage = messageArray[messageArray.length - 1];
let timestamp;
let sender;
let repliedToData;
let firstMessageInChat;
if (index === 0) {
firstMessageInChat = true;
} else {
firstMessageInChat = false;
}
message = {...message, firstMessageInChat}
if (lastGroupedMessage) {
timestamp = lastGroupedMessage.timestamp;
sender = lastGroupedMessage.sender;
repliedToData = lastGroupedMessage.repliedToData;
}
const isSameGroup = Math.abs(timestamp - message.timestamp) < 600000 && sender === message.sender && !repliedToData;
if (isSameGroup) {
messageArray[messageArray.length - 1].messages = [...(messageArray[messageArray.length - 1]?.messages || []), message];
} else {
messageArray.push({
messages: [message],
...message
});
}
return messageArray;
}, [])
return html`
${this.isLoadingMessages ? html`
<div class="spinnerContainer">
<paper-spinner-lite active></paper-spinner-lite>
</div>
` : ''}
<ul id="viewElement" class="chat-list clearfix">
<div id="upObserver"></div>
${repeat(
this.messages,
(message) => message.reference,
(message) => html`<message-template .emojiPicker=${this.emojiPicker} .escapeHTML=${this.escapeHTML} .messageObj=${message} .hideMessages=${this.hideMessages}></message-template>`
)}
${formattedMessages.map((formattedMessage) => {
return repeat(
formattedMessage.messages,
(message) => message.reference,
(message, indexMessage) => html`
<message-template
.emojiPicker=${this.emojiPicker}
.escapeHTML=${this.escapeHTML}
.messageObj=${message}
.hideMessages=${this.hideMessages}
.setRepliedToMessageObj=${this.setRepliedToMessageObj}
.setEditedMessageObj=${this.setEditedMessageObj}
.sendMessage=${this.sendMessage}
.sendMessageForward=${this.sendMessageForward}
?isFirstMessage=${indexMessage === 0}
?isSingleMessageInGroup=${formattedMessage.messages.length > 1}
?isLastMessageInGroup=${indexMessage === formattedMessage.messages.length - 1}
.setToggledMessage=${this.setToggledMessage}
.setForwardProperties=${this.setForwardProperties}
.setOpenPrivateMessage=${(val) => this.setOpenPrivateMessage(val)}
.setOpenTipUser=${(val) => this.setOpenTipUser(val)}
.setOpenUserInfo=${(val) => this.setOpenUserInfo(val)}
.setUserName=${(val) => this.setUserName(val)}
id=${message.reference}
.goToRepliedMessage=${this.goToRepliedMessage}
>
</message-template>`
)
})}
<div id='downObserver'></div>
<div class='last-message-ref'>
<vaadin-icon icon='vaadin:arrow-circle-down' slot='icon' @click=${() => {
this.shadowRoot.getElementById('downObserver').scrollIntoView({
behavior: 'smooth',
})
}}>
</vaadin-icon>
</div>
</ul>
`
}
shouldUpdate(changedProperties) {
if(changedProperties.has('isLoadingMessages')){
return true
}
if(changedProperties.has('chatId') && changedProperties.get('chatId')){
return true
}
if(changedProperties.has('openTipUser')){
return true
}
if(changedProperties.has('openUserInfo')){
return true
}
if(changedProperties.has('userName')){
return true
}
// Only update element if prop1 changed.
return changedProperties.has('messages');
}
async getUpdateComplete() {
await super.getUpdateComplete();
const marginElements = Array.from(this.shadowRoot.querySelectorAll('message-template'));
await Promise.all(marginElements.map(el => el.updateComplete));
return true;
}
setToggledMessage(message) {
toggledMessage = message;
}
async firstUpdated() {
this.viewElement = this.shadowRoot.getElementById('viewElement')
this.upObserverElement = this.shadowRoot.getElementById('upObserver')
this.downObserverElement = this.shadowRoot.getElementById('downObserver')
this.emojiPicker.on('emoji', selection => {
this.sendMessage({
type: 'reaction',
editedMessageObj: toggledMessage,
reaction: selection.emoji,
})
});
this.viewElement = this.shadowRoot.getElementById('viewElement');
this.upObserverElement = this.shadowRoot.getElementById('upObserver');
this.downObserverElement = this.shadowRoot.getElementById('downObserver');
// Intialize Observers
this.upElementObserver()
this.downElementObserver()
await this.updateComplete
this.viewElement.scrollTop = this.viewElement.scrollHeight + 50
this.upElementObserver();
this.downElementObserver();
await this.getUpdateComplete();
this.viewElement.scrollTop = this.viewElement.scrollHeight + 50;
}
_getOldMessage(_scrollElement) {
this.getOldMessage(_scrollElement)
}
_getOldMessageAfter(_scrollElement) {
this.getOldMessageAfter(_scrollElement)
}
_upObserverhandler(entries) {
if (entries[0].isIntersecting) {
let _scrollElement = entries[0].target.nextElementSibling
this._getOldMessage(_scrollElement)
if(this.messages.length < 20){
return
}
this.setIsLoadingMessages(true);
let _scrollElement = entries[0].target.nextElementSibling;
this._getOldMessage(_scrollElement);
}
}
_downObserverHandler(entries) {
if (!entries[0].isIntersecting) {
this.shadowRoot.querySelector(".last-message-ref").style.opacity = '1'
let _scrollElement = entries[0].target.previousElementSibling;
// this._getOldMessageAfter(_scrollElement);
this.showLastMessageRefScroller(true);
} else {
this.shadowRoot.querySelector(".last-message-ref").style.opacity = '0'
this.showLastMessageRefScroller(false);
}
}
@ -100,9 +232,8 @@ class ChatScroller extends LitElement {
rootMargin: '0px',
threshold: 1
};
const observer = new IntersectionObserver(this._upObserverhandler, options)
observer.observe(this.upObserverElement)
const observer = new IntersectionObserver(this._upObserverhandler, options);
observer.observe(this.upObserverElement);
}
downElementObserver() {
@ -111,17 +242,13 @@ class ChatScroller extends LitElement {
rootMargin: '0px',
threshold: 1
}
// identify an element to observe
const elementToObserve = this.downObserverElement
const elementToObserve = this.downObserverElement;
// passing it a callback function
const observer = new IntersectionObserver(this._downObserverHandler, options)
const observer = new IntersectionObserver(this._downObserverHandler, options);
// call `observe()` on that MutationObserver instance,
// passing it the element to observe, and the options object
observer.observe(elementToObserve)
observer.observe(elementToObserve);
}
}
@ -135,9 +262,28 @@ class MessageTemplate extends LitElement {
emojiPicker: { attribute: false },
escapeHTML: { attribute: false },
hideMessages: { type: Array },
openDialogPrivateMessage: {type: Boolean},
openDialogBlockUser: {type: Boolean},
showBlockAddressIcon: { type: Boolean }
openDialogPrivateMessage: { type: Boolean },
openDialogBlockUser: { type: Boolean },
showBlockAddressIcon: { type: Boolean },
setRepliedToMessageObj: { attribute: false },
setEditedMessageObj: { attribute: false },
sendMessage: { attribute: false },
sendMessageForward: { attribute: false },
openDialogImage: { attribute: false },
openDeleteImage: { type: Boolean },
isImageLoaded: { type: Boolean },
isFirstMessage: { type: Boolean },
isSingleMessageInGroup: { type: Boolean },
isLastMessageInGroup: { type: Boolean },
setToggledMessage: { attribute: false },
setForwardProperties: { attribute: false },
viewImage: { type: Boolean },
setOpenPrivateMessage : { attribute: false },
setOpenTipUser: { attribute: false },
setOpenUserInfo: { attribute: false },
setUserName: { attribute: false },
openTipUser:{ type: Boolean },
goToRepliedMessage: { attribute: false },
}
}
@ -148,6 +294,13 @@ class MessageTemplate extends LitElement {
this.openDialogBlockUser = false
this.showBlockAddressIcon = false
this.myAddress = window.parent.reduxStore.getState().app.selectedAddress.address
this.imageFetches = 0
this.openDialogImage = false
this.isImageLoaded = false
this.isFirstMessage = false
this.isSingleMessageInGroup = false
this.isLastMessageInGroup = false
this.viewImage = false
}
static styles = [chatStyles]
@ -171,8 +324,7 @@ class MessageTemplate extends LitElement {
}
showBlockIconFunc(bool) {
this.shadowRoot.querySelector(".chat-hover").focus({ preventScroll: true })
if(bool) {
if (bool) {
this.showBlockAddressIcon = true;
} else {
this.showBlockAddressIcon = false;
@ -180,51 +332,343 @@ class MessageTemplate extends LitElement {
}
render() {
const hidemsg = this.hideMessages
const hidemsg = this.hideMessages;
let message = "";
let messageVersion2 = ""
let reactions = [];
let repliedToData = null;
let image = null;
let isImageDeleted = false;
let version = 0;
let isForwarded = false
let isEdited = false
try {
const parsedMessageObj = JSON.parse(this.messageObj.decodedMessage);
if(parsedMessageObj.version.toString() === '2'){
let avatarImg = ''
let nameMenu = ''
let levelFounder = ''
let hideit = hidemsg.includes(this.messageObj.sender)
levelFounder = html`<level-founder checkleveladdress="${this.messageObj.sender}"></level-founder>`
messageVersion2 = generateHTML(parsedMessageObj.messageText, [
StarterKit,
Underline,
Highlight
// other extensions …
])
}
message = parsedMessageObj.messageText;
repliedToData = this.messageObj.repliedToData;
isImageDeleted = parsedMessageObj.isImageDeleted;
reactions = parsedMessageObj.reactions || [];
version = parsedMessageObj.version
isForwarded = parsedMessageObj.type === 'forward'
isEdited = this.messageObj.editedTimestamp && true
if (parsedMessageObj.images && Array.isArray(parsedMessageObj.images) && parsedMessageObj.images.length > 0) {
image = parsedMessageObj.images[0];
}
} catch (error) {
message = this.messageObj.decodedMessage;
}
let avatarImg = '';
let imageHTML = '';
let imageHTMLDialog = '';
let imageUrl = '';
let nameMenu = '';
let levelFounder = '';
let hideit = hidemsg.includes(this.messageObj.sender);
let forwarded = ''
let edited = ''
levelFounder = html`<level-founder checkleveladdress="${this.messageObj.sender}"></level-founder>`;
if (this.messageObj.senderName) {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]
const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port
const avatarUrl = `${nodeUrl}/arbitrary/THUMBNAIL/${this.messageObj.senderName}/qortal_avatar?async=true&apiKey=${myNode.apiKey}`
avatarImg = html`<img src="${avatarUrl}" style="max-width:100%; max-height:100%;" onerror="this.onerror=null; this.src='/img/incognito.png';" />`
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port;
const avatarUrl = `${nodeUrl}/arbitrary/THUMBNAIL/${this.messageObj.senderName}/qortal_avatar?async=true&apiKey=${myNode.apiKey}`;
avatarImg = html`<img src="${avatarUrl}" style="max-width:100%; max-height:100%;" onerror="this.onerror=null; this.src='/img/qortal-chat-logo.png';" />`;
} else {
avatarImg = html`<img src='/img/qortal-chat-logo.png' style="max-width:100%; max-height:100%;" onerror="this.onerror=null;" />`
}
const createImage = (imageUrl) => {
const imageHTMLRes = new Image();
imageHTMLRes.src = imageUrl;
imageHTMLRes.style= "max-width:45vh; max-height:40vh; border-radius: 5px; cursor: pointer";
imageHTMLRes.onclick= () => {
this.openDialogImage = true;
}
imageHTMLRes.onload = () => {
this.isImageLoaded = true;
}
imageHTMLRes.onerror = () => {
if (this.imageFetches < 4) {
setTimeout(() => {
this.imageFetches = this.imageFetches + 1;
imageHTMLRes.src = imageUrl;
}, 500);
} else {
imageHTMLRes.src = '/img/chain.png';
imageHTMLRes.style= "max-width:45vh; max-height:20vh; border-radius: 5px; filter: opacity(0.5)";
imageHTMLRes.onclick= () => {
}
this.isImageLoaded = true
}
};
return imageHTMLRes;
}
if (image) {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port;
imageUrl = `${nodeUrl}/arbitrary/${image.service}/${image.name}/${image.identifier}?async=true&apiKey=${myNode.apiKey}`;
if(this.viewImage || this.myAddress === this.messageObj.sender){
imageHTML = createImage(imageUrl);
imageHTMLDialog = createImage(imageUrl)
imageHTMLDialog.style= "height: auto; max-height: 80vh; width: auto; max-width: 80vw; object-fit: contain; border-radius: 5px";
}
}
if (this.messageObj.sender === this.myAddress) {
nameMenu = html`<span style="color: #03a9f4;">${this.messageObj.senderName ? this.messageObj.senderName : this.messageObj.sender}</span>`
} else {
nameMenu = html`<span>${this.messageObj.senderName ? this.messageObj.senderName : this.messageObj.sender}</span>`
nameMenu = html`
<span class="${this.messageObj.sender === this.myAddress && 'message-data-my-name'}">
${this.messageObj.senderName ? this.messageObj.senderName : cropAddress(this.messageObj.sender)}
</span>
`;
forwarded = html`
<span class="${this.messageObj.sender === this.myAddress && 'message-data-forward'}">
${translate("blockpage.bcchange17")}
</span>
`;
edited = html`
<span class="edited-message-style">
${translate("chatpage.cchange68")}
</span>
`;
if (repliedToData) {
try {
const parsedMsg = JSON.parse(repliedToData.decodedMessage);
repliedToData.decodedMessage = parsedMsg;
} catch (error) {
console.error(error);
}
}
const escapedMessage = this.escapeHTML(message)
const replacedMessage = escapedMessage.replace(new RegExp('\r?\n','g'), '<br />');
return hideit ? html`<li class="clearfix"></li>` : html`
<li class="clearfix message-parent">
<div class="message-data ${this.messageObj.sender === this.myAddress ? "" : ""}">
<span class="message-data-name">${nameMenu}</span>
<span class="message-data-level">${levelFounder}</span>
<span class="message-data-time"><message-time timestamp=${this.messageObj.timestamp}></message-time></span>
</div>
<div class="message-data-avatar" style="width:42px; height:42px; ${this.messageObj.sender === this.myAddress ? "float:left;" : "float:left;"} margin:3px;">${avatarImg}</div>
<div class="message-container">
<div id="messageContent" class="message ${this.messageObj.sender === this.myAddress ? "my-message float-left" : "other-message float-left"}">${unsafeHTML(this.emojiPicker.parse(this.escapeHTML(this.messageObj.decodedMessage)))}</div>
<chat-menu
tabindex="0"
class="chat-hover"
style=${this.showBlockAddressIcon && "display: block"}
toblockaddress="${this.messageObj.sender}"
.showPrivateMessageModal=${() => this.showPrivateMessageModal()}
.showBlockUserModal=${() => this.showBlockUserModal()}
.showBlockIconFunc=${(props) => this.showBlockIconFunc(props)}
.showBlockAddressIcon=${this.showBlockAddressIcon}
@blur=${() => this.showBlockIconFunc(false)}
>
</chat-menu>
<li
class="clearfix message-parent"
style="${(this.isSingleMessageInGroup === true && this.isLastMessageInGroup === false && reactions.length === 0) ?
'padding-bottom: 0;'
: null}
${this.isFirstMessage && 'margin-top: 20px;'}">
<div>
<div
class="message-container"
style="${(this.isSingleMessageInGroup === true && this.isLastMessageInGroup === false) && 'margin-bottom: 0'}">
<div class="message-subcontainer1">
${(this.isSingleMessageInGroup === false ||
(this.isSingleMessageInGroup === true && this.isLastMessageInGroup === true))
? (
html`
<div
style=${this.myAddress === this.messageObj.sender ? "cursor: auto;" : "cursor: pointer;"}
@click=${() => {
if (this.myAddress === this.messageObj.sender) return;
this.setOpenUserInfo(true);
this.setUserName(this.messageObj);
}} class="message-data-avatar">
${avatarImg}
</div>
`
) :
html`
<div class="message-data-avatar"></div>
`}
<div
class="${`message-subcontainer2
${((this.isFirstMessage === true && this.isSingleMessageInGroup === false) ||
(this.isSingleMessageInGroup === true && this.isLastMessageInGroup === true)) &&
'message-triangle'}`}"
style="${(this.isSingleMessageInGroup === true && this.isLastMessageInGroup === false) ? 'margin-bottom: 0;' : null}
${(this.isFirstMessage === false && this.isSingleMessageInGroup === true && this.isLastMessageInGroup === false)
? 'border-radius: 8px 25px 25px 8px;'
: (this.isFirstMessage === true && this.isSingleMessageInGroup === true && this.isLastMessageInGroup === false)
? 'border-radius: 27px 25px 25px 12px;'
: (this.isFirstMessage === false && this.isSingleMessageInGroup === true && this.isLastMessageInGroup === true) ?
'border-radius: 10px 25px 25px 0;'
: (this.isFirstMessage === true && this.isSingleMessageInGroup === false && this.isLastMessageInGroup === true)
? 'border-radius: 25px 25px 25px 0px;'
: null
}">
<div class="message-user-info">
${this.isFirstMessage ?
html`
<span
style=${this.myAddress === this.messageObj.sender ? "cursor: auto;" : "cursor: pointer;"}
@click=${() => {
if (this.myAddress === this.messageObj.sender) return;
this.setOpenUserInfo(true);
this.setUserName(this.messageObj);
}}
class="message-data-name">
${nameMenu}
</span>
`
: null
}
${isForwarded ?
html`
<span class="forwarded-text">
${forwarded}
</span>
`
: null
}
${this.isFirstMessage ? (
html`
<span class="message-data-level">${levelFounder}</span>
`
) : null}
</div>
${repliedToData && html`
<div class="original-message"
@click=${()=> {
this.goToRepliedMessage(repliedToData)
}}>
<p
class="original-message-sender">
${repliedToData.senderName ?? cropAddress(repliedToData.sender)}
</p>
<p class="replied-message">
${version.toString() === '1' ? html`
${repliedToData.decodedMessage.messageText}
` : ''}
${version.toString() === '2' ? html`
${unsafeHTML(generateHTML(repliedToData.decodedMessage.messageText, [
StarterKit,
Underline,
Highlight
// other extensions …
]))}
` : ''}
<!-- ${repliedToData.decodedMessage.messageText} -->
</p>
</div>
`}
${image && !isImageDeleted && !this.viewImage && this.myAddress !== this.messageObj.sender ? html`
<div
@click=${()=> {
this.viewImage = true
}}
class=${[`image-container`, !this.isImageLoaded ? 'defaultSize' : ''].join(' ')}
style=${this.isFirstMessage && "margin-top: 10px;"}>
<div style="display:flex;width:100%;height:100%;justify-content:center;align-items:center;cursor:pointer;color:var(--black)">
${translate("chatpage.cchange40")}
</div>
</div>
` : html``}
${image && !isImageDeleted && (this.viewImage || this.myAddress === this.messageObj.sender) ? html`
<div
class=${[`image-container`, !this.isImageLoaded ? 'defaultSize' : ''].join(' ')}
style=${this.isFirstMessage && "margin-top: 10px;"}>
${imageHTML}<vaadin-icon
@click=${() => {
this.openDeleteImage = true;
this.chatE
}}
class="image-delete-icon" icon="vaadin:close" slot="icon"></vaadin-icon>
</div>
` : image && isImageDeleted ? html`
<p class="image-deleted-msg">This image has been deleted</p>
` : html``}
<div
id="messageContent"
class="message"
style=${(image && replacedMessage !== "") &&"margin-top: 15px;"}>
${version.toString() === '2' ? html`
${unsafeHTML(messageVersion2)}
` : ''}
${version.toString() === '1' ? html`
${unsafeHTML(this.emojiPicker.parse(replacedMessage))}
` : ''}
<div
style=${isEdited
? "justify-content: space-between;"
: "justify-content: flex-end;"}
class="${((this.isFirstMessage === false &&
this.isSingleMessageInGroup === true &&
this.isLastMessageInGroup === true) ||
(this.isFirstMessage === true &&
this.isSingleMessageInGroup === false &&
this.isLastMessageInGroup === true))
? 'message-data-time'
: 'message-data-time-hidden'
}">
${isEdited ?
html`
<span>
${edited}
</span>
`
: null
}
<message-time timestamp=${this.messageObj.timestamp}></message-time>
</div>
</div>
</div>
<chat-menu
tabindex="0"
class="chat-hover"
style="${this.showBlockAddressIcon && 'display: block;'}"
toblockaddress="${this.messageObj.sender}"
.showPrivateMessageModal=${() => this.showPrivateMessageModal()}
.showBlockUserModal=${() => this.showBlockUserModal()}
.showBlockIconFunc=${(props) => this.showBlockIconFunc(props)}
.showBlockAddressIcon=${this.showBlockAddressIcon}
.originalMessage=${{...this.messageObj, message}}
.setRepliedToMessageObj=${this.setRepliedToMessageObj}
.setEditedMessageObj=${this.setEditedMessageObj}
.myAddress=${this.myAddress}
@blur=${() => this.showBlockIconFunc(false)}
.sendMessage=${this.sendMessage}
.sendMessageForward=${this.sendMessageForward}
version=${version}
.emojiPicker=${this.emojiPicker}
.setToggledMessage=${this.setToggledMessage}
.setForwardProperties=${this.setForwardProperties}
?firstMessageInChat=${this.messageObj.firstMessageInChat}
.setOpenPrivateMessage=${(val) => this.setOpenPrivateMessage(val)}
.setOpenTipUser=${(val) => this.setOpenTipUser(val)}
.setUserName=${(val) => this.setUserName(val)}
>
</chat-menu>
</div>
<div class="message-reactions" style="${reactions.length > 0 &&
'margin-top: 10px; margin-bottom: 5px;'}">
${reactions.map((reaction)=> {
return html`
<span
@click=${() => this.sendMessage({
type: 'reaction',
editedMessageObj: this.messageObj,
reaction: reaction.type,
})}
class="reactions-bg">
${reaction.type} ${reaction.qty}
</span>`
})}
</div>
</div>
</div>
</div>
</li>
<chat-modals
.openDialogPrivateMessage=${this.openDialogPrivateMessage}
@ -235,6 +679,53 @@ class MessageTemplate extends LitElement {
toblockaddress=${this.messageObj.sender}
>
</chat-modals>
<mwc-dialog
id="showDialogPublicKey"
?open=${this.openDialogImage}
@closed=${()=> {
this.openDialogImage = false
}}>
<div class="dialog-header"></div>
<div class="dialog-container imageContainer">
${imageHTMLDialog}
</div>
<mwc-button
slot="primaryAction"
dialogAction="cancel"
class="red"
@click=${()=>{
this.openDialogImage = false
}}
>
${translate("general.close")}
</mwc-button>
</mwc-dialog>
<mwc-dialog
hideActions
?open=${this.openDeleteImage}
@closed=${()=> {
this.openDeleteImage = false;
}}>
<div class="delete-image-msg">
<p>Are you sure you want to delete this image?</p>
</div>
<div class="modal-button-row" @click=${() => this.openDeleteImage = false}>
<button class="modal-button-red">
Cancel
</button>
<button
class="modal-button"
@click=${() => this.sendMessage({
type: 'delete',
name: image.name,
identifier: image.identifier,
editedMessageObj: this.messageObj,
})}>
Yes
</button>
</div>
</mwc-dialog>
`
}
}
@ -245,18 +736,30 @@ class ChatMenu extends LitElement {
static get properties() {
return {
menuItems: { type: Array },
selectedAddress: { type: Object },
showPrivateMessageModal: {type: Function},
showBlockUserModal: {type: Function},
showPrivateMessageModal: {attribute: false},
showBlockUserModal: {attribute: false},
toblockaddress: { type: String, attribute: true },
showBlockIconFunc: {type: Function},
showBlockAddressIcon: {type: Boolean}
showBlockIconFunc: {attribute: false},
showBlockAddressIcon: { type: Boolean },
originalMessage: { type: Object },
setRepliedToMessageObj: {attribute: false},
setEditedMessageObj: {attribute: false},
myAddress: { type: Object },
emojiPicker: { attribute: false },
sendMessage: { attribute: false },
version: { type: String },
setToggledMessage: { attribute: false },
sendMessageForward: { attribute: false },
setForwardProperties: { attribute: false },
firstMessageInChat: { type: Boolean },
setOpenPrivateMessage: { attribute: false },
setOpenTipUser: { attribute: false },
setUserName: { attribute: false },
}
}
constructor() {
super();
this.selectedAddress = window.parent.reduxStore.getState().app.selectedAddress.address;
this.showPrivateMessageModal = () => {};
this.showBlockUserModal = () => {};
}
@ -275,23 +778,149 @@ class ChatMenu extends LitElement {
console.error('Copy to clipboard error:', err)
}
}
versionErrorSnack(){
let errorMsg = get("chatpage.cchange34")
parentEpml.request('showSnackBar', `${errorMsg}`)
}
async messageForwardFunc(){
let parsedMessageObj = {}
let publicKey = {
hasPubKey: false,
key: ''
}
try {
parsedMessageObj = JSON.parse(this.originalMessage.decodedMessage);
} catch (error) {
parsedMessageObj = {}
}
try {
const res = await parentEpml.request('apiCall', {
type: 'api',
url: `/addresses/publickey/${this._chatId}`
})
if (res.error === 102) {
publicKey.key = ''
publicKey.hasPubKey = false
} else if (res !== false) {
publicKey.key = res
publicKey.hasPubKey = true
} else {
publicKey.key = ''
publicKey.hasPubKey = false
}
} catch (error) {
}
try {
const message = {
...parsedMessageObj,
type: 'forward'
}
const stringifyMessageObject = JSON.stringify(message)
this.setForwardProperties(stringifyMessageObject)
} catch (error) {
console.log({error})
}
}
render() {
return html`
<div class="container" style=${this.showBlockAddressIcon && "width: 70px" }>
<div class="menu-icon tooltip" data-text="${translate("blockpage.bcchange9")}" @click="${() => this.showPrivateMessageModal()}">
<div class="container">
<div
class=${`menu-icon reaction ${!this.firstMessageInChat ? "tooltip" : ""}`}
data-text="${translate("blockpage.bcchange13")}"
@click=${(e) => {
if(this.version === '0'){
this.versionErrorSnack()
return
}
try {
this.setToggledMessage(this.originalMessage)
this.emojiPicker.togglePicker(e.target)
} catch (error) {
console.log({error})
}
}}
>
<vaadin-icon icon="vaadin:smiley-o" slot="icon"></vaadin-icon>
</div>
<div
class=${`menu-icon ${!this.firstMessageInChat ? "tooltip" : ""}`}
data-text="${translate("blockpage.bcchange14")}"
@click="${() => {
this.messageForwardFunc()
}}">
<vaadin-icon icon="vaadin:arrow-forward" slot="icon"></vaadin-icon>
</div>
<div
class=${`menu-icon ${!this.firstMessageInChat ? "tooltip" : ""}`}
data-text="${translate("blockpage.bcchange9")}"
@click="${() => this.setOpenPrivateMessage({
name: this.originalMessage.senderName ? this.originalMessage.senderName : this.originalMessage.sender,
open: true
})}">
<vaadin-icon icon="vaadin:paperplane" slot="icon"></vaadin-icon>
</div>
<div class="menu-icon tooltip" data-text="${translate("blockpage.bcchange8")}" @click="${() => this.copyToClipboard(this.toblockaddress)}">
<div class=${`menu-icon ${!this.firstMessageInChat ? "tooltip" : ""}`} data-text="${translate("blockpage.bcchange8")}" @click="${() => this.copyToClipboard(this.toblockaddress)}">
<vaadin-icon icon="vaadin:copy" slot="icon"></vaadin-icon>
</div>
<div class="menu-icon tooltip" data-text="${translate("blockpage.bcchange10")}" @click="${() => this.showBlockIconFunc(true)}">
<div
class=${`menu-icon ${!this.firstMessageInChat ? "tooltip" : ""}`}
data-text="${translate("blockpage.bcchange11")}"
@click="${() => {
if (this.version === '0') {
this.versionErrorSnack()
return
}
this.setRepliedToMessageObj({...this.originalMessage, version: this.version});
}}">
<vaadin-icon icon="vaadin:reply" slot="icon"></vaadin-icon>
</div>
${this.myAddress === this.originalMessage.sender ? (
html`
<div
class=${`menu-icon ${!this.firstMessageInChat ? "tooltip" : ""}`}
data-text="${translate("blockpage.bcchange12")}"
@click=${() => {
if(this.version === '0'){
this.versionErrorSnack()
return
}
this.setEditedMessageObj(this.originalMessage);
}}>
<vaadin-icon icon="vaadin:pencil" slot="icon"></vaadin-icon>
</div>
`
) : html`<div></div>`}
${this.myAddress !== this.originalMessage.sender ? (
html`
<div
class=${`menu-icon ${!this.firstMessageInChat ? "tooltip" : ""}`}
data-text="${translate("blockpage.bcchange18")}"
@click=${(e) => {
e.preventDefault();
this.setUserName(this.originalMessage);
this.setOpenTipUser(true);
}}>
<vaadin-icon icon="vaadin:dollar" slot="icon"></vaadin-icon>
</div>
`
) : html`<div></div>`}
<div class=${`menu-icon ${!this.firstMessageInChat ? "tooltip" : ""}`} data-text="${translate("blockpage.bcchange10")}" @click="${() => this.showBlockIconFunc(true)}">
<vaadin-icon icon="vaadin:ellipsis-dots-h" slot="icon"></vaadin-icon>
</div>
${this.showBlockAddressIcon
? html`
<div class="block-user-container">
<div class="menu-icon block-user" @click="${() => this.showBlockUserModal()}">
<div class="block-user" @click="${() => this.showBlockUserModal()}">
<p>${translate("blockpage.bcchange1")}</p>
<vaadin-icon icon="vaadin:close-circle" slot="icon"></vaadin-icon>
</div>

View File

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

View File

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

View File

@ -0,0 +1,229 @@
import { LitElement, html, css } from 'lit'
import { Epml } from '../../../epml.js'
import '@material/mwc-icon'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class ChatSelect extends LitElement {
static get properties() {
return {
selectedAddress: { type: Object },
config: { type: Object },
chatInfo: { type: Object },
iconName: { type: String },
activeChatHeadUrl: { type: String },
isImageLoaded: { type: Boolean },
setActiveChatHeadUrl: {attribute: false}
}
}
static get styles() {
return css`
ul {
list-style-type: none;
}
li {
padding: 10px 2px 20px 5px;
cursor: pointer;
width: 100%;
display: flex;
box-sizing: border-box;
}
li:hover {
background-color: var(--menuhover);
}
.active {
background: var(--menuactive);
border-left: 4px solid #3498db;
}
.img-icon {
font-size:40px;
color: var(--chat-group);
}
.about {
margin-top: 8px;
}
.about {
padding-left: 8px;
}
.status {
color: #92959e;
}
.name {
user-select: none;
}
.clearfix:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
`
}
constructor() {
super()
this.selectedAddress = {}
this.config = {
user: {
node: {
}
}
}
this.chatInfo = {}
this.iconName = ''
this.activeChatHeadUrl = ''
this.isImageLoaded = false
this.imageFetches = 0
}
createImage(imageUrl) {
const imageHTMLRes = new Image();
imageHTMLRes.src = imageUrl;
imageHTMLRes.style= "width:40px; height:40px; float: left; border-radius:50%";
imageHTMLRes.onclick= () => {
this.openDialogImage = true;
}
imageHTMLRes.onload = () => {
this.isImageLoaded = true;
}
imageHTMLRes.onerror = () => {
if (this.imageFetches < 4) {
setTimeout(() => {
this.imageFetches = this.imageFetches + 1;
imageHTMLRes.src = imageUrl;
}, 500);
} else {
this.isImageLoaded = false
}
};
return imageHTMLRes;
}
render() {
let avatarImg = '';
let backupAvatarImg = ''
if(this.chatInfo.name){
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port;
const avatarUrl = `${nodeUrl}/arbitrary/THUMBNAIL/${this.chatInfo.name}/qortal_avatar?async=true&apiKey=${myNode.apiKey}`;
avatarImg= this.createImage(avatarUrl)
}
return html`
<li
@click=${() => this.getUrl(this.chatInfo.url)}
class="clearfix ${this.activeChatHeadUrl === this.chatInfo.url ? 'active' : ''}">
${this.isImageLoaded ? html`${avatarImg}` : html``}
${!this.isImageLoaded && !this.chatInfo.name && !this.chatInfo.groupName ? html`<mwc-icon class="img-icon">account_circle</mwc-icon>` :
html``
}
${!this.isImageLoaded && this.chatInfo.name ?
html`
<div
style="width:40px; height:40px; float: left; border-radius:50%; background: ${this.activeChatHeadUrl === this.chatInfo.url ?
'var(--chatHeadBgActive)' :
'var(--chatHeadBg)' };
color: ${this.activeChatHeadUrl === this.chatInfo.url ?
'var(--chatHeadTextActive)' :
'var(--chatHeadText)'};
font-weight:bold;
display: flex;
justify-content: center;
align-items: center;
text-transform: capitalize">
${this.chatInfo.name.charAt(0)}
</div>`:
''}
${!this.isImageLoaded && this.chatInfo.groupName ?
html`
<div
style="width:40px;
height:40px;
float: left;
border-radius:50%;
background: ${this.activeChatHeadUrl === this.chatInfo.url ?
'var(--chatHeadBgActive)' :
'var(--chatHeadBg)' };
color: ${this.activeChatHeadUrl === this.chatInfo.url ?
'var(--chatHeadTextActive)' :
'var(--chatHeadText)' };
font-weight:bold;
display: flex;
justify-content: center;
align-items: center;
text-transform: capitalize">
${this.chatInfo.groupName.charAt(0)}
</div>`:
''}
<div class="about">
<div class="name">
<span style="float:left; padding-left: 8px; color: var(--chat-group);">
${this.chatInfo.groupName ?
this.chatInfo.groupName :
this.chatInfo.name !== undefined ? this.chatInfo.name :
this.chatInfo.address.substr(0, 15)}
</span>
</div>
</div>
</li>
`
}
firstUpdated() {
let configLoaded = false
parentEpml.ready().then(() => {
parentEpml.subscribe('selected_address', async selectedAddress => {
this.selectedAddress = {}
selectedAddress = JSON.parse(selectedAddress)
if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
this.selectedAddress = selectedAddress
})
parentEpml.subscribe('config', c => {
if (!configLoaded) {
configLoaded = true
}
this.config = JSON.parse(c)
})
})
parentEpml.imReady()
}
shouldUpdate(changedProperties) {
if(changedProperties.has('activeChatHeadUrl')){
return true
}
if(changedProperties.has('chatInfo')){
return true
}
return false
}
getUrl(chatUrl) {
this.setActiveChatHeadUrl(chatUrl)
}
onPageNavigation(pageUrl) {
parentEpml.request('setPageUrl', pageUrl)
}
}
window.customElements.define('chat-select', ChatSelect)

View File

@ -0,0 +1,205 @@
import { LitElement, html, css } from 'lit'
import { Epml } from '../../../epml.js'
import '@material/mwc-icon'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class ChatSideNavHeads extends LitElement {
static get properties() {
return {
selectedAddress: { type: Object },
config: { type: Object },
chatInfo: { type: Object },
iconName: { type: String },
activeChatHeadUrl: { type: String },
isImageLoaded: { type: Boolean },
setActiveChatHeadUrl: {attribute: false}
}
}
static get styles() {
return css`
ul {
list-style-type: none;
}
li {
padding: 10px 2px 10px 5px;
cursor: pointer;
width: 100%;
display: flex;
box-sizing: border-box;
font-size: 14px;
transition: 0.2s background-color;
}
li:hover {
background-color: var(--lightChatHeadHover);
}
.active {
background: var(--menuactive);
border-left: 4px solid #3498db;
}
.img-icon {
font-size:40px;
color: var(--chat-group);
}
.status {
color: #92959e;
}
.clearfix {
display: flex;
align-items: center;
}
.clearfix:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
`
}
constructor() {
super()
this.selectedAddress = {}
this.config = {
user: {
node: {
}
}
}
this.chatInfo = {}
this.iconName = ''
this.activeChatHeadUrl = ''
this.isImageLoaded = false
this.imageFetches = 0
}
createImage(imageUrl) {
const imageHTMLRes = new Image();
imageHTMLRes.src = imageUrl;
imageHTMLRes.style= "width:30px; height:30px; float: left; border-radius:50%; font-size:14px";
imageHTMLRes.onclick= () => {
this.openDialogImage = true;
}
imageHTMLRes.onload = () => {
this.isImageLoaded = true;
}
imageHTMLRes.onerror = () => {
if (this.imageFetches < 4) {
setTimeout(() => {
this.imageFetches = this.imageFetches + 1;
imageHTMLRes.src = imageUrl;
}, 500);
} else {
this.isImageLoaded = false
}
};
return imageHTMLRes;
}
render() {
let avatarImg = ""
if (this.chatInfo.name) {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port;
const avatarUrl = `${nodeUrl}/arbitrary/THUMBNAIL/${this.chatInfo.name}/qortal_avatar?async=true&apiKey=${myNode.apiKey}`;
avatarImg = this.createImage(avatarUrl)
}
return html`
<li @click=${() => this.getUrl(this.chatInfo)} class="clearfix">
${this.isImageLoaded ? html`${avatarImg}` : html``}
${!this.isImageLoaded && !this.chatInfo.name && !this.chatInfo.groupName
? html`<mwc-icon class="img-icon">account_circle</mwc-icon>`
: html``}
${!this.isImageLoaded && this.chatInfo.name
? html`<div
style="width:30px; height:30px; float: left; border-radius:50%; background: ${this.activeChatHeadUrl === this.chatInfo.url
? "var(--chatHeadBgActive)"
: "var(--chatHeadBg)"}; color: ${this.activeChatHeadUrl ===
this.chatInfo.url
? "var(--chatHeadTextActive)"
: "var(--chatHeadText)"}; font-weight:bold; display: flex; justify-content: center; align-items: center; text-transform: capitalize"
>
${this.chatInfo.name.charAt(0)}
</div>`
: ""}
${!this.isImageLoaded && this.chatInfo.groupName
? html`<div
style="width:30px; height:30px; float: left; border-radius:50%; background: ${this.activeChatHeadUrl === this.chatInfo.url
? "var(--chatHeadBgActive)"
: "var(--chatHeadBg)"}; color: ${this.activeChatHeadUrl === this.chatInfo.url
? "var(--chatHeadTextActive)"
: "var(--chatHeadText)"}; font-weight:bold; display: flex; justify-content: center; align-items: center; text-transform: capitalize"
>
${this.chatInfo.groupName.charAt(0)}
</div>`
: ""}
<div>
<div class="name">
<span style="float:left; padding-left: 8px; color: var(--chat-group);">
${this.chatInfo.groupName
? this.chatInfo.groupName
: this.chatInfo.name !== undefined
? this.chatInfo.name
: this.chatInfo.address.substr(0, 15)}
</span>
</div>
</div>
</li>
`
}
firstUpdated() {
let configLoaded = false
parentEpml.ready().then(() => {
parentEpml.subscribe('selected_address', async selectedAddress => {
this.selectedAddress = {}
selectedAddress = JSON.parse(selectedAddress)
if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
this.selectedAddress = selectedAddress
})
parentEpml.subscribe('config', c => {
if (!configLoaded) {
configLoaded = true
}
this.config = JSON.parse(c)
})
})
parentEpml.imReady();
}
shouldUpdate(changedProperties) {
if(changedProperties.has('activeChatHeadUrl')){
return true
}
if(changedProperties.has('chatInfo')){
return true
}
if(changedProperties.has('isImageLoaded')){
return true
}
return false
}
getUrl(chatUrl) {
this.setActiveChatHeadUrl(chatUrl)
}
onPageNavigation(pageUrl) {
parentEpml.request('setPageUrl', pageUrl)
}
}
window.customElements.define('chat-side-nav-heads', ChatSideNavHeads)

View File

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

View File

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

View File

@ -25,7 +25,8 @@ class ChatWelcomePage extends LitElement {
btnDisable: { type: Boolean },
isLoading: { type: Boolean },
balance: { type: Number },
theme: { type: String, reflect: true }
theme: { type: String, reflect: true },
setOpenPrivateMessage: { attribute: false }
}
}
@ -212,7 +213,14 @@ class ChatWelcomePage extends LitElement {
<div class="center-box">
<mwc-icon class="img-icon">chat</mwc-icon><br>
<span style="font-size: 20px; color: var(--black);">${this.myAddress.address}</span>
<div class="start-chat" @click=${() => this.shadowRoot.querySelector('#startSecondChatDialog').show()}>${translate("welcomepage.wcchange2")}</div>
<div
class="start-chat"
@click="${() => this.setOpenPrivateMessage({
name: "",
open: true
})}">
${translate("welcomepage.wcchange2")}
</div>
</div>
</div>
@ -230,7 +238,11 @@ class ChatWelcomePage extends LitElement {
<textarea class="textarea" @keydown=${(e) => this._textArea(e)} ?disabled=${this.isLoading} id="messageBox" placeholder="${translate("welcomepage.wcchange5")}" rows="1"></textarea>
</p>
<mwc-button ?disabled="${this.isLoading}" slot="primaryAction" @click=${this._sendMessage}>${translate("welcomepage.wcchange6")}</mwc-button>
<mwc-button ?disabled="${this.isLoading}" slot="primaryAction" @click=${() => {
this._sendMessage();
}
}>
${translate("welcomepage.wcchange6")}</mwc-button>
<mwc-button
?disabled="${this.isLoading}"
slot="secondaryAction"
@ -319,90 +331,90 @@ class ChatWelcomePage extends LitElement {
}
_sendMessage() {
this.isLoading = true
const recipient = this.shadowRoot.getElementById('sendTo').value
const messageBox = this.shadowRoot.getElementById('messageBox')
const messageText = messageBox.value
this.isLoading = true;
const recipient = this.shadowRoot.getElementById('sendTo').value;
const messageBox = this.shadowRoot.getElementById('messageBox');
const messageText = messageBox.value;
if (recipient.length === 0) {
this.isLoading = false
this.isLoading = false;
} else if (messageText.length === 0) {
this.isLoading = false
this.isLoading = false;
} else {
this.sendMessage()
this.sendMessage();
}
}
};
async sendMessage(e) {
this.isLoading = true
const _recipient = this.shadowRoot.getElementById('sendTo').value
const messageBox = this.shadowRoot.getElementById('messageBox')
const messageText = messageBox.value
let recipient
async sendMessage() {
this.isLoading = true;
const _recipient = this.shadowRoot.getElementById('sendTo').value;
const messageBox = this.shadowRoot.getElementById('messageBox');
const messageText = messageBox.value;
let recipient;
const validateName = async (receiverName) => {
let myRes
let myRes;
let myNameRes = await parentEpml.request('apiCall', {
type: 'api',
url: `/names/${receiverName}`
})
});
if (myNameRes.error === 401) {
myRes = false
myRes = false;
} else {
myRes = myNameRes
}
myRes = myNameRes;
};
return myRes;
};
return myRes
}
const myNameRes = await validateName(_recipient);
const myNameRes = await validateName(_recipient)
if (!myNameRes) {
recipient = _recipient
recipient = _recipient;
} else {
recipient = myNameRes.owner
}
recipient = myNameRes.owner;
};
let _reference = new Uint8Array(64);
window.crypto.getRandomValues(_reference);
let sendTimestamp = Date.now()
let sendTimestamp = Date.now();
let reference = window.parent.Base58.encode(_reference)
let reference = window.parent.Base58.encode(_reference);
const getAddressPublicKey = async () => {
let isEncrypted
let _publicKey
let isEncrypted;
let _publicKey;
let addressPublicKey = await parentEpml.request('apiCall', {
type: 'api',
url: `/addresses/publickey/${recipient}`
})
if (addressPublicKey.error === 102) {
_publicKey = false
_publicKey = false;
// Do something here...
let err1string = get("welcomepage.wcchange7")
parentEpml.request('showSnackBar', `${err1string}`)
this.isLoading = false
let err1string = get("welcomepage.wcchange7");
parentEpml.request('showSnackBar', `${err1string}`);
this.isLoading = false;
} else if (addressPublicKey !== false) {
isEncrypted = 1
_publicKey = addressPublicKey
sendMessageRequest(isEncrypted, _publicKey)
isEncrypted = 1;
_publicKey = addressPublicKey;
sendMessageRequest(isEncrypted, _publicKey);
} else {
isEncrypted = 0
_publicKey = this.selectedAddress.address
sendMessageRequest(isEncrypted, _publicKey)
}
isEncrypted = 0;
_publicKey = this.selectedAddress.address;
sendMessageRequest(isEncrypted, _publicKey);
};
};
const sendMessageRequest = async (isEncrypted, _publicKey) => {
const messageObject = {
messageText,
images: [''],
repliedTo: '',
version: 1
};
const stringifyMessageObject = JSON.stringify(messageObject);
let chatResponse = await parentEpml.request('chat', {
type: 18,
nonce: this.selectedAddress.nonce,
@ -411,14 +423,13 @@ class ChatWelcomePage extends LitElement {
recipient: recipient,
recipientPublicKey: _publicKey,
hasChatReference: 0,
message: messageText,
message: stringifyMessageObject,
lastReference: reference,
proofOfWorkNonce: 0,
isEncrypted: isEncrypted,
isText: 1
}
})
_computePow(chatResponse)
}

View File

@ -48,32 +48,42 @@ class LevelFounder extends LitElement {
font-weight: 400;
}
.level {
position: relative;
display: inline;
}
.custom {
--paper-tooltip-background: #03a9f4;
--paper-tooltip-text-color: #fff;
}
.level-img-tooltip {
--paper-tooltip-background: #000000;
--paper-tooltip-text-color: #fff;
--paper-tooltip-delay-in: 300;
--paper-tooltip-delay-out: 3000;
}
.message-data {
display: flex;
justify-content: center;
gap: 5px;
}
.message-data-level {
width: 20px;
height: 20px;
}
.badge {
align-items: center;
background: #03a9f4;
background: rgb(3, 169, 244);
border: 1px solid transparent;
border-radius: 99em;
color: #fff;
border-radius: 50%;
color: rgb(255, 255, 255);
display: flex;
font-size: 10px;
font-weight: 400;
height: 12px;
width: 12px;
justify-content: center;
line-height: 1;
min-width: 12px;
position: absolute;
left: -16px;
top: -12px;
cursor: pointer;
}
`
@ -87,7 +97,7 @@ class LevelFounder extends LitElement {
render() {
return html`
<div class="level">
<div class="message-data">
${this.renderFounder()}
${this.renderLevel()}
</div>
@ -135,21 +145,24 @@ class LevelFounder extends LitElement {
}
renderFounder() {
let adressfounder = this.memberInfo.flags
let adressfounder = this.memberInfo.flags;
if (adressfounder === 1) {
return html `
<span id="founderTooltip" class="badge">F</span>
<paper-tooltip class="custom" for="founderTooltip" position="top">FOUNDER</paper-tooltip>
`
return html `
<span id="founderTooltip" class="badge">F</span>
<paper-tooltip class="custom" for="founderTooltip" position="top">FOUNDER</paper-tooltip>
`
} else {
return html ``
return null;
}
}
renderLevel() {
let adresslevel = this.memberInfo.level
let adresslevel = this.memberInfo.level;
return html `
<span id="levelTooltip">${translate("mintingpage.mchange27")} ${adresslevel}</span>
<img id="level-img" src=${`/img/badges/level-${adresslevel}.png`} alt=${`badge-${adresslevel}`} class="message-data-level" />
<paper-tooltip class="level-img-tooltip" for="level-img" position="top" >
${translate("mintingpage.mchange27")} ${adresslevel}
</paper-tooltip>
`
}

View File

@ -232,7 +232,11 @@ class NameMenu extends LitElement {
<p style="margin-bottom:0;">
<textarea class="textarea" @keydown=${(e) => this._textArea(e)} ?disabled=${this.isLoading} id="messageBox" placeholder="${translate("welcomepage.wcchange5")}" rows="1"></textarea>
</p>
<mwc-button ?disabled="${this.isLoading}" slot="primaryAction" @click=${this._sendMessage}>${translate("welcomepage.wcchange6")}</mwc-button>
<mwc-button ?disabled="${this.isLoading}" slot="primaryAction" @click=${() => {
this._sendMessage();
}
}>
${translate("welcomepage.wcchange6")}</mwc-button>
<mwc-button
?disabled="${this.isLoading}"
slot="secondaryAction"
@ -246,15 +250,15 @@ class NameMenu extends LitElement {
}
firstUpdated() {
this.getChatBlockedAdresses()
this.getChatBlockedAdresses();
setInterval(() => {
this.getChatBlockedAdresses();
}, 60000)
window.addEventListener('storage', () => {
const checkLanguage = localStorage.getItem('qortalLanguage')
use(checkLanguage)
const checkLanguage = localStorage.getItem('qortalLanguage');
use(checkLanguage);
})
window.onclick = function(event) {
@ -521,7 +525,13 @@ class NameMenu extends LitElement {
};
const sendMessageRequest = async (isEncrypted, _publicKey) => {
const messageObject = {
messageText,
images: [''],
repliedTo: '',
version: 1
}
const stringifyMessageObject = JSON.stringify(messageObject)
let chatResponse = await parentEpml.request('chat', {
type: 18,
nonce: this.selectedAddress.nonce,
@ -530,7 +540,7 @@ class NameMenu extends LitElement {
recipient: recipient,
recipientPublicKey: _publicKey,
hasChatReference: 0,
message: messageText,
message: stringifyMessageObject,
lastReference: reference,
proofOfWorkNonce: 0,
isEncrypted: isEncrypted,

View File

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

View File

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

View File

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

View File

@ -0,0 +1,119 @@
import { LitElement, html } from 'lit';
import { render } from 'lit/html.js';
import { translate } from 'lit-translate';
import { userInfoStyles } from './UserInfo-css.js';
import { Epml } from '../../../../epml';
import '@vaadin/button';
import '@polymer/paper-progress/paper-progress.js';
import { cropAddress } from '../../../utils/cropAddress.js';
export class UserInfo extends LitElement {
static get properties() {
return {
setOpenUserInfo: { attribute: false },
setOpenTipUser: { attribute: false },
setOpenPrivateMessage: { attribute: false },
userName: { type: String },
selectedHead: { type: Object },
isImageLoaded: { type: Boolean }
}
}
constructor() {
super()
this.isImageLoaded = false
this.selectedHead = {}
this.imageFetches = 0
}
static styles = [userInfoStyles]
createImage(imageUrl) {
const imageHTMLRes = new Image();
imageHTMLRes.src = imageUrl;
imageHTMLRes.classList.add("user-info-avatar");
imageHTMLRes.onload = () => {
this.isImageLoaded = true;
}
imageHTMLRes.onerror = () => {
if (this.imageFetches < 4) {
setTimeout(() => {
this.imageFetches = this.imageFetches + 1;
imageHTMLRes.src = imageUrl;
}, 500);
} else {
this.isImageLoaded = false
}
};
return imageHTMLRes;
}
render() {
let avatarImg = "";
if (this.selectedHead && this.selectedHead.name) {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port;
const avatarUrl = `${nodeUrl}/arbitrary/THUMBNAIL/${this.selectedHead.name}/qortal_avatar?async=true&apiKey=${myNode.apiKey}`;
avatarImg = this.createImage(avatarUrl);
}
return html`
<div style=${"position: relative;"}>
<vaadin-icon
class="close-icon"
icon="vaadin:close-big"
slot="icon"
@click=${() => {
this.setOpenUserInfo(false)
}}>
</vaadin-icon>
${this.isImageLoaded ?
html`
<div class="avatar-container">
${avatarImg}
</div>` :
html``}
${!this.isImageLoaded && this.selectedHead && this.selectedHead.name ?
html`
<div class="avatar-container">
<div class="user-info-no-avatar">
${this.selectedHead.name.charAt(0)}
</div>
</div>
`
: ""}
${!this.isImageLoaded && this.selectedHead && !this.selectedHead.name ?
html`
<div class="avatar-container">
<img src="/img/qortal-chat-logo.png" alt="avatar" />
</div>`
: ""}
<div class="user-info-header">
${this.selectedHead && this.selectedHead.name ? this.selectedHead.name : this.selectedHead ? cropAddress(this.selectedHead.address) : null}
</div>
<div
class="send-message-button"
@click="${() => {
this.setOpenPrivateMessage({
name: this.userName,
open: true
})
this.setOpenUserInfo(false);
}
}">
${translate("chatpage.cchange58")}
</div>
<div
style=${"margin-top: 5px;"}
class="send-message-button"
@click=${() => {
this.setOpenTipUser(true);
this.setOpenUserInfo(false);
}}>
${translate("chatpage.cchange59")}
</div>
</div>
`
}
}
customElements.define('user-info', UserInfo);

View File

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

View File

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

View File

@ -0,0 +1,82 @@
import { Sha256 } from 'asmcrypto.js'
function sbrk(size, heap){
let brk = 512 * 1024 // stack top
let old = brk
brk += size
if (brk > heap.length)
throw new Error('heap exhausted')
return old
}
self.addEventListener('message', async e => {
const response = await computePow(e.data.chatBytes, e.data.path, e.data.difficulty)
postMessage(response)
})
const memory = new WebAssembly.Memory({ initial: 256, maximum: 256 })
const heap = new Uint8Array(memory.buffer)
const computePow = async (chatBytes, path, difficulty) => {
let response = null
await new Promise((resolve, reject)=> {
const _chatBytesArray = Object.keys(chatBytes).map(function (key) { return chatBytes[key]; });
const chatBytesArray = new Uint8Array(_chatBytesArray);
const chatBytesHash = new Sha256().process(chatBytesArray).finish().result;
const hashPtr = sbrk(32, heap);
const hashAry = new Uint8Array(memory.buffer, hashPtr, 32);
hashAry.set(chatBytesHash);
const workBufferLength = 8 * 1024 * 1024;
const workBufferPtr = sbrk(workBufferLength, heap);
const importObject = {
env: {
memory: memory
},
};
function loadWebAssembly(filename, imports) {
// Fetch the file and compile it
return fetch(filename)
.then(response => response.arrayBuffer())
.then(buffer => WebAssembly.compile(buffer))
.then(module => {
// Create the instance.
return new WebAssembly.Instance(module, importObject);
});
}
loadWebAssembly(path)
.then(wasmModule => {
response = {
nonce : wasmModule.exports.compute2(hashPtr, workBufferPtr, workBufferLength, difficulty),
chatBytesArray
}
resolve()
});
})
return response
}

View File

@ -0,0 +1,92 @@
import { Sha256 } from 'asmcrypto.js'
function sbrk(size, heap){
let brk = 512 * 1024 // stack top
let old = brk
brk += size
if (brk > heap.length)
throw new Error('heap exhausted')
return old
}
self.addEventListener('message', async e => {
const response = await computePow(e.data.convertedBytes, e.data.path)
postMessage(response)
})
const memory = new WebAssembly.Memory({ initial: 256, maximum: 256 })
const heap = new Uint8Array(memory.buffer)
const computePow = async (convertedBytes, path) => {
let response = null
await new Promise((resolve, reject)=> {
const _convertedBytesArray = Object.keys(convertedBytes).map(
function (key) {
return convertedBytes[key]
}
)
const convertedBytesArray = new Uint8Array(_convertedBytesArray)
const convertedBytesHash = new Sha256()
.process(convertedBytesArray)
.finish().result
const hashPtr = sbrk(32, heap)
const hashAry = new Uint8Array(
memory.buffer,
hashPtr,
32
)
hashAry.set(convertedBytesHash)
const difficulty = 14
const workBufferLength = 8 * 1024 * 1024
const workBufferPtr = sbrk(
workBufferLength,
heap
)
const importObject = {
env: {
memory: memory
},
};
function loadWebAssembly(filename, imports) {
return fetch(filename)
.then(response => response.arrayBuffer())
.then(buffer => WebAssembly.compile(buffer))
.then(module => {
return new WebAssembly.Instance(module, importObject);
});
}
loadWebAssembly(path)
.then(wasmModule => {
response = {
nonce : wasmModule.exports.compute2(hashPtr, workBufferPtr, workBufferLength, difficulty),
}
resolve()
});
})
return response
}

View File

@ -1820,6 +1820,7 @@ class GroupManagement extends LitElement {
setTimeout(getGroupInvites, 1)
configLoaded = true
}
console.log('parse', JSON.parse(c))
this.config = JSON.parse(c)
})
parentEpml.subscribe('copy_menu_switch', async value => {

View File

@ -0,0 +1,82 @@
import { Sha256 } from 'asmcrypto.js'
function sbrk(size, heap){
let brk = 512 * 1024 // stack top
let old = brk
brk += size
if (brk > heap.length)
throw new Error('heap exhausted')
return old
}
self.addEventListener('message', async e => {
const response = await computePow(e.data.chatBytes, e.data.path, e.data.difficulty)
postMessage(response)
})
const memory = new WebAssembly.Memory({ initial: 256, maximum: 256 })
const heap = new Uint8Array(memory.buffer)
const computePow = async (chatBytes, path, difficulty) => {
let response = null
await new Promise((resolve, reject)=> {
const _chatBytesArray = Object.keys(chatBytes).map(function (key) { return chatBytes[key]; });
const chatBytesArray = new Uint8Array(_chatBytesArray);
const chatBytesHash = new Sha256().process(chatBytesArray).finish().result;
const hashPtr = sbrk(32, heap);
const hashAry = new Uint8Array(memory.buffer, hashPtr, 32);
hashAry.set(chatBytesHash);
const workBufferLength = 8 * 1024 * 1024;
const workBufferPtr = sbrk(workBufferLength, heap);
const importObject = {
env: {
memory: memory
},
};
function loadWebAssembly(filename, imports) {
// Fetch the file and compile it
return fetch(filename)
.then(response => response.arrayBuffer())
.then(buffer => WebAssembly.compile(buffer))
.then(module => {
// Create the instance.
return new WebAssembly.Instance(module, importObject);
});
}
loadWebAssembly(path)
.then(wasmModule => {
response = {
nonce : wasmModule.exports.compute2(hashPtr, workBufferPtr, workBufferLength, difficulty),
chatBytesArray
}
resolve()
});
})
return response
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -272,7 +272,7 @@ class NameRegistration 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
const url = `${nodeUrl}/arbitrary/THUMBNAIL/${name}/qortal_avatar?async=true&apiKey=${this.getApiKey()}`;
return html`<img src="${url}" onerror="this.onerror=null; this.src='/img/incognito.png';">`
return html`<img src="${url}" onerror="this.onerror=null; this.src='/img/qortal-chat-logo.png';">`
}
renderAvatarButton(nameObj) {

View File

@ -703,7 +703,7 @@ class Websites 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
const url = `${nodeUrl}/arbitrary/THUMBNAIL/${name}/qortal_avatar?async=true&apiKey=${this.getApiKey()}`
return html`<a class="visitSite" href="browser/index.html?name=${name}&service=${this.service}"><img src="${url}" onerror="this.src='/img/incognito.png';"></a>`
return html`<a class="visitSite" href="browser/index.html?name=${name}&service=${this.service}"><img src="${url}" onerror="this.src='/img/qortal-chat-logo.png';"></a>`
}
renderRelayModeText() {

View File

@ -534,7 +534,7 @@ class SponsorshipList extends LitElement {
${sponsorship?.name ? html`
<img src=${sponsorship.url}
class="avatar-img"
onerror="this.src='/img/incognito.png'"
onerror="this.src='/img/qortal-chat-logo.png'"
/>
` : ''}
${sponsorship?.name || sponsorship.address}

View File

@ -0,0 +1,8 @@
export function cropAddress(string = "", range = 5) {
const [start, end] = [
string?.substring(0, range),
string?.substring(string?.length - range, string?.length),
//
];
return start + "..." + end;
}

View File

@ -0,0 +1,21 @@
import { Epml } from '../../epml.js';
import { cropAddress } from './cropAddress.js';
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
export const getUserNameFromAddress = async (address) => {
try {
const getNames = await parentEpml.request("apiCall", {
type: "api",
url: `/names/address/${address}`,
});
if (Array.isArray(getNames) && getNames.length > 0 ) {
return getNames[0].name;
} else {
return address;
}
} catch (error) {
console.error(error);
}
}

View File

@ -0,0 +1,143 @@
const getApiKey = () => {
const myNode =
window.parent.reduxStore.getState().app.nodeConfig.knownNodes[
window.parent.reduxStore.getState().app.nodeConfig.node
]
let apiKey = myNode.apiKey
return apiKey
}
export const publishData = async ({
registeredName,
path,
file,
service,
identifier,
parentEpml,
uploadType,
selectedAddress,
worker
}) => {
const validateName = async (receiverName) => {
let nameRes = await parentEpml.request("apiCall", {
type: "api",
url: `/names/${receiverName}`,
})
return nameRes
}
const convertBytesForSigning = async (transactionBytesBase58) => {
let convertedBytes = await parentEpml.request("apiCall", {
type: "api",
method: "POST",
url: `/transactions/convert`,
body: `${transactionBytesBase58}`,
})
return convertedBytes
}
const signAndProcess = async (transactionBytesBase58) => {
let convertedBytesBase58 = await convertBytesForSigning(
transactionBytesBase58
)
if (convertedBytesBase58.error) {
return
}
const convertedBytes =
window.parent.Base58.decode(convertedBytesBase58)
let nonce = null
const computPath =window.parent.location.origin + '/memory-pow/memory-pow.wasm.full'
await new Promise((res, rej) => {
worker.postMessage({convertedBytes, path: computPath});
worker.onmessage = e => {
worker.terminate()
nonce = e.data.nonce
res()
}
})
let response = await parentEpml.request("sign_arbitrary", {
nonce: selectedAddress.nonce,
arbitraryBytesBase58: transactionBytesBase58,
arbitraryBytesForSigningBase58: convertedBytesBase58,
arbitraryNonce: nonce,
})
let myResponse = { error: "" }
if (response === false) {
return
} else {
myResponse = response
}
return myResponse
}
const validate = async () => {
let validNameRes = await validateName(registeredName)
if (validNameRes.error) {
return
}
let transactionBytes = await uploadData(registeredName, path, file)
if (transactionBytes.error) {
return
} else if (
transactionBytes.includes("Error 500 Internal Server Error")
) {
return
}
let signAndProcessRes = await signAndProcess(transactionBytes)
if (signAndProcessRes.error) {
return
}
}
const uploadData = async (registeredName, path, file) => {
if (identifier != null && identifier.trim().length > 0) {
let postBody = path
let urlSuffix = ""
if (file != null) {
// If we're sending zipped data, make sure to use the /zip version of the POST /arbitrary/* API
if (uploadType === "zip") {
urlSuffix = "/zip"
}
// If we're sending file data, use the /base64 version of the POST /arbitrary/* API
else if (uploadType === "file") {
urlSuffix = "/base64"
}
// Base64 encode the file to work around compatibility issues between javascript and java byte arrays
let fileBuffer = new Uint8Array(await file.arrayBuffer())
postBody = Buffer.from(fileBuffer).toString("base64")
}
let uploadDataUrl = `/arbitrary/${service}/${registeredName}${urlSuffix}?apiKey=${getApiKey()}`
if (identifier != null && identifier.trim().length > 0) {
uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}${urlSuffix}?apiKey=${getApiKey()}`
}
let uploadDataRes = await parentEpml.request("apiCall", {
type: "api",
method: "POST",
url: `${uploadDataUrl}`,
body: `${postBody}`,
})
return uploadDataRes
}
}
try {
await validate()
} catch (error) {
throw new Error(error.message)
}
}

View File

@ -0,0 +1,92 @@
export const replaceMessagesEdited = async ({
decodedMessages,
parentEpml,
isReceipient,
decodeMessageFunc,
_publicKey
}) => {
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 response = await parentEpml.request("apiCall", {
type: "api",
url: `/chat/messages?chatreference=${msg.reference}&reverse=true${msgQuery}`,
})
if (response && Array.isArray(response) && response.length !== 0) {
let responseItem = { ...response[0] }
const decodeResponseItem = decodeMessageFunc(responseItem, isReceipient, _publicKey)
delete decodeResponseItem.timestamp
msgItem = {
...msg,
...decodeResponseItem,
editedTimestamp: response[0].timestamp,
}
}
} catch (error) {
console.log(error)
}
return msgItem
})
const updateMessages = await Promise.all(findNewMessages)
const findNewMessages2 = updateMessages.map(async (msg) => {
let parsedMessageObj = msg
try {
parsedMessageObj = JSON.parse(msg.decodedMessage)
} catch (error) {
console.log('error')
return msg
}
let msgItem = msg
try {
let msgQuery = `&involving=${msg.recipient}&involving=${msg.sender}`
if (!isReceipient) {
msgQuery = `&txGroupId=${msg.txGroupId}`
}
if (parsedMessageObj.repliedTo) {
const response = await parentEpml.request("apiCall", {
type: "api",
url: `/chat/messages?chatreference=${parsedMessageObj.repliedTo}&reverse=true${msgQuery}`,
})
if (
response &&
Array.isArray(response) &&
response.length !== 0
) {
msgItem = {
...msg,
repliedToData: decodeMessageFunc(response[0], isReceipient, _publicKey),
}
} else {
const response2 = await parentEpml.request("apiCall", {
type: "api",
url: `/chat/messages?reference=${parsedMessageObj.repliedTo}&reverse=true${msgQuery}`,
})
if (
response2 &&
Array.isArray(response2) &&
response2.length !== 0
) {
msgItem = {
...msg,
repliedToData: decodeMessageFunc(response2[0], isReceipient, _publicKey),
}
}
}
}
} catch (error) {
console.log(error)
}
return msgItem
})
const updateMessages2 = await Promise.all(findNewMessages2)
return updateMessages2
}