Markup tooltip changes:

Support multiple format types
Fix link editor on small devices
This commit is contained in:
Eduard Kuzmenko 2020-12-24 00:03:34 +02:00
parent c5570cd7af
commit bf58a3d147
12 changed files with 227 additions and 603 deletions

View File

@ -11,7 +11,7 @@ import apiManager from "../../lib/mtproto/mtprotoworker";
//import Recorder from '../opus-recorder/dist/recorder.min'; //import Recorder from '../opus-recorder/dist/recorder.min';
import opusDecodeController from "../../lib/opusDecodeController"; import opusDecodeController from "../../lib/opusDecodeController";
import RichTextProcessor from "../../lib/richtextprocessor"; import RichTextProcessor from "../../lib/richtextprocessor";
import { attachClickEvent, blurActiveElement, cancelEvent, cancelSelection, findUpClassName, getSelectedNodes, isInputEmpty, markdownTags, MarkdownType, placeCaretAtEnd, serializeNodes } from "../../helpers/dom"; import { attachClickEvent, blurActiveElement, cancelEvent, cancelSelection, findUpClassName, getRichValue, getSelectedNodes, isInputEmpty, markdownTags, MarkdownType, placeCaretAtEnd, serializeNodes } from "../../helpers/dom";
import { ButtonMenuItemOptions } from '../buttonMenu'; import { ButtonMenuItemOptions } from '../buttonMenu';
import emoticonsDropdown from "../emoticonsDropdown"; import emoticonsDropdown from "../emoticonsDropdown";
import PopupCreatePoll from "../popups/createPoll"; import PopupCreatePoll from "../popups/createPoll";
@ -585,7 +585,7 @@ export default class ChatInput {
}); });
} }
this.listenerSetter.add(this.messageInput, 'beforeinput', (e: Event) => { /* this.listenerSetter.add(this.messageInput, 'beforeinput', (e: Event) => {
// * validate due to manual formatting through browser's context menu // * validate due to manual formatting through browser's context menu
const inputType = (e as InputEvent).inputType; const inputType = (e as InputEvent).inputType;
//console.log('message beforeinput event', e); //console.log('message beforeinput event', e);
@ -597,7 +597,7 @@ export default class ChatInput {
cancelEvent(e); // * cancel legacy markdown event cancelEvent(e); // * cancel legacy markdown event
} }
} }
}); }); */
this.listenerSetter.add(this.messageInput, 'input', this.onMessageInput); this.listenerSetter.add(this.messageInput, 'input', this.onMessageInput);
} }
@ -655,7 +655,7 @@ export default class ChatInput {
/** /**
* * clear previous formatting, due to Telegram's inability to handle several entities * * clear previous formatting, due to Telegram's inability to handle several entities
*/ */
const checkForSingle = () => { /* const checkForSingle = () => {
const nodes = getSelectedNodes(); const nodes = getSelectedNodes();
//console.log('Using formatting:', commandsMap[type], nodes, this.executedHistory); //console.log('Using formatting:', commandsMap[type], nodes, this.executedHistory);
@ -686,11 +686,13 @@ export default class ChatInput {
executed.push(document.execCommand('styleWithCSS', false, 'false')); executed.push(document.execCommand('styleWithCSS', false, 'false'));
//} //}
} }
}; }; */
//if(type === 'monospace') { executed.push(document.execCommand('styleWithCSS', false, 'true'));
if(type === 'monospace') {
let haveThisType = false; let haveThisType = false;
executed.push(document.execCommand('styleWithCSS', false, 'true')); //executed.push(document.execCommand('styleWithCSS', false, 'true'));
const selection = window.getSelection(); const selection = window.getSelection();
if(!selection.isCollapsed) { if(!selection.isCollapsed) {
@ -703,18 +705,20 @@ export default class ChatInput {
} }
} }
executed.push(document.execCommand('removeFormat', false, null)); //executed.push(document.execCommand('removeFormat', false, null));
if(!haveThisType) { if(haveThisType) {
executed.push(document.execCommand('fontName', false, 'Roboto'));
} else {
executed.push(typeof(command) === 'function' ? command() : document.execCommand(command, false, null)); executed.push(typeof(command) === 'function' ? command() : document.execCommand(command, false, null));
} }
} else {
executed.push(document.execCommand('styleWithCSS', false, 'false'));
/* } else {
executed.push(typeof(command) === 'function' ? command() : document.execCommand(command, false, null)); executed.push(typeof(command) === 'function' ? command() : document.execCommand(command, false, null));
} */ }
checkForSingle(); executed.push(document.execCommand('styleWithCSS', false, 'false'));
//checkForSingle();
saveExecuted(); saveExecuted();
if(this.appImManager.markupTooltip) { if(this.appImManager.markupTooltip) {
this.appImManager.markupTooltip.setActiveMarkupButton(); this.appImManager.markupTooltip.setActiveMarkupButton();
@ -794,10 +798,10 @@ export default class ChatInput {
//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 richValue = this.messageInputField.value; const markdownEntities: MessageEntity[] = [];
const richValue = getRichValue(this.messageInputField.input, markdownEntities);
//const entities = RichTextProcessor.parseEntities(value); //const entities = RichTextProcessor.parseEntities(value);
const markdownEntities: MessageEntity[] = [];
const value = RichTextProcessor.parseMarkdown(richValue, markdownEntities); const value = RichTextProcessor.parseMarkdown(richValue, markdownEntities);
const entities = RichTextProcessor.mergeEntities(markdownEntities, RichTextProcessor.parseEntities(value)); const entities = RichTextProcessor.mergeEntities(markdownEntities, RichTextProcessor.parseEntities(value));
@ -815,6 +819,10 @@ export default class ChatInput {
this.stickersHelper.checkEmoticon(emoticon); this.stickersHelper.checkEmoticon(emoticon);
} }
if(!richValue.trim()) {
this.appImManager.markupTooltip.hide();
}
const html = this.messageInput.innerHTML; const html = this.messageInput.innerHTML;
if(this.canRedoFromHTML && html != this.canRedoFromHTML && !this.lockRedo) { if(this.canRedoFromHTML && html != this.canRedoFromHTML && !this.lockRedo) {
this.canRedoFromHTML = ''; this.canRedoFromHTML = '';
@ -1095,19 +1103,18 @@ export default class ChatInput {
return; return;
} }
//let str = this.serializeNodes(Array.from(this.messageInput.childNodes)); const entities: MessageEntity[] = [];
let str = this.messageInputField.value; const str = getRichValue(this.messageInputField.input, entities);
//console.log('childnode str after:', str/* , getRichValue(this.messageInput) */);
//return; //return;
if(this.editMsgId) { if(this.editMsgId) {
this.appMessagesManager.editMessage(this.chat.getMessage(this.editMsgId), str, { this.appMessagesManager.editMessage(this.chat.getMessage(this.editMsgId), str, {
entities,
noWebPage: this.noWebPage noWebPage: this.noWebPage
}); });
} else { } else {
this.appMessagesManager.sendText(this.chat.peerId, str, { this.appMessagesManager.sendText(this.chat.peerId, str, {
entities,
replyToMsgId: this.replyToMsgId, replyToMsgId: this.replyToMsgId,
threadId: this.chat.threadId, threadId: this.chat.threadId,
noWebPage: this.noWebPage, noWebPage: this.noWebPage,

View File

@ -5,6 +5,7 @@ import ButtonIcon from "../buttonIcon";
import { clamp } from "../../helpers/number"; import { clamp } from "../../helpers/number";
import { isTouchSupported } from "../../helpers/touchSupport"; import { isTouchSupported } from "../../helpers/touchSupport";
import { isApple } from "../../helpers/userAgent"; import { isApple } from "../../helpers/userAgent";
//import { logger } from "../../lib/logger";
export default class MarkupTooltip { export default class MarkupTooltip {
public container: HTMLElement; public container: HTMLElement;
@ -17,10 +18,11 @@ export default class MarkupTooltip {
private waitingForMouseUp = false; private waitingForMouseUp = false;
private linkInput: HTMLInputElement; private linkInput: HTMLInputElement;
private savedRange: Range; private savedRange: Range;
mouseUpCounter: number = 0; private mouseUpCounter: number = 0;
//private log: ReturnType<typeof logger>;
constructor(private appImManager: AppImManager) { constructor(private appImManager: AppImManager) {
//this.log = logger('MARKUP');
} }
private init() { private init() {
@ -41,14 +43,20 @@ export default class MarkupTooltip {
tools1.append(this.buttons[c] = button); tools1.append(this.buttons[c] = button);
if(c !== 'link') { if(c !== 'link') {
button.addEventListener('click', () => { button.addEventListener('mousedown', (e) => {
cancelEvent(e);
this.appImManager.chat.input.applyMarkdown(c); this.appImManager.chat.input.applyMarkdown(c);
this.hide(); this.cancelClosening();
/* this.mouseUpCounter = 0;
this.setMouseUpEvent(); */
//this.hide();
}); });
} else { } else {
attachClickEvent(button, (e) => { attachClickEvent(button, (e) => {
cancelEvent(e); cancelEvent(e);
this.showLinkEditor(); this.showLinkEditor();
this.cancelClosening();
}); });
} }
}); });
@ -81,17 +89,19 @@ export default class MarkupTooltip {
this.linkInput.classList.remove('error'); this.linkInput.classList.remove('error');
}); });
attachClickEvent(this.linkBackButton, (e) => { this.linkBackButton.addEventListener('mousedown', (e) => {
//this.log('linkBackButton click');
cancelEvent(e); cancelEvent(e);
this.container.classList.remove('is-link'); this.container.classList.remove('is-link');
//input.value = ''; //input.value = '';
this.resetSelection(); this.resetSelection();
this.setTooltipPosition(); this.setTooltipPosition();
this.cancelClosening();
}); });
this.linkApplyButton = ButtonIcon('check markup-tooltip-link-apply', {noRipple: true}); this.linkApplyButton = ButtonIcon('check markup-tooltip-link-apply', {noRipple: true});
attachClickEvent(this.linkApplyButton, (e) => { this.linkApplyButton.addEventListener('mousedown', (e) => {
cancelEvent(e); //this.log('linkApplyButton click');
this.applyLink(e); this.applyLink(e);
}); });
@ -145,7 +155,9 @@ export default class MarkupTooltip {
cancelEvent(e); cancelEvent(e);
this.resetSelection(); this.resetSelection();
this.appImManager.chat.input.applyMarkdown('link', this.linkInput.value); this.appImManager.chat.input.applyMarkdown('link', this.linkInput.value);
this.hide(); setTimeout(() => {
this.hide();
}, 0);
} }
private isLinkValid() { private isLinkValid() {
@ -160,10 +172,12 @@ export default class MarkupTooltip {
} }
public hide() { public hide() {
//return;
if(this.init) return; if(this.init) return;
this.container.classList.remove('is-visible'); this.container.classList.remove('is-visible');
document.removeEventListener('mouseup', this.onMouseUp); //document.removeEventListener('mouseup', this.onMouseUp);
document.removeEventListener('mouseup', this.onMouseUpSingle); document.removeEventListener('mouseup', this.onMouseUpSingle);
this.waitingForMouseUp = false; this.waitingForMouseUp = false;
@ -178,37 +192,31 @@ export default class MarkupTooltip {
public getActiveMarkupButton() { public getActiveMarkupButton() {
const nodes = getSelectedNodes(); const nodes = getSelectedNodes();
const parents = [...new Set(nodes.map(node => node.parentNode))]; const parents = [...new Set(nodes.map(node => node.parentNode))];
if(parents.length > 1) return undefined; //if(parents.length > 1 && parents) return [];
const node = parents[0] as HTMLElement; const currentMarkups: Set<HTMLElement> = new Set();
let currentMarkup: HTMLElement; (parents as HTMLElement[]).forEach(node => {
for(const type in markdownTags) { for(const type in markdownTags) {
const tag = markdownTags[type as MarkdownType]; const tag = markdownTags[type as MarkdownType];
if(node.matches(tag.match)) { const closest = node.closest(tag.match + ', [contenteditable]');
currentMarkup = this.buttons[type as MarkdownType]; if(closest !== this.appImManager.chat.input.messageInput) {
break; currentMarkups.add(this.buttons[type as MarkdownType]);
}
} }
} });
return currentMarkup;
return [...currentMarkups];
} }
public setActiveMarkupButton() { public setActiveMarkupButton() {
const activeButton = this.getActiveMarkupButton(); const activeButtons = this.getActiveMarkupButton();
for(const i in this.buttons) { for(const i in this.buttons) {
// @ts-ignore // @ts-ignore
const button = this.buttons[i]; const button = this.buttons[i];
if(button != activeButton) { button.classList.toggle('active', activeButtons.includes(button));
button.classList.remove('active');
}
} }
if(activeButton) {
activeButton.classList.add('active');
}
return activeButton;
} }
private setTooltipPosition(isLinkToggle = false) { private setTooltipPosition(isLinkToggle = false) {
@ -253,7 +261,6 @@ export default class MarkupTooltip {
} }
const selection = document.getSelection(); const selection = document.getSelection();
if(!selection.toString().trim().length) { if(!selection.toString().trim().length) {
this.hide(); this.hide();
return; return;
@ -285,21 +292,19 @@ export default class MarkupTooltip {
this.container.classList.add('is-visible'); this.container.classList.add('is-visible');
//console.log('selection', selectionRect, activeButton); //this.log('selection', selectionRect, activeButton);
} }
private onMouseUp = (e: Event) => { /* private onMouseUp = (e: Event) => {
this.log('onMouseUp');
if(findUpClassName(e.target, 'markup-tooltip')) return; if(findUpClassName(e.target, 'markup-tooltip')) return;
/* if(isTouchSupported) {
this.appImManager.chat.input.messageInput.focus();
cancelEvent(e);
} */
this.hide(); this.hide();
document.removeEventListener('mouseup', this.onMouseUp); //document.removeEventListener('mouseup', this.onMouseUp);
}; }; */
private onMouseUpSingle = (e: Event) => { private onMouseUpSingle = (e: Event) => {
//this.log('onMouseUpSingle');
this.waitingForMouseUp = false; this.waitingForMouseUp = false;
if(isTouchSupported) { if(isTouchSupported) {
@ -314,39 +319,51 @@ export default class MarkupTooltip {
this.show(); this.show();
!isTouchSupported && document.addEventListener('mouseup', this.onMouseUp); //!isTouchSupported && document.addEventListener('mouseup', this.onMouseUp);
}; };
public setMouseUpEvent() { public setMouseUpEvent() {
if(this.waitingForMouseUp) return; if(this.waitingForMouseUp) return;
this.waitingForMouseUp = true; this.waitingForMouseUp = true;
console.log('[MARKUP]: setMouseUpEvent'); //this.log('setMouseUpEvent');
document.addEventListener('mouseup', this.onMouseUpSingle, {once: true}); document.addEventListener('mouseup', this.onMouseUpSingle, {once: true});
} }
public cancelClosening() {
if(isTouchSupported && !isApple) {
document.removeEventListener('mouseup', this.onMouseUpSingle);
document.addEventListener('mouseup', (e) => {
cancelEvent(e);
this.mouseUpCounter = 1;
this.waitingForMouseUp = false;
this.setMouseUpEvent();
}, {once: true});
}
}
public handleSelection() { public handleSelection() {
if(this.addedListener) return; if(this.addedListener) return;
this.addedListener = true; this.addedListener = true;
document.addEventListener('selectionchange', (e) => { document.addEventListener('selectionchange', (e) => {
if(document.activeElement == this.linkInput) { //this.log('selectionchange');
if(document.activeElement === this.linkInput) {
return; return;
} }
if(document.activeElement != this.appImManager.chat.input.messageInput) { if(document.activeElement !== this.appImManager.chat.input.messageInput) {
this.hide(); this.hide();
return; return;
} }
const selection = document.getSelection(); const selection = document.getSelection();
if(!selection.toString().trim().length) { if(!selection.toString().trim().length) {
this.hide(); this.hide();
return; return;
} }
console.log('[MARKUP]: selectionchange');
if(isTouchSupported) { if(isTouchSupported) {
if(isApple) { if(isApple) {
this.show(); this.show();

View File

@ -8,6 +8,7 @@ import RadioField from "../radioField";
import Scrollable from "../scrollable"; import Scrollable from "../scrollable";
import { toast } from "../toast"; import { toast } from "../toast";
import SendContextMenu from "../chat/sendContextMenu"; import SendContextMenu from "../chat/sendContextMenu";
import { MessageEntity } from "../../layer";
const MAX_LENGTH_QUESTION = 255; const MAX_LENGTH_QUESTION = 255;
const MAX_LENGTH_OPTION = 100; const MAX_LENGTH_OPTION = 100;
@ -187,7 +188,8 @@ export default class PopupCreatePoll extends PopupElement {
return; return;
} }
const quizSolution = this.quizSolutionField.value || undefined; const quizSolutionEntities: MessageEntity[] = [];
const quizSolution = getRichValue(this.quizSolutionField.input, quizSolutionEntities) || undefined;
if(quizSolution?.length > MAX_LENGTH_SOLUTION) { if(quizSolution?.length > MAX_LENGTH_SOLUTION) {
toast('Explanation is too long.'); toast('Explanation is too long.');
return; return;
@ -236,7 +238,7 @@ export default class PopupCreatePoll extends PopupElement {
}; };
//poll.id = randomIDS; //poll.id = randomIDS;
const inputMediaPoll = this.chat.appPollsManager.getInputMediaPoll(poll, this.correctAnswers, quizSolution); const inputMediaPoll = this.chat.appPollsManager.getInputMediaPoll(poll, this.correctAnswers, quizSolution, quizSolutionEntities);
//console.log('Will try to create poll:', inputMediaPoll); //console.log('Will try to create poll:', inputMediaPoll);

View File

@ -333,7 +333,9 @@ export default class PopupDatePicker extends PopupElement {
} }
} }
this.container.classList.toggle('is-max-lines', (this.month.childElementCount / 7) > 6); const lines = this.month.childElementCount / 7;
this.container.dataset.lines = '' + lines;
this.container.classList.toggle('is-max-lines', lines > 6);
this.monthsContainer.append(this.month); this.monthsContainer.append(this.month);
} }

View File

@ -1,4 +1,6 @@
import { MessageEntity } from "../layer";
import { MOUNT_CLASS_TO } from "../lib/mtproto/mtproto_config"; import { MOUNT_CLASS_TO } from "../lib/mtproto/mtproto_config";
import RichTextProcessor from "../lib/richtextprocessor";
import ListenerSetter from "./listenerSetter"; import ListenerSetter from "./listenerSetter";
import { isTouchSupported } from "./touchSupport"; import { isTouchSupported } from "./touchSupport";
import { isSafari } from "./userAgent"; import { isSafari } from "./userAgent";
@ -101,7 +103,7 @@ export function placeCaretAtEnd(el: HTMLElement) {
return len; return len;
} */ } */
export function getRichValue(field: HTMLElement) { export function getRichValue(field: HTMLElement, entities?: MessageEntity[]) {
if(!field) { if(!field) {
return ''; return '';
} }
@ -109,7 +111,7 @@ export function getRichValue(field: HTMLElement) {
const lines: string[] = []; const lines: string[] = [];
const line: string[] = []; const line: string[] = [];
getRichElementValue(field, lines, line); getRichElementValue(field, lines, line, undefined, undefined, entities);
if(line.length) { if(line.length) {
lines.push(line.join('')); lines.push(line.join(''));
} }
@ -117,6 +119,12 @@ export function getRichValue(field: HTMLElement) {
let value = lines.join('\n'); let value = lines.join('\n');
value = value.replace(/\u00A0/g, ' '); value = value.replace(/\u00A0/g, ' ');
if(entities) {
RichTextProcessor.combineSameEntities(entities);
}
console.log('getRichValue:', value, entities);
return value; return value;
} }
@ -134,64 +142,78 @@ const markdownTypes = {
export type MarkdownType = 'bold' | 'italic' | 'underline' | 'strikethrough' | 'monospace' | 'link'; export type MarkdownType = 'bold' | 'italic' | 'underline' | 'strikethrough' | 'monospace' | 'link';
export type MarkdownTag = { export type MarkdownTag = {
match: string, match: string,
markdown: string | ((node: HTMLElement) => string) markdown: string | ((node: HTMLElement) => string),
entityName: 'messageEntityBold' | 'messageEntityUnderline' | 'messageEntityItalic' | 'messageEntityPre' | 'messageEntityStrike' | 'messageEntityTextUrl';
}; };
export const markdownTags: {[type in MarkdownType]: MarkdownTag} = { export const markdownTags: {[type in MarkdownType]: MarkdownTag} = {
bold: { bold: {
match: '[style*="font-weight"]', match: '[style*="font-weight"], b',
markdown: markdownTypes.bold markdown: markdownTypes.bold,
entityName: 'messageEntityBold'
}, },
underline: { underline: {
match: isSafari ? '[style="text-decoration: underline;"]' : '[style="text-decoration-line: underline;"]', match: '[style*="underline"], u',
markdown: markdownTypes.underline markdown: markdownTypes.underline,
entityName: 'messageEntityUnderline'
}, },
italic: { italic: {
match: '[style="font-style: italic;"]', match: '[style*="italic"], i',
markdown: markdownTypes.italic markdown: markdownTypes.italic,
entityName: 'messageEntityItalic'
}, },
monospace: { monospace: {
match: '[style="font-family: monospace;"]', match: '[style*="monospace"], [face="monospace"]',
markdown: markdownTypes.monospace markdown: markdownTypes.monospace,
entityName: 'messageEntityPre'
}, },
strikethrough: { strikethrough: {
match: isSafari ? '[style="text-decoration: line-through;"]' : '[style="text-decoration-line: line-through;"]', match: '[style*="line-through"], strike',
markdown: markdownTypes.strikethrough markdown: markdownTypes.strikethrough,
entityName: 'messageEntityStrike'
}, },
link: { link: {
match: 'A', match: 'A',
markdown: (node: HTMLElement) => `[${(node.parentElement as HTMLAnchorElement).href}](${node.nodeValue})` markdown: (node: HTMLElement) => `[${(node.parentElement as HTMLAnchorElement).href}](${node.nodeValue})`,
entityName: 'messageEntityTextUrl'
} }
}; };
export function getRichElementValue(node: HTMLElement, lines: string[], line: string[], selNode?: Node, selOffset?: number) { export function getRichElementValue(node: HTMLElement, lines: string[], line: string[], selNode?: Node, selOffset?: number, entities?: MessageEntity[], offset = {offset: 0}) {
if(node.nodeType == 3) { // TEXT if(node.nodeType == 3) { // TEXT
if(selNode === node) { if(selNode === node) {
const value = node.nodeValue; const value = node.nodeValue;
line.push(value.substr(0, selOffset) + '\x01' + value.substr(selOffset)); line.push(value.substr(0, selOffset) + '\x01' + value.substr(selOffset));
} else { } else {
let markdown: string; const nodeValue = node.nodeValue;
if(node.parentNode) { line.push(nodeValue);
const parentElement = node.parentElement;
let markdownTag: MarkdownTag; if(entities && nodeValue.trim()) {
for(const type in markdownTags) { if(node.parentNode) {
const tag = markdownTags[type as MarkdownType]; const parentElement = node.parentElement;
if(parentElement.matches(tag.match)) {
markdownTag = tag; for(const type in markdownTags) {
break; const tag = markdownTags[type as MarkdownType];
const closest = parentElement.closest(tag.match + ', [contenteditable]');
if(closest && closest.getAttribute('contenteditable') === null) {
if(tag.entityName === 'messageEntityTextUrl') {
entities.push({
_: tag.entityName as any,
url: (parentElement as HTMLAnchorElement).href,
offset: offset.offset,
length: nodeValue.length
});
} else {
entities.push({
_: tag.entityName as any,
offset: offset.offset,
length: nodeValue.length
});
}
}
} }
} }
if(markdownTag) {
if(typeof(markdownTag.markdown) === 'function') {
line.push(markdownTag.markdown(node));
return;
}
markdown = markdownTag.markdown;
}
} }
line.push(markdown && node.nodeValue.trim() ? '\x01' + markdown + node.nodeValue + markdown + '\x01' : node.nodeValue); offset.offset += nodeValue.length;
} }
return; return;
@ -207,8 +229,10 @@ export function getRichElementValue(node: HTMLElement, lines: string[], line: st
lines.push(line.join('')); lines.push(line.join(''));
line.splice(0, line.length); line.splice(0, line.length);
} else if(node.tagName == 'IMG') { } else if(node.tagName == 'IMG') {
if((node as HTMLImageElement).alt) { const alt = (node as HTMLImageElement).alt;
line.push((node as HTMLImageElement).alt); if(alt) {
line.push(alt);
offset.offset += alt.length;
} }
} }
@ -218,7 +242,7 @@ export function getRichElementValue(node: HTMLElement, lines: string[], line: st
let curChild = node.firstChild as HTMLElement; let curChild = node.firstChild as HTMLElement;
while(curChild) { while(curChild) {
getRichElementValue(curChild, lines, line, selNode, selOffset); getRichElementValue(curChild, lines, line, selNode, selOffset, entities, offset);
curChild = curChild.nextSibling as any; curChild = curChild.nextSibling as any;
} }

View File

@ -457,6 +457,13 @@ export class AppImManager {
const spliced = this.chats.splice(fromIndex, this.chats.length - fromIndex); const spliced = this.chats.splice(fromIndex, this.chats.length - fromIndex);
// * fix middle chat z-index on animation
if(spliced.length > 1) {
spliced.slice(0, -1).forEach(chat => {
chat.container.remove();
});
}
this.chatsSelectTab(this.chat.container); this.chatsSelectTab(this.chat.container);
if(justReturn) { if(justReturn) {

View File

@ -397,11 +397,8 @@ export class AppMessagesManager {
}); });
} }
let entities = options.entities; let entities = options.entities || [];
if(typeof(text) === 'string' && !entities) { text = RichTextProcessor.parseMarkdown(text, entities);
entities = [];
text = RichTextProcessor.parseMarkdown(text, entities);
}
const schedule_date = options.scheduleDate || (message.pFlags.is_scheduled ? message.date : undefined); const schedule_date = options.scheduleDate || (message.pFlags.is_scheduled ? message.date : undefined);
return apiManager.invokeApi('messages.editMessage', { return apiManager.invokeApi('messages.editMessage', {
@ -494,7 +491,7 @@ export class AppMessagesManager {
reply_to: this.generateReplyHeader(options.replyToMsgId, options.threadId), reply_to: this.generateReplyHeader(options.replyToMsgId, options.threadId),
via_bot_id: options.viaBotId, via_bot_id: options.viaBotId,
reply_markup: options.reply_markup, reply_markup: options.reply_markup,
entities: entities, entities,
views: isBroadcast && 1, views: isBroadcast && 1,
pending: true pending: true
}; };
@ -641,8 +638,8 @@ export class AppMessagesManager {
this.log('sendFile', file, fileType); this.log('sendFile', file, fileType);
const entities = options.entities || [];
if(caption) { if(caption) {
let entities = options.entities || [];
caption = RichTextProcessor.parseMarkdown(caption, entities); caption = RichTextProcessor.parseMarkdown(caption, entities);
} }
@ -792,6 +789,7 @@ export class AppMessagesManager {
id: messageId, id: messageId,
from_id: this.generateFromId(peerId), from_id: this.generateFromId(peerId),
peer_id: appPeersManager.getOutputPeer(peerId), peer_id: appPeersManager.getOutputPeer(peerId),
entities,
pFlags, pFlags,
date, date,
message: caption, message: caption,
@ -913,7 +911,8 @@ export class AppMessagesManager {
random_id: randomIdS, random_id: randomIdS,
reply_to_msg_id: replyToMsgId, reply_to_msg_id: replyToMsgId,
schedule_date: options.scheduleDate, schedule_date: options.scheduleDate,
silent: options.silent silent: options.silent,
entities
}).then((updates) => { }).then((updates) => {
apiUpdatesManager.processUpdateMessage(updates); apiUpdatesManager.processUpdateMessage(updates);
}, (error) => { }, (error) => {
@ -960,9 +959,8 @@ export class AppMessagesManager {
const replyToMsgId = options.replyToMsgId ? this.getLocalMessageId(options.replyToMsgId) : undefined; const replyToMsgId = options.replyToMsgId ? this.getLocalMessageId(options.replyToMsgId) : undefined;
let caption = options.caption || ''; let caption = options.caption || '';
let entities: MessageEntity[]; let entities = options.entities || [];
if(caption) { if(caption) {
entities = options.entities || [];
caption = RichTextProcessor.parseMarkdown(caption, entities); caption = RichTextProcessor.parseMarkdown(caption, entities);
} }

View File

@ -1,5 +1,5 @@
import { copy } from "../../helpers/object"; import { copy } from "../../helpers/object";
import { InputMedia } from "../../layer"; import { InputMedia, MessageEntity } from "../../layer";
import { logger, LogLevels } from "../logger"; import { logger, LogLevels } from "../logger";
import apiManager from "../mtproto/mtprotoworker"; import apiManager from "../mtproto/mtprotoworker";
import { MOUNT_CLASS_TO } from "../mtproto/mtproto_config"; import { MOUNT_CLASS_TO } from "../mtproto/mtproto_config";
@ -143,11 +143,13 @@ export class AppPollsManager {
}; };
} }
public getInputMediaPoll(poll: Poll, correctAnswers?: Uint8Array[], solution?: string): InputMedia.inputMediaPoll { public getInputMediaPoll(poll: Poll, correctAnswers?: Uint8Array[], solution?: string, solutionEntities?: MessageEntity[]): InputMedia.inputMediaPoll {
let solution_entities: any[];
if(solution) { if(solution) {
solution_entities = []; if(!solutionEntities) {
solution = RichTextProcessor.parseMarkdown(solution, solution_entities); solutionEntities = [];
}
solution = RichTextProcessor.parseMarkdown(solution, solutionEntities);
} }
return { return {
@ -155,7 +157,7 @@ export class AppPollsManager {
poll, poll,
correct_answers: correctAnswers, correct_answers: correctAnswers,
solution, solution,
solution_entities solution_entities: solutionEntities
}; };
} }

View File

@ -215,11 +215,12 @@ namespace RichTextProcessor {
}) })
} */ } */
export function parseMarkdown(text: string, entities: MessageEntity[], noTrim?: any): string { export function parseMarkdown(text: string, currentEntities: MessageEntity[], noTrim?: any): string {
  /* if(!markdownTestRegExp.test(text)) {   /* if(!markdownTestRegExp.test(text)) {
return noTrim ? text : text.trim(); return noTrim ? text : text.trim();
} */ } */
const entities: MessageEntity[] = [];
let raw = text; let raw = text;
let match; let match;
let newText: any = []; let newText: any = [];
@ -302,68 +303,12 @@ namespace RichTextProcessor {
newText = newText.trim(); newText = newText.trim();
} }
mergeEntities(currentEntities, entities);
combineSameEntities(currentEntities);
return newText; return newText;
} }
/* export function mergeEntities(currentEntities: MessageEntity[], newEntities: MessageEntity[], fromApi?: boolean) {
const totalEntities = newEntities.slice();
const newLength = newEntities.length;
let startJ = 0;
for(let i = 0, length = currentEntities.length; i < length; i++) {
const curEntity = currentEntities[i];
// if(fromApi &&
// curEntity._ != 'messageEntityLinebreak' &&
// curEntity._ != 'messageEntityEmoji') {
// continue;
// }
// console.log('s', curEntity, newEntities);
const start = curEntity.offset;
const end = start + curEntity.length;
let bad = false;
for(let j = startJ; j < newLength; j++) {
const newEntity = newEntities[j];
const cStart = newEntity.offset;
const cEnd = cStart + newEntity.length;
if(cStart <= start) {
startJ = j;
}
if(start >= cStart && start < cEnd ||
end > cStart && end <= cEnd) {
// console.log('bad', curEntity, newEntity)
if(fromApi && start >= cStart && end <= cEnd) {
if(newEntity.nested === undefined) {
newEntity.nested = [];
}
curEntity.offset -= cStart;
newEntity.nested.push(copy(curEntity));
}
bad = true;
break;
}
if(cStart >= end) {
break;
}
}
if(bad) {
continue;
}
totalEntities.push(curEntity);
}
totalEntities.sort((a, b) => {
return a.offset - b.offset;
});
// console.log('merge', currentEntities, newEntities, totalEntities)
return totalEntities;
} */
export function mergeEntities(currentEntities: MessageEntity[], newEntities: MessageEntity[]) { export function mergeEntities(currentEntities: MessageEntity[], newEntities: MessageEntity[]) {
currentEntities = currentEntities.slice(); currentEntities = currentEntities.slice();
const filtered = newEntities.filter(e => !currentEntities.find(_e => e._ == _e._ && e.offset == _e.offset && e.length == _e.length)); const filtered = newEntities.filter(e => !currentEntities.find(_e => e._ == _e._ && e.offset == _e.offset && e.length == _e.length));
@ -372,14 +317,23 @@ namespace RichTextProcessor {
return currentEntities; return currentEntities;
} }
/* export function wrapRichNestedText(text: string, nested: MessageEntity[], options: any) { export function combineSameEntities(entities: MessageEntity[]) {
if(nested === undefined) { //entities = entities.slice();
return encodeEntities(text); for(let i = 0; i < entities.length; ++i) {
} const entity = entities[i];
options.hasNested = true; let nextEntityIdx = -1;
return wrapRichText(text, {entities: nested, nested: true}); do {
} */ nextEntityIdx = entities.findIndex((e, _i) => _i !== i && e._ === entity._ && (e.offset - entity.length) === entity.offset);
if(nextEntityIdx !== -1) {
const nextEntity = entities[nextEntityIdx];
entity.length += nextEntity.length;
entities.splice(nextEntityIdx, 1);
}
} while(nextEntityIdx !== -1);
}
//return entities;
}
export function wrapRichText(text: string, options: Partial<{ export function wrapRichText(text: string, options: Partial<{
entities: MessageEntity[], entities: MessageEntity[],
@ -620,365 +574,6 @@ namespace RichTextProcessor {
return out; return out;
} }
/* export function wrapRichTextOld(text: string, options: Partial<{
entities: MessageEntity[],
contextSite: string,
highlightUsername: string,
noLinks: true,
noLinebreaks: true,
noCommands: true,
wrappingDraft: true,
fromBot: boolean,
noTextFormat: true,
passEntities: Partial<{
[_ in MessageEntity['_']]: true
}>,
nested?: true,
contextHashtag?: string
}> = {}) {
if(!text || !text.length) {
return '';
}
const passEntities: typeof options.passEntities = options.passEntities || {};
const entities = options.entities || parseEntities(text);
const contextSite = options.contextSite || 'Telegram';
const contextExternal = contextSite != 'Telegram';
//console.log('wrapRichText got entities:', text, entities);
const html: string[] = [];
let lastOffset = 0;
for(let i = 0, len = entities.length; i < len; i++) {
const entity = entities[i];
if(entity.offset > lastOffset) {
html.push(
encodeEntities(text.substr(lastOffset, entity.offset - lastOffset))
);
} else if(entity.offset < lastOffset) {
continue;
}
let skipEntity = false;
const entityText = text.substr(entity.offset, entity.length);
switch(entity._) {
case 'messageEntityMention':
var contextUrl = !options.noLinks && siteMentions[contextSite]
if (!contextUrl) {
skipEntity = true
break
}
var username = entityText.substr(1)
var attr = ''
if (options.highlightUsername &&
options.highlightUsername.toLowerCase() == username.toLowerCase()) {
attr = 'class="im_message_mymention"'
}
html.push(
'<a ',
attr,
contextExternal ? ' target="_blank" rel="noopener noreferrer" ' : '',
' href="',
contextUrl.replace('{1}', encodeURIComponent(username)),
'">',
wrapRichNestedText(entityText, entity.nested, options),
//encodeEntities(entityText),
'</a>'
)
break;
case 'messageEntityMentionName':
if(options.noLinks) {
skipEntity = true;
break;
}
html.push(
'<a href="#/im?p=u',
encodeURIComponent(entity.user_id),
'">',
wrapRichNestedText(entityText, entity.nested, options),
'</a>'
);
break;
case 'messageEntityHashtag':
var contextUrl = !options.noLinks && siteHashtags[contextSite];
if(!contextUrl) {
skipEntity = true;
break;
}
var hashtag = entityText.substr(1);
html.push(
'<a ',
contextExternal ? ' target="_blank" rel="noopener noreferrer" ' : '',
'href="',
contextUrl.replace('{1}', encodeURIComponent(hashtag))
,
'">',
encodeEntities(entityText),
'</a>'
);
break;
case 'messageEntityEmail':
if(options.noLinks) {
skipEntity = true;
break;
}
html.push(
'<a href="',
encodeEntities('mailto:' + entityText),
'" target="_blank" rel="noopener noreferrer">',
encodeEntities(entityText),
'</a>'
);
break;
case 'messageEntityUrl':
case 'messageEntityTextUrl':
let inner: string;
let url: string;
if(entity._ == 'messageEntityTextUrl') {
url = (entity as MessageEntity.messageEntityTextUrl).url;
url = wrapUrl(url, true);
inner = wrapRichNestedText(entityText, entity.nested, options);
} else {
url = wrapUrl(entityText, false);
inner = encodeEntities(replaceUrlEncodings(entityText));
}
if(options.noLinks && !passEntities[entity._]) {
html.push(inner);
} else {
html.push(
'<a href="',
encodeEntities(url),
'" target="_blank" rel="noopener noreferrer">',
inner,
'</a>'
);
}
break;
case 'messageEntityLinebreak':
html.push(options.noLinebreaks ? ' ' : '<br/>');
break;
case 'messageEntityEmoji':
if(options.wrappingDraft && emojiSupported) { // * fix safari emoji
html.push(encodeEntities(entityText));
break;
}
html.push(emojiSupported ? // ! contenteditable="false" нужен для поля ввода, иначе там будет меняться шрифт в Safari, или же рендерить смайлик напрямую, без контейнера
`<span class="emoji">${encodeEntities(entityText)}</span>` :
`<img src="assets/img/emoji/${entity.unicode}.png" alt="${encodeEntities(entityText)}" class="emoji">`);
break;
case 'messageEntityBotCommand':
if(options.noLinks || options.noCommands || contextExternal) {
skipEntity = true;
break;
}
var command = entityText.substr(1);
var bot;
var atPos;
if ((atPos = command.indexOf('@')) != -1) {
bot = command.substr(atPos + 1);
command = command.substr(0, atPos);
} else {
bot = options.fromBot;
}
html.push(
'<a href="',
encodeEntities('tg://bot_command?command=' + encodeURIComponent(command) + (bot ? '&bot=' + encodeURIComponent(bot) : '')),
'">',
encodeEntities(entityText),
'</a>'
);
break;
case 'messageEntityBold': {
if(options.noTextFormat) {
html.push(wrapRichNestedText(entityText, entity.nested, options));
break;
}
if(options.wrappingDraft) {
html.push(`<span style="font-weight: bold;">${wrapRichNestedText(entityText, entity.nested, options)}</span>`);
} else {
html.push(`<strong>${wrapRichNestedText(entityText, entity.nested, options)}</strong>`);
}
break;
}
case 'messageEntityItalic': {
if(options.noTextFormat) {
html.push(wrapRichNestedText(entityText, entity.nested, options));
break;
}
if(options.wrappingDraft) {
html.push(`<span style="font-style: italic;">${wrapRichNestedText(entityText, entity.nested, options)}</span>`);
} else {
html.push(`<em>${wrapRichNestedText(entityText, entity.nested, options)}</em>`);
}
break;
}
case 'messageEntityHighlight':
html.push(
'<i>',
wrapRichNestedText(entityText, entity.nested, options),
'</i>'
);
break;
case 'messageEntityStrike':
if(options.wrappingDraft) {
const styleName = isSafari ? 'text-decoration' : 'text-decoration-line';
html.push(`<span style="${styleName}: line-through;">${wrapRichNestedText(entityText, entity.nested, options)}</span>`);
} else {
html.push(`<del>${wrapRichNestedText(entityText, entity.nested, options)}</del>`);
}
break;
case 'messageEntityUnderline':
if(options.wrappingDraft) {
const styleName = isSafari ? 'text-decoration' : 'text-decoration-line';
html.push(`<span style="${styleName}: underline;">${wrapRichNestedText(entityText, entity.nested, options)}</span>`);
} else {
html.push(`<u>${wrapRichNestedText(entityText, entity.nested, options)}</u>`);
}
break;
case 'messageEntityCode':
if(options.noTextFormat) {
html.push(encodeEntities(entityText));
break;
}
if(options.wrappingDraft) {
html.push(`<span style="font-family: monospace;">${encodeEntities(entityText)}</span>`);
} else {
html.push(
'<code>',
encodeEntities(entityText),
'</code>'
);
}
break;
case 'messageEntityPre':
if(options.noTextFormat) {
html.push(encodeEntities(entityText));
break;
}
html.push(
'<pre><code', (entity.language ? ' class="language-' + encodeEntities(entity.language) + '"' : ''), '>',
encodeEntities(entityText),
'</code></pre>'
);
break;
default:
skipEntity = true;
}
lastOffset = entity.offset + (skipEntity ? 0 : entity.length);
}
html.push(encodeEntities(text.substr(lastOffset))); // may be empty string
//console.log(html);
text = html.join('');
return text;
} */
/* export function wrapDraftText(text: string, options: any = {}) {
if(!text || !text.length) {
return '';
}
var entities = options.entities;
if(entities === undefined) {
entities = parseEntities(text);
}
var i = 0;
var len = entities.length;
var entity;
var entityText;
var skipEntity;
var code = [];
var lastOffset = 0;
for(i = 0; i < len; i++) {
entity = entities[i];
if(entity.offset > lastOffset) {
code.push(
text.substr(lastOffset, entity.offset - lastOffset)
);
} else if(entity.offset < lastOffset) {
continue;
}
skipEntity = false;
entityText = text.substr(entity.offset, entity.length);
switch(entity._) {
case 'messageEntityEmoji':
code.push(
':',
entity.title,
':'
);
break;
case 'messageEntityCode':
code.push(
'`', entityText, '`'
);
break;
case 'messageEntityBold':
code.push(
'**', entityText, '**'
);
break;
case 'messageEntityItalic':
code.push(
'__', entityText, '__'
);
break;
case 'messageEntityPre':
code.push(
'```', entityText, '```'
);
break;
case 'messageEntityMentionName':
code.push(
'@', entity.user_id, ' (', entityText, ')'
);
break;
default:
skipEntity = true;
}
lastOffset = entity.offset + (skipEntity ? 0 : entity.length);
}
code.push(text.substr(lastOffset));
return code.join('');
} */
export function wrapDraftText(text: string, options: Partial<{ export function wrapDraftText(text: string, options: Partial<{
entities: MessageEntity[] entities: MessageEntity[]
}> = {}) { }> = {}) {
@ -998,31 +593,6 @@ namespace RichTextProcessor {
}); });
} }
//const draftEntityTypes: MessageEntity['_'][] = (['messageEntityTextUrl', 'messageEntityEmoji'] as MessageEntity['_'][]).concat(Object.values(markdownEntities) as any);
/* const draftEntityTypes: Partial<{[_ in MessageEntity['_']]: true}> = {
messageEntityCode: true,
messageEntityPre: true,
messageEntityBold: true,
messageEntityItalic: true,
messageEntityStrike: true,
messageEntityEmoji: true,
messageEntityLinebreak: true,
messageEntityUnderline: true,
messageEntityTextUrl: true
};
export function wrapDraftText(text: string, options: Partial<{
entities: MessageEntity[]
}> = {}) {
const checkEntity = (entity: MessageEntity) => {
return draftEntityTypes[entity._];
};
const entities = options.entities ? options.entities.filter(entity => {
return draftEntityTypes[entity._];
}) : [];
return wrapRichText(text, {entities});
} */
export function checkBrackets(url: string) { export function checkBrackets(url: string) {
var urlLength = url.length; var urlLength = url.length;
var urlOpenBrackets = url.split('(').length - 1; var urlOpenBrackets = url.split('(').length - 1;
@ -1150,29 +720,6 @@ namespace RichTextProcessor {
return !text ? null : text.match(urlRegExp); return !text ? null : text.match(urlRegExp);
} }
/* const el = document.createElement('span');
export function getAbbreviation(str: string, onlyFirst = false) {
const wrapped = wrapEmojiText(str);
el.innerHTML = wrapped;
const childNodes = el.childNodes;
let first = '', last = '';
const firstNode = childNodes[0];
if('length' in firstNode) first = (firstNode as any).textContent.trim().charAt(0).toUpperCase();
else first = (firstNode as HTMLElement).outerHTML;
if(onlyFirst) return first;
if(str.indexOf(' ') !== -1) {
const lastNode = childNodes[childNodes.length - 1];
if(lastNode == firstNode) last = lastNode.textContent.split(' ').pop().trim().charAt(0).toUpperCase();
else if('length' in lastNode) last = (lastNode as any).textContent.trim().charAt(0).toUpperCase();
else last = (lastNode as HTMLElement).outerHTML;
}
return first + last;
} */
export function getAbbreviation(str: string, onlyFirst = false) { export function getAbbreviation(str: string, onlyFirst = false) {
const splitted = str.trim().split(' '); const splitted = str.trim().split(' ');
if(!splitted[0]) return ''; if(!splitted[0]) return '';

View File

@ -1,5 +1,5 @@
import CacheStorageController from './cacheStorage'; import CacheStorageController from './cacheStorage';
import { MOUNT_CLASS_TO } from './mtproto/mtproto_config'; import { DEBUG, MOUNT_CLASS_TO } from './mtproto/mtproto_config';
//import { stringify } from '../helpers/json'; //import { stringify } from '../helpers/json';
class AppStorage { class AppStorage {
@ -72,12 +72,14 @@ class AppStorage {
key = prefix + key; key = prefix + key;
this.cache[key] = value; this.cache[key] = value;
let perf = performance.now(); let perf = /* DEBUG */false ? performance.now() : 0;
value = JSON.stringify(value); value = JSON.stringify(value);
let elapsedTime = performance.now() - perf; if(perf) {
if(elapsedTime > 10) { let elapsedTime = performance.now() - perf;
console.warn('LocalStorage set: stringify time by JSON.stringify:', elapsedTime, key); if(elapsedTime > 10) {
console.warn('LocalStorage set: stringify time by JSON.stringify:', elapsedTime, key);
}
} }
/* perf = performance.now(); /* perf = performance.now();
value = stringify(value); value = stringify(value);

View File

@ -9,11 +9,16 @@
opacity: 0; opacity: 0;
transition: opacity var(--layer-transition), transform var(--layer-transition), width var(--layer-transition); transition: opacity var(--layer-transition), transform var(--layer-transition), width var(--layer-transition);
position: fixed; position: fixed;
left: 0;
top: 0; top: 0;
right: 0;
bottom: 0;
left: 0;
height: 44px; height: 44px;
width: $widthRegular; width: $widthRegular;
overflow: hidden; overflow: hidden;
z-index: 1;
display: flex;
justify-content: flex-start;
&-wrapper { &-wrapper {
position: absolute; position: absolute;
@ -27,6 +32,7 @@
height: 100%; height: 100%;
transform: translateX(0); transform: translateX(0);
transition: transform var(--layer-transition); transition: transform var(--layer-transition);
max-width: 100%;
} }
&-tools { &-tools {
@ -34,6 +40,8 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: $padding; padding: $padding;
flex: 0 0 auto;
max-width: 100%;
&:first-child { &:first-child {
width: $widthRegular; width: $widthRegular;

View File

@ -142,6 +142,14 @@
width: 312px; width: 312px;
padding: 4px 14px 14px 14px; padding: 4px 14px 14px 14px;
} }
&[data-lines="5"] {
top: -16px;
}
&[data-lines="7"] {
top: 16px;
}
} }
.date-picker { .date-picker {