Browse Source

Markup tooltip changes:

Support multiple format types
Fix link editor on small devices
master
Eduard Kuzmenko 4 years ago
parent
commit
bf58a3d147
  1. 51
      src/components/chat/input.ts
  2. 107
      src/components/chat/markupTooltip.ts
  3. 6
      src/components/popups/createPoll.ts
  4. 4
      src/components/popups/datePicker.ts
  5. 100
      src/helpers/dom.ts
  6. 7
      src/lib/appManagers/appImManager.ts
  7. 18
      src/lib/appManagers/appMessagesManager.ts
  8. 14
      src/lib/appManagers/appPollsManager.ts
  9. 497
      src/lib/richtextprocessor.ts
  10. 12
      src/lib/storage.ts
  11. 10
      src/scss/partials/_chatMarkupTooltip.scss
  12. 8
      src/scss/partials/popups/_datePicker.scss

51
src/components/chat/input.ts

@ -11,7 +11,7 @@ import apiManager from "../../lib/mtproto/mtprotoworker"; @@ -11,7 +11,7 @@ import apiManager from "../../lib/mtproto/mtprotoworker";
//import 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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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,

107
src/components/chat/markupTooltip.ts

@ -5,6 +5,7 @@ import ButtonIcon from "../buttonIcon"; @@ -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 { @@ -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<typeof logger>;
constructor(private appImManager: AppImManager) {
//this.log = logger('MARKUP');
}
private init() {
@ -41,14 +43,20 @@ export default class MarkupTooltip { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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<HTMLElement> = 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 { @@ -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 { @@ -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 { @@ -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();

6
src/components/popups/createPoll.ts

@ -8,6 +8,7 @@ import RadioField from "../radioField"; @@ -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 { @@ -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 { @@ -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);

4
src/components/popups/datePicker.ts

@ -333,7 +333,9 @@ export default class PopupDatePicker extends PopupElement { @@ -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);
}

100
src/helpers/dom.ts

@ -1,4 +1,6 @@ @@ -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) { @@ -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) { @@ -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) { @@ -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 = { @@ -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 @@ -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 @@ -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;
}

7
src/lib/appManagers/appImManager.ts

@ -457,6 +457,13 @@ export class AppImManager { @@ -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) {

18
src/lib/appManagers/appMessagesManager.ts

@ -397,11 +397,8 @@ export class AppMessagesManager { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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);
}

14
src/lib/appManagers/appPollsManager.ts

@ -1,5 +1,5 @@ @@ -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 { @@ -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 { @@ -155,7 +157,7 @@ export class AppPollsManager {
poll,
correct_answers: correctAnswers,
solution,
solution_entities
solution_entities: solutionEntities
};
}

497
src/lib/richtextprocessor.ts

@ -215,11 +215,12 @@ namespace RichTextProcessor { @@ -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 { @@ -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 { @@ -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 { @@ -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(
'<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<{
entities: MessageEntity[]
}> = {}) {
@ -998,31 +593,6 @@ namespace RichTextProcessor { @@ -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 { @@ -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 '';

12
src/lib/storage.ts

@ -1,5 +1,5 @@ @@ -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 { @@ -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);

10
src/scss/partials/_chatMarkupTooltip.scss

@ -9,11 +9,16 @@ @@ -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 @@ @@ -27,6 +32,7 @@
height: 100%;
transform: translateX(0);
transition: transform var(--layer-transition);
max-width: 100%;
}
&-tools {
@ -34,6 +40,8 @@ @@ -34,6 +40,8 @@
align-items: center;
justify-content: space-between;
padding: $padding;
flex: 0 0 auto;
max-width: 100%;
&:first-child {
width: $widthRegular;

8
src/scss/partials/popups/_datePicker.scss

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

Loading…
Cancel
Save