Browse Source

Markdown support by CTRL+B/I/U/S, CTRL+Z/SHIFT

Fix show webpage on edit
Fix message input CTRL+Z after changing dialog
master
Eduard Kuzmenko 4 years ago
parent
commit
b4c244a718
  1. 771
      src/components/chat/input.ts
  2. 2
      src/lib/appManagers/appImManager.ts
  3. 53
      src/lib/richtextprocessor.ts
  4. 6
      src/scss/partials/_button.scss

771
src/components/chat/input.ts

@ -11,7 +11,7 @@ import apiManager from "../../lib/mtproto/mtprotoworker"; @@ -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 { @@ -72,27 +72,28 @@ export class ChatInput {
private helperType: Exclude<ChatInputHelperType, 'webpage'>;
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 { @@ -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 { @@ -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 += '<br>';
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 { @@ -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 { @@ -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 += '<br>';
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 <br>
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 { @@ -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 { @@ -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 { @@ -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;

2
src/lib/appManagers/appImManager.ts

@ -1130,7 +1130,7 @@ export class AppImManager { @@ -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

53
src/lib/richtextprocessor.ts

@ -66,7 +66,7 @@ const botCommandRegExp = '\\/([a-zA-Z\\d_]{1,32})(?:@(' + usernameRegExp + '))?( @@ -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} = { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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(
'<strong>',
`<${tag}>`,
wrapRichNestedText(entityText, entity.nested, options),
'</strong>'
`</${tag}>`
);
break;
}
case 'messageEntityItalic':
case 'messageEntityItalic': {
if(options.noTextFormat) {
html.push(wrapRichNestedText(entityText, entity.nested, options));
break;
}
const tag = options.noEmphasis ? 'i' : 'em';
html.push(
'<em>',
`<${tag}>`,
wrapRichNestedText(entityText, entity.nested, options),
'</em>'
`</${tag}>`
);
break;
}
case 'messageEntityHighlight':
html.push(
@ -714,6 +726,7 @@ namespace RichTextProcessor { @@ -714,6 +726,7 @@ namespace RichTextProcessor {
return wrapRichText(text, {
...options,
noLinks: true,
noEmphasis: true,
passEntities: {
messageEntityTextUrl: true
}

6
src/scss/partials/_button.scss

@ -94,6 +94,12 @@ @@ -94,6 +94,12 @@
transform: scale(1);
}
&:not(.active) {
&, .btn-menu-item {
pointer-events: none !important;
}
}
&.bottom-left {
right: 0;
top: 100%;

Loading…
Cancel
Save