diff --git a/img/file-icon.png b/img/file-icon.png new file mode 100644 index 00000000..0e12769d Binary files /dev/null and b/img/file-icon.png differ diff --git a/plugins/plugins/core/components/ChatPage.js b/plugins/plugins/core/components/ChatPage.js index 030126b3..b6047770 100644 --- a/plugins/plugins/core/components/ChatPage.js +++ b/plugins/plugins/core/components/ChatPage.js @@ -8,6 +8,7 @@ import { escape } from 'html-escaper' import { inputKeyCodes, replaceMessagesEdited, generateIdFromAddresses } from '../../utils/functions' import { publishData, modalHelper, RequestQueue } from '../../utils/classes' import { EmojiPicker } from 'emoji-picker-js' +import { Slice, Fragment, Node } from 'prosemirror-model' import { chatpageStyles } from './plugins-css' import localForage from 'localforage' import StarterKit from '@tiptap/starter-kit' @@ -84,11 +85,17 @@ class ChatPage extends LitElement { editedMessageObj: { type: Object }, iframeHeight: { type: Number }, imageFile: { type: Object }, + gifFile: { type: Object }, attachment: { type: Object }, + appFile: { type: Object }, isUploadingImage: { type: Boolean }, - isDeletingImage: { type: Boolean }, + isUploadingGif: { type: Boolean }, isUploadingAttachment: { type: Boolean }, + isUploadingAppFile: { type: Boolean }, + isDeletingImage: { type: Boolean }, + isDeletingGif: { type: Boolean }, isDeletingAttachment: { type: Boolean }, + isDeletingAppFile: { type: Boolean }, userLanguage: { type: String }, lastMessageRefVisible: { type: Boolean }, isLoadingOldMessages: { type: Boolean }, @@ -172,7 +179,9 @@ class ChatPage extends LitElement { this.editedMessageObj = null this.iframeHeight = 42 this.imageFile = null + this.gifFile = null this.attachment = null + this.appFile = null this.uid = new ShortUniqueId() this.userLanguage = "" this.lastMessageRefVisible = false @@ -391,6 +400,19 @@ class ChatPage extends LitElement { ` : '' } + ${(this.isUploadingGif || this.isDeletingGif) ? + html` +
+
+
+
+

${this.isDeletingImage ? translate("chatpage.cchange104") : translate("chatpage.cchange103")}

+
+
+
+ ` + : '' + } ${(this.isUploadingAttachment || this.isDeletingAttachment) ? html`
@@ -404,9 +426,23 @@ class ChatPage extends LitElement { ` : '' } + ${(this.isUploadingAppFile || this.isDeletingAppFile) ? + html` +
+
+
+
+

${this.isDeletingAppFile ? translate("chatpage.cchange99") : translate("chatpage.cchange98")}

+
+
+
+ ` + : '' + } {this.removeImage();}} style=${(this.imageFile && !this.isUploadingImage) ? "visibility:visible; z-index:50" : "visibility: hidden;z-index:-100"}>
+

${translate("chatpage.cchange110")}

${this.imageFile && html`
+ {this.removeGif();}} style=${(this.gifFile && !this.isUploadingGif) ? "visibility:visible; z-index:50" : "visibility: hidden;z-index:-100"}> +
+
+

${translate("chatpage.cchange111")}

+ ${this.gifFile && + html` + dialog-gif + ` + } +
+ this.updatePlaceholder(editor, value)} + > + +
+ +
+
+
{this.removeAttachment();}} style=${this.attachment && !this.isUploadingAttachment ? "visibility: visible; z-index: 50" : "visibility: hidden; z-index: -100"}>
+

${translate("chatpage.cchange112")}

${this.attachment && html`
attachment-icon
@@ -499,6 +588,50 @@ class ChatPage extends LitElement {
+ {this.removeAppFile();}} style=${this.appFile && !this.isUploadingAppFile ? "visibility: visible; z-index: 50" : "visibility: hidden; z-index: -100"}> +
+
+

${translate("chatpage.cchange113")}

+ ${this.appFile && + html` +
file-icon
+ ` + } +

${this.appFile && this.appFile.name}

+
+ this.updatePlaceholder(editor, value)} + > + +
+ +
+
+

${translate("chatpage.cchange41")}


@@ -834,36 +967,70 @@ class ChatPage extends LitElement { async connectedCallback() { super.connectedCallback() await this.initUpdate() + if (!this.webWorker) { this.webWorker = new WebWorker() } + if (!this.webWorkerFile) { this.webWorkerFile = new WebWorkerFile() } + if (!this.webWorkerSortMessages) { this.webWorkerSortMessages = new WebWorkerSortMessages() - } + if (!this.webWorkerDecodeMessages) { this.webWorkerDecodeMessages = new WebWorkerDecodeMessages() } + await this.getUpdateCompleteTextEditor() const elementChatId = this.shadowRoot.getElementById('_chatEditorDOM').shadowRoot.getElementById('_chatEditorDOM') const elementChatImageId = this.shadowRoot.getElementById('chatTextCaption').shadowRoot.getElementById('newChat') + const elementChatGifId = this.shadowRoot.getElementById('chatGifId').shadowRoot.getElementById('newGifChat') const elementChatAttachmentId = this.shadowRoot.getElementById('chatAttachmentId').shadowRoot.getElementById('newAttachmentChat') + const elementChatFileId = this.shadowRoot.getElementById('chatFileId').shadowRoot.getElementById('newFileChat') + + const placeholderString = get('chatpage.cchange114') + + const clipboardTextParser = (text, context, plain) => { + const splitLines = text.replace().split(/(?:\r\n?|\n)/) + const nodesLines = [] + + splitLines.forEach(line => { + let nodeJson = {type: "paragraph"} + + if (line.length === 0) { + nodeJson.content = [{type: "hardBreak"}] + } else if (line.length > 0) { + nodeJson.content = [{type: "text", text: line}] + } + + let modifiedLine = Node.fromJSON(context.doc.type.schema, nodeJson) + + nodesLines.push(modifiedLine) + }) + + const fragment = Fragment.fromArray(nodesLines) + + return Slice.maxOpen(fragment) + } + this.editor = new Editor({ + editorProps: { + clipboardTextParser: clipboardTextParser + }, onUpdate: () => { this.shadowRoot.getElementById('_chatEditorDOM').getMessageSize(this.editor.getJSON()) }, - element: elementChatId, extensions: [ StarterKit, Underline, Highlight, Placeholder.configure({ - placeholder: 'Write something …', + placeholder: `${placeholderString}` }), Extension.create({ name: 'shortcuts', @@ -901,7 +1068,7 @@ class ChatPage extends LitElement { Underline, Highlight, Placeholder.configure({ - placeholder: 'Write something …', + placeholder: `${placeholderString}` }), Extension.create({ addKeyboardShortcuts: () => { @@ -919,6 +1086,34 @@ class ChatPage extends LitElement { ] }) + this.editorGif = new Editor({ + onUpdate: () => { + this.shadowRoot.getElementById('chatGifId').getMessageSize(this.editorGif.getJSON()) + }, + element: elementChatGifId, + extensions: [ + StarterKit, + Underline, + Highlight, + Placeholder.configure({ + placeholder: `${placeholderString}` + }), + Extension.create({ + addKeyboardShortcuts: () => { + return { + 'Enter': () => { + const chatTextEditor = this.shadowRoot.getElementById('chatGifId') + chatTextEditor.sendMessageFunc({ + type: 'gif' + }) + return true + } + } + } + }) + ] + }) + this.editorAttachment = new Editor({ onUpdate: () => { this.shadowRoot.getElementById('chatAttachmentId').getMessageSize(this.editorAttachment.getJSON()) @@ -929,7 +1124,7 @@ class ChatPage extends LitElement { Underline, Highlight, Placeholder.configure({ - placeholder: 'Write something …', + placeholder: `${placeholderString}` }), Extension.create({ addKeyboardShortcuts: () => { @@ -947,6 +1142,34 @@ class ChatPage extends LitElement { ] }) + this.editorFile = new Editor({ + onUpdate: () => { + this.shadowRoot.getElementById('chatFileId').getMessageSize(this.editorFile.getJSON()) + }, + element: elementChatFileId, + extensions: [ + StarterKit, + Underline, + Highlight, + Placeholder.configure({ + placeholder: `${placeholderString}` + }), + Extension.create({ + addKeyboardShortcuts: () => { + return { + 'Enter': () => { + const chatTextEditor = this.shadowRoot.getElementById('chatFileId') + chatTextEditor.sendMessageFunc({ + type: 'file' + }) + return true + } + } + } + }) + ] + }) + document.addEventListener('keydown', this.initialChat) document.addEventListener('paste', this.pasteImage) @@ -983,25 +1206,44 @@ class ChatPage extends LitElement { disconnectedCallback() { super.disconnectedCallback() + if (this.webSocket) { this.webSocket.close(1000, 'switch chat') this.webSocket = '' } + if (this.webWorker) { this.webWorker.terminate() } + if (this.webWorkerFile) { this.webWorkerFile.terminate() } + if (this.webWorkerSortMessages) { this.webWorkerSortMessages.terminate() } + if (this.editor) { this.editor.destroy() } + if (this.editorImage) { this.editorImage.destroy() } + + if (this.editorGif) { + this.editorGif.destroy() + } + + if (this.editorAttachment) { + this.editorAttachment.destroy() + } + + if (this.editorFile) { + this.editorFile.destroy() + } + if (this.observer) { this.observer.disconnect() } @@ -1023,7 +1265,6 @@ class ChatPage extends LitElement { } async pasteImage(e) { - const event = e const handleTransferIntoURL = (dataTransfer) => { try { const [firstItem] = dataTransfer.items @@ -1031,15 +1272,19 @@ class ChatPage extends LitElement { return blob } catch (error) { /* empty */ } } - if (event.clipboardData) { - const blobFound = handleTransferIntoURL(event.clipboardData) + + if (e.clipboardData) { + const blobFound = handleTransferIntoURL(e.clipboardData) + if (blobFound) { this.insertFile(blobFound) e.preventDefault() return } else { const item_list = await navigator.clipboard.read() + let image_type + const item = item_list.find(item => item.types.some(type => { if (type.startsWith('image/')) { @@ -1048,10 +1293,11 @@ class ChatPage extends LitElement { } }) ) + if (item) { try { const blob = item && await item.getType(image_type) - let file = new File([blob], "name", { + let file = new File([blob], 'name', { type: image_type }) this.insertFile(file) @@ -1196,17 +1442,47 @@ class ChatPage extends LitElement { } insertFile(file) { + const acceptedFileExtension = [ + 'zip', 'jar', 'gzip', 'exe', 'deb', + 'rar', 'dmg', 'pkg', '7z', 'gz', 'psd', + 'mp4', 'rpm', 'snap', 'AppImage' + ] + + const acceptedAttachmentExtension = [ + 'pdf', 'txt', 'odt', 'ods', 'doc', 'html', + 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'sh', 'log' + ] + + const fileExtension = file.name.split('.').pop() + if (file.identifier) { this.imageFile = file this.currentEditor = 'newChat' + this.editorImage.commands.setContent('') + return + } else if (file.type === 'image/gif') { + this.gifFile = file + this.currentEditor = 'newGifChat' + this.editorGif.commands.setContent('') return } else if (file.type.includes('image')) { this.imageFile = file this.currentEditor = 'newChat' + this.editorImage.commands.setContent('') return - } else { + } else if (acceptedFileExtension.includes(fileExtension)) { + this.appFile = file + this.currentEditor = 'newFileChat' + this.editorFile.commands.setContent('') + return + } else if (acceptedAttachmentExtension.includes(fileExtension)){ this.attachment = file this.currentEditor = "newAttachmentChat" + this.editorAttachment.commands.setContent('') + return + } else { + this.resetChatEditor() + parentEpml.request('showSnackBar', get("chatpage.cchange109")) return } } @@ -1217,12 +1493,24 @@ class ChatPage extends LitElement { this.currentEditor = '_chatEditorDOM' } + removeGif() { + this.gifFile = null + this.resetChatEditor() + this.currentEditor = '_chatEditorDOM' + } + removeAttachment() { this.attachment = null this.resetChatEditor() this.currentEditor = '_chatEditorDOM' } + removeAppFile() { + this.appFile = null + this.resetChatEditor() + this.currentEditor = '_chatEditorDOM' + } + changeMsgInput(id) { this.chatMessageInput = this.shadowRoot.getElementById(id) this.initChatEditor() @@ -1233,7 +1521,9 @@ class ChatPage extends LitElement { this.webSocket.close(1000, 'switch chat') this.webSocket = '' } + this.pageNumber = 1 + const getAddressPublicKey = () => { parentEpml.request('apiCall', { type: 'api', @@ -1260,18 +1550,16 @@ class ChatPage extends LitElement { this.chatId.includes('direct') === true ? this.isReceipient = true : this.isReceipient = false this._chatId = this.chatId.split('/')[1] - const mstring = get("chatpage.cchange8") + const mstring = get('chatpage.cchange114') const placeholder = isRecipient === true ? `Message ${this._chatId}` : `${mstring}` this.chatEditorPlaceholder = placeholder isRecipient ? getAddressPublicKey() : this.fetchChatMessages(this._chatId) - - // Init ChatEditor - // this.initChatEditor() }, 100) const isRecipient = this.chatId.includes('direct') === true ? true : false const groupId = this.chatId.split('/')[1] + if (!isRecipient && groupId.toString() !== '0') { try { const getMembers = await parentEpml.request("apiCall", { @@ -1314,7 +1602,9 @@ class ChatPage extends LitElement { } catch (error) { /* empty */ } return memberItem }) + const membersWithName = await Promise.all(getMembersWithName) + this.groupAdmin = membersAdminsWithName this.groupMembers = membersWithName this.groupInfo = getGroupInfo @@ -1337,7 +1627,7 @@ class ChatPage extends LitElement { const userLang = changedProperties.get('userLanguage') if (userLang) { await new Promise(r => setTimeout(r, 100)) - this.chatEditorPlaceholder = this.isReceipient === true ? `Message ${this._chatId}` : `${get("chatpage.cchange8")}` + this.chatEditorPlaceholder = this.isReceipient === true ? `Message ${this._chatId}` : `${get('chatpage.cchange114')}` } } @@ -1345,10 +1635,12 @@ class ChatPage extends LitElement { if (this.isLoading === true && this.currentEditor === '_chatEditorDOM' && this.editor && this.editor.setEditable) { this.editor.setEditable(false) } + if (this.isLoading === false && this.currentEditor === '_chatEditorDOM' && this.editor && this.editor.setEditable) { this.editor.setEditable(true) } } + if (changedProperties && changedProperties.has('chatId') && this.webSocket) { const previousChatId = changedProperties.get('chatId') @@ -1427,12 +1719,16 @@ class ChatPage extends LitElement { return "" } } - let userName = "" + + let userName = '' + if (this.isReceipient) { userName = await getName(this._chatId) } - const mstring = get("chatpage.cchange8") + + const mstring = get('chatpage.cchange114') const placeholder = this.isReceipient === true ? `Message ${userName ? userName : this._chatId}` : `${mstring}` + return placeholder } @@ -1865,13 +2161,14 @@ class ChatPage extends LitElement { const signature = item.originalSignature || item.signature newObj[signature] = item }) + this.updateMessageHash = { ...this.updateMessageHash, ...newObj } + this.requestUpdate() await this.getUpdateComplete() - } async clearUpdateMessageHashmap() { @@ -1895,31 +2192,35 @@ class ChatPage extends LitElement { } } } + return null } async processMessages(messages, isInitial, isUnread, count) { const isReceipient = this.chatId.includes('direct') let decodedMessages = [] + if (!this.webWorkerDecodeMessages) { this.webWorkerDecodeMessages = new WebWorkerDecodeMessages() } + if (!this.webWorkerSortMessages) { this.webWorkerSortMessages = new WebWorkerSortMessages() } + await new Promise((res, rej) => { this.webWorkerDecodeMessages.postMessage({ messages: messages, isReceipient: this.isReceipient, _publicKey: this._publicKey, privateKey: window.parent.reduxStore.getState().app.selectedAddress.keyPair.privateKey }) this.webWorkerDecodeMessages.onmessage = e => { decodedMessages = e.data res() - } + this.webWorkerDecodeMessages.onerror = () => { rej() - } }) + if (isInitial) { this.chatEditorPlaceholder = await this.renderPlaceholder() @@ -1953,7 +2254,6 @@ class ChatPage extends LitElement { const lastReadMessageTimestamp = this.lastReadMessageTimestamp if (isUnread) { - this.messagesRendered = { messages: this._messages, type: 'initialLastSeen', @@ -2045,6 +2345,7 @@ class ChatPage extends LitElement { } let viewElement = this.shadowRoot.querySelector('chat-scroller') + if (viewElement) { viewElement = viewElement.shadowRoot.getElementById('viewElement') } else { @@ -2052,34 +2353,34 @@ class ChatPage extends LitElement { } if (newMessage.sender === this.selectedAddress.address) { - - this.messagesRendered = { messages: [newMessage], type: 'newComingInAuto', } + await this.getUpdateComplete() // viewElement.scrollTop = viewElement.scrollHeight } else if (this.isUserDown) { - this.messagesRendered = { messages: [newMessage], type: 'newComingInAuto', } + // Append the message and scroll to the bottom if user is down the page // this.messagesRendered = [...this.messagesRendered, newMessage] await this.getUpdateComplete() + if (viewElement) { viewElement.scrollTop = viewElement.scrollHeight } } else { - this.messagesRendered = { messages: [newMessage], type: 'newComingInAuto', } + await this.getUpdateComplete() this.showNewMessageBar() @@ -2094,6 +2395,7 @@ class ChatPage extends LitElement { decodeMessage(encodedMessageObj, isReceipient, _publicKey) { let isReceipientVar let _publicKeyVar + try { isReceipientVar = this.isReceipient === undefined ? isReceipient : this.isReceipient _publicKeyVar = this._publicKey === undefined ? _publicKey : this._publicKey @@ -2115,12 +2417,12 @@ class ChatPage extends LitElement { } else { decodedMessageObj = { ...encodedMessageObj, decodedMessage: "Cannot Decrypt Message!" } } - } else { // group chat let decodedMessage = window.parent.Base64.decode(encodedMessageObj.data) decodedMessageObj = { ...encodedMessageObj, decodedMessage } } + return decodedMessageObj } @@ -2144,6 +2446,7 @@ class ChatPage extends LitElement { } this.webSocket = new WebSocket(directSocketLink) + // Open Connection this.webSocket.onopen = () => { setTimeout(pingDirectSocket, 50) @@ -2375,9 +2678,19 @@ class ChatPage extends LitElement { if (this.currentEditor === '_chatEditorDOM') { this.editor.commands.setContent('') } + if (this.currentEditor === 'newChat') { this.editorImage.commands.setContent('') } + + if (this.currentEditor === 'newGifChat') { + this.editorGif.commands.setContent('') + } + + if (this.currentEditor === 'newFileChat') { + this.editorAttachment.commands.setContent('') + } + if (this.currentEditor === 'newAttachmentChat') { this.editorAttachment.commands.setContent('') } @@ -2386,8 +2699,9 @@ class ChatPage extends LitElement { async _sendMessage(outSideMsg, msg, messageQueue) { const _chatId = this._chatId const isReceipient = this.isReceipient + const str = "iVBORw0KGgoAAAANSUhEUgAAAsAAAAGMAQMAAADuk4YmAAAAA1BMVEX///+nxBvIAAAAAXRSTlMAQObYZgAAADlJREFUeF7twDEBAAAAwiD7p7bGDlgYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAGJrAABgPqdWQAAAABJRU5ErkJggg==" + let _publicKey = this._publicKey - const attachment = this.attachment try { if (this.isReceipient) { @@ -2420,14 +2734,16 @@ class ChatPage extends LitElement { } } + // 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 // need whole original message object, transform the data and put it in local storage // create new var called repliedToData and use that to modify the UI // find specific object property in local + let typeMessage = 'regular' - // this.isLoading = true + const trimmedMessage = msg const getName = async (recipient) => { @@ -2444,16 +2760,18 @@ class ChatPage extends LitElement { } } catch (error) { - return "" + return '' } } if (outSideMsg && outSideMsg.type === 'delete') { this.isDeletingImage = true - const userName = outSideMsg.name - const identifier = outSideMsg.identifier - let compressedFile = '' - var str = "iVBORw0KGgoAAAANSUhEUgAAAsAAAAGMAQMAAADuk4YmAAAAA1BMVEX///+nxBvIAAAAAXRSTlMAQObYZgAAADlJREFUeF7twDEBAAAAwiD7p7bGDlgYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAGJrAABgPqdWQAAAABJRU5ErkJggg==" + + let userName + let identifier + + userName = outSideMsg.name + identifier = outSideMsg.identifier if (this.webWorkerFile) { this.webWorkerFile.terminate() @@ -2481,7 +2799,11 @@ class ChatPage extends LitElement { const blob = new Blob(byteArrays, { type: contentType }) return blob } + const blob = b64toBlob(str, 'image/png') + + let compressedFile = '' + await new Promise(resolve => { new Compressor(blob, { quality: 0.6, @@ -2498,13 +2820,15 @@ class ChatPage extends LitElement { }, }) }) + const arbitraryFeeData = await modalHelper.getArbitraryFee() - const res = await modalHelper.showModalAndWaitPublish( - { - feeAmount: arbitraryFeeData.feeToShow - } - ) + + const res = await modalHelper.showModalAndWaitPublish({ + feeAmount: arbitraryFeeData.feeToShow + }) + if (res.action !== 'accept') throw new Error('User declined publish') + try { await publishData({ registeredName: userName, @@ -2519,37 +2843,158 @@ class ChatPage extends LitElement { withFee: true, feeAmount: arbitraryFeeData.fee }) + this.isDeletingImage = false } catch (error) { this.isLoading = false return } + typeMessage = 'edit' + let chatReference = outSideMsg.editedMessageObj.signature if (outSideMsg.editedMessageObj.chatReference) { chatReference = outSideMsg.editedMessageObj.chatReference } - let message = "" + let message = '' + try { const parsedMessageObj = JSON.parse(outSideMsg.editedMessageObj.decodedMessage) message = parsedMessageObj } catch (error) { message = outSideMsg.editedMessageObj.decodedMessage } + const messageObject = { ...message, isImageDeleted: true } + + const stringifyMessageObject = JSON.stringify(messageObject) + + return this.sendMessage({ messageText: stringifyMessageObject, typeMessage, chatReference, isForward: false, isReceipient, _chatId, _publicKey, messageQueue }) + } else if (outSideMsg && outSideMsg.type === 'deleteGif') { + this.isDeletingGif = true + + let userName + let identifier + + userName = outSideMsg.name + identifier = outSideMsg.identifier + + if (this.webWorkerFile) { + this.webWorkerFile.terminate() + this.webWorkerFile = null + } + + this.webWorkerFile = new WebWorkerFile() + + const b64toBlob = (b64Data, contentType = '', sliceSize = 512) => { + const byteCharacters = atob(b64Data) + const byteArrays = [] + + for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { + const slice = byteCharacters.slice(offset, offset + sliceSize) + + const byteNumbers = new Array(slice.length) + for (let i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i) + } + + const byteArray = new Uint8Array(byteNumbers) + byteArrays.push(byteArray) + } + + const blob = new Blob(byteArrays, { type: contentType }) + return blob + } + + const blob = b64toBlob(str, 'image/png') + + let compressedFile = '' + + await new Promise(resolve => { + new Compressor(blob, { + quality: 0.6, + maxWidth: 500, + success(result) { + const file = new File([result], "name", { + type: 'image/png' + }) + + compressedFile = file + resolve() + }, + error() { + }, + }) + }) + + const arbitraryFeeData = await modalHelper.getArbitraryFee() + + const res = await modalHelper.showModalAndWaitPublish({ + feeAmount: arbitraryFeeData.feeToShow + }) + + if (res.action !== 'accept') throw new Error('User declined publish') + + try { + await publishData({ + registeredName: userName, + file: compressedFile, + service: 'IMAGE', + identifier: identifier, + parentEpml, + metaData: undefined, + uploadType: 'file', + selectedAddress: this.selectedAddress, + worker: this.webWorkerFile, + withFee: true, + feeAmount: arbitraryFeeData.fee + }) + + this.isDeletingGif = false + } catch (error) { + this.isLoading = false + return + } + + typeMessage = 'edit' + + let chatReference = outSideMsg.editedMessageObj.signature + + if (outSideMsg.editedMessageObj.chatReference) { + chatReference = outSideMsg.editedMessageObj.chatReference + } + + let message = '' + + try { + const parsedMessageObj = JSON.parse(outSideMsg.editedMessageObj.decodedMessage) + message = parsedMessageObj + + } catch (error) { + message = outSideMsg.editedMessageObj.decodedMessage + } + + const messageObject = { + ...message, + isGifDeleted: true + } + const stringifyMessageObject = JSON.stringify(messageObject) + return this.sendMessage({ messageText: stringifyMessageObject, typeMessage, chatReference, isForward: false, isReceipient, _chatId, _publicKey, messageQueue }) } else if (outSideMsg && outSideMsg.type === 'deleteAttachment') { this.isDeletingAttachment = true - let compressedFile = '' - const str = "iVBORw0KGgoAAAANSUhEUgAAAsAAAAGMAQMAAADuk4YmAAAAA1BMVEX///+nxBvIAAAAAXRSTlMAQObYZgAAADlJREFUeF7twDEBAAAAwiD7p7bGDlgYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAGJrAABgPqdWQAAAABJRU5ErkJggg==" - const userName = outSideMsg.name - const identifier = outSideMsg.identifier + + let userName + let identifier + + userName = outSideMsg.name + identifier = outSideMsg.identifier if (this.webWorkerFile) { this.webWorkerFile.terminate() @@ -2579,6 +3024,9 @@ class ChatPage extends LitElement { } const blob = b64toBlob(str, 'image/png') + + let compressedFile = '' + await new Promise(resolve => { new Compressor(blob, { quality: 0.6, @@ -2595,18 +3043,20 @@ class ChatPage extends LitElement { }, }) }) + const arbitraryFeeData = await modalHelper.getArbitraryFee() - const res = await modalHelper.showModalAndWaitPublish( - { - feeAmount: arbitraryFeeData.feeToShow - } - ) + + const res = await modalHelper.showModalAndWaitPublish({ + feeAmount: arbitraryFeeData.feeToShow + }) + if (res.action !== 'accept') throw new Error('User declined publish') + try { await publishData({ registeredName: userName, file: compressedFile, - service: 'QCHAT_ATTACHMENT', + service: 'ATTACHMENT', identifier: identifier, parentEpml, metaData: undefined, @@ -2616,19 +3066,23 @@ class ChatPage extends LitElement { withFee: true, feeAmount: arbitraryFeeData.fee }) + this.isDeletingAttachment = false } catch (error) { this.isLoading = false return } + typeMessage = 'edit' + let chatReference = outSideMsg.editedMessageObj.signature if (outSideMsg.editedMessageObj.chatReference) { chatReference = outSideMsg.editedMessageObj.chatReference } - let message = "" + let message = '' + try { const parsedMessageObj = JSON.parse(outSideMsg.editedMessageObj.decodedMessage) message = parsedMessageObj @@ -2636,17 +3090,135 @@ class ChatPage extends LitElement { } catch (error) { message = outSideMsg.editedMessageObj.decodedMessage } + const messageObject = { ...message, isAttachmentDeleted: true } + const stringifyMessageObject = JSON.stringify(messageObject) + + return this.sendMessage({ messageText: stringifyMessageObject, typeMessage, chatReference, isForward: false, isReceipient, _chatId, _publicKey, messageQueue }) + } else if (outSideMsg && outSideMsg.type === 'deleteFile') { + this.isDeletingAppFile = true + + let userName + let identifier + + userName = outSideMsg.name + identifier = outSideMsg.identifier + + if (this.webWorkerFile) { + this.webWorkerFile.terminate() + this.webWorkerFile = null + } + + this.webWorkerFile = new WebWorkerFile() + + const b64toBlob = (b64Data, contentType = '', sliceSize = 512) => { + const byteCharacters = atob(b64Data) + const byteArrays = [] + + for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { + const slice = byteCharacters.slice(offset, offset + sliceSize) + + const byteNumbers = new Array(slice.length) + for (let i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i) + } + + const byteArray = new Uint8Array(byteNumbers) + byteArrays.push(byteArray) + } + + const blob = new Blob(byteArrays, { type: contentType }) + return blob + } + + const blob = b64toBlob(str, 'image/png') + + let compressedFile = '' + + await new Promise(resolve => { + new Compressor(blob, { + quality: 0.6, + maxWidth: 500, + success(result) { + const file = new File([result], "name", { + type: 'image/png' + }) + + compressedFile = file + resolve() + }, + error() { + }, + }) + }) + + const arbitraryFeeData = await modalHelper.getArbitraryFee() + + const res = await modalHelper.showModalAndWaitPublish({ + feeAmount: arbitraryFeeData.feeToShow + }) + + if (res.action !== 'accept') throw new Error('User declined publish') + + try { + await publishData({ + registeredName: userName, + file: compressedFile, + service: 'FILE', + identifier: identifier, + parentEpml, + metaData: undefined, + uploadType: 'file', + selectedAddress: this.selectedAddress, + worker: this.webWorkerFile, + withFee: true, + feeAmount: arbitraryFeeData.fee + }) + + this.isDeletingAppFile = false + } catch (error) { + this.isDeletingAppFile = false + this.isLoading = false + return + } + + typeMessage = 'edit' + + let chatReference = outSideMsg.editedMessageObj.signature + + if (outSideMsg.editedMessageObj.chatReference) { + chatReference = outSideMsg.editedMessageObj.chatReference + } + + let message = '' + + try { + const parsedMessageObj = JSON.parse(outSideMsg.editedMessageObj.decodedMessage) + message = parsedMessageObj + + } catch (error) { + message = outSideMsg.editedMessageObj.decodedMessage + } + + const messageObject = { + ...message, + isFileDeleted: true + } + + const stringifyMessageObject = JSON.stringify(messageObject) + return this.sendMessage({ messageText: stringifyMessageObject, typeMessage, chatReference, isForward: false, isReceipient, _chatId, _publicKey, messageQueue }) } else if (outSideMsg && outSideMsg.type === 'image') { if (!this.imageFile.identifier) { this.isUploadingImage = true } + const userName = await getName(this.selectedAddress.address) + if (!userName) { parentEpml.request('showSnackBar', get("chatpage.cchange27")) this.isLoading = false @@ -2655,21 +3227,21 @@ class ChatPage extends LitElement { return } - let service = "QCHAT_IMAGE" let name = userName let identifier + if (this.imageFile.identifier) { identifier = this.imageFile.identifier name = this.imageFile.name service = this.imageFile.service } else { const arbitraryFeeData = await modalHelper.getArbitraryFee() - const res = await modalHelper.showModalAndWaitPublish( - { - feeAmount: arbitraryFeeData.feeToShow - } - ) + + const res = await modalHelper.showModalAndWaitPublish({ + feeAmount: arbitraryFeeData.feeToShow + }) + if (res.action !== 'accept') throw new Error('User declined publish') if (this.webWorkerFile) { @@ -2678,16 +3250,21 @@ class ChatPage extends LitElement { } this.webWorkerFile = new WebWorkerFile() + const image = this.imageFile const id = this.uid.rnd() let groupPart + if (this.isReceipient) { groupPart = `direct_${generateIdFromAddresses(this._chatId, this.selectedAddress.address)}` } else { groupPart = `group_${this._chatId}` } + identifier = `qchat_${groupPart}_${id}` + let compressedFile = '' + await new Promise(resolve => { new Compressor(image, { quality: .6, @@ -2704,7 +3281,9 @@ class ChatPage extends LitElement { }, }) }) + const fileSize = compressedFile.size + if (fileSize > 500000) { parentEpml.request('showSnackBar', get("chatpage.cchange26")) this.isLoading = false @@ -2713,7 +3292,6 @@ class ChatPage extends LitElement { } try { - await publishData({ registeredName: userName, file: compressedFile, @@ -2727,6 +3305,7 @@ class ChatPage extends LitElement { withFee: true, feeAmount: arbitraryFeeData.fee }) + this.isUploadingImage = false this.removeImage() } catch (error) { @@ -2742,42 +3321,124 @@ class ChatPage extends LitElement { images: [{ service: service, name: name, - identifier: identifier, + identifier: identifier }], isImageDeleted: false, repliedTo: '', version: 3 } + const stringifyMessageObject = JSON.stringify(messageObject) + this.removeImage() + return this.sendMessage({ messageText: stringifyMessageObject, typeMessage, chatReference: undefined, isForward: false, isReceipient, _chatId, _publicKey, messageQueue }) } else if (outSideMsg && outSideMsg.type === 'gif') { + if (!this.gifFile.identifier) { + this.isUploadingGif = true + } + const userName = await getName(this.selectedAddress.address) + if (!userName) { parentEpml.request('showSnackBar', get("chatpage.cchange27")) this.isLoading = false + this.isUploadingGif = false + this.gifFile = null + return + } + + let identifier + let groupPart + + if (this.webWorkerFile) { + this.webWorkerFile.terminate() + this.webWorkerFile = null + } + + this.webWorkerFile = new WebWorkerFile() + + const gifFile = this.gifFile + const id = this.uid.rnd() + + if (this.isReceipient) { + groupPart = `direct_${generateIdFromAddresses(this._chatId, this.selectedAddress.address)}` + } else { + groupPart = `group_${this._chatId}` + } + + identifier = `qchat_${groupPart}_gif_${id}` + + const fileSize = gifFile.size + + if (fileSize > 3000000) { + parentEpml.request('showSnackBar', get("chatpage.cchange103")) + this.isLoading = false + this.isUploadingGif = false + return + } + + const arbitraryFeeData = await modalHelper.getArbitraryFee() + + const res = await modalHelper.showModalAndWaitPublish({ + feeAmount: arbitraryFeeData.feeToShow + }) + + if (res.action !== 'accept') throw new Error('User declined publish') + + try { + await publishData({ + registeredName: userName, + file: gifFile, + service: 'IMAGE', + identifier: identifier, + parentEpml, + metaData: undefined, + uploadType: 'file', + selectedAddress: this.selectedAddress, + worker: this.webWorkerFile, + withFee: true, + feeAmount: arbitraryFeeData.fee + }) + + this.isUploadingGif = false + this.removeGif() + } catch (error) { + this.isLoading = false + this.isUploadingGif = false return } const messageObject = { - messageText: '', + messageText: trimmedMessage, gifs: [{ - service: outSideMsg.service, - name: outSideMsg.name, - identifier: outSideMsg.identifier, - filePath: outSideMsg.filePath + service: 'IMAGE', + name: userName, + identifier: identifier }], + isGifDeleted: false, repliedTo: '', version: 3 } + const stringifyMessageObject = JSON.stringify(messageObject) + return this.sendMessage({ messageText: stringifyMessageObject, typeMessage, chatReference: undefined, isForward: false, isReceipient, _chatId, _publicKey, messageQueue }) } else if (outSideMsg && outSideMsg.type === 'attachment') { - this.isUploadingAttachment = true + if (!this.attachment.identifier) { + this.isUploadingAttachment = true + } + + let identifier + let groupPart + const userName = await getName(this.selectedAddress.address) + if (!userName) { parentEpml.request('showSnackBar', get("chatpage.cchange27")) + this.isUploadingAttachment = false this.isLoading = false + this.attachment = null return } @@ -2788,28 +3449,39 @@ class ChatPage extends LitElement { this.webWorkerFile = new WebWorkerFile() - // const attachment = attachment - const id = this.uid() - const identifier = `qchat_${id}` + const attachment = this.attachment + const id = this.uid.rnd() + + if (this.isReceipient) { + groupPart = `direct_${generateIdFromAddresses(this._chatId, this.selectedAddress.address)}` + } else { + groupPart = `group_${this._chatId}` + } + + identifier = `qchat_${groupPart}_attachment_${id}` + const fileSize = attachment.size - if (fileSize > 1000000) { + + if (fileSize > 10000000) { parentEpml.request('showSnackBar', get("chatpage.cchange77")) this.isLoading = false this.isUploadingAttachment = false return } + const arbitraryFeeData = await modalHelper.getArbitraryFee() - const res = await modalHelper.showModalAndWaitPublish( - { - feeAmount: arbitraryFeeData.feeToShow - } - ) + + const res = await modalHelper.showModalAndWaitPublish({ + feeAmount: arbitraryFeeData.feeToShow + }) + if (res.action !== 'accept') throw new Error('User declined publish') + try { await publishData({ registeredName: userName, file: attachment, - service: 'QCHAT_ATTACHMENT', + service: 'ATTACHMENT', identifier: identifier, parentEpml, metaData: undefined, @@ -2819,6 +3491,7 @@ class ChatPage extends LitElement { withFee: true, feeAmount: arbitraryFeeData.fee }) + this.isUploadingAttachment = false this.removeAttachment() } catch (error) { @@ -2826,10 +3499,11 @@ class ChatPage extends LitElement { this.isUploadingAttachment = false return } + const messageObject = { messageText: trimmedMessage, attachments: [{ - service: 'QCHAT_ATTACHMENT', + service: 'ATTACHMENT', name: userName, identifier: identifier, attachmentName: attachment.name, @@ -2839,18 +3513,115 @@ class ChatPage extends LitElement { repliedTo: '', version: 3 } + + const stringifyMessageObject = JSON.stringify(messageObject) + + return this.sendMessage({ messageText: stringifyMessageObject, typeMessage, chatReference: undefined, isForward: false, isReceipient, _chatId, _publicKey, messageQueue }) + } else if (outSideMsg && outSideMsg.type === 'file') { + if (!this.appFile.identifier) { + this.isUploadingAppFile = true + } + + let identifier + let groupPart + + const userName = await getName(this.selectedAddress.address) + + if (!userName) { + parentEpml.request('showSnackBar', get("chatpage.cchange27")) + this.isUploadingAppFile = false + this.isLoading = false + this.appFile = null + return + } + + if (this.webWorkerFile) { + this.webWorkerFile.terminate() + this.webWorkerFile = null + } + + this.webWorkerFile = new WebWorkerFile() + + const appFile = this.appFile + const id = this.uid.rnd() + + if (this.isReceipient) { + groupPart = `direct_${generateIdFromAddresses(this._chatId, this.selectedAddress.address)}` + } else { + groupPart = `group_${this._chatId}` + } + + identifier = `qchat_${groupPart}_file_${id}` + + const fileSize = appFile.size + + if (fileSize > 125000000) { + parentEpml.request('showSnackBar', get("chatpage.cchange100")) + this.isLoading = false + this.isUploadingAppFile = false + return + } + + const arbitraryFeeData = await modalHelper.getArbitraryFee() + + const res = await modalHelper.showModalAndWaitPublish({ + feeAmount: arbitraryFeeData.feeToShow + }) + + if (res.action !== 'accept') throw new Error('User declined publish') + + try { + await publishData({ + registeredName: userName, + file: appFile, + service: 'FILE', + identifier: identifier, + parentEpml, + metaData: undefined, + uploadType: 'file', + selectedAddress: this.selectedAddress, + worker: this.webWorkerFile, + withFee: true, + feeAmount: arbitraryFeeData.fee + }) + + this.isUploadingAppFile = false + this.removeAppFile() + } catch (error) { + this.isLoading = false + this.isUploadingAppFile = false + return + } + + const messageObject = { + messageText: trimmedMessage, + files: [{ + service: 'FILE', + name: userName, + identifier: identifier, + appFileName: appFile.name, + appFileSize: appFile.size + }], + isFileDeleted: false, + repliedTo: '', + version: 3 + } + const stringifyMessageObject = JSON.stringify(messageObject) + return this.sendMessage({ messageText: stringifyMessageObject, typeMessage, chatReference: undefined, isForward: false, isReceipient, _chatId, _publicKey, messageQueue }) } else if (outSideMsg && outSideMsg.type === 'reaction') { const userName = await getName(this.selectedAddress.address) + typeMessage = 'edit' + let chatReference = outSideMsg.editedMessageObj.signature if (outSideMsg.editedMessageObj.chatReference) { chatReference = outSideMsg.editedMessageObj.chatReference } - let message = "" + let message = '' try { const parsedMessageObj = JSON.parse(outSideMsg.editedMessageObj.decodedMessage) @@ -2860,10 +3631,13 @@ class ChatPage extends LitElement { } let reactions = message.reactions || [] + const findEmojiIndex = reactions.findIndex((reaction) => reaction.type === outSideMsg.reaction) + if (findEmojiIndex !== -1) { let users = reactions[findEmojiIndex].users || [] const findUserIndex = users.findIndex((user) => user.address === this.selectedAddress.address) + if (findUserIndex !== -1) { users.splice(findUserIndex, 1) } else { @@ -2872,11 +3646,13 @@ class ChatPage extends LitElement { name: userName }) } + reactions[findEmojiIndex] = { ...reactions[findEmojiIndex], qty: users.length, users } + if (users.length === 0) { reactions.splice(findEmojiIndex, 1) } @@ -2890,27 +3666,35 @@ class ChatPage extends LitElement { }] }] } + const messageObject = { ...message, reactions } + const stringifyMessageObject = JSON.stringify(messageObject) + return this.sendMessage({ messageText: stringifyMessageObject, typeMessage, chatReference, isForward: false, isReceipient, _chatId, _publicKey, messageQueue }) } else if (/^\s*$/.test(trimmedMessage)) { this.isLoading = false } else if (this.repliedToMessageObj) { let chatReference = this.repliedToMessageObj.signature + if (this.repliedToMessageObj.chatReference) { chatReference = this.repliedToMessageObj.chatReference } + typeMessage = 'reply' + const messageObject = { messageText: trimmedMessage, images: [''], repliedTo: chatReference, version: 3 } + const stringifyMessageObject = JSON.stringify(messageObject) + return this.sendMessage({ messageText: stringifyMessageObject, typeMessage, chatReference: undefined, isForward: false, isReceipient, _chatId, _publicKey, messageQueue }) } else if (this.editedMessageObj) { typeMessage = 'edit' @@ -2920,7 +3704,8 @@ class ChatPage extends LitElement { chatReference = this.editedMessageObj.chatReference } - let message = "" + let message = '' + try { const parsedMessageObj = JSON.parse(this.editedMessageObj.decodedMessage) message = parsedMessageObj @@ -2928,12 +3713,15 @@ class ChatPage extends LitElement { } catch (error) { message = this.editedMessageObj.decodedMessage } + const messageObject = { ...message, messageText: trimmedMessage, isEdited: true } + const stringifyMessageObject = JSON.stringify(messageObject) + return this.sendMessage({ messageText: stringifyMessageObject, typeMessage, chatReference, isForward: false, isReceipient, _chatId, _publicKey, messageQueue }) } else { const messageObject = { @@ -2942,6 +3730,7 @@ class ChatPage extends LitElement { repliedTo: '', version: 3 } + const stringifyMessageObject = JSON.stringify(messageObject) if (this.balance < 4) { @@ -2955,6 +3744,9 @@ class ChatPage extends LitElement { } catch (error) { this.isLoading = false this.isUploadingImage = false + this.isUploadingGif = false + this.isUploadingAttachment = false + this.isUploadingAppFile = false return } @@ -2968,6 +3760,7 @@ class ChatPage extends LitElement { this.closeRepliedToContainer() return } + if (isForward) { this.isLoading = true } @@ -2975,6 +3768,7 @@ class ChatPage extends LitElement { let _reference = new Uint8Array(64) window.crypto.getRandomValues(_reference) let reference = window.parent.Base58.encode(_reference) + const sendMessageRequest = async () => { if (isReceipient === true) { let chatResponse = await parentEpml.request('chat', { diff --git a/plugins/plugins/core/components/ChatScroller.js b/plugins/plugins/core/components/ChatScroller.js index 14963901..8086fe8a 100644 --- a/plugins/plugins/core/components/ChatScroller.js +++ b/plugins/plugins/core/components/ChatScroller.js @@ -21,6 +21,9 @@ import './ChatImage' import '@material/mwc-button' import '@material/mwc-dialog' import '@material/mwc-icon' +import '@polymer/paper-dialog/paper-dialog.js' +import '@polymer/paper-icon-button/paper-icon-button.js' +import '@polymer/iron-icons/iron-icons.js' import '@vaadin/icon' import '@vaadin/icons' import '@vaadin/tooltip' @@ -96,6 +99,7 @@ function processText(input) { parts.forEach((part) => { if (part.startsWith('qortal://')) { const link = document.createElement('span') + // Store the URL in a data attribute link.setAttribute('data-url', part) link.textContent = part @@ -107,7 +111,9 @@ function processText(input) { e.preventDefault() try { const res = await extractComponents(part) + if (!res) return + if (res.type && res.groupid && res.action === 'join') { window.parent.reduxStore.dispatch( window.parent.reduxAction.setNewTab({ @@ -130,7 +136,7 @@ function processText(input) { window.parent.reduxStore.dispatch( window.parent.reduxAction.setSideEffectAction({ type: 'openJoinGroupModal', - data: +res.groupid + data: res.groupid }) ) return @@ -987,7 +993,9 @@ class MessageTemplate extends LitElement { openDialogImage: { type: Boolean }, openDialogGif: { type: Boolean }, openDeleteImage: { type: Boolean }, + openDeleteGif: { type: Boolean }, openDeleteAttachment: { type: Boolean }, + openDeleteFile: { type: Boolean }, isImageLoaded: { type: Boolean }, isGifLoaded: { type: Boolean }, isFirstMessage: { type: Boolean }, @@ -1032,6 +1040,10 @@ class MessageTemplate extends LitElement { this.isLastMessageInGroup = false this.viewImage = false this.isInProgress = false + this.openDeleteImage = false + this.openDeleteGif = false + this.openDeleteAttachment = false + this.openDeleteFile = false } render() { @@ -1044,36 +1056,50 @@ class MessageTemplate extends LitElement { let repliedToData = null let image = null let gif = null + let attachment = null + let file = null let isImageDeleted = false + let isGifDeleted = false let isAttachmentDeleted = false + let isFileDeleted = false let version = 0 let isForwarded = false let isEdited = false - let attachment = null try { const parsedMessageObj = JSON.parse(this.messageObj.decodedMessage) + if (+parsedMessageObj.version > 1 && parsedMessageObj.messageText) { messageVersion2 = generateHTML(parsedMessageObj.messageText, [StarterKit, Underline, Highlight]) messageVersion2WithLink = processText(messageVersion2) } + message = parsedMessageObj.messageText repliedToData = this.messageObj.repliedToData isImageDeleted = parsedMessageObj.isImageDeleted + isGifDeleted = parsedMessageObj.isGifDeleted isAttachmentDeleted = parsedMessageObj.isAttachmentDeleted + isFileDeleted = parsedMessageObj.isFileDeleted // reactions = parsedMessageObj.reactions || [] version = parsedMessageObj.version isForwarded = parsedMessageObj.type === 'forward' isEdited = parsedMessageObj.isEdited && true - if (parsedMessageObj.attachments && Array.isArray(parsedMessageObj.attachments) && parsedMessageObj.attachments.length > 0) { - attachment = parsedMessageObj.attachments[0] - } + if (parsedMessageObj.images && Array.isArray(parsedMessageObj.images) && parsedMessageObj.images.length > 0) { image = parsedMessageObj.images[0] } + if (parsedMessageObj.gifs && Array.isArray(parsedMessageObj.gifs) && parsedMessageObj.gifs.length > 0) { gif = parsedMessageObj.gifs[0] } + + if (parsedMessageObj.attachments && Array.isArray(parsedMessageObj.attachments) && parsedMessageObj.attachments.length > 0) { + attachment = parsedMessageObj.attachments[0] + } + + if (parsedMessageObj.files && Array.isArray(parsedMessageObj.files) && parsedMessageObj.files.length > 0) { + file = parsedMessageObj.files[0] + } } catch (error) { message = this.messageObj.decodedMessage } @@ -1141,7 +1167,7 @@ class MessageTemplate extends LitElement { if (gif) { const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port - gifUrl = `${nodeUrl}/arbitrary/${gif.service}/${gif.name}/${gif.identifier}?filepath=${gif.filePath}` + gifUrl = `${nodeUrl}/arbitrary/${gif.service}/${gif.name}/${gif.identifier}?async=true` if (this.viewImage || this.myAddress === this.messageObj.sender) { gifHTML = createGif(gifUrl) gifHTMLDialog = createGif(gifUrl) @@ -1280,7 +1306,7 @@ class MessageTemplate extends LitElement { class=${[`image-container`, !this.isImageLoaded ? 'defaultSize' : '',].join(' ')} style=${this.isFirstMessage && 'margin-top: 10px;'} > -
+
${translate('chatpage.cchange40')}
@@ -1293,7 +1319,12 @@ class MessageTemplate extends LitElement { ${imageHTML} ${this.myAddress === this.messageObj.sender ? html` - {this.openDeleteImage = true;}} class="image-delete-icon" icon="vaadin:close" slot="icon"> + this.openDeleteImageDialog()} + icon="vaadin:close" + slot="icon" + class="image-delete-icon" + > ` : '' } @@ -1301,35 +1332,62 @@ class MessageTemplate extends LitElement { ` : image && isImageDeleted ? html` -

${translate('chatpage.cchange80')}

+
+
+

+ ${translate('chatpage.cchange80')} +

+
+
` : html`` } - ${gif && !this.viewImage && this.myAddress !== this.messageObj.sender ? + ${gif && !isGifDeleted && !this.viewImage && this.myAddress !== this.messageObj.sender ? html`
{this.viewImage = true;}} - class=${[`image-container`, !this.isImageLoaded ? 'defaultSize' : '', ].join(' ')} + class=${[`image-container`, !this.isGifoaded ? 'defaultSize' : '', ].join(' ')} style=${this.isFirstMessage && 'margin-top: 10px;'} > -
+
${translate('gifs.gchange25')}
` : html`` } - ${gif && (this.viewImage || this.myAddress === this.messageObj.sender) ? + ${gif && !isGifDeleted && (this.viewImage || this.myAddress === this.messageObj.sender) ? html`
${gifHTML} + ${this.myAddress === this.messageObj.sender ? + html` + this.openDeleteGifDialog()} + icon="vaadin:close" + slot="icon" + class="image-delete-icon" + > + ` + : '' + }
` + : gif && isGifDeleted ? + html` +
+
+

+ ${translate('chatpage.cchange107')} +

+
+
+ ` : html`` } ${attachment && !isAttachmentDeleted ? html` -
await this.downloadAttachment(attachment)} class="attachment-container"> +
attachment-icon
@@ -1341,11 +1399,16 @@ class MessageTemplate extends LitElement { ${roundToNearestDecimal(attachment.attachmentSize)} mb

- + await this.downloadAttachment(attachment)} + icon="vaadin:download-alt" + slot="icon" + class="download-icon" + > ${this.myAddress === this.messageObj.sender ? html` {e.stopPropagation(); this.openDeleteAttachment = true;}} + @click=${() => this.openDeleteAttachmentDialog()} class="image-delete-icon" icon="vaadin:close" slot="icon" @@ -1368,6 +1431,51 @@ class MessageTemplate extends LitElement { ` : html`` } + ${file && !isFileDeleted ? + html` +
+
+ file-icon +
+
+

+ ${file && file.appFileName} +

+

+ ${roundToNearestDecimal(file.appFileSize)} mb +

+
+ await this.downloadFile(file)} + icon="vaadin:download-alt" + slot="icon" + class="download-icon" + > + ${this.myAddress === this.messageObj.sender ? + html` + this.openDeleteFileDialog()} + class="image-delete-icon" + icon="vaadin:close" + slot="icon" + > + ` + : html`` + } +
+ ` + : file && isFileDeleted ? + html` +
+
+

+ ${translate('chatpage.cchange102')} +

+
+
+ ` + : html`` + }
${+version > 1 ? messageVersion2WithLink ? html` @@ -1511,7 +1619,6 @@ class MessageTemplate extends LitElement { {this.openDialogGif = false;}}> - MessageTemplate
${gifHTMLDialog} @@ -1519,34 +1626,88 @@ class MessageTemplate extends LitElement { {this.openDialogGif = false;}}> ${translate('general.close')} - MessageTemplate - {this.openDeleteImage = false;}}> +

${translate('chatpage.cchange78')}

-
- {this.openDeleteAttachment = false;}}> + +
+

${translate('chatpage.cchange106')}

+
+ +
+

${translate('chatpage.cchange79')}

- +
+ +
+

${translate('chatpage.cchange101')}

+
+
+ + +
+
+
+
+
+
+
+
+
+
+

${translate('appspage.schange41')}

+
+ + ${translate('chatpage.cchange108')} + ` } @@ -1574,6 +1735,14 @@ class MessageTemplate extends LitElement { }, 60000) } + async closeDownloadProgressDialog() { + const closeDelay = ms => new Promise(res => setTimeout(res, ms)) + this.shadowRoot.getElementById('downloadProgressDialog').close() + this.shadowRoot.getElementById('closeProgressDialog').open() + await closeDelay(3000) + this.shadowRoot.getElementById('closeProgressDialog').close() + } + // Open & Close Private Message Chat Modal showPrivateMessageModal() { this.openDialogPrivateMessage = true @@ -1600,17 +1769,82 @@ class MessageTemplate extends LitElement { } } - async downloadAttachment(attachment) { + openDeleteImageDialog() { + this.openDeleteImage = true + this.shadowRoot.querySelector('#deleteImageDialog').show() + } + + closeDeleteImageDialog() { + this.shadowRoot.querySelector('#deleteImageDialog').close() + this.openDeleteImage = false + } + + openDeleteGifDialog() { + this.openDeleteGif = true + this.shadowRoot.querySelector('#deleteGifDialog').show() + } + + closeDeleteGifDialog() { + this.shadowRoot.querySelector('#deleteGifDialog').close() + this.openDeleteGif = false + } + + openDeleteAttachmentDialog() { + this.openDeleteAttachment = true + this.shadowRoot.querySelector('#deleteAttachmentDialog').show() + } + + closeDeleteAttachmentDialog() { + this.shadowRoot.querySelector('#deleteAttachmentDialog').close() + this.openDeleteAttachment = false + } + + openDeleteFileDialog() { + this.openDeleteFile = true + this.shadowRoot.querySelector('#deleteFileDialog').show() + } + + closeDeleteFileDialog() { + this.shadowRoot.querySelector('#deleteFileDialog').close() + this.openDeleteFile = false + } + + downloadAttachment(attachment) { const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port + this.shadowRoot.getElementById('downloadProgressDialog').open() + try { axios.get( - `${nodeUrl}/arbitrary/QCHAT_ATTACHMENT/${attachment.name}/${attachment.identifier}`, + `${nodeUrl}/arbitrary/ATTACHMENT/${attachment.name}/${attachment.identifier}`, { responseType: 'blob' } ).then((response) => { + this.shadowRoot.getElementById('downloadProgressDialog').close() let filename = attachment.attachmentName let blob = new Blob([response.data], { type: 'application/octet-stream' }) + this.shadowRoot.getElementById('downloadProgressDialog').close() + this.saveFileToDisk(blob, filename) + }) + } catch (error) { + console.error(error) + } + } + + downloadFile(file) { + const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] + const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port + + this.shadowRoot.getElementById('downloadProgressDialog').open() + + try { + axios.get( + `${nodeUrl}/arbitrary/FILE/${file.name}/${file.identifier}`, + { responseType: 'blob' } + ).then((response) => { + this.shadowRoot.getElementById('downloadProgressDialog').close() + let filename = file.appFileName + let blob = new Blob([response.data], { type: 'application/octet-stream' }) this.saveFileToDisk(blob, filename) }) } catch (error) { @@ -1630,7 +1864,8 @@ class MessageTemplate extends LitElement { await writable.write(contents) await writable.close() } - writeFile(fileHandle, blob).then(() => console.log('FILE SAVED')) + + await writeFile(fileHandle, blob).then(() => console.log('FILE SAVED')) } catch (error) { console.log(error) } diff --git a/plugins/plugins/core/components/ChatTextEditor.js b/plugins/plugins/core/components/ChatTextEditor.js index 3d6d07d4..805fdd99 100644 --- a/plugins/plugins/core/components/ChatTextEditor.js +++ b/plugins/plugins/core/components/ChatTextEditor.js @@ -135,14 +135,18 @@ class ChatTextEditor extends LitElement { this.insertFile(e.target.files[0]) const filePickerInput = this.shadowRoot.getElementById('file-picker') if (filePickerInput) { - filePickerInput.value = "" + filePickerInput.value = '' } }}" id="file-picker" class="file-picker-input" type="file" name="myImage" - accept="image/*, .doc, .docx, .pdf, .zip, .pdf, .txt, .odt, .ods, .xls, .xlsx, .ppt, .pptx" + accept=" + image/*, .doc, .docx, .zip, .pdf, .txt, .odt, .ods, .html, + .xls, .xlsx, .ppt, .pptx, .jar, .gzip, .exe, .deb, .rar, .log, + .sh, .dmg, .pkg, .7z, .gz, .psd, .mp4, .rpm, .snap, .AppImage + " >
@@ -295,7 +299,7 @@ class ChatTextEditor extends LitElement { } sendMessageFunc(props) { - if (this.editor.isEmpty && (this.iframeId !== 'newChat' && this.iframeId !== 'newAttachmentChat')) return + if (this.editor.isEmpty && (this.iframeId !== 'newChat' && this.iframeId !== 'newGifChat' && this.iframeId !== 'newAttachmentChat' && this.iframeId !== 'newFileChat')) return this.getMessageSize(this.editor.getJSON()) @@ -351,18 +355,42 @@ class ChatTextEditor extends LitElement { repliedTo: '', version: 3 } + } else if (this.gifFile && this.iframeId === 'newGifChat') { + messageObject = { + messageText: trimmedMessage, + images: [{ + service: "IMAGE", + name: '123456789123456789123456789', + identifier: '123456' + }], + repliedTo: '', + version: 3 + } } else if (this.attachment && this.iframeId === 'newAttachmentChat') { messageObject = { messageText: trimmedMessage, attachments: [{ - service: "QCHAT_ATTACHMENT", + service: "ATTACHMENT", name: '123456789123456789123456789', identifier: '123456', attachmentName: "123456789123456789123456789", attachmentSize: "123456" }], repliedTo: '', - version: 2 + version: 3 + } + } else if (this.appFile && this.iframeId === 'newFileChat') { + messageObject = { + messageText: trimmedMessage, + files: [{ + service: "FILE", + name: '123456789123456789123456789', + identifier: '123456', + appFileName: "123456789123456789123456789", + appFileSize: "123456" + }], + repliedTo: '', + version: 3 } } else { messageObject = { diff --git a/plugins/plugins/core/components/plugins-css.js b/plugins/plugins/core/components/plugins-css.js index 6ee176c2..b5c570db 100644 --- a/plugins/plugins/core/components/plugins-css.js +++ b/plugins/plugins/core/components/plugins-css.js @@ -426,7 +426,6 @@ export const chatpageStyles = css` width: 800px; } - .close-icon { color: #676b71; width: 18px; @@ -1152,6 +1151,22 @@ export const chatpageStyles = css` width: 70%; } + .file-icon-container { + display: flex; + align-items: center; + justify-content: center; + height: 128px; + width: 128px; + border-radius: 50%; + border: none; + background-color: transparent; + } + + .file-icon { + height: 128px; + width: 128px; + } + .attachment-name { font-family: Work Sans, sans-serif; font-size: 20px; @@ -1440,6 +1455,7 @@ export const chatStyles = css` --mdc-theme-secondary: var(--mdc-theme-primary); --mdc-dialog-max-width: 85vw; --mdc-dialog-max-height: 95vh; + } * :focus-visible { @@ -1567,7 +1583,6 @@ export const chatStyles = css` min-width: 150px; } - .message-myBg { background-color: var(--chat-bubble-myBg) !important; } @@ -2107,10 +2122,8 @@ export const chatStyles = css` justify-content: space-evenly; padding: 5px 0 10px 0; gap: 20px; - cursor: pointer; } - .attachment-icon-container { display: flex; align-items: center; @@ -2126,6 +2139,30 @@ export const chatStyles = css` width: 70%; } + .file-container { + display: flex; + align-items: center; + justify-content: space-evenly; + padding: 5px 0 10px 0; + gap: 20px; + } + + .file-icon-container { + display: flex; + align-items: center; + justify-content: center; + height: 50px; + width: 50px; + border-radius: 50%; + border: none; + background-color: transparent; + } + + .file-icon { + height: 50px; + width: 50px; + } + .attachment-info { display: flex; flex-direction: column; @@ -2158,6 +2195,7 @@ export const chatStyles = css` color: var(--chat-bubble-msg-color); width: 19px; background-color: transparent; + cursor: pointer; } .download-icon:hover::before { @@ -2247,6 +2285,155 @@ export const chatStyles = css` transform: rotate(360deg); } } + + paper-dialog.progress { + width: auto; + max-width: 50vw; + height: auto; + max-height: 30vh; + background-color: var(--white); + color: var(--black); + border: 1px solid var(--black); + border-radius: 15px; + text-align: center; + padding: 15px; + line-height: 1.6; + overflow: hidden; + } + + paper-dialog.close-progress { + min-width: 550px; + max-width: 550px; + height: auto; + background-color: var(--white); + color: var(--black); + border: 1px solid var(--black); + border-radius: 15px; + text-align: center; + padding: 15px; + font-size: 17px; + font-weight: 500; + line-height: 20px; + overflow: hidden; + } + + .lds-roller { + display: inline-block; + position: relative; + width: 80px; + height: 80px; + } + + .lds-roller div { + animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; + transform-origin: 40px 40px; + } + + .lds-roller div:after { + content: " "; + display: block; + position: absolute; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--black); + margin: -4px 0 0 -4px; + } + + .lds-roller div:nth-child(1) { + animation-delay: -0.036s; + } + + .lds-roller div:nth-child(1):after { + top: 63px; + left: 63px; + } + + .lds-roller div:nth-child(2) { + animation-delay: -0.072s; + } + + .lds-roller div:nth-child(2):after { + top: 68px; + left: 56px; + } + + .lds-roller div:nth-child(3) { + animation-delay: -0.108s; + } + + .lds-roller div:nth-child(3):after { + top: 71px; + left: 48px; + } + + .lds-roller div:nth-child(4) { + animation-delay: -0.144s; + } + + .lds-roller div:nth-child(4):after { + top: 72px; + left: 40px; + } + + .lds-roller div:nth-child(5) { + animation-delay: -0.18s; + } + + .lds-roller div:nth-child(5):after { + top: 71px; + left: 32px; + } + + .lds-roller div:nth-child(6) { + animation-delay: -0.216s; + } + + .lds-roller div:nth-child(6):after { + top: 68px; + left: 24px; + } + + .lds-roller div:nth-child(7) { + animation-delay: -0.252s; + } + + .lds-roller div:nth-child(7):after { + top: 63px; + left: 17px; + } + + .lds-roller div:nth-child(8) { + animation-delay: -0.288s; + } + + .lds-roller div:nth-child(8):after { + top: 56px; + left: 12px; + } + + @keyframes lds-roller { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } + } + + .close-download { + color: var(--black); + font-size: 14px; + font-weight: bold; + position: absolute; + top: -15px; + right: -15px; + } + + .close-download:hover { + color: #df3636; + } ` export const toolTipStyles = css` @@ -3631,6 +3818,12 @@ export const chatTextEditorStyles = css` cursor: pointer; } + .ProseMirror { + word-wrap: break-word; + white-space: pre-wrap; + white-space: break-spaces; + } + .ProseMirror:focus { outline: none; }