missing files
This commit is contained in:
parent
f66326e1b2
commit
d327a75360
68
qortal-ui-plugins/plugins/core/components/ButtonIconCopy.js
Normal file
68
qortal-ui-plugins/plugins/core/components/ButtonIconCopy.js
Normal file
@ -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`
|
||||
<mwc-icon-button
|
||||
title=${this.title}
|
||||
label=${this.title}
|
||||
icon="content_copy"
|
||||
@click=${() => this.saveToClipboard(this.textToCopy)}
|
||||
>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
}
|
||||
|
||||
async saveToClipboard(text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
parentEpml.request('showSnackBar', this.onSuccessMessage)
|
||||
} catch (err) {
|
||||
parentEpml.request('showSnackBar', this.onErrorMessage)
|
||||
console.error('Copy to clipboard error:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('button-icon-copy', ButtonIconCopy)
|
156
qortal-ui-plugins/plugins/core/components/ChatHead.js
Normal file
156
qortal-ui-plugins/plugins/core/components/ChatHead.js
Normal file
@ -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`
|
||||
<li @click=${() => this.getUrl(this.chatInfo.url)} class="clearfix ${this.activeChatHeadUrl === this.chatInfo.url ? 'active' : ''}">
|
||||
<mwc-icon class="img-icon">account_circle</mwc-icon>
|
||||
<div class="about">
|
||||
<div class="name"><span style="float:left; padding-left: 8px;">${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;">${this.chatInfo.groupId !== undefined ? 'lock_open' : 'lock'}</mwc-icon> </div>
|
||||
</div>
|
||||
</li>
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
// 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)
|
151
qortal-ui-plugins/plugins/core/components/ChatMessage.js
Normal file
151
qortal-ui-plugins/plugins/core/components/ChatMessage.js
Normal file
@ -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`
|
||||
<li class="clearfix">
|
||||
<div class="message-data ${this.message.sender === this.selectedAddress.address ? "align-right" : ""}">
|
||||
<span class="message-data-name">${this.message.sender}</span>
|
||||
<span class="message-data-time">10:10 AM, Today</span>
|
||||
|
||||
</div>
|
||||
<div class="message ${this.message.sender === this.selectedAddress.address ? "my-message float-right" : "other-message"}">
|
||||
${this.message.decodedMessage}
|
||||
</div>
|
||||
</li>
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
firstUpdated() {
|
||||
let configLoaded = false
|
||||
|
||||
parentEpml.ready().then(() => {
|
||||
parentEpml.subscribe('selected_address', async selectedAddress => {
|
||||
this.selectedAddress = {}
|
||||
selectedAddress = JSON.parse(selectedAddress)
|
||||
if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
|
||||
this.selectedAddress = selectedAddress
|
||||
})
|
||||
parentEpml.subscribe('config', c => {
|
||||
if (!configLoaded) {
|
||||
configLoaded = true
|
||||
}
|
||||
this.config = JSON.parse(c)
|
||||
})
|
||||
})
|
||||
|
||||
parentEpml.imReady()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
window.customElements.define('chat-message', ChatMessage)
|
988
qortal-ui-plugins/plugins/core/components/ChatPage.js
Normal file
988
qortal-ui-plugins/plugins/core/components/ChatPage.js
Normal file
@ -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`<h1>Loading Messages...</h1>` : this.renderChatScroller(this._initialMessages)}
|
||||
|
||||
<div class="chat-text-area">
|
||||
<div class="typing-area">
|
||||
<textarea tabindex='1' ?autofocus=${true} ?disabled=${this.isLoading || this.isLoadingMessages} id="messageBox" rows="1"></textarea>
|
||||
|
||||
<iframe class="chat-editor" id="_chatEditorDOM" tabindex="-1"></iframe>
|
||||
<button class="emoji-button" ?disabled=${this.isLoading || this.isLoadingMessages}>
|
||||
${this.isLoading === false ? html`<img class="emoji" draggable="false" alt="😀" src="/emoji/svg/1f600.svg">` : html`<paper-spinner-lite active></paper-spinner-lite>`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
renderChatScroller(initialMessages) {
|
||||
|
||||
return html`<chat-scroller .initialMessages=${initialMessages} .emojiPicker=${this.emojiPicker} .escapeHTML=${escape} .getOldMessage=${this.getOldMessage} > </chat-scroller>`
|
||||
}
|
||||
|
||||
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 `
|
||||
<li class="clearfix">
|
||||
<div class="message-data ${messageObj.sender === this.selectedAddress.address ? "align-right" : ""}">
|
||||
<span class="message-data-name">${messageObj.senderName ? messageObj.senderName : messageObj.sender}</span>
|
||||
<span class="message-data-time"><message-time timestamp=${messageObj.timestamp}></message-time></span>
|
||||
</div>
|
||||
<div class="message ${messageObj.sender === this.selectedAddress.address ? "my-message float-right" : "other-message"}">${this.emojiPicker.parse(escape(messageObj.decodedMessage))}</div>
|
||||
</li>
|
||||
`
|
||||
}
|
||||
|
||||
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(/<br\s*[\/]?>/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 = `<img class="emoji" draggable="false" alt="${selection.emoji}" src="${selection.url}">`;
|
||||
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(/<img.*?alt=".*?/g, '').replace(/".?src=.*?>/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)
|
243
qortal-ui-plugins/plugins/core/components/ChatScroller.js
Normal file
243
qortal-ui-plugins/plugins/core/components/ChatScroller.js
Normal file
@ -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`
|
||||
<ul id="viewElement" class="chat-list clearfix">
|
||||
<div id="upObserver"></div>
|
||||
<div id="downObserver"></div>
|
||||
</ul>
|
||||
`
|
||||
}
|
||||
|
||||
chatMessageTemplate(messageObj) {
|
||||
|
||||
return `
|
||||
<li class="clearfix">
|
||||
<div class="message-data ${messageObj.sender === this.myAddress ? "align-right" : ""}">
|
||||
<span class="message-data-name">${messageObj.senderName ? messageObj.senderName : messageObj.sender}</span>
|
||||
<span class="message-data-time"><message-time timestamp=${messageObj.timestamp}></message-time></span>
|
||||
</div>
|
||||
<div id="messageContent" class="message ${messageObj.sender === this.myAddress ? "my-message float-right" : "other-message"}">${this.emojiPicker.parse(this.escapeHTML(messageObj.decodedMessage))}</div>
|
||||
</li>
|
||||
`
|
||||
}
|
||||
|
||||
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)
|
439
qortal-ui-plugins/plugins/core/components/ChatWelcomePage.js
Normal file
439
qortal-ui-plugins/plugins/core/components/ChatWelcomePage.js
Normal file
@ -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`
|
||||
<div>
|
||||
<div>
|
||||
<span class="welcome-title">Welcome to Q-Chat</span>
|
||||
<hr style="color: #eee; border-radius: 80%; margin-bottom: 2rem;">
|
||||
</div>
|
||||
<div class="sub-main">
|
||||
<div class="center-box">
|
||||
<mwc-icon class="img-icon">chat</mwc-icon><br>
|
||||
<span style="font-size: 20px;">${this.myAddress.address}</span>
|
||||
<div class="start-chat" @click=${() => this.shadowRoot.querySelector('#startSecondChatDialog').show()}>New Private Message</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Start Chatting Dialog -->
|
||||
<mwc-dialog id="startSecondChatDialog" scrimClickAction="${this.isLoading ? '' : 'close'}">
|
||||
<div style="text-align:center">
|
||||
<h1>New Private Message</h1>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<p>Type the name or address of who you want to chat with to send a private message!</p>
|
||||
|
||||
<textarea class="input" ?disabled=${this.isLoading} id="sendTo" placeholder="Name / Address" rows="1"></textarea>
|
||||
<p style="margin-bottom:0;">
|
||||
<textarea class="textarea" @keydown=${(e) => this._textArea(e)} ?disabled=${this.isLoading} id="messageBox" placeholder="Message..." rows="1"></textarea>
|
||||
</p>
|
||||
|
||||
<mwc-button ?disabled="${this.isLoading}" slot="primaryAction" @click=${this._sendMessage}>Send</mwc-button>
|
||||
<mwc-button
|
||||
?disabled="${this.isLoading}"
|
||||
slot="secondaryAction"
|
||||
dialogAction="cancel"
|
||||
class="red">
|
||||
Close
|
||||
</mwc-button>
|
||||
</mwc-dialog>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
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)
|
53
qortal-ui-plugins/plugins/core/components/TimeAgo.js
Normal file
53
qortal-ui-plugins/plugins/core/components/TimeAgo.js
Normal file
@ -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`
|
||||
<time-ago datetime=${this.timeIso} format=${this.format}> </time-ago>
|
||||
`
|
||||
}
|
||||
|
||||
renderTime(timestamp) {
|
||||
timestamp === undefined ? this.timeIso = '' : this.timeIso = new Date(timestamp).toISOString();
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('message-time', TimeAgo)
|
135
qortal-ui-plugins/plugins/core/components/ToolTip.js
Normal file
135
qortal-ui-plugins/plugins/core/components/ToolTip.js
Normal file
@ -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`
|
||||
<span id="myTool" class="tooltiptext">${this.toolTipMessage}</span>
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
// {
|
||||
// "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)
|
Loading…
x
Reference in New Issue
Block a user