Markdown support by CTRL+B/I/U/S, CTRL+Z/SHIFT
Fix show webpage on edit Fix message input CTRL+Z after changing dialog
This commit is contained in:
parent
1fdd69924b
commit
b4c244a718
@ -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<ChatInputHelperType, 'webpage'>;
|
||||
private helperFunc: () => void;
|
||||
private helperWaitingForward: boolean;
|
||||
|
||||
private willAttachType: 'document' | 'media';
|
||||
|
||||
private lockRedo = false;
|
||||
private canRedoFromHTML = '';
|
||||
readonly undoHistory: string[] = [];
|
||||
readonly executedHistory: string[] = [];
|
||||
private canUndoFromHTML = '';
|
||||
|
||||
constructor() {
|
||||
const messageInputField = InputField({
|
||||
placeholder: 'Message',
|
||||
name: 'message'
|
||||
});
|
||||
|
||||
messageInputField.input.className = '';
|
||||
this.inputScroll.container.append(messageInputField.input);
|
||||
this.messageInput = messageInputField.input;
|
||||
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 += '<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;
|
||||
}
|
||||
document.addEventListener('paste', this.onDocumentPaste, true);
|
||||
|
||||
//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);
|
||||
|
||||
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'
|
||||
});
|
||||
|
||||
if(this.helperType) {
|
||||
//if(this.helperFunc) {
|
||||
this.helperFunc();
|
||||
//}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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.clearHelper();
|
||||
this.updateSendBtn();
|
||||
};
|
||||
|
||||
this.replyElements.cancelBtn.addEventListener(isTouchSupported ? 'touchend' : 'click', onCancelHelper);
|
||||
|
||||
let d = false;
|
||||
this.replyElements.container.addEventListener(isTouchSupported ? 'touchend' : 'click', (e) => {
|
||||
cancelEvent(e);
|
||||
|
||||
if(!findUpClassName(e.target, 'reply-wrapper')) return;
|
||||
if(this.helperType == 'forward') {
|
||||
if(d) return;
|
||||
d = true;
|
||||
|
||||
const mids = this.forwardingMids.slice();
|
||||
const helperFunc = this.helperFunc;
|
||||
this.clearHelper();
|
||||
let selected = false;
|
||||
new PopupForward(mids, () => {
|
||||
selected = true;
|
||||
}, () => {
|
||||
d = 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);
|
||||
|
||||
this.sendMessage();
|
||||
} else if(e.ctrlKey || e.metaKey) {
|
||||
this.handleMarkdownShortcut(e);
|
||||
}
|
||||
});
|
||||
|
||||
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')
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
//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;
|
||||
|
||||
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 || '');
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
'<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 {
|
||||
return wrapRichText(text, {
|
||||
...options,
|
||||
noLinks: true,
|
||||
noEmphasis: true,
|
||||
passEntities: {
|
||||
messageEntityTextUrl: true
|
||||
}
|
||||
|
@ -94,6 +94,12 @@
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
&, .btn-menu-item {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.bottom-left {
|
||||
right: 0;
|
||||
top: 100%;
|
||||
|
Loading…
x
Reference in New Issue
Block a user