diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index 3c730376..35aed7e6 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -11,7 +11,7 @@ import apiManager from "../../lib/mtproto/mtprotoworker"; import opusDecodeController from "../../lib/opusDecodeController"; import { RichTextProcessor } from "../../lib/richtextprocessor"; import rootScope from '../../lib/rootScope'; -import { cancelEvent, findUpClassName, getRichValue, isInputEmpty, placeCaretAtEnd, serializeNodes } from "../../helpers/dom"; +import { cancelEvent, CLICK_EVENT_NAME, findUpClassName, getRichValue, isInputEmpty, placeCaretAtEnd, serializeNodes } from "../../helpers/dom"; import ButtonMenu, { ButtonMenuItemOptions } from '../buttonMenu'; import emoticonsDropdown from "../emoticonsDropdown"; import PopupCreatePoll from "../popupCreatePoll"; @@ -72,27 +72,28 @@ export class ChatInput { private helperType: Exclude; private helperFunc: () => void; + private helperWaitingForward: boolean; - constructor() { - const messageInputField = InputField({ - placeholder: 'Message', - name: 'message' - }); + private willAttachType: 'document' | 'media'; - messageInputField.input.className = ''; - this.inputScroll.container.append(messageInputField.input); - this.messageInput = messageInputField.input; + private lockRedo = false; + private canRedoFromHTML = ''; + readonly undoHistory: string[] = []; + readonly executedHistory: string[] = []; + private canUndoFromHTML = ''; + + constructor() { + this.attachMessageInputField(); this.attachMenu = document.getElementById('attach-file') as HTMLButtonElement; - let willAttachType: 'document' | 'media'; this.attachMenuButtons = [{ icon: 'photo', text: 'Photo or Video', onClick: () => { this.fileInput.value = ''; this.fileInput.setAttribute('accept', 'image/*, video/*'); - willAttachType = 'media'; + this.willAttachType = 'media'; this.fileInput.click(); }, verify: (peerID: number) => peerID > 0 || appChatsManager.hasRights(peerID, 'send', 'send_media') @@ -102,7 +103,7 @@ export class ChatInput { onClick: () => { this.fileInput.value = ''; this.fileInput.removeAttribute('accept'); - willAttachType = 'document'; + this.willAttachType = 'document'; this.fileInput.click(); }, verify: (peerID: number) => peerID > 0 || appChatsManager.hasRights(peerID, 'send', 'send_media') @@ -164,261 +165,19 @@ export class ChatInput { this.updateSendBtn(); }); - this.messageInput.addEventListener('keydown', (e: KeyboardEvent) => { - if(e.key == 'Enter' && !isTouchSupported) { - /* if(e.ctrlKey || e.metaKey) { - this.messageInput.innerHTML += '
'; - placeCaretAtEnd(this.message) - return; - } */ - - if(e.shiftKey || e.ctrlKey || e.metaKey) { - return; - } - - this.sendMessage(); - } - }); - - if(isTouchSupported) { - this.messageInput.addEventListener('touchend', (e) => { - this.saveScroll(); - emoticonsDropdown.toggle(false); - }); - - window.addEventListener('resize', () => { - this.restoreScroll(); - }); - } - - this.messageInput.addEventListener('input', (e) => { - //console.log('messageInput input', this.messageInput.innerText, this.serializeNodes(Array.from(this.messageInput.childNodes))); - const value = this.messageInput.innerText; - - const entities = RichTextProcessor.parseEntities(value); - //console.log('messageInput entities', entities); - - const urlEntities = entities.filter(e => e._ == 'messageEntityUrl'); - if(urlEntities.length) { - const richEntities: MessageEntity[] = []; - const richValue = RichTextProcessor.parseMarkdown(getRichValue(this.messageInput), richEntities); - //console.log('messageInput url', entities, richEntities); - for(const entity of urlEntities) { - const url = value.slice(entity.offset, entity.offset + entity.length); - - if(!(url.includes('http://') || url.includes('https://')) && !richEntities.find(e => e._ == 'messageEntityTextUrl')) { - continue; - } - - //console.log('messageInput url:', url); - - if(this.lastUrl != url) { - this.lastUrl = url; - this.willSendWebPage = null; - apiManager.invokeApi('messages.getWebPage', { - url: url, - hash: 0 - }).then((webpage) => { - webpage = appWebPagesManager.saveWebPage(webpage); - if(webpage._ == 'webPage') { - if(this.lastUrl != url) return; - //console.log('got webpage: ', webpage); - - this.setTopInfo('webpage', () => {}, webpage.site_name || webpage.title || 'Webpage', webpage.description || webpage.url || ''); - - delete this.noWebPage; - this.willSendWebPage = webpage; - } - }); - } - - break; - } - } else if(this.lastUrl) { - this.lastUrl = ''; - delete this.noWebPage; - this.willSendWebPage = null; - - if(this.helperType) { - this.helperFunc(); - } else { - this.clearHelper(); - } - } - - if(!value.trim() && !serializeNodes(Array.from(this.messageInput.childNodes)).trim()) { - this.messageInput.innerHTML = ''; - - appMessagesManager.setTyping(rootScope.selectedPeerID, 'sendMessageCancelAction'); - } else { - const time = Date.now(); - if(time - this.lastTimeType >= 6000) { - this.lastTimeType = time; - appMessagesManager.setTyping(rootScope.selectedPeerID, 'sendMessageTypingAction'); - } - } - - this.updateSendBtn(); - }); - - /* if(!RichTextProcessor.emojiSupported) { - this.messageInput.addEventListener('copy', (e) => { - const selection = document.getSelection(); - - let range = selection.getRangeAt(0); - let ancestorContainer = range.commonAncestorContainer; - - let str = ''; - - let selectedNodes = Array.from(ancestorContainer.childNodes).slice(range.startOffset, range.endOffset); - if(selectedNodes.length) { - str = serializeNodes(selectedNodes); - } else { - str = selection.toString(); - } - - //console.log('messageInput copy', str, ancestorContainer.childNodes, range); - - //let str = getRichValueWithCaret(this.messageInput); - //console.log('messageInput childNode copy:', str); - - // @ts-ignore - event.clipboardData.setData('text/plain', str); - event.preventDefault(); - }); - } */ - this.fileInput.addEventListener('change', (e) => { let files = (e.target as HTMLInputElement & EventTarget).files; if(!files.length) { return; } - new PopupNewMedia(Array.from(files).slice(), willAttachType); + new PopupNewMedia(Array.from(files).slice(), this.willAttachType); this.fileInput.value = ''; }, false); - document.addEventListener('paste', (e) => { - const peerID = rootScope.selectedPeerID; - if(!peerID || rootScope.overlayIsActive || (peerID < 0 && !appChatsManager.hasRights(peerID, 'send', 'send_media'))) { - return; - } - - //console.log('document paste'); - - // @ts-ignore - const items = (e.clipboardData || e.originalEvent.clipboardData).items; - //console.log('item', event.clipboardData.getData()); - let foundFile = false; - for(let i = 0; i < items.length; ++i) { - if(items[i].kind == 'file') { - e.preventDefault() - e.cancelBubble = true; - e.stopPropagation(); - foundFile = true; - - let file = items[i].getAsFile(); - //console.log(items[i], file); - if(!file) continue; - - willAttachType = file.type.indexOf('image/') === 0 ? 'media' : "document"; - new PopupNewMedia([file], willAttachType); - } - } - }, true); - - const onBtnSendClick = (e: Event) => { - cancelEvent(e); - - if(!this.recorder || this.recording || !this.isInputEmpty() || this.forwardingMids.length) { - if(this.recording) { - if((Date.now() - this.recordStartTime) < RECORD_MIN_TIME) { - this.btnCancelRecord.click(); - } else { - this.recorder.stop(); - } - } else { - this.sendMessage(); - } - } else { - if(rootScope.selectedPeerID < 0 && !appChatsManager.hasRights(rootScope.selectedPeerID, 'send', 'send_media')) { - toast(POSTING_MEDIA_NOT_ALLOWED); - return; - } - - this.chatInput.classList.add('is-locked'); - this.recorder.start().then(() => { - this.recordCanceled = false; - - this.chatInput.classList.add('is-recording'); - this.recording = true; - this.updateSendBtn(); - opusDecodeController.setKeepAlive(true); + document.addEventListener('paste', this.onDocumentPaste, true); - this.recordStartTime = Date.now(); - - const sourceNode: MediaStreamAudioSourceNode = this.recorder.sourceNode; - const context = sourceNode.context; - - const analyser = context.createAnalyser(); - sourceNode.connect(analyser); - //analyser.connect(context.destination); - analyser.fftSize = 32; - - const frequencyData = new Uint8Array(analyser.frequencyBinCount); - const max = frequencyData.length * 255; - const min = 54 / 150; - let r = () => { - if(!this.recording) return; - - analyser.getByteFrequencyData(frequencyData); - - let sum = 0; - frequencyData.forEach(value => { - sum += value; - }); - - let percents = Math.min(1, (sum / max) + min); - //console.log('frequencyData', frequencyData, percents); - - this.recordRippleEl.style.transform = `scale(${percents})`; - - let diff = Date.now() - this.recordStartTime; - let ms = diff % 1000; - - let formatted = ('' + (diff / 1000)).toHHMMSS() + ',' + ('00' + Math.round(ms / 10)).slice(-2); - - this.recordTimeEl.innerText = formatted; - - window.requestAnimationFrame(r); - }; - - r(); - }).catch((e: Error) => { - switch(e.name as string) { - case 'NotAllowedError': { - toast('Please allow access to your microphone'); - break; - } - - case 'NotReadableError': { - toast(e.message); - break; - } - - default: - console.error('Recorder start error:', e, e.name, e.message); - toast(e.message); - break; - } - - this.chatInput.classList.remove('is-recording', 'is-locked'); - }); - } - }; - - this.btnSend.addEventListener('touchend', onBtnSendClick); - this.btnSend.addEventListener('click', onBtnSendClick); + this.btnSend.addEventListener(CLICK_EVENT_NAME, this.onBtnSendClick); if(this.recorder) { const onCancelRecordClick = (e: Event) => { @@ -427,8 +186,7 @@ export class ChatInput { this.recorder.stop(); opusDecodeController.setKeepAlive(false); }; - this.btnCancelRecord.addEventListener('touchend', onCancelRecordClick); - this.btnCancelRecord.addEventListener('click', onCancelRecordClick); + this.btnCancelRecord.addEventListener(CLICK_EVENT_NAME, onCancelRecordClick); this.recorder.onstop = () => { this.recording = false; @@ -503,56 +261,475 @@ export class ChatInput { }; } - const onCancelHelper = (e: Event) => { - cancelEvent(e); + this.replyElements.cancelBtn.addEventListener(CLICK_EVENT_NAME, this.onHelperCancel); + this.replyElements.container.addEventListener(CLICK_EVENT_NAME, this.onHelperClick); + } - if(this.willSendWebPage) { - this.noWebPage = true; - this.willSendWebPage = null; + private attachMessageInputField() { + const messageInputField = InputField({ + placeholder: 'Message', + name: 'message' + }); + + messageInputField.input.className = ''; + this.messageInput = messageInputField.input; + this.attachMessageInputListeners(); + + const container = this.inputScroll.container; + if(container.firstElementChild) { + container.replaceChild(messageInputField.input, container.firstElementChild); + } else { + container.append(messageInputField.input); + } + } - if(this.helperType) { - //if(this.helperFunc) { - this.helperFunc(); - //} + private attachMessageInputListeners() { + this.messageInput.addEventListener('keydown', (e: KeyboardEvent) => { + if(e.key == 'Enter' && !isTouchSupported) { + /* if(e.ctrlKey || e.metaKey) { + this.messageInput.innerHTML += '
'; + placeCaretAtEnd(this.message) + return; + } */ + if(e.shiftKey || e.ctrlKey || e.metaKey) { return; } + + this.sendMessage(); + } else if(e.ctrlKey || e.metaKey) { + this.handleMarkdownShortcut(e); } + }); - this.clearHelper(); - this.updateSendBtn(); + if(isTouchSupported) { + this.messageInput.addEventListener('touchend', (e) => { + this.saveScroll(); + emoticonsDropdown.toggle(false); + }); + + window.addEventListener('resize', () => { + this.restoreScroll(); + }); + } + + this.messageInput.addEventListener('input', this.onMessageInput); + } + + private onDocumentPaste = (e: ClipboardEvent) => { + const peerID = rootScope.selectedPeerID; + if(!peerID || rootScope.overlayIsActive || (peerID < 0 && !appChatsManager.hasRights(peerID, 'send', 'send_media'))) { + return; + } + + //console.log('document paste'); + + // @ts-ignore + const items = (e.clipboardData || e.originalEvent.clipboardData).items; + //console.log('item', event.clipboardData.getData()); + //let foundFile = false; + for(let i = 0; i < items.length; ++i) { + if(items[i].kind == 'file') { + e.preventDefault() + e.cancelBubble = true; + e.stopPropagation(); + //foundFile = true; + + let file = items[i].getAsFile(); + //console.log(items[i], file); + if(!file) continue; + + this.willAttachType = file.type.indexOf('image/') === 0 ? 'media' : "document"; + new PopupNewMedia([file], this.willAttachType); + } + } + }; + + private prepareDocumentExecute = () => { + this.executedHistory.push(this.messageInput.innerHTML); + return () => this.canUndoFromHTML = this.messageInput.innerHTML; + }; + + private undoRedo = (e: Event, type: 'undo' | 'redo', needHTML: string) => { + cancelEvent(e); // cancel legacy event + + let html = this.messageInput.innerHTML; + if(html && html != needHTML) { + this.lockRedo = true; + + let sameHTMLTimes = 0; + do { + document.execCommand(type, false, null); + const currentHTML = this.messageInput.innerHTML; + if(html == currentHTML) { + if(++sameHTMLTimes > 2) { // * unlink, removeFormat (а может и нет, случай: заболдить подчёркнутый текст (выделить ровно его), попробовать отменить) + break; + } + } else { + sameHTMLTimes = 0; + } + + html = currentHTML; + } while(html != needHTML); + + this.lockRedo = false; + } + }; + + private handleMarkdownShortcut = (e: KeyboardEvent) => { + const formatKeys: {[key: string]: string | (() => void)} = { + 'B': 'Bold', + 'I': 'Italic', + 'U': 'Underline', + 'S': 'Strikethrough', + 'M': () => document.execCommand('fontName', false, 'monospace') }; - this.replyElements.cancelBtn.addEventListener(isTouchSupported ? 'touchend' : 'click', onCancelHelper); + for(const key in formatKeys) { + const good = e.code == ('Key' + key); + if(good) { + const getSelectedNodes = () => { + const nodes: Node[] = []; + const selection = window.getSelection(); + for(let i = 0; i < selection.rangeCount; ++i) { + const range = selection.getRangeAt(i); + let {startContainer, endContainer} = range; + if(endContainer.nodeType != 3) endContainer = endContainer.firstChild; + + while(startContainer && startContainer != endContainer) { + nodes.push(startContainer.nodeType == 3 ? startContainer : startContainer.firstChild); + startContainer = startContainer.nextSibling; + } + + if(nodes[nodes.length - 1] != endContainer) { + nodes.push(endContainer); + } + } + + // * filter null's due to
+ return nodes.filter(node => !!node); + }; + + const saveExecuted = this.prepareDocumentExecute(); + const executed: any[] = []; + /** + * * clear previous formatting, due to Telegram's inability to handle several entities + */ + const checkForSingle = () => { + const nodes = getSelectedNodes(); + console.log('Using formatting:', formatKeys[key], nodes, this.executedHistory); + + const parents = [...new Set(nodes.map(node => node.parentNode))]; + //const differentParents = !!nodes.find(node => node.parentNode != firstParent); + const differentParents = parents.length > 1; + + let notSingle = false; + if(differentParents) { + notSingle = true; + } else { + const node = nodes[0]; + if(node && (node.parentNode as HTMLElement) != this.messageInput && (node.parentNode.parentNode as HTMLElement) != this.messageInput) { + notSingle = true; + } + } + + if(notSingle) { + if(key == 'M') { + executed.push(document.execCommand('styleWithCSS', false, 'true')); + } + + executed.push(document.execCommand('unlink', false, null)); + executed.push(document.execCommand('removeFormat', false, null)); + // @ts-ignore + executed.push(typeof(formatKeys[key]) === 'function' ? formatKeys[key]() : document.execCommand(formatKeys[key], false, null)); + + if(key == 'M') { + executed.push(document.execCommand('styleWithCSS', false, 'false')); + } + } + }; + + if(key == 'M') { + let haveMonospace = false; + executed.push(document.execCommand('styleWithCSS', false, 'true')); + + const selection = window.getSelection(); + if(!selection.isCollapsed) { + const range = selection.getRangeAt(0); + // @ts-ignore + if(range.commonAncestorContainer.parentNode.tagName == 'SPAN' || range.commonAncestorContainer.tagName == 'SPAN') { + haveMonospace = true; + } + } + + executed.push(document.execCommand('removeFormat', false, null)); + + if(!haveMonospace) { + // @ts-ignore + executed.push(typeof(formatKeys[key]) === 'function' ? formatKeys[key]() : document.execCommand(formatKeys[key], false, null)); + } + + executed.push(document.execCommand('styleWithCSS', false, 'false')); + } else { + // @ts-ignore + executed.push(typeof(formatKeys[key]) === 'function' ? formatKeys[key]() : document.execCommand(formatKeys[key], false, null)); + } + + checkForSingle(); + saveExecuted(); + cancelEvent(e); // cancel legacy event + break; + } + } - let d = false; - this.replyElements.container.addEventListener(isTouchSupported ? 'touchend' : 'click', (e) => { - cancelEvent(e); + //return; + if(e.code == 'KeyZ') { + const html = this.messageInput.innerHTML; + + if(e.shiftKey) { + if(this.undoHistory.length) { + this.executedHistory.push(this.messageInput.innerHTML); + const html = this.undoHistory.pop(); + this.undoRedo(e, 'redo', html); + this.canRedoFromHTML = this.undoHistory.length ? this.messageInput.innerHTML : ''; + this.canUndoFromHTML = this.messageInput.innerHTML; + } + } else { + // * подождём, когда пользователь сам восстановит поле до нужного состояния, которое стало сразу после saveExecuted + if(this.executedHistory.length && (!this.canUndoFromHTML || html == this.canUndoFromHTML)) { + this.undoHistory.push(this.messageInput.innerHTML); + const html = this.executedHistory.pop(); + this.undoRedo(e, 'undo', html); + + // * поставим новое состояние чтобы снова подождать, если пользователь изменит что-то, и потом попробует откатить до предыдущего состояния + this.canUndoFromHTML = this.canRedoFromHTML = this.messageInput.innerHTML; + } + } + } + }; + + private onMessageInput = (/* e: Event */) => { + //console.log('messageInput input', this.messageInput.innerText, this.serializeNodes(Array.from(this.messageInput.childNodes))); + const value = this.messageInput.innerText; - if(!findUpClassName(e.target, 'reply-wrapper')) return; - if(this.helperType == 'forward') { - if(d) return; - d = true; + const entities = RichTextProcessor.parseEntities(value); + //console.log('messageInput entities', entities); + + const html = this.messageInput.innerHTML; + if(this.canRedoFromHTML && html != this.canRedoFromHTML && !this.lockRedo) { + this.canRedoFromHTML = ''; + this.undoHistory.length = 0; + } + + const urlEntities = entities.filter(e => e._ == 'messageEntityUrl'); + if(urlEntities.length) { + const richEntities: MessageEntity[] = []; + const richValue = RichTextProcessor.parseMarkdown(getRichValue(this.messageInput), richEntities); + //console.log('messageInput url', entities, richEntities); + for(const entity of urlEntities) { + const url = value.slice(entity.offset, entity.offset + entity.length); + + if(!(url.includes('http://') || url.includes('https://')) && !richEntities.find(e => e._ == 'messageEntityTextUrl')) { + continue; + } + + //console.log('messageInput url:', url); + + if(this.lastUrl != url) { + this.lastUrl = url; + this.willSendWebPage = null; + apiManager.invokeApi('messages.getWebPage', { + url: url, + hash: 0 + }).then((webpage) => { + webpage = appWebPagesManager.saveWebPage(webpage); + if(webpage._ == 'webPage') { + if(this.lastUrl != url) return; + //console.log('got webpage: ', webpage); + + this.setTopInfo('webpage', () => {}, webpage.site_name || webpage.title || 'Webpage', webpage.description || webpage.url || ''); - const mids = this.forwardingMids.slice(); - const helperFunc = this.helperFunc; + delete this.noWebPage; + this.willSendWebPage = webpage; + } + }); + } + + break; + } + } else if(this.lastUrl) { + this.lastUrl = ''; + delete this.noWebPage; + this.willSendWebPage = null; + + if(this.helperType) { + this.helperFunc(); + } else { this.clearHelper(); - let selected = false; - new PopupForward(mids, () => { - selected = true; - }, () => { - d = false; - - if(!selected) { - helperFunc(); + } + } + + if(!value.trim() && !serializeNodes(Array.from(this.messageInput.childNodes)).trim()) { + this.messageInput.innerHTML = ''; + + appMessagesManager.setTyping(rootScope.selectedPeerID, 'sendMessageCancelAction'); + } else { + const time = Date.now(); + if(time - this.lastTimeType >= 6000) { + this.lastTimeType = time; + appMessagesManager.setTyping(rootScope.selectedPeerID, 'sendMessageTypingAction'); + } + } + + this.updateSendBtn(); + }; + + private onBtnSendClick = (e: Event) => { + cancelEvent(e); + + if(!this.recorder || this.recording || !this.isInputEmpty() || this.forwardingMids.length) { + if(this.recording) { + if((Date.now() - this.recordStartTime) < RECORD_MIN_TIME) { + this.btnCancelRecord.click(); + } else { + this.recorder.stop(); + } + } else { + this.sendMessage(); + } + } else { + if(rootScope.selectedPeerID < 0 && !appChatsManager.hasRights(rootScope.selectedPeerID, 'send', 'send_media')) { + toast(POSTING_MEDIA_NOT_ALLOWED); + return; + } + + this.chatInput.classList.add('is-locked'); + this.recorder.start().then(() => { + this.recordCanceled = false; + + this.chatInput.classList.add('is-recording'); + this.recording = true; + this.updateSendBtn(); + opusDecodeController.setKeepAlive(true); + + this.recordStartTime = Date.now(); + + const sourceNode: MediaStreamAudioSourceNode = this.recorder.sourceNode; + const context = sourceNode.context; + + const analyser = context.createAnalyser(); + sourceNode.connect(analyser); + //analyser.connect(context.destination); + analyser.fftSize = 32; + + const frequencyData = new Uint8Array(analyser.frequencyBinCount); + const max = frequencyData.length * 255; + const min = 54 / 150; + let r = () => { + if(!this.recording) return; + + analyser.getByteFrequencyData(frequencyData); + + let sum = 0; + frequencyData.forEach(value => { + sum += value; + }); + + let percents = Math.min(1, (sum / max) + min); + //console.log('frequencyData', frequencyData, percents); + + this.recordRippleEl.style.transform = `scale(${percents})`; + + let diff = Date.now() - this.recordStartTime; + let ms = diff % 1000; + + let formatted = ('' + (diff / 1000)).toHHMMSS() + ',' + ('00' + Math.round(ms / 10)).slice(-2); + + this.recordTimeEl.innerText = formatted; + + window.requestAnimationFrame(r); + }; + + r(); + }).catch((e: Error) => { + switch(e.name as string) { + case 'NotAllowedError': { + toast('Please allow access to your microphone'); + break; } - }); - } else if(this.helperType == 'reply') { - appImManager.setPeer(rootScope.selectedPeerID, this.replyToMsgID); - } else if(this.helperType == 'edit') { - appImManager.setPeer(rootScope.selectedPeerID, this.editMsgID); + + case 'NotReadableError': { + toast(e.message); + break; + } + + default: + console.error('Recorder start error:', e, e.name, e.message); + toast(e.message); + break; + } + + this.chatInput.classList.remove('is-recording', 'is-locked'); + }); + } + }; + + private onHelperCancel = (e: Event) => { + cancelEvent(e); + + if(this.willSendWebPage) { + this.noWebPage = true; + this.willSendWebPage = null; + + if(this.helperType) { + //if(this.helperFunc) { + this.helperFunc(); + //} + + return; } - }); + } + + this.clearHelper(); + this.updateSendBtn(); + }; + + private onHelperClick = (e: Event) => { + cancelEvent(e); + + if(!findUpClassName(e.target, 'reply-wrapper')) return; + if(this.helperType == 'forward') { + if(this.helperWaitingForward) return; + this.helperWaitingForward = true; + + const mids = this.forwardingMids.slice(); + const helperFunc = this.helperFunc; + this.clearHelper(); + let selected = false; + new PopupForward(mids, () => { + selected = true; + }, () => { + this.helperWaitingForward = false; + + if(!selected) { + helperFunc(); + } + }); + } else if(this.helperType == 'reply') { + appImManager.setPeer(rootScope.selectedPeerID, this.replyToMsgID); + } else if(this.helperType == 'edit') { + appImManager.setPeer(rootScope.selectedPeerID, this.editMsgID); + } + }; + + public clearInput() { + this.attachMessageInputField(); + + // clear executions + this.canRedoFromHTML = ''; + this.undoHistory.length = 0; + this.executedHistory.length = 0; + this.canUndoFromHTML = ''; } public isInputEmpty() { @@ -579,7 +756,7 @@ export class ChatInput { this.lastUrl = ''; delete this.noWebPage; this.willSendWebPage = null; - this.messageInput.innerText = ''; + this.clearInput(); } if(clearReply || clearInput) { @@ -683,7 +860,7 @@ export class ChatInput { public clearHelper(type?: ChatInputHelperType) { if(this.helperType == 'edit' && type != 'edit') { - this.messageInput.innerText = ''; + this.clearInput(); } if(type) { @@ -718,7 +895,9 @@ export class ChatInput { } */ if(input !== undefined) { + this.clearInput(); this.messageInput.innerHTML = input || ''; + this.onMessageInput(); window.requestAnimationFrame(() => { placeCaretAtEnd(this.messageInput); this.inputScroll.scrollTop = this.inputScroll.scrollHeight; diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 8ca692a2..50df2bfb 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -1130,7 +1130,7 @@ export class AppImManager { //this.lazyLoadQueue.clear(); // clear input - this.chatInputC.messageInput.innerHTML = ''; + this.chatInputC.clearInput(); this.chatInputC.replyElements.cancelBtn.click(); // clear messages diff --git a/src/lib/richtextprocessor.ts b/src/lib/richtextprocessor.ts index 54f9870f..948be8b2 100644 --- a/src/lib/richtextprocessor.ts +++ b/src/lib/richtextprocessor.ts @@ -66,7 +66,7 @@ const botCommandRegExp = '\\/([a-zA-Z\\d_]{1,32})(?:@(' + usernameRegExp + '))?( const fullRegExp = new RegExp('(^| )(@)(' + usernameRegExp + ')|(' + urlRegExp + ')|(\\n)|(' + emojiRegExp + ')|(^|[\\s\\(\\]])(#[' + alphaNumericRegExp + ']{2,64})|(^|\\s)' + botCommandRegExp, 'i') const emailRegExp = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; //const markdownTestRegExp = /[`_*@~]/; -const markdownRegExp = /(^|\s|\n)(````?)([\s\S]+?)(````?)([\s\n\.,:?!;]|$)|(^|\s)(`|~~|\*\*|__)([^\n]+?)\7([\s\.,:?!;]|$)|@(\d+)\s*\((.+?)\)|(\[(.+?)\]\((.+?)\))/m; +const markdownRegExp = /(^|\s|\n)(````?)([\s\S]+?)(````?)([\s\n\.,:?!;]|$)|(^|\s|\x01)(`|~~|\*\*|__|_-_)([^\n]+?)\7([\x01\s\.,:?!;]|$)|@(\d+)\s*\((.+?)\)|(\[(.+?)\]\((.+?)\))/m; const siteHashtags: {[siteName: string]: string} = { Telegram: 'tg://search_hashtag?hashtag={1}', Twitter: 'https://twitter.com/hashtag/{1}', @@ -80,12 +80,13 @@ const siteMentions: {[siteName: string]: string} = { Instagram: 'https://instagram.com/{1}/', GitHub: 'https://github.com/{1}' }; -const markdownEntities = { +const markdownEntities: {[markdown: string]: any} = { '`': 'messageEntityCode', '``': 'messageEntityPre', '**': 'messageEntityBold', '__': 'messageEntityItalic', - '~~': 'messageEntityStrike' + '~~': 'messageEntityStrike', + '_-_': 'messageEntityUnderline' }; namespace RichTextProcessor { @@ -219,15 +220,14 @@ namespace RichTextProcessor { return noTrim ? text : text.trim(); } */ - var raw = text; - var match; - var newText: any = []; - var rawOffset = 0; - var matchIndex; + let raw = text; + let match; + let newText: any = []; + let rawOffset = 0; while(match = raw.match(markdownRegExp)) { - matchIndex = rawOffset + match.index; + const matchIndex = rawOffset + match.index; newText.push(raw.substr(0, match.index)); - var text = (match[3] || match[8] || match[11] || match[14]); + let text = (match[3] || match[8] || match[11] || match[14]); rawOffset -= text.length; text = text.replace(/^\s+|\s+$/g, ''); rawOffset += text.length; @@ -250,15 +250,21 @@ namespace RichTextProcessor { rawOffset -= match[2].length + match[4].length; } else if(match[7]) { // code|italic|bold - newText.push(match[6] + text + match[9]); + const isSOH = match[6] == '\x01'; + if(!isSOH) { + newText.push(match[6] + text + match[9]); + } else { + newText.push(text); + } + entities.push({ - // @ts-ignore _: markdownEntities[match[7]], - offset: matchIndex + match[6].length, + //offset: matchIndex + match[6].length, + offset: matchIndex + (isSOH ? 0 : match[6].length), length: text.length }); - rawOffset -= match[7].length * 2; + rawOffset -= match[7].length * 2 + (isSOH ? 2 : 0); } else if(match[11]) { // custom mention newText.push(text) entities.push({ @@ -373,6 +379,7 @@ namespace RichTextProcessor { noLinks: true, noLinebreaks: true, noCommands: true, + noEmphasis: true, fromBot: boolean, noTextFormat: true, passEntities: Partial<{ @@ -542,31 +549,36 @@ namespace RichTextProcessor { ); break; - case 'messageEntityBold': + case 'messageEntityBold': { if(options.noTextFormat) { html.push(wrapRichNestedText(entityText, entity.nested, options)); break; } + const tag = options.noEmphasis ? 'b' : 'strong'; html.push( - '', + `<${tag}>`, wrapRichNestedText(entityText, entity.nested, options), - '' + `` ); break; + } + - case 'messageEntityItalic': + case 'messageEntityItalic': { if(options.noTextFormat) { html.push(wrapRichNestedText(entityText, entity.nested, options)); break; } + const tag = options.noEmphasis ? 'i' : 'em'; html.push( - '', + `<${tag}>`, wrapRichNestedText(entityText, entity.nested, options), - '' + `` ); break; + } case 'messageEntityHighlight': html.push( @@ -714,6 +726,7 @@ namespace RichTextProcessor { return wrapRichText(text, { ...options, noLinks: true, + noEmphasis: true, passEntities: { messageEntityTextUrl: true } diff --git a/src/scss/partials/_button.scss b/src/scss/partials/_button.scss index 3e824e5f..b4e8cbfc 100644 --- a/src/scss/partials/_button.scss +++ b/src/scss/partials/_button.scss @@ -94,6 +94,12 @@ transform: scale(1); } + &:not(.active) { + &, .btn-menu-item { + pointer-events: none !important; + } + } + &.bottom-left { right: 0; top: 100%;