From bf58a3d1475fd8481d3383b028f1e69d6d2baa41 Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Thu, 24 Dec 2020 00:03:34 +0200 Subject: [PATCH] Markup tooltip changes: Support multiple format types Fix link editor on small devices --- src/components/chat/input.ts | 51 ++- src/components/chat/markupTooltip.ts | 107 +++-- src/components/popups/createPoll.ts | 6 +- src/components/popups/datePicker.ts | 4 +- src/helpers/dom.ts | 100 +++-- src/lib/appManagers/appImManager.ts | 7 + src/lib/appManagers/appMessagesManager.ts | 18 +- src/lib/appManagers/appPollsManager.ts | 14 +- src/lib/richtextprocessor.ts | 497 +--------------------- src/lib/storage.ts | 12 +- src/scss/partials/_chatMarkupTooltip.scss | 10 +- src/scss/partials/popups/_datePicker.scss | 8 + 12 files changed, 229 insertions(+), 605 deletions(-) diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index 90d24225..359848fd 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -11,7 +11,7 @@ import apiManager from "../../lib/mtproto/mtprotoworker"; //import Recorder from '../opus-recorder/dist/recorder.min'; import opusDecodeController from "../../lib/opusDecodeController"; 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 emoticonsDropdown from "../emoticonsDropdown"; 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 const inputType = (e as InputEvent).inputType; //console.log('message beforeinput event', e); @@ -597,7 +597,7 @@ export default class ChatInput { cancelEvent(e); // * cancel legacy markdown event } } - }); + }); */ 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 */ - const checkForSingle = () => { + /* const checkForSingle = () => { const nodes = getSelectedNodes(); //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, 'true')); - //if(type === 'monospace') { + if(type === 'monospace') { let haveThisType = false; - executed.push(document.execCommand('styleWithCSS', false, 'true')); + //executed.push(document.execCommand('styleWithCSS', false, 'true')); const selection = window.getSelection(); if(!selection.isCollapsed) { @@ -703,18 +705,20 @@ export default class ChatInput { } } - executed.push(document.execCommand('removeFormat', false, null)); - - if(!haveThisType) { + //executed.push(document.execCommand('removeFormat', false, null)); + + if(haveThisType) { + executed.push(document.execCommand('fontName', false, 'Roboto')); + } else { executed.push(typeof(command) === 'function' ? command() : document.execCommand(command, false, null)); } - - executed.push(document.execCommand('styleWithCSS', false, 'false')); - /* } else { + } else { executed.push(typeof(command) === 'function' ? command() : document.execCommand(command, false, null)); - } */ + } + + executed.push(document.execCommand('styleWithCSS', false, 'false')); - checkForSingle(); + //checkForSingle(); saveExecuted(); if(this.appImManager.markupTooltip) { 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))); //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 markdownEntities: MessageEntity[] = []; const value = RichTextProcessor.parseMarkdown(richValue, markdownEntities); const entities = RichTextProcessor.mergeEntities(markdownEntities, RichTextProcessor.parseEntities(value)); @@ -815,6 +819,10 @@ export default class ChatInput { this.stickersHelper.checkEmoticon(emoticon); } + if(!richValue.trim()) { + this.appImManager.markupTooltip.hide(); + } + const html = this.messageInput.innerHTML; if(this.canRedoFromHTML && html != this.canRedoFromHTML && !this.lockRedo) { this.canRedoFromHTML = ''; @@ -1095,19 +1103,18 @@ export default class ChatInput { return; } - //let str = this.serializeNodes(Array.from(this.messageInput.childNodes)); - let str = this.messageInputField.value; - - //console.log('childnode str after:', str/* , getRichValue(this.messageInput) */); + const entities: MessageEntity[] = []; + const str = getRichValue(this.messageInputField.input, entities); //return; - if(this.editMsgId) { this.appMessagesManager.editMessage(this.chat.getMessage(this.editMsgId), str, { + entities, noWebPage: this.noWebPage }); } else { this.appMessagesManager.sendText(this.chat.peerId, str, { + entities, replyToMsgId: this.replyToMsgId, threadId: this.chat.threadId, noWebPage: this.noWebPage, diff --git a/src/components/chat/markupTooltip.ts b/src/components/chat/markupTooltip.ts index 3fffc7c5..a7a0068a 100644 --- a/src/components/chat/markupTooltip.ts +++ b/src/components/chat/markupTooltip.ts @@ -5,6 +5,7 @@ import ButtonIcon from "../buttonIcon"; import { clamp } from "../../helpers/number"; import { isTouchSupported } from "../../helpers/touchSupport"; import { isApple } from "../../helpers/userAgent"; +//import { logger } from "../../lib/logger"; export default class MarkupTooltip { public container: HTMLElement; @@ -17,10 +18,11 @@ export default class MarkupTooltip { private waitingForMouseUp = false; private linkInput: HTMLInputElement; private savedRange: Range; - mouseUpCounter: number = 0; + private mouseUpCounter: number = 0; + //private log: ReturnType; constructor(private appImManager: AppImManager) { - + //this.log = logger('MARKUP'); } private init() { @@ -41,14 +43,20 @@ export default class MarkupTooltip { tools1.append(this.buttons[c] = button); if(c !== 'link') { - button.addEventListener('click', () => { + button.addEventListener('mousedown', (e) => { + cancelEvent(e); this.appImManager.chat.input.applyMarkdown(c); - this.hide(); + this.cancelClosening(); + + /* this.mouseUpCounter = 0; + this.setMouseUpEvent(); */ + //this.hide(); }); } else { attachClickEvent(button, (e) => { cancelEvent(e); this.showLinkEditor(); + this.cancelClosening(); }); } }); @@ -81,17 +89,19 @@ export default class MarkupTooltip { this.linkInput.classList.remove('error'); }); - attachClickEvent(this.linkBackButton, (e) => { + this.linkBackButton.addEventListener('mousedown', (e) => { + //this.log('linkBackButton click'); cancelEvent(e); this.container.classList.remove('is-link'); //input.value = ''; this.resetSelection(); this.setTooltipPosition(); + this.cancelClosening(); }); this.linkApplyButton = ButtonIcon('check markup-tooltip-link-apply', {noRipple: true}); - attachClickEvent(this.linkApplyButton, (e) => { - cancelEvent(e); + this.linkApplyButton.addEventListener('mousedown', (e) => { + //this.log('linkApplyButton click'); this.applyLink(e); }); @@ -145,7 +155,9 @@ export default class MarkupTooltip { cancelEvent(e); this.resetSelection(); this.appImManager.chat.input.applyMarkdown('link', this.linkInput.value); - this.hide(); + setTimeout(() => { + this.hide(); + }, 0); } private isLinkValid() { @@ -160,10 +172,12 @@ export default class MarkupTooltip { } public hide() { + //return; + if(this.init) return; this.container.classList.remove('is-visible'); - document.removeEventListener('mouseup', this.onMouseUp); + //document.removeEventListener('mouseup', this.onMouseUp); document.removeEventListener('mouseup', this.onMouseUpSingle); this.waitingForMouseUp = false; @@ -178,37 +192,31 @@ export default class MarkupTooltip { public getActiveMarkupButton() { const nodes = getSelectedNodes(); const parents = [...new Set(nodes.map(node => node.parentNode))]; - if(parents.length > 1) return undefined; - - const node = parents[0] as HTMLElement; - let currentMarkup: HTMLElement; - for(const type in markdownTags) { - const tag = markdownTags[type as MarkdownType]; - if(node.matches(tag.match)) { - currentMarkup = this.buttons[type as MarkdownType]; - break; + //if(parents.length > 1 && parents) return []; + + const currentMarkups: Set = new Set(); + (parents as HTMLElement[]).forEach(node => { + for(const type in markdownTags) { + const tag = markdownTags[type as MarkdownType]; + const closest = node.closest(tag.match + ', [contenteditable]'); + if(closest !== this.appImManager.chat.input.messageInput) { + currentMarkups.add(this.buttons[type as MarkdownType]); + } } - } + }); + - return currentMarkup; + return [...currentMarkups]; } public setActiveMarkupButton() { - const activeButton = this.getActiveMarkupButton(); + const activeButtons = this.getActiveMarkupButton(); for(const i in this.buttons) { // @ts-ignore const button = this.buttons[i]; - if(button != activeButton) { - button.classList.remove('active'); - } + button.classList.toggle('active', activeButtons.includes(button)); } - - if(activeButton) { - activeButton.classList.add('active'); - } - - return activeButton; } private setTooltipPosition(isLinkToggle = false) { @@ -253,7 +261,6 @@ export default class MarkupTooltip { } const selection = document.getSelection(); - if(!selection.toString().trim().length) { this.hide(); return; @@ -285,21 +292,19 @@ export default class MarkupTooltip { 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(isTouchSupported) { - this.appImManager.chat.input.messageInput.focus(); - cancelEvent(e); - } */ this.hide(); - document.removeEventListener('mouseup', this.onMouseUp); - }; + //document.removeEventListener('mouseup', this.onMouseUp); + }; */ private onMouseUpSingle = (e: Event) => { + //this.log('onMouseUpSingle'); this.waitingForMouseUp = false; if(isTouchSupported) { @@ -314,39 +319,51 @@ export default class MarkupTooltip { this.show(); - !isTouchSupported && document.addEventListener('mouseup', this.onMouseUp); + //!isTouchSupported && document.addEventListener('mouseup', this.onMouseUp); }; public setMouseUpEvent() { if(this.waitingForMouseUp) return; this.waitingForMouseUp = true; - console.log('[MARKUP]: setMouseUpEvent'); + //this.log('setMouseUpEvent'); 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() { if(this.addedListener) return; this.addedListener = true; document.addEventListener('selectionchange', (e) => { - if(document.activeElement == this.linkInput) { + //this.log('selectionchange'); + + if(document.activeElement === this.linkInput) { return; } - if(document.activeElement != this.appImManager.chat.input.messageInput) { + if(document.activeElement !== this.appImManager.chat.input.messageInput) { this.hide(); return; } const selection = document.getSelection(); - if(!selection.toString().trim().length) { this.hide(); return; } - console.log('[MARKUP]: selectionchange'); if(isTouchSupported) { if(isApple) { this.show(); diff --git a/src/components/popups/createPoll.ts b/src/components/popups/createPoll.ts index cb092b8d..553b12b0 100644 --- a/src/components/popups/createPoll.ts +++ b/src/components/popups/createPoll.ts @@ -8,6 +8,7 @@ import RadioField from "../radioField"; import Scrollable from "../scrollable"; import { toast } from "../toast"; import SendContextMenu from "../chat/sendContextMenu"; +import { MessageEntity } from "../../layer"; const MAX_LENGTH_QUESTION = 255; const MAX_LENGTH_OPTION = 100; @@ -187,7 +188,8 @@ export default class PopupCreatePoll extends PopupElement { return; } - const quizSolution = this.quizSolutionField.value || undefined; + const quizSolutionEntities: MessageEntity[] = []; + const quizSolution = getRichValue(this.quizSolutionField.input, quizSolutionEntities) || undefined; if(quizSolution?.length > MAX_LENGTH_SOLUTION) { toast('Explanation is too long.'); return; @@ -236,7 +238,7 @@ export default class PopupCreatePoll extends PopupElement { }; //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); diff --git a/src/components/popups/datePicker.ts b/src/components/popups/datePicker.ts index 45635261..aaa80d80 100644 --- a/src/components/popups/datePicker.ts +++ b/src/components/popups/datePicker.ts @@ -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); } diff --git a/src/helpers/dom.ts b/src/helpers/dom.ts index 56a224f7..4adb7722 100644 --- a/src/helpers/dom.ts +++ b/src/helpers/dom.ts @@ -1,4 +1,6 @@ +import { MessageEntity } from "../layer"; import { MOUNT_CLASS_TO } from "../lib/mtproto/mtproto_config"; +import RichTextProcessor from "../lib/richtextprocessor"; import ListenerSetter from "./listenerSetter"; import { isTouchSupported } from "./touchSupport"; import { isSafari } from "./userAgent"; @@ -101,7 +103,7 @@ export function placeCaretAtEnd(el: HTMLElement) { return len; } */ -export function getRichValue(field: HTMLElement) { +export function getRichValue(field: HTMLElement, entities?: MessageEntity[]) { if(!field) { return ''; } @@ -109,7 +111,7 @@ export function getRichValue(field: HTMLElement) { const lines: string[] = []; const line: string[] = []; - getRichElementValue(field, lines, line); + getRichElementValue(field, lines, line, undefined, undefined, entities); if(line.length) { lines.push(line.join('')); } @@ -117,6 +119,12 @@ export function getRichValue(field: HTMLElement) { let value = lines.join('\n'); value = value.replace(/\u00A0/g, ' '); + if(entities) { + RichTextProcessor.combineSameEntities(entities); + } + + console.log('getRichValue:', value, entities); + return value; } @@ -134,64 +142,78 @@ const markdownTypes = { export type MarkdownType = 'bold' | 'italic' | 'underline' | 'strikethrough' | 'monospace' | 'link'; export type MarkdownTag = { 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} = { bold: { - match: '[style*="font-weight"]', - markdown: markdownTypes.bold + match: '[style*="font-weight"], b', + markdown: markdownTypes.bold, + entityName: 'messageEntityBold' }, underline: { - match: isSafari ? '[style="text-decoration: underline;"]' : '[style="text-decoration-line: underline;"]', - markdown: markdownTypes.underline + match: '[style*="underline"], u', + markdown: markdownTypes.underline, + entityName: 'messageEntityUnderline' }, italic: { - match: '[style="font-style: italic;"]', - markdown: markdownTypes.italic + match: '[style*="italic"], i', + markdown: markdownTypes.italic, + entityName: 'messageEntityItalic' }, monospace: { - match: '[style="font-family: monospace;"]', - markdown: markdownTypes.monospace + match: '[style*="monospace"], [face="monospace"]', + markdown: markdownTypes.monospace, + entityName: 'messageEntityPre' }, strikethrough: { - match: isSafari ? '[style="text-decoration: line-through;"]' : '[style="text-decoration-line: line-through;"]', - markdown: markdownTypes.strikethrough + match: '[style*="line-through"], strike', + markdown: markdownTypes.strikethrough, + entityName: 'messageEntityStrike' }, link: { 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(selNode === node) { const value = node.nodeValue; line.push(value.substr(0, selOffset) + '\x01' + value.substr(selOffset)); } else { - let markdown: string; - if(node.parentNode) { - const parentElement = node.parentElement; - - let markdownTag: MarkdownTag; - for(const type in markdownTags) { - const tag = markdownTags[type as MarkdownType]; - if(parentElement.matches(tag.match)) { - markdownTag = tag; - break; + const nodeValue = node.nodeValue; + line.push(nodeValue); + + if(entities && nodeValue.trim()) { + if(node.parentNode) { + const parentElement = node.parentElement; + + for(const type in markdownTags) { + 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; @@ -207,8 +229,10 @@ export function getRichElementValue(node: HTMLElement, lines: string[], line: st lines.push(line.join('')); line.splice(0, line.length); } else if(node.tagName == 'IMG') { - if((node as HTMLImageElement).alt) { - line.push((node as HTMLImageElement).alt); + const alt = (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; while(curChild) { - getRichElementValue(curChild, lines, line, selNode, selOffset); + getRichElementValue(curChild, lines, line, selNode, selOffset, entities, offset); curChild = curChild.nextSibling as any; } diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index cd2d6d1a..778bdc9b 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -457,6 +457,13 @@ export class AppImManager { 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); if(justReturn) { diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 5b3dce60..aaf02f1c 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -397,11 +397,8 @@ export class AppMessagesManager { }); } - let entities = options.entities; - if(typeof(text) === 'string' && !entities) { - entities = []; - text = RichTextProcessor.parseMarkdown(text, entities); - } + let entities = options.entities || []; + text = RichTextProcessor.parseMarkdown(text, entities); const schedule_date = options.scheduleDate || (message.pFlags.is_scheduled ? message.date : undefined); return apiManager.invokeApi('messages.editMessage', { @@ -494,7 +491,7 @@ export class AppMessagesManager { reply_to: this.generateReplyHeader(options.replyToMsgId, options.threadId), via_bot_id: options.viaBotId, reply_markup: options.reply_markup, - entities: entities, + entities, views: isBroadcast && 1, pending: true }; @@ -641,8 +638,8 @@ export class AppMessagesManager { this.log('sendFile', file, fileType); + const entities = options.entities || []; if(caption) { - let entities = options.entities || []; caption = RichTextProcessor.parseMarkdown(caption, entities); } @@ -792,6 +789,7 @@ export class AppMessagesManager { id: messageId, from_id: this.generateFromId(peerId), peer_id: appPeersManager.getOutputPeer(peerId), + entities, pFlags, date, message: caption, @@ -913,7 +911,8 @@ export class AppMessagesManager { random_id: randomIdS, reply_to_msg_id: replyToMsgId, schedule_date: options.scheduleDate, - silent: options.silent + silent: options.silent, + entities }).then((updates) => { apiUpdatesManager.processUpdateMessage(updates); }, (error) => { @@ -960,9 +959,8 @@ export class AppMessagesManager { const replyToMsgId = options.replyToMsgId ? this.getLocalMessageId(options.replyToMsgId) : undefined; let caption = options.caption || ''; - let entities: MessageEntity[]; + let entities = options.entities || []; if(caption) { - entities = options.entities || []; caption = RichTextProcessor.parseMarkdown(caption, entities); } diff --git a/src/lib/appManagers/appPollsManager.ts b/src/lib/appManagers/appPollsManager.ts index 4f61a344..6cdf2257 100644 --- a/src/lib/appManagers/appPollsManager.ts +++ b/src/lib/appManagers/appPollsManager.ts @@ -1,5 +1,5 @@ import { copy } from "../../helpers/object"; -import { InputMedia } from "../../layer"; +import { InputMedia, MessageEntity } from "../../layer"; import { logger, LogLevels } from "../logger"; import apiManager from "../mtproto/mtprotoworker"; 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 { - let solution_entities: any[]; + public getInputMediaPoll(poll: Poll, correctAnswers?: Uint8Array[], solution?: string, solutionEntities?: MessageEntity[]): InputMedia.inputMediaPoll { if(solution) { - solution_entities = []; - solution = RichTextProcessor.parseMarkdown(solution, solution_entities); + if(!solutionEntities) { + solutionEntities = []; + } + + solution = RichTextProcessor.parseMarkdown(solution, solutionEntities); } return { @@ -155,7 +157,7 @@ export class AppPollsManager { poll, correct_answers: correctAnswers, solution, - solution_entities + solution_entities: solutionEntities }; } diff --git a/src/lib/richtextprocessor.ts b/src/lib/richtextprocessor.ts index d424c72e..b2e4876a 100644 --- a/src/lib/richtextprocessor.ts +++ b/src/lib/richtextprocessor.ts @@ -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)) { return noTrim ? text : text.trim(); } */ - + + const entities: MessageEntity[] = []; let raw = text; let match; let newText: any = []; @@ -302,68 +303,12 @@ namespace RichTextProcessor { newText = newText.trim(); } + mergeEntities(currentEntities, entities); + combineSameEntities(currentEntities); + 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[]) { currentEntities = currentEntities.slice(); 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; } - /* export function wrapRichNestedText(text: string, nested: MessageEntity[], options: any) { - if(nested === undefined) { - return encodeEntities(text); + export function combineSameEntities(entities: MessageEntity[]) { + //entities = entities.slice(); + for(let i = 0; i < entities.length; ++i) { + const entity = entities[i]; + + let nextEntityIdx = -1; + 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); } - - options.hasNested = true; - return wrapRichText(text, {entities: nested, nested: true}); - } */ + //return entities; + } export function wrapRichText(text: string, options: Partial<{ entities: MessageEntity[], @@ -620,365 +574,6 @@ namespace RichTextProcessor { 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( - '', - wrapRichNestedText(entityText, entity.nested, options), - //encodeEntities(entityText), - '' - ) - break; - - case 'messageEntityMentionName': - if(options.noLinks) { - skipEntity = true; - break; - } - - html.push( - '', - wrapRichNestedText(entityText, entity.nested, options), - '' - ); - break; - - case 'messageEntityHashtag': - var contextUrl = !options.noLinks && siteHashtags[contextSite]; - if(!contextUrl) { - skipEntity = true; - break; - } - - var hashtag = entityText.substr(1); - html.push( - '', - encodeEntities(entityText), - '' - ); - break; - - case 'messageEntityEmail': - if(options.noLinks) { - skipEntity = true; - break; - } - - html.push( - '', - encodeEntities(entityText), - '' - ); - 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( - '', - inner, - '' - ); - } - break; - - case 'messageEntityLinebreak': - html.push(options.noLinebreaks ? ' ' : '
'); - break; - - case 'messageEntityEmoji': - if(options.wrappingDraft && emojiSupported) { // * fix safari emoji - html.push(encodeEntities(entityText)); - break; - } - - html.push(emojiSupported ? // ! contenteditable="false" нужен для поля ввода, иначе там будет меняться шрифт в Safari, или же рендерить смайлик напрямую, без контейнера - `${encodeEntities(entityText)}` : - `${encodeEntities(entityText)}`); - 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( - '', - encodeEntities(entityText), - '' - ); - break; - - case 'messageEntityBold': { - if(options.noTextFormat) { - html.push(wrapRichNestedText(entityText, entity.nested, options)); - break; - } - - if(options.wrappingDraft) { - html.push(`${wrapRichNestedText(entityText, entity.nested, options)}`); - } else { - html.push(`${wrapRichNestedText(entityText, entity.nested, options)}`); - } - break; - } - - case 'messageEntityItalic': { - if(options.noTextFormat) { - html.push(wrapRichNestedText(entityText, entity.nested, options)); - break; - } - - if(options.wrappingDraft) { - html.push(`${wrapRichNestedText(entityText, entity.nested, options)}`); - } else { - html.push(`${wrapRichNestedText(entityText, entity.nested, options)}`); - } - - break; - } - - case 'messageEntityHighlight': - html.push( - '', - wrapRichNestedText(entityText, entity.nested, options), - '' - ); - break; - - case 'messageEntityStrike': - if(options.wrappingDraft) { - const styleName = isSafari ? 'text-decoration' : 'text-decoration-line'; - html.push(`${wrapRichNestedText(entityText, entity.nested, options)}`); - } else { - html.push(`${wrapRichNestedText(entityText, entity.nested, options)}`); - } - break; - - case 'messageEntityUnderline': - if(options.wrappingDraft) { - const styleName = isSafari ? 'text-decoration' : 'text-decoration-line'; - html.push(`${wrapRichNestedText(entityText, entity.nested, options)}`); - } else { - html.push(`${wrapRichNestedText(entityText, entity.nested, options)}`); - } - break; - - case 'messageEntityCode': - if(options.noTextFormat) { - html.push(encodeEntities(entityText)); - break; - } - - if(options.wrappingDraft) { - html.push(`${encodeEntities(entityText)}`); - } else { - html.push( - '', - encodeEntities(entityText), - '' - ); - } - - break; - - case 'messageEntityPre': - if(options.noTextFormat) { - html.push(encodeEntities(entityText)); - break; - } - - html.push( - '
',
-            encodeEntities(entityText),
-            '
' - ); - 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<{ 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) { var urlLength = url.length; var urlOpenBrackets = url.split('(').length - 1; @@ -1150,29 +720,6 @@ namespace RichTextProcessor { 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) { const splitted = str.trim().split(' '); if(!splitted[0]) return ''; diff --git a/src/lib/storage.ts b/src/lib/storage.ts index 93a4d776..e628c7b2 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -1,5 +1,5 @@ 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'; class AppStorage { @@ -72,12 +72,14 @@ class AppStorage { key = prefix + key; this.cache[key] = value; - let perf = performance.now(); + let perf = /* DEBUG */false ? performance.now() : 0; value = JSON.stringify(value); - let elapsedTime = performance.now() - perf; - if(elapsedTime > 10) { - console.warn('LocalStorage set: stringify time by JSON.stringify:', elapsedTime, key); + if(perf) { + let elapsedTime = performance.now() - perf; + if(elapsedTime > 10) { + console.warn('LocalStorage set: stringify time by JSON.stringify:', elapsedTime, key); + } } /* perf = performance.now(); value = stringify(value); diff --git a/src/scss/partials/_chatMarkupTooltip.scss b/src/scss/partials/_chatMarkupTooltip.scss index 8500ec01..6e4cecb0 100644 --- a/src/scss/partials/_chatMarkupTooltip.scss +++ b/src/scss/partials/_chatMarkupTooltip.scss @@ -9,11 +9,16 @@ opacity: 0; transition: opacity var(--layer-transition), transform var(--layer-transition), width var(--layer-transition); position: fixed; - left: 0; top: 0; + right: 0; + bottom: 0; + left: 0; height: 44px; width: $widthRegular; overflow: hidden; + z-index: 1; + display: flex; + justify-content: flex-start; &-wrapper { position: absolute; @@ -27,6 +32,7 @@ height: 100%; transform: translateX(0); transition: transform var(--layer-transition); + max-width: 100%; } &-tools { @@ -34,6 +40,8 @@ align-items: center; justify-content: space-between; padding: $padding; + flex: 0 0 auto; + max-width: 100%; &:first-child { width: $widthRegular; diff --git a/src/scss/partials/popups/_datePicker.scss b/src/scss/partials/popups/_datePicker.scss index bb79865d..3f6256aa 100644 --- a/src/scss/partials/popups/_datePicker.scss +++ b/src/scss/partials/popups/_datePicker.scss @@ -142,6 +142,14 @@ width: 312px; padding: 4px 14px 14px 14px; } + + &[data-lines="5"] { + top: -16px; + } + + &[data-lines="7"] { + top: 16px; + } } .date-picker {