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 opusDecodeController from "../../lib/opusDecodeController";
|
||||||
import { RichTextProcessor } from "../../lib/richtextprocessor";
|
import { RichTextProcessor } from "../../lib/richtextprocessor";
|
||||||
import rootScope from '../../lib/rootScope';
|
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 ButtonMenu, { ButtonMenuItemOptions } from '../buttonMenu';
|
||||||
import emoticonsDropdown from "../emoticonsDropdown";
|
import emoticonsDropdown from "../emoticonsDropdown";
|
||||||
import PopupCreatePoll from "../popupCreatePoll";
|
import PopupCreatePoll from "../popupCreatePoll";
|
||||||
@ -72,27 +72,28 @@ export class ChatInput {
|
|||||||
|
|
||||||
private helperType: Exclude<ChatInputHelperType, 'webpage'>;
|
private helperType: Exclude<ChatInputHelperType, 'webpage'>;
|
||||||
private helperFunc: () => void;
|
private helperFunc: () => void;
|
||||||
|
private helperWaitingForward: boolean;
|
||||||
|
|
||||||
|
private willAttachType: 'document' | 'media';
|
||||||
|
|
||||||
|
private lockRedo = false;
|
||||||
|
private canRedoFromHTML = '';
|
||||||
|
readonly undoHistory: string[] = [];
|
||||||
|
readonly executedHistory: string[] = [];
|
||||||
|
private canUndoFromHTML = '';
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const messageInputField = InputField({
|
this.attachMessageInputField();
|
||||||
placeholder: 'Message',
|
|
||||||
name: 'message'
|
|
||||||
});
|
|
||||||
|
|
||||||
messageInputField.input.className = '';
|
|
||||||
this.inputScroll.container.append(messageInputField.input);
|
|
||||||
this.messageInput = messageInputField.input;
|
|
||||||
|
|
||||||
this.attachMenu = document.getElementById('attach-file') as HTMLButtonElement;
|
this.attachMenu = document.getElementById('attach-file') as HTMLButtonElement;
|
||||||
|
|
||||||
let willAttachType: 'document' | 'media';
|
|
||||||
this.attachMenuButtons = [{
|
this.attachMenuButtons = [{
|
||||||
icon: 'photo',
|
icon: 'photo',
|
||||||
text: 'Photo or Video',
|
text: 'Photo or Video',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
this.fileInput.value = '';
|
this.fileInput.value = '';
|
||||||
this.fileInput.setAttribute('accept', 'image/*, video/*');
|
this.fileInput.setAttribute('accept', 'image/*, video/*');
|
||||||
willAttachType = 'media';
|
this.willAttachType = 'media';
|
||||||
this.fileInput.click();
|
this.fileInput.click();
|
||||||
},
|
},
|
||||||
verify: (peerID: number) => peerID > 0 || appChatsManager.hasRights(peerID, 'send', 'send_media')
|
verify: (peerID: number) => peerID > 0 || appChatsManager.hasRights(peerID, 'send', 'send_media')
|
||||||
@ -102,7 +103,7 @@ export class ChatInput {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
this.fileInput.value = '';
|
this.fileInput.value = '';
|
||||||
this.fileInput.removeAttribute('accept');
|
this.fileInput.removeAttribute('accept');
|
||||||
willAttachType = 'document';
|
this.willAttachType = 'document';
|
||||||
this.fileInput.click();
|
this.fileInput.click();
|
||||||
},
|
},
|
||||||
verify: (peerID: number) => peerID > 0 || appChatsManager.hasRights(peerID, 'send', 'send_media')
|
verify: (peerID: number) => peerID > 0 || appChatsManager.hasRights(peerID, 'send', 'send_media')
|
||||||
@ -164,6 +165,125 @@ export class ChatInput {
|
|||||||
this.updateSendBtn();
|
this.updateSendBtn();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.fileInput.addEventListener('change', (e) => {
|
||||||
|
let files = (e.target as HTMLInputElement & EventTarget).files;
|
||||||
|
if(!files.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
new PopupNewMedia(Array.from(files).slice(), this.willAttachType);
|
||||||
|
this.fileInput.value = '';
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
document.addEventListener('paste', this.onDocumentPaste, true);
|
||||||
|
|
||||||
|
this.btnSend.addEventListener(CLICK_EVENT_NAME, this.onBtnSendClick);
|
||||||
|
|
||||||
|
if(this.recorder) {
|
||||||
|
const onCancelRecordClick = (e: Event) => {
|
||||||
|
cancelEvent(e);
|
||||||
|
this.recordCanceled = true;
|
||||||
|
this.recorder.stop();
|
||||||
|
opusDecodeController.setKeepAlive(false);
|
||||||
|
};
|
||||||
|
this.btnCancelRecord.addEventListener(CLICK_EVENT_NAME, onCancelRecordClick);
|
||||||
|
|
||||||
|
this.recorder.onstop = () => {
|
||||||
|
this.recording = false;
|
||||||
|
this.chatInput.classList.remove('is-recording', 'is-locked');
|
||||||
|
this.updateSendBtn();
|
||||||
|
this.recordRippleEl.style.transform = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
this.recorder.ondataavailable = (typedArray: Uint8Array) => {
|
||||||
|
if(this.recordCanceled) return;
|
||||||
|
|
||||||
|
const duration = (Date.now() - this.recordStartTime) / 1000 | 0;
|
||||||
|
const dataBlob = new Blob([typedArray], {type: 'audio/ogg'});
|
||||||
|
/* const fileName = new Date().toISOString() + ".opus";
|
||||||
|
console.log('Recorder data received', typedArray, dataBlob); */
|
||||||
|
|
||||||
|
/* var url = URL.createObjectURL( dataBlob );
|
||||||
|
|
||||||
|
var audio = document.createElement('audio');
|
||||||
|
audio.controls = true;
|
||||||
|
audio.src = url;
|
||||||
|
|
||||||
|
var link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = fileName;
|
||||||
|
link.innerHTML = link.download;
|
||||||
|
|
||||||
|
var li = document.createElement('li');
|
||||||
|
li.appendChild(link);
|
||||||
|
li.appendChild(audio);
|
||||||
|
|
||||||
|
document.body.append(li);
|
||||||
|
|
||||||
|
return; */
|
||||||
|
|
||||||
|
//let perf = performance.now();
|
||||||
|
opusDecodeController.decode(typedArray, true).then(result => {
|
||||||
|
//console.log('WAVEFORM!:', /* waveform, */performance.now() - perf);
|
||||||
|
|
||||||
|
opusDecodeController.setKeepAlive(false);
|
||||||
|
|
||||||
|
let peerID = appImManager.peerID;
|
||||||
|
// тут objectURL ставится уже с audio/wav
|
||||||
|
appMessagesManager.sendFile(peerID, dataBlob, {
|
||||||
|
isVoiceMessage: true,
|
||||||
|
isMedia: true,
|
||||||
|
duration,
|
||||||
|
waveform: result.waveform,
|
||||||
|
objectURL: result.url,
|
||||||
|
replyToMsgID: this.replyToMsgID
|
||||||
|
});
|
||||||
|
|
||||||
|
this.onMessageSent(false, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* const url = URL.createObjectURL(dataBlob);
|
||||||
|
|
||||||
|
var audio = document.createElement('audio');
|
||||||
|
audio.controls = true;
|
||||||
|
audio.src = url;
|
||||||
|
|
||||||
|
var link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = fileName;
|
||||||
|
link.innerHTML = link.download;
|
||||||
|
|
||||||
|
var li = document.createElement('li');
|
||||||
|
li.appendChild(link);
|
||||||
|
li.appendChild(audio);
|
||||||
|
|
||||||
|
recordingslist.appendChild(li); */
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.replyElements.cancelBtn.addEventListener(CLICK_EVENT_NAME, this.onHelperCancel);
|
||||||
|
this.replyElements.container.addEventListener(CLICK_EVENT_NAME, this.onHelperClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private attachMessageInputListeners() {
|
||||||
this.messageInput.addEventListener('keydown', (e: KeyboardEvent) => {
|
this.messageInput.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
if(e.key == 'Enter' && !isTouchSupported) {
|
if(e.key == 'Enter' && !isTouchSupported) {
|
||||||
/* if(e.ctrlKey || e.metaKey) {
|
/* if(e.ctrlKey || e.metaKey) {
|
||||||
@ -177,6 +297,8 @@ export class ChatInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.sendMessage();
|
this.sendMessage();
|
||||||
|
} else if(e.ctrlKey || e.metaKey) {
|
||||||
|
this.handleMarkdownShortcut(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -191,13 +313,214 @@ export class ChatInput {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.messageInput.addEventListener('input', (e) => {
|
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)));
|
//console.log('messageInput input', this.messageInput.innerText, this.serializeNodes(Array.from(this.messageInput.childNodes)));
|
||||||
const value = this.messageInput.innerText;
|
const value = this.messageInput.innerText;
|
||||||
|
|
||||||
const entities = RichTextProcessor.parseEntities(value);
|
const entities = RichTextProcessor.parseEntities(value);
|
||||||
//console.log('messageInput entities', entities);
|
//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');
|
const urlEntities = entities.filter(e => e._ == 'messageEntityUrl');
|
||||||
if(urlEntities.length) {
|
if(urlEntities.length) {
|
||||||
const richEntities: MessageEntity[] = [];
|
const richEntities: MessageEntity[] = [];
|
||||||
@ -259,75 +582,9 @@ export class ChatInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.updateSendBtn();
|
this.updateSendBtn();
|
||||||
});
|
};
|
||||||
|
|
||||||
/* if(!RichTextProcessor.emojiSupported) {
|
private onBtnSendClick = (e: Event) => {
|
||||||
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);
|
|
||||||
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);
|
cancelEvent(e);
|
||||||
|
|
||||||
if(!this.recorder || this.recording || !this.isInputEmpty() || this.forwardingMids.length) {
|
if(!this.recorder || this.recording || !this.isInputEmpty() || this.forwardingMids.length) {
|
||||||
@ -417,93 +674,7 @@ export class ChatInput {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.btnSend.addEventListener('touchend', onBtnSendClick);
|
private onHelperCancel = (e: Event) => {
|
||||||
this.btnSend.addEventListener('click', onBtnSendClick);
|
|
||||||
|
|
||||||
if(this.recorder) {
|
|
||||||
const onCancelRecordClick = (e: Event) => {
|
|
||||||
cancelEvent(e);
|
|
||||||
this.recordCanceled = true;
|
|
||||||
this.recorder.stop();
|
|
||||||
opusDecodeController.setKeepAlive(false);
|
|
||||||
};
|
|
||||||
this.btnCancelRecord.addEventListener('touchend', onCancelRecordClick);
|
|
||||||
this.btnCancelRecord.addEventListener('click', onCancelRecordClick);
|
|
||||||
|
|
||||||
this.recorder.onstop = () => {
|
|
||||||
this.recording = false;
|
|
||||||
this.chatInput.classList.remove('is-recording', 'is-locked');
|
|
||||||
this.updateSendBtn();
|
|
||||||
this.recordRippleEl.style.transform = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
this.recorder.ondataavailable = (typedArray: Uint8Array) => {
|
|
||||||
if(this.recordCanceled) return;
|
|
||||||
|
|
||||||
const duration = (Date.now() - this.recordStartTime) / 1000 | 0;
|
|
||||||
const dataBlob = new Blob([typedArray], {type: 'audio/ogg'});
|
|
||||||
/* const fileName = new Date().toISOString() + ".opus";
|
|
||||||
console.log('Recorder data received', typedArray, dataBlob); */
|
|
||||||
|
|
||||||
/* var url = URL.createObjectURL( dataBlob );
|
|
||||||
|
|
||||||
var audio = document.createElement('audio');
|
|
||||||
audio.controls = true;
|
|
||||||
audio.src = url;
|
|
||||||
|
|
||||||
var link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = fileName;
|
|
||||||
link.innerHTML = link.download;
|
|
||||||
|
|
||||||
var li = document.createElement('li');
|
|
||||||
li.appendChild(link);
|
|
||||||
li.appendChild(audio);
|
|
||||||
|
|
||||||
document.body.append(li);
|
|
||||||
|
|
||||||
return; */
|
|
||||||
|
|
||||||
//let perf = performance.now();
|
|
||||||
opusDecodeController.decode(typedArray, true).then(result => {
|
|
||||||
//console.log('WAVEFORM!:', /* waveform, */performance.now() - perf);
|
|
||||||
|
|
||||||
opusDecodeController.setKeepAlive(false);
|
|
||||||
|
|
||||||
let peerID = appImManager.peerID;
|
|
||||||
// тут objectURL ставится уже с audio/wav
|
|
||||||
appMessagesManager.sendFile(peerID, dataBlob, {
|
|
||||||
isVoiceMessage: true,
|
|
||||||
isMedia: true,
|
|
||||||
duration,
|
|
||||||
waveform: result.waveform,
|
|
||||||
objectURL: result.url,
|
|
||||||
replyToMsgID: this.replyToMsgID
|
|
||||||
});
|
|
||||||
|
|
||||||
this.onMessageSent(false, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* const url = URL.createObjectURL(dataBlob);
|
|
||||||
|
|
||||||
var audio = document.createElement('audio');
|
|
||||||
audio.controls = true;
|
|
||||||
audio.src = url;
|
|
||||||
|
|
||||||
var link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = fileName;
|
|
||||||
link.innerHTML = link.download;
|
|
||||||
|
|
||||||
var li = document.createElement('li');
|
|
||||||
li.appendChild(link);
|
|
||||||
li.appendChild(audio);
|
|
||||||
|
|
||||||
recordingslist.appendChild(li); */
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCancelHelper = (e: Event) => {
|
|
||||||
cancelEvent(e);
|
cancelEvent(e);
|
||||||
|
|
||||||
if(this.willSendWebPage) {
|
if(this.willSendWebPage) {
|
||||||
@ -523,16 +694,13 @@ export class ChatInput {
|
|||||||
this.updateSendBtn();
|
this.updateSendBtn();
|
||||||
};
|
};
|
||||||
|
|
||||||
this.replyElements.cancelBtn.addEventListener(isTouchSupported ? 'touchend' : 'click', onCancelHelper);
|
private onHelperClick = (e: Event) => {
|
||||||
|
|
||||||
let d = false;
|
|
||||||
this.replyElements.container.addEventListener(isTouchSupported ? 'touchend' : 'click', (e) => {
|
|
||||||
cancelEvent(e);
|
cancelEvent(e);
|
||||||
|
|
||||||
if(!findUpClassName(e.target, 'reply-wrapper')) return;
|
if(!findUpClassName(e.target, 'reply-wrapper')) return;
|
||||||
if(this.helperType == 'forward') {
|
if(this.helperType == 'forward') {
|
||||||
if(d) return;
|
if(this.helperWaitingForward) return;
|
||||||
d = true;
|
this.helperWaitingForward = true;
|
||||||
|
|
||||||
const mids = this.forwardingMids.slice();
|
const mids = this.forwardingMids.slice();
|
||||||
const helperFunc = this.helperFunc;
|
const helperFunc = this.helperFunc;
|
||||||
@ -541,7 +709,7 @@ export class ChatInput {
|
|||||||
new PopupForward(mids, () => {
|
new PopupForward(mids, () => {
|
||||||
selected = true;
|
selected = true;
|
||||||
}, () => {
|
}, () => {
|
||||||
d = false;
|
this.helperWaitingForward = false;
|
||||||
|
|
||||||
if(!selected) {
|
if(!selected) {
|
||||||
helperFunc();
|
helperFunc();
|
||||||
@ -552,7 +720,16 @@ export class ChatInput {
|
|||||||
} else if(this.helperType == 'edit') {
|
} else if(this.helperType == 'edit') {
|
||||||
appImManager.setPeer(rootScope.selectedPeerID, this.editMsgID);
|
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() {
|
public isInputEmpty() {
|
||||||
@ -579,7 +756,7 @@ export class ChatInput {
|
|||||||
this.lastUrl = '';
|
this.lastUrl = '';
|
||||||
delete this.noWebPage;
|
delete this.noWebPage;
|
||||||
this.willSendWebPage = null;
|
this.willSendWebPage = null;
|
||||||
this.messageInput.innerText = '';
|
this.clearInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(clearReply || clearInput) {
|
if(clearReply || clearInput) {
|
||||||
@ -683,7 +860,7 @@ export class ChatInput {
|
|||||||
|
|
||||||
public clearHelper(type?: ChatInputHelperType) {
|
public clearHelper(type?: ChatInputHelperType) {
|
||||||
if(this.helperType == 'edit' && type != 'edit') {
|
if(this.helperType == 'edit' && type != 'edit') {
|
||||||
this.messageInput.innerText = '';
|
this.clearInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(type) {
|
if(type) {
|
||||||
@ -718,7 +895,9 @@ export class ChatInput {
|
|||||||
} */
|
} */
|
||||||
|
|
||||||
if(input !== undefined) {
|
if(input !== undefined) {
|
||||||
|
this.clearInput();
|
||||||
this.messageInput.innerHTML = input || '';
|
this.messageInput.innerHTML = input || '';
|
||||||
|
this.onMessageInput();
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
placeCaretAtEnd(this.messageInput);
|
placeCaretAtEnd(this.messageInput);
|
||||||
this.inputScroll.scrollTop = this.inputScroll.scrollHeight;
|
this.inputScroll.scrollTop = this.inputScroll.scrollHeight;
|
||||||
|
@ -1130,7 +1130,7 @@ export class AppImManager {
|
|||||||
//this.lazyLoadQueue.clear();
|
//this.lazyLoadQueue.clear();
|
||||||
|
|
||||||
// clear input
|
// clear input
|
||||||
this.chatInputC.messageInput.innerHTML = '';
|
this.chatInputC.clearInput();
|
||||||
this.chatInputC.replyElements.cancelBtn.click();
|
this.chatInputC.replyElements.cancelBtn.click();
|
||||||
|
|
||||||
// clear messages
|
// 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 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 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 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} = {
|
const siteHashtags: {[siteName: string]: string} = {
|
||||||
Telegram: 'tg://search_hashtag?hashtag={1}',
|
Telegram: 'tg://search_hashtag?hashtag={1}',
|
||||||
Twitter: 'https://twitter.com/hashtag/{1}',
|
Twitter: 'https://twitter.com/hashtag/{1}',
|
||||||
@ -80,12 +80,13 @@ const siteMentions: {[siteName: string]: string} = {
|
|||||||
Instagram: 'https://instagram.com/{1}/',
|
Instagram: 'https://instagram.com/{1}/',
|
||||||
GitHub: 'https://github.com/{1}'
|
GitHub: 'https://github.com/{1}'
|
||||||
};
|
};
|
||||||
const markdownEntities = {
|
const markdownEntities: {[markdown: string]: any} = {
|
||||||
'`': 'messageEntityCode',
|
'`': 'messageEntityCode',
|
||||||
'``': 'messageEntityPre',
|
'``': 'messageEntityPre',
|
||||||
'**': 'messageEntityBold',
|
'**': 'messageEntityBold',
|
||||||
'__': 'messageEntityItalic',
|
'__': 'messageEntityItalic',
|
||||||
'~~': 'messageEntityStrike'
|
'~~': 'messageEntityStrike',
|
||||||
|
'_-_': 'messageEntityUnderline'
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace RichTextProcessor {
|
namespace RichTextProcessor {
|
||||||
@ -219,15 +220,14 @@ namespace RichTextProcessor {
|
|||||||
return noTrim ? text : text.trim();
|
return noTrim ? text : text.trim();
|
||||||
} */
|
} */
|
||||||
|
|
||||||
var raw = text;
|
let raw = text;
|
||||||
var match;
|
let match;
|
||||||
var newText: any = [];
|
let newText: any = [];
|
||||||
var rawOffset = 0;
|
let rawOffset = 0;
|
||||||
var matchIndex;
|
|
||||||
while(match = raw.match(markdownRegExp)) {
|
while(match = raw.match(markdownRegExp)) {
|
||||||
matchIndex = rawOffset + match.index;
|
const matchIndex = rawOffset + match.index;
|
||||||
newText.push(raw.substr(0, 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;
|
rawOffset -= text.length;
|
||||||
text = text.replace(/^\s+|\s+$/g, '');
|
text = text.replace(/^\s+|\s+$/g, '');
|
||||||
rawOffset += text.length;
|
rawOffset += text.length;
|
||||||
@ -250,15 +250,21 @@ namespace RichTextProcessor {
|
|||||||
|
|
||||||
rawOffset -= match[2].length + match[4].length;
|
rawOffset -= match[2].length + match[4].length;
|
||||||
} else if(match[7]) { // code|italic|bold
|
} else if(match[7]) { // code|italic|bold
|
||||||
|
const isSOH = match[6] == '\x01';
|
||||||
|
if(!isSOH) {
|
||||||
newText.push(match[6] + text + match[9]);
|
newText.push(match[6] + text + match[9]);
|
||||||
|
} else {
|
||||||
|
newText.push(text);
|
||||||
|
}
|
||||||
|
|
||||||
entities.push({
|
entities.push({
|
||||||
// @ts-ignore
|
|
||||||
_: markdownEntities[match[7]],
|
_: markdownEntities[match[7]],
|
||||||
offset: matchIndex + match[6].length,
|
//offset: matchIndex + match[6].length,
|
||||||
|
offset: matchIndex + (isSOH ? 0 : match[6].length),
|
||||||
length: text.length
|
length: text.length
|
||||||
});
|
});
|
||||||
|
|
||||||
rawOffset -= match[7].length * 2;
|
rawOffset -= match[7].length * 2 + (isSOH ? 2 : 0);
|
||||||
} else if(match[11]) { // custom mention
|
} else if(match[11]) { // custom mention
|
||||||
newText.push(text)
|
newText.push(text)
|
||||||
entities.push({
|
entities.push({
|
||||||
@ -373,6 +379,7 @@ namespace RichTextProcessor {
|
|||||||
noLinks: true,
|
noLinks: true,
|
||||||
noLinebreaks: true,
|
noLinebreaks: true,
|
||||||
noCommands: true,
|
noCommands: true,
|
||||||
|
noEmphasis: true,
|
||||||
fromBot: boolean,
|
fromBot: boolean,
|
||||||
noTextFormat: true,
|
noTextFormat: true,
|
||||||
passEntities: Partial<{
|
passEntities: Partial<{
|
||||||
@ -542,31 +549,36 @@ namespace RichTextProcessor {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'messageEntityBold':
|
case 'messageEntityBold': {
|
||||||
if(options.noTextFormat) {
|
if(options.noTextFormat) {
|
||||||
html.push(wrapRichNestedText(entityText, entity.nested, options));
|
html.push(wrapRichNestedText(entityText, entity.nested, options));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tag = options.noEmphasis ? 'b' : 'strong';
|
||||||
html.push(
|
html.push(
|
||||||
'<strong>',
|
`<${tag}>`,
|
||||||
wrapRichNestedText(entityText, entity.nested, options),
|
wrapRichNestedText(entityText, entity.nested, options),
|
||||||
'</strong>'
|
`</${tag}>`
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'messageEntityItalic':
|
|
||||||
|
case 'messageEntityItalic': {
|
||||||
if(options.noTextFormat) {
|
if(options.noTextFormat) {
|
||||||
html.push(wrapRichNestedText(entityText, entity.nested, options));
|
html.push(wrapRichNestedText(entityText, entity.nested, options));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tag = options.noEmphasis ? 'i' : 'em';
|
||||||
html.push(
|
html.push(
|
||||||
'<em>',
|
`<${tag}>`,
|
||||||
wrapRichNestedText(entityText, entity.nested, options),
|
wrapRichNestedText(entityText, entity.nested, options),
|
||||||
'</em>'
|
`</${tag}>`
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'messageEntityHighlight':
|
case 'messageEntityHighlight':
|
||||||
html.push(
|
html.push(
|
||||||
@ -714,6 +726,7 @@ namespace RichTextProcessor {
|
|||||||
return wrapRichText(text, {
|
return wrapRichText(text, {
|
||||||
...options,
|
...options,
|
||||||
noLinks: true,
|
noLinks: true,
|
||||||
|
noEmphasis: true,
|
||||||
passEntities: {
|
passEntities: {
|
||||||
messageEntityTextUrl: true
|
messageEntityTextUrl: true
|
||||||
}
|
}
|
||||||
|
@ -94,6 +94,12 @@
|
|||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:not(.active) {
|
||||||
|
&, .btn-menu-item {
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.bottom-left {
|
&.bottom-left {
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user