mirror of
https://github.com/Qortal/qortal-ui.git
synced 2025-02-11 17:55:51 +00:00
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