4
1
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:
AlphaX-Projects 2021-12-25 16:01:33 +01:00 committed by GitHub
parent f66326e1b2
commit d327a75360
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 2233 additions and 0 deletions

View 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)

View 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)

View 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> &nbsp;
<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)

View 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(/&nbsp;/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('&nbsp;');
} 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)

View 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)

View 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)

View 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)

View 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)