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,7 +2,8 @@
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'),
@ -10,11 +11,48 @@
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,6 +28,7 @@ html {
--relaynodetxt: #646464;
--menuhover: #eeeeee;
--menuactive: #ebebeb;
--menuactivergb: 235, 235, 235;
--mainmenutext: #080808;
--mainmenutexthover: #080808;
--switchbackground: #666666;
@ -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,6 +83,7 @@ html[theme="dark"] {
--relaynodetxt: #d4d4d4;
--menuhover: #008fd5;
--menuactive: #008fd5;
--menuactivergb: 0, 143, 213;
--mainmenutext: #008fd5;
--mainmenutexthover: #0f1a2e;
--switchbackground: #eeeeee;
@ -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
}

View File

@ -513,8 +513,8 @@
"cchange3": "Blocked Users",
"cchange4": "New Message",
"cchange5": "(Click to scroll down)",
"cchange6":"Type the name or address of who you want to chat with to send a private message!",
"cchange7":"Name / Address",
"cchange6": "Type the name or address of who you want to chat with to send a private message! You can validate the person's name by clicking on the book icon.",
"cchange7": "Username / Address",
"cchange8": "Message...",
"cchange9": "Send",
"cchange10": "Blocked Users List",
@ -526,14 +526,54 @@
"cchange16": "Successfully unblocked this user.",
"cchange17": "Error occurred when trying to unblock this user. Please try again!",
"cchange18": "unblock",
"cchange19":"Invalid Name / Address, Check the name / address and retry...",
"cchange19": "Invalid Username / Address, Check the name / address and retry...",
"cchange20": "Message Sent Successfully!",
"cchange21": "Sending failed, Please retry...",
"cchange22": "Loading Messages...",
"cchange23": "Cannot Decrypt Message!",
"cchange24": "Maximum Characters per message is 255",
"cchange25":"Your Balance Is Under 4.20 QORT",
"cchange26":"Out of the need to combat spam, accounts with under 4.20 Qort balance will take a long time to SEND messages in Q-Chat. If you wish to immediately increase the send speed for Q-Chat messages, obtain over 4.20 QORT to your address. This can be done with trades in the Trade Portal, or by way of another Qortian giving you the QORT. Once you have over 4.20 QORT in your account, Q-Chat messages will be instant and this dialog will no more show. Thank you for your understanding of this necessary spam prevention method, and we hope you enjoy Qortal!"
"cchange25": "Edit Message",
"cchange26": "File size exceeds 0.5 MB",
"cchange27": "A registered name is required to send images",
"cchange28": "This file is not an image",
"cchange29": "Maximum message size is 1000 bytes",
"cchange30": "Uploading image. This may take up to one minute.",
"cchange31": "Deleting image. This may take up to one minute.",
"cchange33": "Cancel",
"cchange34": "This chat message is using an older message version and cannot use this feature.",
"cchange35": "Error when trying to fetch the user's name. Please try again!",
"cchange36": "Search Results",
"cchange37": "No Results Found",
"cchange38": "User Verified",
"cchange39": "Cannot send an encrypted message to this user since they do not have their publickey on chain.",
"cchange40": "IMAGE (click to view)",
"cchange41": "Your Balance Is Under 4.20 QORT",
"cchange42": "Out of the need to combat spam, accounts with under 4.20 Qort balance will take a long time to SEND messages in Q-Chat. If you wish to immediately increase the send speed for Q-Chat messages, obtain over 4.20 QORT to your address. This can be done with trades in the Trade Portal, or by way of another Qortian giving you the QORT. Once you have over 4.20 QORT in your account, Q-Chat messages will be instant and this dialog will no more show. Thank you for your understanding of this necessary spam prevention method, and we hope you enjoy Qortal!",
"cchange43": "Tip QORT to",
"cchange44": "SEND MESSAGE",
"cchange45": "TIP USER",
"cchange46": "Tip Amount",
"cchange47": "Available Balance",
"cchange48": "Failed to Fetch QORT Balance. Try again!",
"cchange49": "Current static fee",
"cchange50": "Send",
"cchange51": "Insufficient Funds!",
"cchange52": "Invalid Amount!",
"cchange53": "Receiver cannot be empty!",
"cchange54": "Invalid Receiver!",
"cchange55": "Transaction Successful!",
"cchange56": "Transaction Failed!",
"cchange57": "User Info",
"cchange58": "SEND MESSAGE",
"cchange59": "TIP USER",
"cchange60": "Group Invites Pending",
"cchange61": "Error when fetching group invites. Please try again!",
"cchange62": "Wrong Username and Address Inputted! Please try again!",
"cchange63": "Enter Enabled",
"cchange64": "Enter Disabled",
"cchange65": "Please enter a recipient",
"cchange66": "Cannot fetch replied-to message. Message is too old.",
"cchange68": "edited"
},
"welcomepage": {
"wcchange1": "Welcome to Q-Chat",
@ -556,7 +596,15 @@
"bcchange7": "MENU",
"bcchange8": "Copy Address",
"bcchange9": "Private Message",
"bcchange10":"More"
"bcchange10": "More",
"bcchange11": "Reply",
"bcchange12": "Edit",
"bcchange13": "Reaction",
"bcchange14": "Forward",
"bcchange15": "Message Forwarded",
"bcchange16": "Choose Recipient or Search for One Below",
"bcchange17": "FORWARDED",
"bcchange18": "Tip User"
},
"grouppage": {
"gchange1": "Qortal Groups",

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,9 +112,113 @@ 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()

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 {
@ -188,19 +188,21 @@ class AppView extends connect(store)(LitElement) {
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;
@ -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;
@ -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() {
@ -11,13 +12,16 @@ export default class PublicizeTransaction extends ChatBase {
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
const params = super.params;
params.push(
this._proofOfWorkNonce,
this._feeBytes
)
return params
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 {
color: black;
padding: 12px 10px;
.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;
white-space: pre-line;
word-wrap: break-word;
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 {
display: flex;
flex-direction: column;
color: var(--chat-bubble-msg-color);
line-height: 19px;
overflow-wrap: anywhere;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
font-size: 16px;
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 {
@ -149,26 +259,6 @@ export const chatStyles = css`
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}
}
}
@ -37,60 +65,164 @@ class ChatScroller extends LitElement {
this._downObserverHandler = this._downObserverHandler.bind(this)
this.myAddress = window.parent.reduxStore.getState().app.selectedAddress.address
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,
${formattedMessages.map((formattedMessage) => {
return repeat(
formattedMessage.messages,
(message) => message.reference,
(message) => html`<message-template .emojiPicker=${this.emojiPicker} .escapeHTML=${this.escapeHTML} .messageObj=${message} .hideMessages=${this.hideMessages}></message-template>`
)}
(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);
}
}
@ -137,7 +264,26 @@ class MessageTemplate extends LitElement {
hideMessages: { type: Array },
openDialogPrivateMessage: { type: Boolean },
openDialogBlockUser: { type: Boolean },
showBlockAddressIcon: { 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,7 +324,6 @@ class MessageTemplate extends LitElement {
}
showBlockIconFunc(bool) {
this.shadowRoot.querySelector(".chat-hover").focus({ preventScroll: true })
if (bool) {
this.showBlockAddressIcon = true;
} else {
@ -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;" />`
}
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>`
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";
}
}
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>
<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>
<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"}
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 = () => {};
}
@ -276,22 +779,148 @@ class ChatMenu extends LitElement {
}
}
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>
`
} 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
}