diff --git a/qortal-ui-plugins/plugins/core/components/ChatPage.js b/qortal-ui-plugins/plugins/core/components/ChatPage.js index 93e5e841..0a6b6cac 100644 --- a/qortal-ui-plugins/plugins/core/components/ChatPage.js +++ b/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()} { - this.chatEditor.enable(); - this.caption = ""; this.imageFile = null; + this.chatEditor.enable() + }}>
@@ -557,25 +525,26 @@ class ChatPage extends LitElement { `}
- -
- ${this.isLoading === false ? html` - send-icon { - this._sendMessage({ - type: 'image', - imageFile: this.imageFile, - caption: this.caption, - }) - }} /> - ` : - html` - - `} -
+ this.setChatEditorNewChat(editor)} + .chatEditor=${this.chatEditorNewChat} + .imageFile=${this.imageFile} + .insertImage=${this.insertImage} + + .editedMessageObj=${this.editedMessageObj} + + ?isLoading=${this.isLoading} + ?isLoadingMessages=${this.isLoadingMessages} + > + + +
${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 {
`}
-
- ${this.accountName && ( - html` -
- this.closeEditMessageContainer()} - > - -
- -
-
- ` - )} - - - - ${this.editedMessageObj ? ( - html` -
- ${this.isLoading === false ? html` - this._sendMessage()} - > - - ` : - html` - - `} -
- ` - ) : - html` -
- ${this.isLoading === false ? html` - send-icon this._sendMessage()} /> - ` : - html` - - `} -
- ` - } -
- ${this.chatMessageSize >= 750 ? - html` -
-
- ${`Your message size is of ${this.chatMessageSize} bytes out of a maximum of 1000`} -
-
- ` : - html``} -
+ 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} + > + @@ -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(' '); - } 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 = `${selection.emoji}`; - 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(' '); + // } 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` this.setRepliedToMessageObj(val)} @@ -932,7 +818,8 @@ class ChatPage extends LitElement { .focusChatEditor=${() => this.focusChatEditor()} .sendMessage=${(val)=> this._sendMessage(val)} > - ` + + ` } async getUpdateComplete() { @@ -1057,55 +944,6 @@ class ChatPage extends LitElement { } } - getMessageSize(message){ - try { - const messageText = message; - // Format and Sanitize Message - const sanitizedMessage = messageText.replace(/ /gi, ' ').replace(//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 = `` - - 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 = `` - } - - if (messageObj.sender === this.myAddress) { - nameMenu = `${messageObj.senderName ? messageObj.senderName : messageObj.sender}` - } else { - nameMenu = `` - } - - if (hideit === true) { - return ` -
  • - ` - } else { - return ` -
  • -
    - ${nameMenu} - ${levelFounder} - -
    -
    ${avatarImg}
    -
    ${this.emojiPicker.parse(escape(messageObj.decodedMessage))}
    -
  • - ` - } - } 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(/ /gi, ' ').replace(//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(/ /gi, ' ').replace(//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(//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) diff --git a/qortal-ui-plugins/plugins/core/components/ChatScroller.js b/qortal-ui-plugins/plugins/core/components/ChatScroller.js index c0f50600..173b76b6 100644 --- a/qortal-ui-plugins/plugins/core/components/ChatScroller.js +++ b/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``; - 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, diff --git a/qortal-ui-plugins/plugins/core/components/ChatTextEditor.js b/qortal-ui-plugins/plugins/core/components/ChatTextEditor.js new file mode 100644 index 00000000..3641ed6d --- /dev/null +++ b/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` +
    +
    + + +
    + +
    +
    + + + + ${this.editedMessageObj ? ( + html` +
    + ${this.isLoading === false ? html` + this._sendMessage()} + > + + ` : + html` + + `} +
    + ` + ) : + html` +
    + ${this.isLoading === false ? html` + send-icon this._sendMessage()} /> + ` : + html` + + `} +
    + ` + } +
    + ${this.chatMessageSize >= 750 ? + html` +
    +
    + ${`Your message size is of ${this.chatMessageSize} bytes out of a maximum of 1000`} +
    +
    + ` : + html``} + + ` + } + + 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(' '); + } 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 = `${selection.emoji}`; + 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(/ /gi, ' ').replace(//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(//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)