Browse Source

Merge branch 'feature/reusable-editor' into feature/implement-logic-edit-reply-messages

q-apps
Phillip Lang Martinez 2 years ago
parent
commit
62302f5efb
  1. 728
      qortal-ui-plugins/plugins/core/components/ChatPage.js
  2. 26
      qortal-ui-plugins/plugins/core/components/ChatScroller.js
  3. 691
      qortal-ui-plugins/plugins/core/components/ChatTextEditor.js

728
qortal-ui-plugins/plugins/core/components/ChatPage.js

@ -14,7 +14,7 @@ import './ChatScroller.js'
import './LevelFounder.js'
import './NameMenu.js'
import './TimeAgo.js'
import { EmojiPicker } from 'emoji-picker-js';
import './ChatTextEditor'
import '@polymer/paper-spinner/paper-spinner-lite.js'
import '@material/mwc-button'
import '@material/mwc-dialog'
@ -62,7 +62,8 @@ class ChatPage extends LitElement {
imageFile: {type: Object},
isUploadingImage: {type: Boolean},
caption: { type: String },
chatEditor: {type: Object}
chatEditor: {type: Object},
chatEditorNewChat: {type: Object}
}
}
@ -72,6 +73,7 @@ class ChatPage extends LitElement {
/* Styling mdc dialog native props */
--mdc-dialog-min-width: 300px;
--mdc-dialog-box-shadow:rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 2px 6px 2px;
--mdc-dialog-z-index: 5
}
html {
@ -231,6 +233,9 @@ class ChatPage extends LitElement {
height: auto;
overflow: hidden;
justify-content: center;
background: white;
padding: 5px;
border-radius: 1px;
}
.chatbar-caption {
@ -348,47 +353,6 @@ class ChatPage extends LitElement {
border-radius: 25%;
}
.paperclip-icon {
color: #494949;
width: 25px;
}
.paperclip-icon:hover {
cursor: pointer;
}
.send-icon {
width: 30px;
margin-left: 5px;
transition: all 0.1s ease-in-out;
cursor: pointer;
}
.send-icon:hover {
filter: brightness(1.1);
}
.file-picker-container {
position: relative;
height: 25px;
width: 25px;
}
.file-picker-input-container {
position: absolute;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
z-index: 10;
opacity: 0;
overflow: hidden;
}
input[type=file]::-webkit-file-upload-button {
cursor: pointer;
}
.dialogCustom {
position: fixed;
z-index: 10000;
@ -471,6 +435,7 @@ class ChatPage extends LitElement {
.dialog-container {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 0 10px;
gap: 10px;
@ -490,12 +455,13 @@ class ChatPage extends LitElement {
constructor() {
super()
this.changeMsgInput = this.changeMsgInput.bind(this)
this.getOldMessage = this.getOldMessage.bind(this)
this._sendMessage = this._sendMessage.bind(this)
this.insertImage = this.insertImage.bind(this)
this.getMessageSize = this.getMessageSize.bind(this)
// this.getMessageSize = this.getMessageSize.bind(this)
this._downObserverhandler = this._downObserverhandler.bind(this)
this.calculateIFrameHeight = this.calculateIFrameHeight.bind(this)
// this.calculateIFrameHeight = this.calculateIFrameHeight.bind(this)
this.selectedAddress = {}
this.chatId = ''
this.myAddress = ''
@ -543,12 +509,14 @@ class ChatPage extends LitElement {
` :
this.renderChatScroller()}
<mwc-dialog
id="showDialogPublicKey"
?open=${this.imageFile}
@closed=${() => {
this.chatEditor.enable();
this.caption = "";
this.imageFile = null;
this.chatEditor.enable()
}}>
<div class="dialog-header"></div>
<div class="dialog-container mdc-dialog mdc-dialog__surface">
@ -557,25 +525,26 @@ class ChatPage extends LitElement {
`}
<!-- Replace by reusable chatbar component -->
<div class="caption-container">
<textarea @change=${(e) => this.onCaptionChange(e.target.value)} .value=${this.caption} placeholder="Caption" class="chatbar-caption" tabindex='1' rows="1"></textarea>
<div style="display:flex; ${this.chatMessageInput && this.chatMessageInput.contentDocument.body.scrollHeight > 60 ? 'margin-bottom: 5px' : "margin-bottom: 0"}">
${this.isLoading === false ? html`
<img
src="/img/qchat-send-message-icon.svg"
alt="send-icon"
class="send-icon"
@click=${()=> {
this._sendMessage({
type: 'image',
imageFile: this.imageFile,
caption: this.caption,
})
}} />
` :
html`
<paper-spinner-lite active></paper-spinner-lite>
`}
</div>
<chat-text-editor
iframeId="newChat"
?hasGlobalEvents=${false}
placeholder=${this.chatEditorPlaceholder}
._sendMessage=${this._sendMessage}
.setChatEditor=${(editor)=> this.setChatEditorNewChat(editor)}
.chatEditor=${this.chatEditorNewChat}
.imageFile=${this.imageFile}
.insertImage=${this.insertImage}
.editedMessageObj=${this.editedMessageObj}
?isLoading=${this.isLoading}
?isLoadingMessages=${this.isLoadingMessages}
></chat-text-editor>
<!-- <iframe
}}" id="newChat" class="chat-editor" tabindex="-1" height=${this.iframeHeight}>
</iframe> -->
</div>
${this.chatMessageSize >= 750 ?
html`
@ -604,7 +573,6 @@ class ChatPage extends LitElement {
this._sendMessage({
type: 'image',
imageFile: this.imageFile,
caption: this.caption,
})
}}
>
@ -649,78 +617,22 @@ class ChatPage extends LitElement {
</div>
`}
<div class="chatbar" style="${this.chatMessageSize >= 750 && 'padding-bottom: 7px'}">
<div class="chatbar-container" style="${this.chatMessageInput && this.chatMessageInput.contentDocument.body.scrollHeight > 60 ? 'align-items: flex-end' : "align-items: center"}"
>
${this.accountName && (
html`
<div class="file-picker-container">
<vaadin-icon
class="paperclip-icon"
icon="vaadin:paperclip"
slot="icon"
@click=${() => this.closeEditMessageContainer()}
>
</vaadin-icon>
<div class="file-picker-input-container">
<input
.value="${this.imageFile}"
@change="${e => this.insertImage(e.target.files[0])}"
class="file-picker-input" type="file" name="myImage" accept="image/*" />
</div>
</div>
`
)}
<textarea style="color: var(--black);" tabindex='1' ?autofocus=${true} ?disabled=${this.isLoading || this.isLoadingMessages} id="messageBox" rows="1"></textarea>
<iframe
id="_chatEditorDOM" class="chat-editor" tabindex="-1" height=${this.iframeHeight}>
</iframe>
<button class="emoji-button" ?disabled=${this.isLoading || this.isLoadingMessages}>
${html`<img class="emoji" draggable="false" alt="😀" src="/emoji/svg/1f600.svg" />`}
</button>
${this.editedMessageObj ? (
html`
<div>
${this.isLoading === false ? html`
<vaadin-icon
class="checkmark-icon"
icon="vaadin:check"
slot="icon"
@click=${() => this._sendMessage()}
>
</vaadin-icon>
` :
html`
<paper-spinner-lite active></paper-spinner-lite>
`}
</div>
`
) :
html`
<div style="display:flex; ${this.chatMessageInput && this.chatMessageInput.contentDocument.body.scrollHeight > 60 ? 'margin-bottom: 5px' : "margin-bottom: 0"}">
${this.isLoading === false ? html`
<img
src="/img/qchat-send-message-icon.svg"
alt="send-icon"
class="send-icon"
@click=${() => this._sendMessage()} />
` :
html`
<paper-spinner-lite active></paper-spinner-lite>
`}
</div>
`
}
</div>
${this.chatMessageSize >= 750 ?
html`
<div class="message-size-container">
<div class="message-size" style="${this.chatMessageSize >= 1000 && 'color: #bd1515'}">
${`Your message size is of ${this.chatMessageSize} bytes out of a maximum of 1000`}
</div>
</div>
` :
html``}
</div>
<chat-text-editor
?hasGlobalEvents=${true}
iframeId="_chatEditorDOM"
placeholder=${this.chatEditorPlaceholder}
._sendMessage=${this._sendMessage}
.setChatEditor=${(editor)=> this.setChatEditor(editor)}
.chatEditor=${this.chatEditor}
.imageFile=${this.imageFile}
.insertImage=${this.insertImage}
.chatMessageInput=${this.chatMessageInput}
.editedMessageObj=${this.editedMessageObj}
.mirrorChatInput=${this.mirrorChatInput}
?isLoading=${this.isLoading}
?isLoadingMessages=${this.isLoadingMessages}
></chat-text-editor>
</div>
</div>
</div>
@ -742,11 +654,23 @@ class ChatPage extends LitElement {
`
}
setChatEditor(editor){
console.log({editor})
this.chatEditor = editor
}
setChatEditorNewChat(editor){
this.chatEditorNewChat = editor
}
insertImage(file){
if(file.type.includes('image')){
this.imageFile = file
this.chatEditor.disable();
this.chatEditor.disable()
// this.changeMsgInput('newChat')
// this.initChatEditor();
// this.chatEditor.disable();
return
}
@ -754,48 +678,31 @@ class ChatPage extends LitElement {
}
changeMsgInput(id){
this.chatEditor.remove()
this.chatMessageInput = this.shadowRoot.getElementById(id);
this.initChatEditor();
}
async firstUpdated() {
// TODO: Load and fetch messages from localstorage (maybe save messages to localstorage...)
// this.changeLanguage();
this.emojiPickerHandler = this.shadowRoot.querySelector('.emoji-button');
this.mirrorChatInput = this.shadowRoot.getElementById('messageBox');
this.chatMessageInput = this.shadowRoot.getElementById('_chatEditorDOM');
this.accountName = window.parent.reduxStore.getState().app.accountInfo.names[0];
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);
});
// 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();
// }
// }
// });
// Attach Event Handler
this.emojiPickerHandler.addEventListener('click', () => this.emojiPicker.togglePicker(this.emojiPickerHandler));
window.addEventListener('storage', () => {
const checkLanguage = localStorage.getItem('qortalLanguage')
use(checkLanguage)
@ -836,9 +743,9 @@ class ChatPage extends LitElement {
this.chatEditorPlaceholder = placeholder;
this.isReceipient ? getAddressPublicKey() : this.fetchChatMessages(this._chatId);
// Init ChatEditor
this.initChatEditor();
// this.initChatEditor();
}, 100)
parentEpml.ready().then(() => {
@ -868,35 +775,15 @@ class ChatPage extends LitElement {
async updated(changedProperties) {
if (changedProperties.has('messagesRendered')) {
const chatReference1 = this.isReceipient ? 'direct' : 'group';
const chatReference2 = this._chatId
// if (chatReference1 && chatReference2) {
// await messagesCache.setItem(`${chatReference1}-${chatReference2}`, this.messagesRendered);
// }
}
if (changedProperties && changedProperties.has('editedMessageObj')) {
this.chatEditor.insertText(this.editedMessageObj.message)
}
if (changedProperties && changedProperties.has('chatMessageSize')) {
console.log(this.chatMessageSize, "Chat Message Size");
}
if(changedProperties && changedProperties.has("imageFile")) {
this.chatbarCaption = this.shadowRoot.querySelector('.chatbar-caption');
this.chatbarCaption.focus();
}
}
calculateIFrameHeight(height) {
setTimeout(()=> {
const editorTest = this.shadowRoot.getElementById('_chatEditorDOM').contentWindow.document.getElementById('testingId').scrollHeight
console.log('editor', editorTest)
this.iframeHeight = editorTest + 20
}, 50)
// if(changedProperties && changedProperties.has("imageFile")) {
// this.chatbarCaption = this.shadowRoot.querySelector('.chatbar-caption');
// this.chatbarCaption.focus();
// }
}
onCaptionChange(e) {
@ -917,14 +804,13 @@ class ChatPage extends LitElement {
renderPlaceholder() {
const mstring = get("chatpage.cchange8")
const placeholder = this.isReceipient === true ? `Message ${this._chatId}` : `${mstring}`;
this.chatEditorPlaceholder = placeholder;
return placeholder;
}
renderChatScroller() {
return html`
<chat-scroller
.messages=${this.messagesRendered}
.emojiPicker=${this.emojiPicker}
.escapeHTML=${escape}
.getOldMessage=${this.getOldMessage}
.setRepliedToMessageObj=${(val) => this.setRepliedToMessageObj(val)}
@ -932,7 +818,8 @@ class ChatPage extends LitElement {
.focusChatEditor=${() => this.focusChatEditor()}
.sendMessage=${(val)=> this._sendMessage(val)}
>
</chat-scroller>`
</chat-scroller>
`
}
async getUpdateComplete() {
@ -1057,55 +944,6 @@ class ChatPage extends LitElement {
}
}
getMessageSize(message){
try {
const messageText = message;
// Format and Sanitize Message
const sanitizedMessage = messageText.replace(/&nbsp;/gi, ' ').replace(/<br\s*[\/]?>/gi, '\n');
const trimmedMessage = sanitizedMessage.trim();
let messageObject = {};
if (this.repliedToMessageObj) {
let chatReference = this.repliedToMessageObj.reference;
if (this.repliedToMessageObj.chatReference) {
chatReference = this.repliedToMessageObj.chatReference;
}
messageObject = {
messageText: trimmedMessage,
images: [''],
repliedTo: chatReference,
version: 1
}
} else if (this.editedMessageObj) {
let message = "";
try {
const parsedMessageObj = JSON.parse(this.editedMessageObj.decodedMessage);
message = parsedMessageObj;
} catch (error) {
message = this.messageObj.decodedMessage
}
messageObject = {
...message,
messageText: trimmedMessage,
}
} else {
messageObject = {
messageText: trimmedMessage,
images: [''],
repliedTo: '',
version: 1
};
}
const stringified = JSON.stringify(messageObject);
const size = new Blob([stringified]).size;
this.chatMessageSize = size;
} catch (error) {
console.error(error)
}
}
// set replied to message in chat editor
@ -1144,47 +982,6 @@ class ChatPage extends LitElement {
* @property id or index
* @property sender and other info..
*/
chatMessageTemplate(messageObj) {
const hidemsg = this.hideMessages
let avatarImg = ''
let nameMenu = ''
let levelFounder = ''
let hideit = hidemsg.includes(messageObj.sender)
levelFounder = `<level-founder checkleveladdress="${messageObj.sender}"></level-founder>`
if (messageObj.senderName) {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]
const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port
const avatarUrl = `${nodeUrl}/arbitrary/THUMBNAIL/${messageObj.senderName}/qortal_avatar?async=true&apiKey=${myNode.apiKey}`
avatarImg = `<img src="${avatarUrl}" style="max-width:100%; max-height:100%;" onerror="this.onerror=null; this.src='/img/qortal-chat-logo.png';" />`
}
if (messageObj.sender === this.myAddress) {
nameMenu = `<span style="color: #03a9f4;">${messageObj.senderName ? messageObj.senderName : messageObj.sender}</span>`
} else {
nameMenu = `<name-menu toblockaddress="${messageObj.sender}" nametodialog="${messageObj.senderName ? messageObj.senderName : messageObj.sender}"></name-menu>`
}
if (hideit === true) {
return `
<li class="clearfix"></li>
`
} else {
return `
<li class="clearfix">
<div class="message-data ${messageObj.sender === this.selectedAddress.address ? "" : ""}">
<span class="message-data-name">${nameMenu}</span>
<span class="message-data-level">${levelFounder}</span>
<span class="message-data-time"><message-time timestamp=${messageObj.timestamp}></message-time></span>
</div>
<div class="message-data-avatar" style="width:42px; height:42px; ${messageObj.sender === this.selectedAddress.address ? "float:left;" : "float:left;"} margin:3px;">${avatarImg}</div>
<div class="message ${messageObj.sender === this.selectedAddress.address ? "my-message float-left" : "other-message float-left"}">${this.emojiPicker.parse(escape(messageObj.decodedMessage))}</div>
</li>
`
}
}
async renderNewMessage(newMessage) {
if(newMessage.chatReference){
@ -1457,6 +1254,7 @@ class ChatPage extends LitElement {
}
async _sendMessage(outSideMsg) {
// have params to determine if it's a reply or not
// have variable to determine if it's a response, holds signature in constructor
// need original message signature
@ -1467,7 +1265,8 @@ class ChatPage extends LitElement {
this.isLoading = true;
this.chatEditor.disable();
const messageText = this.mirrorChatInput.value;
this.chatEditorNewChat.disable()
const messageText = this.chatEditor.mirror.value;
// Format and Sanitize Message
const sanitizedMessage = messageText.replace(/&nbsp;/gi, ' ').replace(/<br\s*[\/]?>/gi, '\n');
const trimmedMessage = sanitizedMessage.trim();
@ -1552,6 +1351,7 @@ class ChatPage extends LitElement {
console.error(error)
this.isLoading = false;
this.chatEditor.enable();
this.chatEditorNewChat.enable()
return
}
typeMessage = 'edit';
@ -1585,18 +1385,20 @@ class ChatPage extends LitElement {
parentEpml.request('showSnackBar', get("chatpage.cchange27"));
this.isLoading = false;
this.chatEditor.enable();
this.chatEditorNewChat.enable()
return;
}
const image = this.imageFile
const id = this.uid();
const identifier = `qchat_${id}`;
let compressedFile = '';
await new Promise(resolve => {
new Compressor(outSideMsg.imageFile, {
new Compressor( image, {
quality: .6,
maxWidth: 500,
success(result){
const file = new File([result], "name", {
type: outSideMsg.imageFile.type
type: image.type
});
compressedFile = file
resolve()
@ -1611,6 +1413,7 @@ class ChatPage extends LitElement {
parentEpml.request('showSnackBar', get("chatpage.cchange26"));
this.isLoading = false;
this.chatEditor.enable();
this.chatEditorNewChat.enable()
return;
}
@ -1632,10 +1435,15 @@ class ChatPage extends LitElement {
console.error(error)
this.isLoading = false;
this.chatEditor.enable();
this.chatEditorNewChat.enable()
return
}
const messageTextWithImage = this.chatEditorNewChat.mirror.value;
// Format and Sanitize Message
const sanitizedMessageWithImage = messageTextWithImage.replace(/&nbsp;/gi, ' ').replace(/<br\s*[\/]?>/gi, '\n');
const trimmedMessageWithImage = sanitizedMessageWithImage.trim();
const messageObject = {
messageText: outSideMsg.caption,
messageText: trimmedMessageWithImage,
images: [{
service: "IMAGE",
name: userName,
@ -1702,9 +1510,11 @@ class ChatPage extends LitElement {
} else if (/^\s*$/.test(trimmedMessage)) {
this.isLoading = false;
this.chatEditor.enable();
this.chatEditorNewChat.enable()
} else if (this.chatMessageSize >= 1000) {
this.isLoading = false;
this.chatEditor.enable();
this.chatEditorNewChat.enable()
let err1string = get("chatpage.cchange29");
parentEpml.request('showSnackBar', `${err1string}`);
} else if (this.repliedToMessageObj) {
@ -1813,7 +1623,6 @@ class ChatPage extends LitElement {
let nonce = null
let chatBytesArray = null
await new Promise((res, rej) => {
console.log({chatBytes})
worker.postMessage({chatBytes, path, difficulty});
worker.onmessage = e => {
@ -1847,6 +1656,7 @@ class ChatPage extends LitElement {
this.isLoading = false;
this.chatEditor.enable();
this.chatEditorNewChat.enable()
this.closeEditMessageContainer()
this.closeRepliedToContainer()
};
@ -1909,320 +1719,6 @@ class ChatPage extends LitElement {
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.contentDiv.innerHTML;
}
};
ChatEditor.prototype.setValue = function (value) {
const editor = this;
if (value) {
editor.contentDiv.innerHTML = value;
editor.updateMirror();
}
editor.focus();
};
ChatEditor.prototype.resetValue = function () {
const editor = this;
editor.contentDiv.innerHTML = '';
editor.updateMirror();
editor.focus();
editorConfig.calculateIFrameHeight()
};
ChatEditor.prototype.styles = function () {
const editor = this;
editor.styles = document.createElement('style');
editor.styles.setAttribute('type', 'text/css');
editor.styles.innerText = `
html {
cursor: text;
}
div {
font-size: 1rem;
line-height: 1.38rem;
font-weight: 400;
font-family: "Open Sans", helvetica, sans-serif;
padding-right: 3px;
text-align: left;
white-space: break-spaces;
word-break: break-word;
outline: none;
min-height: 20px;
}
div[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;
}
div[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.contentDiv.setAttribute('contenteditable', 'true');
editor.focus();
};
ChatEditor.prototype.disable = function () {
const editor = this;
editor.contentDiv.setAttribute('contenteditable', 'false');
};
ChatEditor.prototype.state = function () {
const editor = this;
return editor.contentDiv.getAttribute('contenteditable');
};
ChatEditor.prototype.focus = function () {
const editor = this;
editor.contentDiv.focus();
};
ChatEditor.prototype.clearSelection = function () {
const editor = this;
let selection = editor.content.getSelection().toString();
if (!/^\s*$/.test(selection)) editor.content.getSelection().removeAllRanges();
};
ChatEditor.prototype.insertEmoji = function (emojiImg) {
const editor = this;
const doInsert = () => {
if (editor.content.queryCommandSupported("InsertHTML")) {
editor.content.execCommand("insertHTML", false, emojiImg);
editor.updateMirror();
}
};
editor.focus();
return doInsert();
};
ChatEditor.prototype.insertText = function (text) {
const editor = this;
const parsedText = editorConfig.emojiPicker.parse(text);
const doPaste = () => {
if (editor.content.queryCommandSupported("InsertHTML")) {
editor.content.execCommand("insertHTML", false, parsedText);
editor.updateMirror();
}
};
editor.focus();
return doPaste();
};
ChatEditor.prototype.updateMirror = function () {
const editor = this;
const chatInputValue = editor.getValue();
const filteredValue = chatInputValue.replace(/<img.*?alt=".*?/g, '').replace(/".?src=.*?>/g, '');
let unescapedValue = editorConfig.unescape(filteredValue);
editor.mirror.value = unescapedValue;
};
ChatEditor.prototype.listenChanges = function () {
const editor = this;
const events = ['drop', 'contextmenu', 'mouseup', 'click', 'touchend', 'keydown', 'blur', 'paste']
for (let i = 0; i < events.length; i++) {
const event = events[i]
editor.content.body.addEventListener(event, async function (e) {
console.log({event})
if (e.type === 'click') {
e.preventDefault();
e.stopPropagation();
}
if (e.type === 'paste') {
e.preventDefault();
const item_list = await navigator.clipboard.read();
let image_type; // we will feed this later
const item = item_list.find( item => // choose the one item holding our image
item.types.some( type => {
if (type.startsWith( 'image/')) {
image_type = type;
return true;
}
})
);
if(item){
const blob = item && await item.getType( image_type );
var file = new File([blob], "name", {
type: image_type
});
editorConfig.insertImage(file)
} else {
navigator.clipboard.readText().then(clipboardText => {
let escapedText = editorConfig.escape(clipboardText);
editor.insertText(escapedText);
}).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') {
editorConfig.calculateIFrameHeight(editorConfig.editableElement.contentDocument.body.scrollHeight);
editorConfig.getMessageSize(editorConfig.editableElement.contentDocument.body.innerHTML);
// 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);
let elemDiv = document.createElement('div');
elemDiv.setAttribute('contenteditable', 'true');
elemDiv.setAttribute('spellcheck', 'false');
elemDiv.setAttribute('data-placeholder', editorConfig.placeholder);
elemDiv.style.cssText = 'width:100%';
elemDiv.id = 'testingId'
editor.content.body.appendChild(elemDiv);
console.log('body', editor.frame.contentDocument.body, 'div', editor.frame.contentDocument.body.firstChild)
editor.contentDiv = editor.frame.contentDocument.body.firstChild
editor.styles();
editor.listenChanges();
};
function doInit() {
return new ChatEditor();
}
return doInit();
};
const editorConfig = {
getMessageSize: this.getMessageSize,
calculateIFrameHeight: this.calculateIFrameHeight,
mirrorElement: this.mirrorChatInput,
editableElement: this.chatMessageInput,
sendFunc: this._sendMessage,
emojiPicker: this.emojiPicker,
escape: escape,
unescape: unescape,
placeholder: this.chatEditorPlaceholder,
imageFile: this.imageFile,
requestUpdate: this.requestUpdate,
insertImage: this.insertImage,
chatMessageSize: this.chatMessageSize
};
this.chatEditor = new ChatEditor(editorConfig);
}
}
window.customElements.define('chat-page', ChatPage)

26
qortal-ui-plugins/plugins/core/components/ChatScroller.js

@ -22,7 +22,6 @@ class ChatScroller extends LitElement {
return {
getNewMessage: { attribute: false },
getOldMessage: { attribute: false },
emojiPicker: { attribute: false },
escapeHTML: { attribute: false },
messages: { type: Array },
hideMessages: { type: Array },
@ -42,11 +41,19 @@ class ChatScroller extends LitElement {
this._downObserverHandler = this._downObserverHandler.bind(this)
this.myAddress = window.parent.reduxStore.getState().app.selectedAddress.address
this.hideMessages = JSON.parse(localStorage.getItem("MessageBlockedAddresses") || "[]")
this.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'
});
}
render() {
console.log({messages: this.messages})
let formattedMessages = this.messages.reduce((messageArray, message) => {
const lastGroupedMessage = messageArray[messageArray.length - 1];
@ -118,10 +125,12 @@ class ChatScroller extends LitElement {
this.upObserverElement = this.shadowRoot.getElementById('upObserver');
this.downObserverElement = this.shadowRoot.getElementById('downObserver');
// Intialize Observers
this.upElementObserver();
this.downElementObserver();
await this.updateComplete;
this.viewElement.scrollTop = this.viewElement.scrollHeight;
this.upElementObserver()
this.downElementObserver()
await this.updateComplete
this.viewElement.scrollTop = this.viewElement.scrollHeight + 50
}
_getOldMessage(_scrollElement) {
@ -272,7 +281,6 @@ class MessageTemplate extends LitElement {
let hideit = hidemsg.includes(this.messageObj.sender);
levelFounder = html`<level-founder checkleveladdress="${this.messageObj.sender}"></level-founder>`;
console.log({message})
if (this.messageObj.senderName) {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port;
@ -518,7 +526,7 @@ class ChatMenu extends LitElement {
focusChatEditor: { type: Function },
myAddress: { type: Object },
emojiPicker: { attribute: false },
sendMessage: {type: Function}
sendMessage: {type: Function},
}
}
@ -557,7 +565,7 @@ class ChatMenu extends LitElement {
this.emojiPicker.on('emoji', selection => {
this.sendMessage({
type: 'reaction',
editedMessageObj: this.originalMessage,
editedMessageObj: this.originalMessage,
reaction: selection.emoji,

691
qortal-ui-plugins/plugins/core/components/ChatTextEditor.js

@ -0,0 +1,691 @@
import { LitElement, html, css } from "lit"
import { render } from "lit/html.js"
import { escape, unescape } from 'html-escaper';
import { EmojiPicker } from 'emoji-picker-js';
import { inputKeyCodes } from '../../utils/keyCodes.js'
class ChatTextEditor extends LitElement {
static get properties() {
return {
isLoading: { type: Boolean },
isLoadingMessages: { type: Boolean },
_sendMessage: {attribute: false},
placeholder: {type: String},
imageFile: {type: Object},
insertImage: {attribute: false},
iframeHeight: { type: Number },
editedMessageObj: {type: Object},
chatEditor: {type: Object},
setChatEditor: {attribute: false},
iframeId: {type: String},
hasGlobalEvents: {type: Boolean}
}
}
static get styles() {
return css`
:host {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: auto;
overflow-y: hidden;
width: 100%;
}
.chatbar-container {
width: 100%;
display: flex;
height: auto;
overflow: hidden;
}
.emoji-button {
width: 45px;
height: 40px;
padding-top: 4px;
border: none;
outline: none;
background: transparent;
cursor: pointer;
max-height: 40px;
color: var(--black);
}
.message-size-container {
display: flex;
justify-content: flex-end;
width: 100%;
}
.message-size {
font-family: Roboto, sans-serif;
font-size: 12px;
color: black;
}
.paperclip-icon {
color: #494949;
width: 25px;
}
.paperclip-icon:hover {
cursor: pointer;
}
.send-icon {
width: 30px;
margin-left: 5px;
transition: all 0.1s ease-in-out;
cursor: pointer;
}
.send-icon:hover {
filter: brightness(1.1);
}
.file-picker-container {
position: relative;
height: 25px;
width: 25px;
}
.file-picker-input-container {
position: absolute;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
z-index: 10;
opacity: 0;
overflow: hidden;
}
input[type=file]::-webkit-file-upload-button {
cursor: pointer;
}
.chatbar-container textarea {
display: none;
}
.chatbar-container .chat-editor {
display: flex;
max-height: -webkit-fill-available;
width: 100%;
border-color: transparent;
margin: 0;
padding: 0;
border: none;
}
`
}
constructor() {
super()
this.isLoadingMessages = true
this.isLoading = false
this.getMessageSize = this.getMessageSize.bind(this)
this.calculateIFrameHeight = this.calculateIFrameHeight.bind(this)
this.addGlobalEventListener = this.addGlobalEventListener.bind(this)
this.removeGlobalEventListener = this.removeGlobalEventListener.bind(this)
this.initialChat = this.initialChat.bind(this)
this.iframeHeight = 42
}
render() {
return html`
<div class="chatbar-container" style="${this.chatMessageInput && this.chatMessageInput.contentDocument.body.scrollHeight > 60 ? 'align-items: flex-end' : "align-items: center"}"
>
<div class="file-picker-container">
<vaadin-icon
class="paperclip-icon"
icon="vaadin:paperclip"
slot="icon"
>
</vaadin-icon>
<div class="file-picker-input-container">
<input
.value="${this.imageFile}"
@change="${e => this.insertImage(e.target.files[0])}"
class="file-picker-input" type="file" name="myImage" accept="image/*" />
</div>
</div>
<textarea style="color: var(--black);" tabindex='1' ?autofocus=${true} ?disabled=${this.isLoading || this.isLoadingMessages} id="messageBox" rows="1"></textarea>
<iframe
}}" id=${this.iframeId} class="chat-editor" tabindex="-1" height=${this.iframeHeight}>
</iframe>
<button class="emoji-button" ?disabled=${this.isLoading || this.isLoadingMessages}>
${html`<img class="emoji" draggable="false" alt="😀" src="/emoji/svg/1f600.svg" />`}
</button>
${this.editedMessageObj ? (
html`
<div>
${this.isLoading === false ? html`
<vaadin-icon
class="checkmark-icon"
icon="vaadin:check"
slot="icon"
@click=${() => this._sendMessage()}
>
</vaadin-icon>
` :
html`
<paper-spinner-lite active></paper-spinner-lite>
`}
</div>
`
) :
html`
<div style="display:flex; ${this.chatMessageInput && this.chatMessageInput.contentDocument.body.scrollHeight > 60 ? 'margin-bottom: 5px' : "margin-bottom: 0"}">
${this.isLoading === false ? html`
<img
src="/img/qchat-send-message-icon.svg"
alt="send-icon"
class="send-icon"
@click=${() => this._sendMessage()} />
` :
html`
<paper-spinner-lite active></paper-spinner-lite>
`}
</div>
`
}
</div>
${this.chatMessageSize >= 750 ?
html`
<div class="message-size-container">
<div class="message-size" style="${this.chatMessageSize >= 1000 && 'color: #bd1515'}">
${`Your message size is of ${this.chatMessageSize} bytes out of a maximum of 1000`}
</div>
</div>
` :
html``}
</div>
`
}
initialChat(e) {
console.log('hello initial', this.chatEditor)
if (!this.chatEditor?.contentDiv.matches(':focus')) {
// WARNING: Deprecated methods from KeyBoard Event
if (e.code === "Space" || e.keyCode === 32 || e.which === 32) {
this.chatEditor.insertText('&nbsp;');
} else if (inputKeyCodes.includes(e.keyCode)) {
this.chatEditor.insertText(e.key);
return this.chatEditor.focus();
} else {
return this.chatEditor.focus();
}
}
}
addGlobalEventListener(){
document.addEventListener('keydown', this.initialChat);
}
removeGlobalEventListener(){
document.removeEventListener('keydown', this.initialChat);
}
async firstUpdated() {
console.log('this.hasGlobalEvents', this.hasGlobalEvents)
if(this.hasGlobalEvents){
this.addGlobalEventListener()
}
this.emojiPickerHandler = this.shadowRoot.querySelector('.emoji-button');
this.mirrorChatInput = this.shadowRoot.getElementById('messageBox');
this.chatMessageInput = this.shadowRoot.getElementById(this.iframeId);
console.log('test', this.chatMessageInput )
this.emojiPicker = new EmojiPicker({
style: "twemoji",
twemojiBaseUrl: '/emoji/',
showPreview: false,
showVariants: false,
showAnimation: false,
position: 'top-start',
boxShadow: 'rgba(4, 4, 5, 0.15) 0px 0px 0px 1px, rgba(0, 0, 0, 0.24) 0px 8px 16px 0px',
zIndex: 100
});
this.emojiPicker.on('emoji', selection => {
console.log('hello selection')
const emojiHtmlString = `<img class="emoji" draggable="false" alt="${selection.emoji}" src="${selection.url}">`;
this.chatEditor.insertEmoji(emojiHtmlString);
});
this.emojiPickerHandler.addEventListener('click', () => this.emojiPicker.togglePicker(this.emojiPickerHandler));
await this.updateComplete;
this.initChatEditor();
}
async updated(changedProperties) {
console.log({changedProperties})
if (changedProperties && changedProperties.has('editedMessageObj')) {
this.chatEditor.insertText(this.editedMessageObj.message)
}
}
shouldUpdate(changedProperties) {
// Only update element if prop1 changed.
if(changedProperties.has('setChatEditor') && changedProperties.size === 1) return false
return true
}
getMessageSize(message){
try {
const messageText = message;
// Format and Sanitize Message
const sanitizedMessage = messageText.replace(/&nbsp;/gi, ' ').replace(/<br\s*[\/]?>/gi, '\n');
const trimmedMessage = sanitizedMessage.trim();
let messageObject = {};
if (this.repliedToMessageObj) {
let chatReference = this.repliedToMessageObj.reference;
if (this.repliedToMessageObj.chatReference) {
chatReference = this.repliedToMessageObj.chatReference;
}
messageObject = {
messageText: trimmedMessage,
images: [''],
repliedTo: chatReference,
version: 1
}
} else if (this.editedMessageObj) {
let message = "";
try {
const parsedMessageObj = JSON.parse(this.editedMessageObj.decodedMessage);
message = parsedMessageObj;
} catch (error) {
message = this.messageObj.decodedMessage
}
messageObject = {
...message,
messageText: trimmedMessage,
}
} else {
messageObject = {
messageText: trimmedMessage,
images: [''],
repliedTo: '',
version: 1
};
}
const stringified = JSON.stringify(messageObject);
const size = new Blob([stringified]).size;
this.chatMessageSize = size;
} catch (error) {
console.error(error)
}
}
calculateIFrameHeight(height) {
setTimeout(()=> {
const editorTest = this.shadowRoot.getElementById(this.iframeId).contentWindow.document.getElementById('testingId').scrollHeight
this.iframeHeight = editorTest + 20
}, 50)
}
initChatEditor() {
console.log('hello editor')
const ChatEditor = function (editorConfig) {
const ChatEditor = function () {
const editor = this;
editor.init();
};
ChatEditor.prototype.getValue = function () {
const editor = this;
if (editor.contentDiv) {
return editor.contentDiv.innerHTML;
}
};
ChatEditor.prototype.setValue = function (value) {
const editor = this;
if (value) {
editor.contentDiv.innerHTML = value;
editor.updateMirror();
}
editor.focus();
};
ChatEditor.prototype.resetValue = function () {
const editor = this;
editor.contentDiv.innerHTML = '';
editor.updateMirror();
editor.focus();
editorConfig.calculateIFrameHeight()
};
ChatEditor.prototype.styles = function () {
const editor = this;
editor.styles = document.createElement('style');
editor.styles.setAttribute('type', 'text/css');
editor.styles.innerText = `
html {
cursor: text;
}
div {
font-size: 1rem;
line-height: 1.38rem;
font-weight: 400;
font-family: "Open Sans", helvetica, sans-serif;
padding-right: 3px;
text-align: left;
white-space: break-spaces;
word-break: break-word;
outline: none;
min-height: 20px;
}
div[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;
}
div[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.contentDiv.setAttribute('contenteditable', 'true');
editor.focus();
};
ChatEditor.prototype.getMirrorElement = function (){
return editor.mirror
}
ChatEditor.prototype.disable = function () {
const editor = this;
editor.contentDiv.setAttribute('contenteditable', 'false');
};
ChatEditor.prototype.state = function () {
const editor = this;
return editor.contentDiv.getAttribute('contenteditable');
};
ChatEditor.prototype.focus = function () {
const editor = this;
editor.contentDiv.focus();
};
ChatEditor.prototype.clearSelection = function () {
const editor = this;
let selection = editor.content.getSelection().toString();
if (!/^\s*$/.test(selection)) editor.content.getSelection().removeAllRanges();
};
ChatEditor.prototype.insertEmoji = function (emojiImg) {
const editor = this;
const doInsert = () => {
if (editor.content.queryCommandSupported("InsertHTML")) {
editor.content.execCommand("insertHTML", false, emojiImg);
editor.updateMirror();
}
};
editor.focus();
return doInsert();
};
ChatEditor.prototype.insertText = function (text) {
const editor = this;
const parsedText = editorConfig.emojiPicker.parse(text);
const doPaste = () => {
if (editor.content.queryCommandSupported("InsertHTML")) {
editor.content.execCommand("insertHTML", false, parsedText);
editor.updateMirror();
}
};
editor.focus();
return doPaste();
};
ChatEditor.prototype.updateMirror = function () {
const editor = this;
const chatInputValue = editor.getValue();
const filteredValue = chatInputValue.replace(/<img.*?alt=".*?/g, '').replace(/".?src=.*?>/g, '');
let unescapedValue = editorConfig.unescape(filteredValue);
console.log({unescapedValue})
editor.mirror.value = unescapedValue;
};
ChatEditor.prototype.listenChanges = function () {
const editor = this;
const events = ['drop', 'contextmenu', 'mouseup', 'click', 'touchend', 'keydown', 'blur', 'paste']
for (let i = 0; i < events.length; i++) {
const event = events[i]
editor.content.body.addEventListener(event, async function (e) {
if (e.type === 'click') {
e.preventDefault();
e.stopPropagation();
}
if (e.type === 'paste') {
e.preventDefault();
const item_list = await navigator.clipboard.read();
let image_type; // we will feed this later
const item = item_list.find( item => // choose the one item holding our image
item.types.some( type => {
if (type.startsWith( 'image/')) {
image_type = type;
return true;
}
})
);
if(item){
const blob = item && await item.getType( image_type );
var file = new File([blob], "name", {
type: image_type
});
editorConfig.insertImage(file)
} else {
navigator.clipboard.readText().then(clipboardText => {
let escapedText = editorConfig.escape(clipboardText);
editor.insertText(escapedText);
}).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') {
editorConfig.calculateIFrameHeight(editorConfig.editableElement.contentDocument.body.scrollHeight);
editorConfig.getMessageSize(editorConfig.editableElement.contentDocument.body.innerHTML);
// Handle Enter
if (e.keyCode === 13 && !e.shiftKey) {
// Update Mirror
editor.updateMirror();
if (editor.state() === 'false') return false;
if(editorConfig.iframeId === 'newChat'){
editorConfig.sendFunc(
{
type: 'image',
imageFile: editorConfig.imageFile,
}
);
} else {
editorConfig.sendFunc();
}
e.preventDefault();
return false;
}
// Handle Commands with CTR or CMD
if (e.ctrlKey || e.metaKey) {
switch (e.keyCode) {
case 66:
case 98: e.preventDefault();
return false;
case 73:
case 105: e.preventDefault();
return false;
case 85:
case 117: e.preventDefault();
return false;
}
return false;
}
}
if (e.type === 'blur') {
editor.clearSelection();
}
if (e.type === 'drop') {
e.preventDefault();
let droppedText = e.dataTransfer.getData('text/plain')
let escapedText = editorConfig.escape(droppedText)
editor.insertText(escapedText);
return false;
}
editor.updateMirror();
});
}
editor.content.addEventListener('click', function (event) {
event.preventDefault();
editor.focus();
});
};
ChatEditor.prototype.remove = function () {
const editor = this;
var old_element = editor.content.body
var new_element = old_element.cloneNode(true);
editor.content.body.parentNode.replaceChild(new_element, old_element);
while (editor.content.body.firstChild) {
editor.content.body.removeChild(editor.content.body.lastChild);
}
};
ChatEditor.prototype.init = function () {
const editor = this;
editor.frame = editorConfig.editableElement;
editor.mirror = editorConfig.mirrorElement;
editor.content = (editor.frame.contentDocument || editor.frame.document);
let elemDiv = document.createElement('div');
elemDiv.setAttribute('contenteditable', 'true');
elemDiv.setAttribute('spellcheck', 'false');
elemDiv.setAttribute('data-placeholder', editorConfig.placeholder);
elemDiv.style.cssText = 'width:100%';
elemDiv.id = 'testingId'
editor.content.body.appendChild(elemDiv);
editor.contentDiv = editor.frame.contentDocument.body.firstChild
editor.styles();
editor.listenChanges();
};
function doInit() {
return new ChatEditor();
}
return doInit();
};
const editorConfig = {
getMessageSize: this.getMessageSize,
calculateIFrameHeight: this.calculateIFrameHeight,
mirrorElement: this.mirrorChatInput,
editableElement: this.chatMessageInput,
sendFunc: this._sendMessage,
emojiPicker: this.emojiPicker,
escape: escape,
unescape: unescape,
placeholder: this.placeholder,
imageFile: this.imageFile,
requestUpdate: this.requestUpdate,
insertImage: this.insertImage,
chatMessageSize: this.chatMessageSize,
addGlobalEventListener: this.addGlobalEventListener,
removeGlobalEventListener: this.removeGlobalEventListener,
iframeId: this.iframeId
};
console.log('after')
const newChat = new ChatEditor(editorConfig)
console.log({newChat})
this.setChatEditor(newChat)
}
}
window.customElements.define("chat-text-editor", ChatTextEditor)
Loading…
Cancel
Save