diff --git a/qortal-ui-plugins/plugins/core/components/ButtonIconCopy.js b/qortal-ui-plugins/plugins/core/components/ButtonIconCopy.js
new file mode 100644
index 00000000..43e09a15
--- /dev/null
+++ b/qortal-ui-plugins/plugins/core/components/ButtonIconCopy.js
@@ -0,0 +1,68 @@
+import { LitElement, html, css } from 'lit-element'
+import { Epml } from '../../../epml.js'
+
+import '@material/mwc-icon-button'
+
+const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
+
+class ButtonIconCopy extends LitElement {
+ static get properties() {
+ return {
+ textToCopy: { type: String },
+ title: { type: String },
+ onSuccessMessage: { type: String },
+ onErrorMessage: { type: String },
+ buttonSize: { type: String },
+ iconSize: { type: String },
+ color: { type: String },
+ offsetLeft: { type: String },
+ offsetRight: { type: String }
+ }
+ }
+
+ constructor() {
+ super()
+ this.textToCopy = ''
+ this.title = 'Copy to clipboard'
+ this.onSuccessMessage = 'Copied to clipboard'
+ this.onErrorMessage = 'Unable to copy'
+ this.buttonSize = '48px'
+ this.iconSize = '24px'
+ this.color = 'inherit'
+ this.offsetLeft = '0'
+ this.offsetRight = '0'
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.style.setProperty('--mdc-icon-button-size', this.buttonSize)
+ this.style.setProperty('--mdc-icon-size', this.iconSize)
+ this.style.setProperty('color', this.color)
+ this.style.setProperty('margin-left', this.offsetLeft)
+ this.style.setProperty('margin-right', this.offsetRight)
+ }
+
+ render() {
+ return html`
+ this.saveToClipboard(this.textToCopy)}
+ >
+
+ `
+ }
+
+ async saveToClipboard(text) {
+ try {
+ await navigator.clipboard.writeText(text)
+ parentEpml.request('showSnackBar', this.onSuccessMessage)
+ } catch (err) {
+ parentEpml.request('showSnackBar', this.onErrorMessage)
+ console.error('Copy to clipboard error:', err)
+ }
+ }
+}
+
+window.customElements.define('button-icon-copy', ButtonIconCopy)
diff --git a/qortal-ui-plugins/plugins/core/components/ChatHead.js b/qortal-ui-plugins/plugins/core/components/ChatHead.js
new file mode 100644
index 00000000..d50cec1a
--- /dev/null
+++ b/qortal-ui-plugins/plugins/core/components/ChatHead.js
@@ -0,0 +1,156 @@
+import { LitElement, html, css } from 'lit-element'
+// import { render } from 'lit-html'
+import { Epml } from '../../../epml.js'
+
+import '@material/mwc-icon'
+
+const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
+
+class ChatHead extends LitElement {
+ static get properties() {
+ return {
+ selectedAddress: { type: Object },
+ config: { type: Object },
+ chatInfo: { type: Object },
+ iconName: { type: String },
+ activeChatHeadUrl: { type: String }
+ }
+ }
+
+ static get styles() {
+ return css`
+
+ li {
+ padding: 10px 2px 20px 5px;
+ cursor: pointer;
+ width: 100%;
+ }
+
+ li:hover {
+ background-color: #eee;
+ }
+
+ .active {
+ background: #ebebeb;
+ border-left: 4px solid #3498db;
+ }
+
+ .img-icon {
+ float: left;
+ font-size:40px;
+ }
+
+ .about {
+ margin-top: 8px;
+ }
+
+ .about {
+ padding-left: 8px;
+ }
+
+ .status {
+ color: #92959e;
+ }
+
+ .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 = ''
+ }
+
+ render() {
+
+ return html`
+
this.getUrl(this.chatInfo.url)} class="clearfix ${this.activeChatHeadUrl === this.chatInfo.url ? 'active' : ''}">
+ account_circle
+
+
${this.chatInfo.groupName ? this.chatInfo.groupName : this.chatInfo.name !== undefined ? this.chatInfo.name : this.chatInfo.address.substr(0, 15)} ${this.chatInfo.groupId !== undefined ? 'lock_open' : 'lock'}
+
+
+ `
+ }
+
+
+ // renderEncryptedIcon(chatInfo) {
+
+ // if (chatInfo.groupId !== undefined) {
+ // this.iconName = 'lock_open'
+ // } else {
+
+ // parentEpml.request('apiCall', {
+ // type: 'api',
+ // url: `/addresses/publickey/${chatInfo.address}`
+ // }).then(res => {
+
+ // if (res.error === 102) {
+ // // Do something here...
+ // } else if (res !== false) {
+ // this.iconName = 'lock'
+ // } else {
+ // this.iconName = 'lock'
+ // }
+ // })
+
+ // }
+
+ // }
+
+ getUrl(chatUrl) {
+
+ this.onPageNavigation(`/app/q-chat/${chatUrl}`)
+ }
+
+ onPageNavigation(pageUrl) {
+
+ parentEpml.request('setPageUrl', pageUrl)
+ }
+
+ firstUpdated() {
+ let configLoaded = false
+
+ // this.renderEncryptedIcon(this.chatInfo)
+
+ parentEpml.ready().then(() => {
+ parentEpml.subscribe('selected_address', async selectedAddress => {
+ this.selectedAddress = {}
+ selectedAddress = JSON.parse(selectedAddress)
+ if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
+ this.selectedAddress = selectedAddress
+ })
+ parentEpml.subscribe('config', c => {
+ if (!configLoaded) {
+ configLoaded = true
+ }
+ this.config = JSON.parse(c)
+ })
+ })
+
+
+ parentEpml.imReady()
+ }
+
+
+}
+
+window.customElements.define('chat-head', ChatHead)
diff --git a/qortal-ui-plugins/plugins/core/components/ChatMessage.js b/qortal-ui-plugins/plugins/core/components/ChatMessage.js
new file mode 100644
index 00000000..0e0eb73b
--- /dev/null
+++ b/qortal-ui-plugins/plugins/core/components/ChatMessage.js
@@ -0,0 +1,151 @@
+import { LitElement, html, css } from 'lit-element'
+import { render } from 'lit-html'
+import { Epml } from '../../../epml.js'
+
+
+const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
+
+class ChatMessage extends LitElement {
+ static get properties() {
+ return {
+ selectedAddress: { type: Object },
+ config: { type: Object },
+ message: { type: Object, reflect: true }
+ }
+ }
+
+ static get styles() {
+ return css`
+
+ .message-data {
+ margin-bottom: 15px;
+ }
+
+ .message-data-time {
+ color: #a8aab1;
+ font-size: 13px;
+ padding-left: 6px;
+ }
+
+ .message {
+ color: black;
+ padding: 12px 10px;
+ line-height: 19px;
+ font-size: 16px;
+ border-radius: 7px;
+ margin-bottom: 20px;
+ width: 90%;
+ position: relative;
+ }
+
+ .message:after {
+ bottom: 100%;
+ left: 93%;
+ border: solid transparent;
+ content: " ";
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+ border-bottom-color: #ddd;
+ border-width: 10px;
+ margin-left: -10px;
+ }
+
+ .my-message {
+ background: #ddd;
+ border: 2px #ccc solid;
+ }
+
+ .other-message {
+ background: #f1f1f1;
+ border: 2px solid #dedede;
+ }
+
+ .other-message:after {
+ border-bottom-color: #f1f1f1;
+ left: 7%;
+ }
+
+ .align-left {
+ text-align: left;
+ }
+
+ .align-right {
+ text-align: right;
+ }
+
+ .float-right {
+ float: right;
+ }
+
+ .clearfix:after {
+ visibility: hidden;
+ display: block;
+ font-size: 0;
+ content: " ";
+ clear: both;
+ height: 0;
+ }
+ `
+ }
+
+ // attributeChangedCallback(name, oldVal, newVal) {
+ // console.log('attribute change: ', name, newVal.address);
+ // super.attributeChangedCallback(name, oldVal, newVal);
+ // }
+
+ constructor() {
+ super()
+ this.selectedAddress = {}
+ this.config = {
+ user: {
+ node: {
+
+ }
+ }
+ }
+ this.message = {}
+ }
+
+ render() {
+
+ return html`
+
+
+ ${this.message.sender}
+ 10:10 AM, Today
+
+
+
+ ${this.message.decodedMessage}
+
+
+ `
+ }
+
+
+ firstUpdated() {
+ let configLoaded = false
+
+ parentEpml.ready().then(() => {
+ parentEpml.subscribe('selected_address', async selectedAddress => {
+ this.selectedAddress = {}
+ selectedAddress = JSON.parse(selectedAddress)
+ if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
+ this.selectedAddress = selectedAddress
+ })
+ parentEpml.subscribe('config', c => {
+ if (!configLoaded) {
+ configLoaded = true
+ }
+ this.config = JSON.parse(c)
+ })
+ })
+
+ parentEpml.imReady()
+ }
+
+}
+
+window.customElements.define('chat-message', ChatMessage)
diff --git a/qortal-ui-plugins/plugins/core/components/ChatPage.js b/qortal-ui-plugins/plugins/core/components/ChatPage.js
new file mode 100644
index 00000000..fc238751
--- /dev/null
+++ b/qortal-ui-plugins/plugins/core/components/ChatPage.js
@@ -0,0 +1,988 @@
+import { LitElement, html, css } from 'lit-element'
+import { Epml } from '../../../epml.js'
+
+import { escape, unescape } from 'html-escaper';
+import { inputKeyCodes } from '../../utils/keyCodes.js';
+
+import './ChatScroller.js'
+import './TimeAgo.js'
+
+import { EmojiPicker } from 'emoji-picker-js';
+import '@polymer/paper-spinner/paper-spinner-lite.js'
+
+const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
+
+class ChatPage extends LitElement {
+ static get properties() {
+ return {
+ selectedAddress: { type: Object },
+ config: { type: Object },
+ messages: { type: Array },
+ _messages: { type: Array },
+ newMessages: { type: Array },
+ chatId: { type: String },
+ myAddress: { type: String },
+ isReceipient: { type: Boolean },
+ isLoading: { type: Boolean },
+ _publicKey: { type: Object },
+ balance: { type: Number },
+ socketTimeout: { type: Number },
+ messageSignature: { type: String },
+ _initialMessages: { type: Array },
+ isUserDown: { type: Boolean },
+ isPasteMenuOpen: { type: Boolean },
+ showNewMesssageBar: { attribute: false },
+ hideNewMesssageBar: { attribute: false }
+ }
+ }
+
+ static get styles() {
+ return css`
+ html {
+ scroll-behavior: smooth;
+ }
+ .chat-text-area {
+ display: flex;
+ justify-content: center;
+ overflow: hidden;
+ }
+ .chat-text-area .typing-area {
+ display: flex;
+ flex-direction: row;
+ position: absolute;
+ bottom: 0;
+ width: 98%;
+ box-sizing: border-box;
+ padding: 5px;
+ margin-bottom: 8px;
+ border: 1px solid rgba(0, 0, 0, 0.3);
+ border-radius: 10px;
+ }
+ .chat-text-area .typing-area textarea {
+ display: none;
+ }
+ .chat-text-area .typing-area .chat-editor {
+ border-color: transparent;
+ flex: 1;
+ max-height: 40px;
+ height: 40px;
+ margin: 0;
+ padding: 0;
+ border: none;
+ }
+ .chat-text-area .typing-area .emoji-button {
+ width: 45px;
+ height: 40px;
+ padding: 5px;
+ border: none;
+ outline: none;
+ background: transparent;
+ cursor: pointer;
+ max-height: 40px;
+ }
+ `
+ }
+
+ updated(changedProps) {
+ // changedProps.forEach((OldProp, name) => {
+ // if (name === 'messages') {
+ // this.scrollDownPage()
+ // }
+
+ // // if (name === 'newMessages') {
+ // // this.updateChatHistory(this.newMessages)
+ // // }
+ // });
+ }
+
+ constructor() {
+ super()
+ this.getOldMessage = this.getOldMessage.bind(this)
+ this._sendMessage = this._sendMessage.bind(this)
+ this._downObserverhandler = this._downObserverhandler.bind(this)
+
+ this.selectedAddress = {}
+ this.chatId = ''
+ this.myAddress = ''
+ this.messages = []
+ this._messages = []
+ this.newMessages = []
+ this._publicKey = { key: '', hasPubKey: false }
+ this.messageSignature = ''
+ this._initialMessages = []
+ this.balance = 1
+ this.isReceipient = false
+ this.isLoadingMessages = true
+ this.isLoading = false
+ this.isUserDown = false
+ this.isPasteMenuOpen = false
+ }
+
+ render() {
+
+ // TODO: Build a nice preloader for loading messages...
+ return html`
+ ${this.isLoadingMessages ? html`Loading Messages...
` : this.renderChatScroller(this._initialMessages)}
+
+
+
+
+
+
+
+
+
+ `
+ }
+
+ renderChatScroller(initialMessages) {
+
+ return html` `
+ }
+
+ getOldMessage(scrollElement) {
+
+ if (this._messages.length <= 15 && this._messages.length >= 1) { // 15 is the default number of messages...
+
+ let __msg = [...this._messages]
+ this._messages = []
+
+ return { oldMessages: __msg, scrollElement: scrollElement }
+ } else if (this._messages.length > 15) {
+
+ return { oldMessages: this._messages.splice(this._messages.length - 15), scrollElement: scrollElement }
+ } else {
+
+ return false
+ }
+ }
+
+ processMessages(messages, isInitial) {
+
+ if (isInitial) {
+
+ this.messages = messages.map((eachMessage) => {
+
+ if (eachMessage.isText === true) {
+ this.messageSignature = eachMessage.signature
+ let _eachMessage = this.decodeMessage(eachMessage)
+ return _eachMessage
+ }
+ })
+
+ this._messages = [...this.messages]
+
+ const adjustMessages = () => {
+
+ let __msg = [...this._messages]
+ this._messages = []
+ this._initialMessages = __msg
+ }
+
+ // TODO: Determine number of initial messages by screen height...
+ this._messages.length <= 15 ? adjustMessages() : this._initialMessages = this._messages.splice(this._messages.length - 15);
+
+ this.isLoadingMessages = false
+ setTimeout(() => this.downElementObserver(), 500)
+ } else {
+
+ let _newMessages = messages.map((eachMessage) => {
+
+ if (eachMessage.isText === true) {
+ let _eachMessage = this.decodeMessage(eachMessage)
+
+ if (this.messageSignature !== eachMessage.signature) {
+
+ this.messageSignature = eachMessage.signature
+
+ // What are we waiting for, send in the message immediately...
+ this.renderNewMessage(_eachMessage)
+ }
+
+ return _eachMessage
+ }
+ })
+
+ this.newMessages = this.newMessages.concat(_newMessages)
+
+ }
+
+
+ }
+
+ /**
+ * New Message Template implementation, takes in a message object.
+ * @param { Object } messageObj
+ * @property id or index
+ * @property sender and other info..
+ */
+ chatMessageTemplate(messageObj) {
+
+ return `
+
+
+ ${messageObj.senderName ? messageObj.senderName : messageObj.sender}
+
+
+ ${this.emojiPicker.parse(escape(messageObj.decodedMessage))}
+
+ `
+ }
+
+ renderNewMessage(newMessage) {
+
+ const viewElement = this.shadowRoot.querySelector('chat-scroller').shadowRoot.getElementById('viewElement');
+ const downObserver = this.shadowRoot.querySelector('chat-scroller').shadowRoot.getElementById('downObserver');
+ const li = document.createElement('li');
+ li.innerHTML = this.chatMessageTemplate(newMessage);
+ li.id = newMessage.signature;
+
+ if (newMessage.sender === this.selectedAddress.address) {
+
+ viewElement.insertBefore(li, downObserver);
+ viewElement.scrollTop = viewElement.scrollHeight;
+ } else if (this.isUserDown) {
+
+ // Append the message and scroll to the bottom if user is down the page
+ viewElement.insertBefore(li, downObserver);
+ viewElement.scrollTop = viewElement.scrollHeight;
+ } else {
+
+ viewElement.insertBefore(li, downObserver);
+ this.showNewMesssageBar();
+ }
+ }
+
+ /**
+ * Decode Message Method. Takes in a message object and returns a decoded message object
+ * @param {Object} encodedMessageObj
+ *
+ */
+ decodeMessage(encodedMessageObj) {
+ let decodedMessageObj = {}
+
+ if (this.isReceipient === true) {
+ // direct chat
+
+ if (encodedMessageObj.isEncrypted === true && this._publicKey.hasPubKey === true) {
+
+ let decodedMessage = window.parent.decryptChatMessage(encodedMessageObj.data, window.parent.reduxStore.getState().app.selectedAddress.keyPair.privateKey, this._publicKey.key, encodedMessageObj.reference)
+ decodedMessageObj = { ...encodedMessageObj, decodedMessage }
+ } else if (encodedMessageObj.isEncrypted === false) {
+
+ let bytesArray = window.parent.Base58.decode(encodedMessageObj.data)
+ let decodedMessage = new TextDecoder('utf-8').decode(bytesArray)
+ decodedMessageObj = { ...encodedMessageObj, decodedMessage }
+ } else {
+
+ decodedMessageObj = { ...encodedMessageObj, decodedMessage: "Cannot Decrypt Message!" }
+ }
+
+ } else {
+ // group chat
+
+ let bytesArray = window.parent.Base58.decode(encodedMessageObj.data)
+ let decodedMessage = new TextDecoder('utf-8').decode(bytesArray)
+ decodedMessageObj = { ...encodedMessageObj, decodedMessage }
+ }
+
+ return decodedMessageObj
+ }
+
+ async fetchChatMessages(chatId) {
+
+ const initDirect = (cid) => {
+
+ let initial = 0
+
+ let directSocketTimeout
+
+ let myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]
+ let nodeUrl = myNode.domain + ":" + myNode.port
+
+ let directSocketLink
+
+ if (window.parent.location.protocol === "https:") {
+
+ directSocketLink = `wss://${nodeUrl}/websockets/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${cid}`;
+ } else {
+
+ // Fallback to http
+ directSocketLink = `ws://${nodeUrl}/websockets/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${cid}`;
+ }
+
+ const directSocket = new WebSocket(directSocketLink);
+
+ // Open Connection
+ directSocket.onopen = () => {
+
+ setTimeout(pingDirectSocket, 50)
+ }
+
+ // Message Event
+ directSocket.onmessage = (e) => {
+
+ if (initial === 0) {
+
+ this.isLoadingMessages = true
+ this.processMessages(JSON.parse(e.data), true)
+ initial = initial + 1
+ } else {
+
+ this.processMessages(JSON.parse(e.data), false)
+ }
+ }
+
+ // Closed Event
+ directSocket.onclose = () => {
+ clearTimeout(directSocketTimeout)
+ }
+
+ // Error Event
+ directSocket.onerror = (e) => {
+ clearTimeout(directSocketTimeout)
+ console.log(`[DIRECT-SOCKET ==> ${cid}]: ${e.type}`);
+ }
+
+ const pingDirectSocket = () => {
+ directSocket.send('ping')
+
+ directSocketTimeout = setTimeout(pingDirectSocket, 295000)
+ }
+
+ };
+
+ const initGroup = (gId) => {
+ let groupId = Number(gId)
+
+ let initial = 0
+
+ let groupSocketTimeout
+
+ let myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]
+ let nodeUrl = myNode.domain + ":" + myNode.port
+
+ let groupSocketLink
+
+ if (window.parent.location.protocol === "https:") {
+
+ groupSocketLink = `wss://${nodeUrl}/websockets/chat/messages?txGroupId=${groupId}`;
+ } else {
+
+ // Fallback to http
+ groupSocketLink = `ws://${nodeUrl}/websockets/chat/messages?txGroupId=${groupId}`;
+ }
+
+ const groupSocket = new WebSocket(groupSocketLink);
+
+ // Open Connection
+ groupSocket.onopen = () => {
+
+ setTimeout(pingGroupSocket, 50)
+ }
+
+ // Message Event
+ groupSocket.onmessage = (e) => {
+
+ if (initial === 0) {
+
+ this.isLoadingMessages = true
+ this.processMessages(JSON.parse(e.data), true)
+ initial = initial + 1
+ } else {
+
+ this.processMessages(JSON.parse(e.data), false)
+ }
+ }
+
+ // Closed Event
+ groupSocket.onclose = () => {
+ clearTimeout(groupSocketTimeout)
+ }
+
+ // Error Event
+ groupSocket.onerror = (e) => {
+ clearTimeout(groupSocketTimeout)
+ console.log(`[GROUP-SOCKET ==> ${groupId}]: ${e.type}`);
+ }
+
+ const pingGroupSocket = () => {
+ groupSocket.send('ping')
+
+ groupSocketTimeout = setTimeout(pingGroupSocket, 295000)
+ }
+
+ };
+
+
+ if (chatId !== undefined) {
+
+ if (this.isReceipient) {
+ initDirect(chatId)
+ } else {
+ let groupChatId = Number(chatId)
+ initGroup(groupChatId)
+ }
+
+ } else {
+ // ... Render a nice "Error, Go Back" component.
+ }
+
+ // Add to the messages... TODO: Save messages to localstorage and fetch from it to make it persistent...
+ }
+
+ _sendMessage() {
+ this.isLoading = true;
+ this.chatEditor.disable();
+ const messageText = this.mirrorChatInput.value;
+
+ // Format and Sanitize Message
+ const sanitizedMessage = messageText.replace(/ /gi, ' ').replace(/
/gi, '\n');
+ const trimmedMessage = sanitizedMessage.trim();
+
+ if (/^\s*$/.test(trimmedMessage)) {
+ this.isLoading = false;
+ this.chatEditor.enable();
+ } else if (trimmedMessage.length >= 256) {
+ this.isLoading = false;
+ this.chatEditor.enable();
+ parentEpml.request('showSnackBar', "Maximum Characters per message is 255");
+ } else {
+ this.sendMessage(trimmedMessage);
+ }
+ }
+
+ async sendMessage(messageText) {
+ this.isLoading = true;
+
+ let _reference = new Uint8Array(64);
+ window.crypto.getRandomValues(_reference);
+ let reference = window.parent.Base58.encode(_reference);
+
+ const sendMessageRequest = async () => {
+ if (this.isReceipient === true) {
+ let chatResponse = await parentEpml.request('chat', {
+ type: 18,
+ nonce: this.selectedAddress.nonce,
+ params: {
+ timestamp: Date.now(),
+ recipient: this._chatId,
+ recipientPublicKey: this._publicKey.key,
+ message: messageText,
+ lastReference: reference,
+ proofOfWorkNonce: 0,
+ isEncrypted: this._publicKey.hasPubKey === false ? 0 : 1,
+ isText: 1
+ }
+ });
+
+ _computePow(chatResponse)
+ } else {
+ let groupResponse = await parentEpml.request('chat', {
+ type: 181,
+ nonce: this.selectedAddress.nonce,
+ params: {
+ timestamp: Date.now(),
+ groupID: Number(this._chatId),
+ hasReceipient: 0,
+ message: messageText,
+ lastReference: reference,
+ proofOfWorkNonce: 0,
+ isEncrypted: 0, // Set default to not encrypted for groups
+ isText: 1
+ }
+ });
+
+ _computePow(groupResponse)
+ }
+ };
+
+ const _computePow = async (chatBytes) => {
+ const _chatBytesArray = Object.keys(chatBytes).map(function (key) { return chatBytes[key]; });
+ const chatBytesArray = new Uint8Array(_chatBytesArray);
+ const chatBytesHash = new window.parent.Sha256().process(chatBytesArray).finish().result;
+ const hashPtr = window.parent.sbrk(32, window.parent.heap);
+ const hashAry = new Uint8Array(window.parent.memory.buffer, hashPtr, 32);
+ hashAry.set(chatBytesHash);
+
+ const difficulty = this.balance === 0 ? 14 : 8;
+ const workBufferLength = 8 * 1024 * 1024;
+ const workBufferPtr = window.parent.sbrk(workBufferLength, window.parent.heap);
+ let nonce = window.parent.computePow(hashPtr, workBufferPtr, workBufferLength, difficulty);
+
+ let _response = await parentEpml.request('sign_chat', {
+ nonce: this.selectedAddress.nonce,
+ chatBytesArray: chatBytesArray,
+ chatNonce: nonce
+ });
+ getSendChatResponse(_response);
+ };
+
+ const getSendChatResponse = (response) => {
+ if (response === true) {
+ this.chatEditor.resetValue();
+ } else if (response.error) {
+ parentEpml.request('showSnackBar', response.message);
+ } else {
+ parentEpml.request('showSnackBar', "Sending failed, Please retry...");
+ }
+
+ this.isLoading = false;
+ this.chatEditor.enable();
+ };
+
+ // Exec..
+ sendMessageRequest();
+ }
+
+ /**
+ * Method to set if the user's location is down in the chat
+ * @param { Boolean } isDown
+ */
+ setIsUserDown(isDown) {
+
+ this.isUserDown = isDown;
+ }
+
+ _downObserverhandler(entries) {
+
+ if (entries[0].isIntersecting) {
+
+ this.setIsUserDown(true)
+ this.hideNewMesssageBar()
+ } else {
+
+ this.setIsUserDown(false)
+ }
+ }
+
+ downElementObserver() {
+ const downObserver = this.shadowRoot.querySelector('chat-scroller').shadowRoot.getElementById('downObserver');
+
+ const options = {
+ root: this.shadowRoot.querySelector('chat-scroller').shadowRoot.getElementById('viewElement'),
+ rootMargin: '100px',
+ threshold: 1
+ }
+ const observer = new IntersectionObserver(this._downObserverhandler, options)
+ observer.observe(downObserver)
+ }
+
+
+ firstUpdated() {
+ // TODO: Load and fetch messages from localstorage (maybe save messages to localstorage...)
+
+ this.emojiPickerHandler = this.shadowRoot.querySelector('.emoji-button');
+ this.mirrorChatInput = this.shadowRoot.getElementById('messageBox');
+ this.chatMessageInput = this.shadowRoot.getElementById('_chatEditorDOM');
+
+ document.addEventListener('keydown', (e) => {
+ if (!this.chatEditor.content.body.matches(':focus')) {
+ // WARNING: Deprecated methods from KeyBoard Event
+ if (e.code === "Space" || e.keyCode === 32 || e.which === 32) {
+ this.chatEditor.insertText(' ');
+ } else if (inputKeyCodes.includes(e.keyCode)) {
+ this.chatEditor.insertText(e.key);
+ return this.chatEditor.focus();
+ } else {
+ return this.chatEditor.focus();
+ }
+ };
+ });
+
+ // Init EmojiPicker
+ 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'
+ });
+
+ this.emojiPicker.on('emoji', selection => {
+ const emojiHtmlString = `
`;
+ this.chatEditor.insertEmoji(emojiHtmlString);
+ });
+
+ // Attach Event Handler
+ this.emojiPickerHandler.addEventListener('click', () => this.emojiPicker.togglePicker(this.emojiPickerHandler));
+
+ const getAddressPublicKey = () => {
+
+ parentEpml.request('apiCall', {
+ type: 'api',
+ url: `/addresses/publickey/${this._chatId}`
+ }).then(res => {
+
+ if (res.error === 102) {
+
+ this._publicKey.key = ''
+ this._publicKey.hasPubKey = false
+ this.fetchChatMessages(this._chatId)
+ } else if (res !== false) {
+
+ this._publicKey.key = res
+ this._publicKey.hasPubKey = true
+ this.fetchChatMessages(this._chatId)
+ } else {
+
+ this._publicKey.key = ''
+ this._publicKey.hasPubKey = false
+ this.fetchChatMessages(this._chatId)
+ }
+ })
+ };
+
+ setTimeout(() => {
+ this.chatId.includes('direct') === true ? this.isReceipient = true : this.isReceipient = false;
+ this._chatId = this.chatId.split('/')[1];
+
+ const placeholder = this.isReceipient === true ? `Message ${this._chatId}` : 'Message...';
+ this.chatEditorPlaceholder = placeholder;
+
+ this.isReceipient ? getAddressPublicKey() : this.fetchChatMessages(this._chatId);
+
+ // Init ChatEditor
+ this.initChatEditor();
+ }, 100)
+
+ parentEpml.ready().then(() => {
+ parentEpml.subscribe('selected_address', async selectedAddress => {
+ this.selectedAddress = {}
+ selectedAddress = JSON.parse(selectedAddress)
+ if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
+ this.selectedAddress = selectedAddress
+ })
+ parentEpml.request('apiCall', {
+ url: `/addresses/balance/${window.parent.reduxStore.getState().app.selectedAddress.address}`
+ }).then(res => {
+ this.balance = res
+ })
+ parentEpml.subscribe('frame_paste_menu_switch', async res => {
+
+ res = JSON.parse(res)
+ if (res.isOpen === false && this.isPasteMenuOpen === true) {
+
+ this.pasteToTextBox(textarea)
+ this.isPasteMenuOpen = false
+ }
+ })
+ })
+
+ parentEpml.imReady();
+ }
+
+ pasteToTextBox(textarea) {
+
+ // Return focus to the window
+ window.focus()
+
+ navigator.clipboard.readText().then(clipboardText => {
+
+ textarea.value += clipboardText
+ textarea.focus()
+ });
+ }
+
+ pasteMenu(event) {
+
+ let eventObject = { pageX: event.pageX, pageY: event.pageY, clientX: event.clientX, clientY: event.clientY }
+ parentEpml.request('openFramePasteMenu', eventObject)
+ }
+
+ isEmptyArray(arr) {
+ if (!arr) { return true }
+ return arr.length === 0
+ }
+
+ initChatEditor() {
+
+ const ChatEditor = function (editorConfig) {
+
+ const ChatEditor = function () {
+ const editor = this;
+
+ editor.init();
+ };
+
+ ChatEditor.prototype.getValue = function () {
+ const editor = this;
+
+ if (editor.content) {
+ return editor.content.body.innerHTML;
+ }
+ };
+
+ ChatEditor.prototype.setValue = function (value) {
+ const editor = this;
+
+ if (value) {
+ editor.content.body.innerHTML = value;
+ editor.updateMirror();
+ }
+
+ editor.focus();
+ };
+
+ ChatEditor.prototype.resetValue = function () {
+ const editor = this;
+
+ editor.content.body.innerHTML = '';
+ editor.updateMirror();
+
+ editor.focus();
+ };
+
+ ChatEditor.prototype.styles = function () {
+ const editor = this;
+
+ editor.styles = document.createElement('style');
+ editor.styles.setAttribute('type', 'text/css');
+ editor.styles.innerText = `
+ html {
+ cursor: text;
+ }
+ body {
+ 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;
+ }
+ body[contentEditable=true]:empty:before {
+ content: attr(data-placeholder);
+ display: block;
+ color: rgb(103, 107, 113);
+ text-overflow: ellipsis;
+ overflow: hidden;
+ user-select: none;
+ white-space: nowrap;
+ }
+ body[contentEditable=false]{
+ background: rgba(0,0,0,0.1);
+ }
+ 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.content.body.setAttribute('contenteditable', 'true');
+ editor.focus();
+ };
+
+ ChatEditor.prototype.disable = function () {
+ const editor = this;
+
+ editor.content.body.setAttribute('contenteditable', 'false');
+ };
+
+ ChatEditor.prototype.state = function () {
+ const editor = this;
+
+ return editor.content.body.getAttribute('contenteditable');
+ };
+
+ ChatEditor.prototype.focus = function () {
+ const editor = this;
+
+ editor.content.body.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(//g, '');
+
+ let unescapedValue = editorConfig.unescape(filteredValue);
+ editor.mirror.value = unescapedValue;
+ };
+
+ ChatEditor.prototype.listenChanges = function () {
+ const editor = this;
+
+ ['drop', 'contextmenu', 'mouseup', 'click', 'touchend', 'keydown', 'blur', 'paste'].map(function (event) {
+ editor.content.body.addEventListener(event, function (e) {
+
+ if (e.type === 'click') {
+
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ if (e.type === 'paste') {
+ e.preventDefault();
+
+ navigator.clipboard.readText().then(clipboardText => {
+
+ let escapedText = editorConfig.escape(clipboardText);
+
+ editor.insertText(escapedText);
+ }).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') {
+
+ // Handle Enter
+ if (e.keyCode === 13 && !e.shiftKey) {
+
+ // Update Mirror
+ editor.updateMirror();
+
+ if (editor.state() === 'false') return false;
+
+ 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.init = function () {
+ const editor = this;
+
+ editor.frame = editorConfig.editableElement;
+ editor.mirror = editorConfig.mirrorElement;
+
+ editor.content = (editor.frame.contentDocument || editor.frame.document);
+ editor.content.body.setAttribute('contenteditable', 'true');
+ editor.content.body.setAttribute('data-placeholder', editorConfig.placeholder);
+ editor.content.body.setAttribute('spellcheck', 'false');
+
+ editor.styles();
+ editor.listenChanges();
+ };
+
+
+ function doInit() {
+
+ return new ChatEditor();
+ };
+
+ return doInit();
+ };
+
+ const editorConfig = {
+ mirrorElement: this.mirrorChatInput,
+ editableElement: this.chatMessageInput,
+ sendFunc: this._sendMessage,
+ emojiPicker: this.emojiPicker,
+ escape: escape,
+ unescape: unescape,
+ placeholder: this.chatEditorPlaceholder
+ };
+
+ this.chatEditor = new ChatEditor(editorConfig);
+ }
+
+}
+
+window.customElements.define('chat-page', ChatPage)
diff --git a/qortal-ui-plugins/plugins/core/components/ChatScroller.js b/qortal-ui-plugins/plugins/core/components/ChatScroller.js
new file mode 100644
index 00000000..a0446f86
--- /dev/null
+++ b/qortal-ui-plugins/plugins/core/components/ChatScroller.js
@@ -0,0 +1,243 @@
+import { LitElement, html, css } from 'lit-element'
+
+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 }
+ }
+ }
+
+ static get styles() {
+ return css`
+ html {
+ --scrollbarBG: #a1a1a1;
+ --thumbBG: #6a6c75;
+ }
+
+ *::-webkit-scrollbar {
+ width: 11px;
+ }
+
+ * {
+ scrollbar-width: thin;
+ scrollbar-color: var(--thumbBG) var(--scrollbarBG);
+ }
+
+ *::-webkit-scrollbar-track {
+ background: var(--scrollbarBG);
+ }
+
+ *::-webkit-scrollbar-thumb {
+ background-color: var(--thumbBG);
+ border-radius: 6px;
+ border: 3px solid var(--scrollbarBG);
+ }
+
+ ul {
+ list-style: none;
+ margin: 0;
+ padding: 20px;
+ }
+ .chat-list {
+ overflow-y: auto;
+ height: 91vh;
+ box-sizing: border-box;
+ }
+
+ .message-data {
+ margin-bottom: 15px;
+ }
+
+ .message-data-time {
+ color: #a8aab1;
+ font-size: 13px;
+ padding-left: 6px;
+ }
+
+ .message {
+ color: black;
+ padding: 12px 10px;
+ line-height: 19px;
+ white-space: pre-line;
+ word-wrap: break-word;
+ -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%;
+ 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;
+ }
+
+ .emoji {
+ width: 1.7em;
+ height: 1.5em;
+ margin-bottom: -2px;
+ vertical-align: bottom;
+ object-fit: contain;
+ }
+
+ .my-message {
+ background: #ddd;
+ border: 2px #ccc solid;
+ }
+
+ .other-message {
+ background: #f1f1f1;
+ border: 2px solid #dedede;
+ }
+
+ .other-message:after {
+ border-bottom-color: #f1f1f1;
+ left: 7%;
+ }
+
+ .align-left {
+ text-align: left;
+ }
+
+ .align-right {
+ text-align: right;
+ }
+
+ .float-right {
+ float: right;
+ }
+
+ .clearfix:after {
+ visibility: hidden;
+ display: block;
+ font-size: 0;
+ content: " ";
+ clear: both;
+ height: 0;
+ }
+ `
+ }
+
+ constructor() {
+ super()
+
+ this.messages = []
+ this._upObserverhandler = this._upObserverhandler.bind(this)
+ this.isLoading = false
+ this.myAddress = window.parent.reduxStore.getState().app.selectedAddress.address
+ }
+
+
+ render() {
+
+ return html`
+
+ `
+ }
+
+ chatMessageTemplate(messageObj) {
+
+ return `
+
+
+ ${messageObj.senderName ? messageObj.senderName : messageObj.sender}
+
+
+ ${this.emojiPicker.parse(this.escapeHTML(messageObj.decodedMessage))}
+
+ `
+ }
+
+ renderChatMessages(messages) {
+
+ messages.forEach(message => {
+ const li = document.createElement('li');
+ li.innerHTML = this.chatMessageTemplate(message);
+ li.id = message.signature;
+ this.downObserverElement.before(li);
+ });
+ }
+
+ renderOldMessages(listOfOldMessages) {
+
+ let { oldMessages, scrollElement } = listOfOldMessages;
+
+ let _oldMessages = oldMessages.reverse();
+ _oldMessages.forEach(oldMessage => {
+ const li = document.createElement('li');
+ li.innerHTML = this.chatMessageTemplate(oldMessage);
+ li.id = oldMessage.signature;
+ this.upObserverElement.after(li);
+ scrollElement.scrollIntoView({ behavior: 'auto', block: 'center' });
+ });
+ }
+
+ _getOldMessage(_scrollElement) {
+
+ let listOfOldMessages = this.getOldMessage(_scrollElement)
+
+ if (listOfOldMessages) {
+ this.renderOldMessages(listOfOldMessages)
+ }
+ }
+
+ _upObserverhandler(entries) {
+
+ if (entries[0].isIntersecting) {
+ let _scrollElement = entries[0].target.nextElementSibling
+
+ this._getOldMessage(_scrollElement)
+ }
+ }
+
+ upElementObserver() {
+ const options = {
+ root: this.viewElement,
+ rootMargin: '0px',
+ threshold: 1
+ };
+
+ const observer = new IntersectionObserver(this._upObserverhandler, options)
+ observer.observe(this.upObserverElement)
+ }
+
+ firstUpdated() {
+
+ this.viewElement = this.shadowRoot.getElementById('viewElement');
+ this.upObserverElement = this.shadowRoot.getElementById('upObserver');
+ this.downObserverElement = this.shadowRoot.getElementById('downObserver');
+
+ this.renderChatMessages(this.initialMessages)
+
+ // Intialize Observers
+ this.upElementObserver()
+
+ this.viewElement.scrollTop = this.viewElement.scrollHeight + 50;
+ }
+
+}
+
+window.customElements.define('chat-scroller', ChatScroller)
diff --git a/qortal-ui-plugins/plugins/core/components/ChatWelcomePage.js b/qortal-ui-plugins/plugins/core/components/ChatWelcomePage.js
new file mode 100644
index 00000000..9cd92435
--- /dev/null
+++ b/qortal-ui-plugins/plugins/core/components/ChatWelcomePage.js
@@ -0,0 +1,439 @@
+import { LitElement, html, css } from 'lit-element'
+import { render } from 'lit-html'
+import { Epml } from '../../../epml.js'
+
+import '@material/mwc-icon'
+import '@material/mwc-button'
+import '@material/mwc-dialog'
+import '@polymer/paper-spinner/paper-spinner-lite.js'
+
+const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
+
+class ChatWelcomePage extends LitElement {
+ static get properties() {
+ return {
+ selectedAddress: { type: Object },
+ config: { type: Object },
+ myAddress: { type: Object, reflect: true },
+ messages: { type: Array },
+ btnDisable: { type: Boolean },
+ isLoading: { type: Boolean },
+ balance: { type: Number }
+ }
+ }
+
+ static get styles() {
+ return css`
+ @keyframes moveInBottom {
+ 0% {
+ opacity: 0;
+ transform: translateY(30px);
+ }
+
+ 100% {
+ opacity: 1;
+ transform: translate(0);
+ }
+ }
+
+ paper-spinner-lite{
+ height: 24px;
+ width: 24px;
+ --paper-spinner-color: var(--mdc-theme-primary);
+ --paper-spinner-stroke-width: 2px;
+ }
+
+ .welcome-title {
+ display: block;
+ overflow: hidden;
+ font-size: 40px;
+ color: black;
+ font-weight: 400;
+ text-align: center;
+ white-space: pre-wrap;
+ overflow-wrap: break-word;
+ word-break: break-word;
+ cursor: inherit;
+ margin-top: 2rem;
+ }
+
+ .sub-main {
+ position: relative;
+ text-align: center;
+ }
+
+ .center-box {
+ position: absolute;
+ top: 45%;
+ left: 50%;
+ transform: translate(-50%, 0%);
+ text-align: center;
+ }
+
+ .img-icon {
+ font-size: 150px;
+ }
+
+ .start-chat {
+ display: inline-flex;
+ flex-direction: column;
+ justify-content: center;
+ align-content: center;
+ border: none;
+ border-radius: 20px;
+ padding-left: 25px;
+ padding-right: 25px;
+ color: white;
+ background: #6a6c75;
+ width: 50%;
+ font-size: 17px;
+ cursor: pointer;
+ height: 50px;
+ margin-top: 1rem;
+ text-transform: uppercase;
+ text-decoration: none;
+ transition: all .2s;
+ position: relative;
+ animation: moveInBottom .3s ease-out .50s;
+ animation-fill-mode: backwards;
+ }
+
+ .start-chat:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 10px 20px rgba(0, 0, 0, .2);
+ }
+
+ .start-chat::after {
+ content: "";
+ display: inline-flex;
+ height: 100%;
+ width: 100%;
+ border-radius: 100px;
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: -1;
+ transition: all .4s;
+ }
+
+ .red {
+ --mdc-theme-primary: red;
+ }
+
+ h2 {
+ margin:0;
+ }
+
+ h2, h3, h4, h5 {
+ color:#333;
+ font-weight: 400;
+ }
+
+ [hidden] {
+ display: hidden !important;
+ visibility: none !important;
+ }
+
+ .details {
+ display: flex;
+ font-size: 18px;
+ }
+
+ .title {
+ font-weight:600;
+ font-size:12px;
+ line-height: 32px;
+ opacity: 0.66;
+ }
+
+ .input {
+ width: 90%;
+ border: none;
+ display: inline-block;
+ font-size: 16px;
+ padding: 10px 20px;
+ border-radius: 5px;
+ resize: none;
+ background: #eee;
+ }
+
+ .textarea {
+ width: 90%;
+ border: none;
+ display: inline-block;
+ font-size: 16px;
+ padding: 10px 20px;
+ border-radius: 5px;
+ height: 120px;
+ resize: none;
+ background: #eee;
+ }
+ `
+ }
+
+ constructor() {
+ super()
+ this.selectedAddress = window.parent.reduxStore.getState().app.selectedAddress.address
+ this.myAddress = {}
+ this.balance = 1
+ this.messages = []
+ this.btnDisable = false
+ this.isLoading = false
+ }
+
+ render() {
+ return html`
+
+
+ Welcome to Q-Chat
+
+
+
+
+
chat
+
${this.myAddress.address}
+
this.shadowRoot.querySelector('#startSecondChatDialog').show()}>New Private Message
+
+
+
+
+
+
+
New Private Message
+
+
+
+ Type the name or address of who you want to chat with to send a private message!
+
+
+
+
+
+
+ Send
+
+ Close
+
+
+
+ `
+ }
+
+ firstUpdated() {
+ const stopKeyEventPropagation = (e) => {
+ e.stopPropagation();
+ return false;
+ }
+
+ this.shadowRoot.getElementById('sendTo').addEventListener('keydown', stopKeyEventPropagation);
+ this.shadowRoot.getElementById('messageBox').addEventListener('keydown', stopKeyEventPropagation);
+
+ const getDataFromURL = () => {
+ let tempUrl = document.location.href
+ let splitedUrl = decodeURI(tempUrl).split('?')
+ let urlData = splitedUrl[1]
+ if (urlData !== undefined) {
+ this.chatId = urlData
+ }
+ }
+
+ let configLoaded = false
+
+ parentEpml.ready().then(() => {
+ parentEpml.subscribe('selected_address', async selectedAddress => {
+ this.selectedAddress = {}
+ selectedAddress = JSON.parse(selectedAddress)
+ if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
+ this.selectedAddress = selectedAddress
+ })
+ parentEpml.request('apiCall', {
+ url: `/addresses/balance/${window.parent.reduxStore.getState().app.selectedAddress.address}`
+ }).then(res => {
+ this.balance = res
+ })
+ })
+
+ parentEpml.imReady()
+ }
+
+ _sendMessage() {
+
+ this.isLoading = true
+
+ const recipient = this.shadowRoot.getElementById('sendTo').value
+ const messageBox = this.shadowRoot.getElementById('messageBox')
+ const messageText = messageBox.value
+
+ if (recipient.length === 0) {
+ this.isLoading = false
+ } else if (messageText.length === 0) {
+ this.isLoading = false
+ } else {
+ this.sendMessage()
+ }
+ }
+
+ async sendMessage(e) {
+ this.isLoading = true
+
+ const _recipient = this.shadowRoot.getElementById('sendTo').value
+ const messageBox = this.shadowRoot.getElementById('messageBox')
+ const messageText = messageBox.value
+ let recipient
+
+ const validateName = async (receiverName) => {
+ let myRes
+ let myNameRes = await parentEpml.request('apiCall', {
+ type: 'api',
+ url: `/names/${receiverName}`
+ })
+
+ if (myNameRes.error === 401) {
+ myRes = false
+ } else {
+ myRes = myNameRes
+ }
+
+ return myRes
+ }
+
+ const myNameRes = await validateName(_recipient)
+ if (!myNameRes) {
+
+ recipient = _recipient
+ } else {
+
+ recipient = myNameRes.owner
+ }
+
+ let _reference = new Uint8Array(64);
+ window.crypto.getRandomValues(_reference);
+
+ let sendTimestamp = Date.now()
+
+ let reference = window.parent.Base58.encode(_reference)
+
+ const getAddressPublicKey = async () => {
+ let isEncrypted
+ let _publicKey
+
+ let addressPublicKey = await parentEpml.request('apiCall', {
+ type: 'api',
+ url: `/addresses/publickey/${recipient}`
+ })
+
+
+ if (addressPublicKey.error === 102) {
+ _publicKey = false
+ // Do something here...
+ parentEpml.request('showSnackBar', "Invalid Name / Address, Check the name / address and retry...")
+ this.isLoading = false
+ } else if (addressPublicKey !== false) {
+ isEncrypted = 1
+ _publicKey = addressPublicKey
+ sendMessageRequest(isEncrypted, _publicKey)
+ } else {
+ isEncrypted = 0
+ _publicKey = this.selectedAddress.address
+ sendMessageRequest(isEncrypted, _publicKey)
+ }
+ };
+
+ const sendMessageRequest = async (isEncrypted, _publicKey) => {
+
+ let chatResponse = await parentEpml.request('chat', {
+ type: 18,
+ nonce: this.selectedAddress.nonce,
+ params: {
+ timestamp: sendTimestamp,
+ recipient: recipient,
+ recipientPublicKey: _publicKey,
+ message: messageText,
+ lastReference: reference,
+ proofOfWorkNonce: 0,
+ isEncrypted: isEncrypted,
+ isText: 1
+ }
+ })
+
+ _computePow(chatResponse)
+ }
+
+ const _computePow = async (chatBytes) => {
+
+ const _chatBytesArray = Object.keys(chatBytes).map(function (key) { return chatBytes[key]; });
+ const chatBytesArray = new Uint8Array(_chatBytesArray)
+ const chatBytesHash = new window.parent.Sha256().process(chatBytesArray).finish().result
+ const hashPtr = window.parent.sbrk(32, window.parent.heap);
+ const hashAry = new Uint8Array(window.parent.memory.buffer, hashPtr, 32);
+ hashAry.set(chatBytesHash);
+
+ const difficulty = this.balance === 0 ? 14 : 8;
+
+ const workBufferLength = 8 * 1024 * 1024;
+ const workBufferPtr = window.parent.sbrk(workBufferLength, window.parent.heap);
+
+ let nonce = window.parent.computePow(hashPtr, workBufferPtr, workBufferLength, difficulty)
+
+ let _response = await parentEpml.request('sign_chat', {
+ nonce: this.selectedAddress.nonce,
+ chatBytesArray: chatBytesArray,
+ chatNonce: nonce
+ })
+
+ getSendChatResponse(_response)
+ }
+
+ const getSendChatResponse = (response) => {
+
+ if (response === true) {
+ messageBox.value = ""
+ parentEpml.request('showSnackBar', "Message Sent Successfully!")
+ this.isLoading = false
+ } else if (response.error) {
+ parentEpml.request('showSnackBar', response.message)
+ this.isLoading = false
+ } else {
+ parentEpml.request('showSnackBar', "Sending failed, Please retry...")
+ this.isLoading = false
+ }
+
+ }
+ getAddressPublicKey()
+ }
+
+ _textMenu(event) {
+ const getSelectedText = () => {
+ var text = "";
+ if (typeof window.getSelection != "undefined") {
+ text = window.getSelection().toString();
+ } else if (typeof this.shadowRoot.selection != "undefined" && this.shadowRoot.selection.type == "Text") {
+ text = this.shadowRoot.selection.createRange().text;
+ }
+ return text;
+ }
+
+ const checkSelectedTextAndShowMenu = () => {
+ let selectedText = getSelectedText();
+ if (selectedText && typeof selectedText === 'string') {
+
+ let _eve = { pageX: event.pageX, pageY: event.pageY, clientX: event.clientX, clientY: event.clientY }
+
+ let textMenuObject = { selectedText: selectedText, eventObject: _eve, isFrame: true }
+
+ parentEpml.request('openCopyTextMenu', textMenuObject)
+ }
+ }
+ checkSelectedTextAndShowMenu()
+ }
+
+ _textArea(e) {
+ if (e.keyCode === 13 && !e.shiftKey) this._sendMessage()
+ }
+}
+
+window.customElements.define('chat-welcome-page', ChatWelcomePage)
diff --git a/qortal-ui-plugins/plugins/core/components/TimeAgo.js b/qortal-ui-plugins/plugins/core/components/TimeAgo.js
new file mode 100644
index 00000000..b5231e29
--- /dev/null
+++ b/qortal-ui-plugins/plugins/core/components/TimeAgo.js
@@ -0,0 +1,53 @@
+import { LitElement, html, css } from 'lit-element'
+
+import '@github/time-elements'
+
+class TimeAgo extends LitElement {
+ static get properties() {
+ return {
+ selectedAddress: { type: Object },
+ config: { type: Object },
+ timestamp: { type: Number },
+ format: { type: String, reflect: true },
+ timeIso: { type: String }
+ }
+ }
+
+ static get styles() {
+ return css``
+ }
+
+ updated(changedProps) {
+ changedProps.forEach((OldProp, name) => {
+ if (name === 'timeIso') {
+ this.renderTime(this.timestamp)
+ }
+ });
+
+ this.shadowRoot.querySelector('time-ago').setAttribute('title', '');
+ }
+
+ constructor() {
+ super();
+ this.timestamp = 0
+ this.timeIso = ''
+ this.format = ''
+ }
+
+ render() {
+
+ return html`
+
+ `
+ }
+
+ renderTime(timestamp) {
+ timestamp === undefined ? this.timeIso = '' : this.timeIso = new Date(timestamp).toISOString();
+ }
+
+ firstUpdated() {
+ // ...
+ }
+}
+
+window.customElements.define('message-time', TimeAgo)
diff --git a/qortal-ui-plugins/plugins/core/components/ToolTip.js b/qortal-ui-plugins/plugins/core/components/ToolTip.js
new file mode 100644
index 00000000..14afe03b
--- /dev/null
+++ b/qortal-ui-plugins/plugins/core/components/ToolTip.js
@@ -0,0 +1,135 @@
+import { LitElement, html, css } from 'lit-element'
+import { Epml } from '../../../epml.js'
+
+
+const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
+
+class ToolTip extends LitElement {
+ static get properties() {
+ return {
+ selectedAddress: { type: Object },
+ config: { type: Object },
+ toolTipMessage: { type: String, reflect: true },
+ showToolTip: { type: Boolean, reflect: true }
+ }
+ }
+
+ static get styles() {
+ return css`
+ .tooltip {
+ position: relative;
+ display: inline-block;
+ border-bottom: 1px dotted black;
+ }
+
+ .tooltiptext {
+ margin-bottom: 100px;
+ display: inline;
+ visibility: visible;
+ width: 120px;
+ background-color: #555;
+ color: #fff;
+ text-align: center;
+ border-radius: 6px;
+ padding: 5px 0;
+ position: absolute;
+ z-index: 1;
+ bottom: 125%;
+ left: 50%;
+ margin-left: -60px;
+ opacity: 1;
+ transition: opacity 0.3s;
+ }
+
+ .tooltiptext::after {
+ content: "";
+ position: absolute;
+ top: 100%;
+ left: 50%;
+ margin-left: -5px;
+ border-width: 5px;
+ border-style: solid;
+ border-color: #555 transparent transparent transparent;
+ }
+
+ .hide-tooltip {
+ display: none;
+ visibility: hidden;
+ opacity: 0;
+ }
+ `
+ }
+
+ // attributeChangedCallback(name, oldVal, newVal) {
+ // console.log('attribute change: ', name, newVal.address);
+ // super.attributeChangedCallback(name, oldVal, newVal);
+ // }
+
+ constructor() {
+ super()
+ this.selectedAddress = {}
+ this.config = {
+ user: {
+ node: {
+
+ }
+ }
+ }
+ this.toolTipMessage = ''
+ this.showToolTip = false
+ }
+
+ render() {
+
+ console.log(this.toolTipMessage, "tool");
+
+ return html`
+ ${this.toolTipMessage}
+ `
+ }
+
+
+ // {
+ // "type": "CHAT",
+ // "timestamp": 1589189772000,
+ // "reference": "1111111111111111111111111111111111111111111111111111111111111111",
+ // "fee": "0.00000000",
+ // "signature": "7gXr4sZ3W6Lq7sRHwoxQ6nEq4LvV7aiVkhfi2xtsf6v1P4M2v4oYptMowRXvbtEhJQfg2wfr3BMDmhCEcrAENRn",
+ // "txGroupId": 0,
+ // "approvalStatus": "NOT_REQUIRED",
+ // "creatorAddress": "QdevPHFK86KNuzoYKLqFz7DPkr2x4juzvi",
+ // "senderPublicKey": "31J8KD24kFbNtdrQg5iUEHXGxGSxKC9jxLDakE1QChyG",
+ // "sender": "QdevPHFK86KNuzoYKLqFz7DPkr2x4juzvi",
+ // "nonce": 89955,
+ // "data": "35sYULUFnjz7SCRSb",
+ // "isText": false,
+ // "isEncrypted": false
+ // }
+
+ 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) {
+ // setTimeout(getGroupIdFromURL, 1)
+ configLoaded = true
+ }
+ this.config = JSON.parse(c)
+ })
+ })
+
+
+ parentEpml.imReady()
+ }
+
+
+}
+
+window.customElements.define('tool-tip', ToolTip)