From bdefbcfa41dd8bb22e5711f73ab07cfcc954456f Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Wed, 16 Dec 2020 00:13:54 +0200 Subject: [PATCH] Refactored text formatting Handle following by t.me links Handle following by @username --- src/components/chat/bubbles.ts | 38 ++- src/lib/appManagers/appImManager.ts | 33 +++ src/lib/appManagers/appMessagesManager.ts | 13 +- src/lib/appManagers/appUsersManager.ts | 15 +- src/lib/richtextprocessor.ts | 268 +++++++++++++++++++++- 5 files changed, 341 insertions(+), 26 deletions(-) diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index a5a623f1..9d417d78 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -548,14 +548,14 @@ export default class ChatBubbles { return; } - if(['IMG', 'DIV', "AVATAR-ELEMENT"].indexOf(target.tagName) === -1) target = findUpTag(target, 'DIV'); + if(['IMG', 'DIV', "AVATAR-ELEMENT", 'A'].indexOf(target.tagName) === -1) target = findUpTag(target, 'DIV'); - if(target.tagName == 'DIV' || target.tagName == "AVATAR-ELEMENT") { + if(target.tagName == 'DIV' || target.tagName == "AVATAR-ELEMENT" || target.tagName == 'A') { if(target.classList.contains('goto-original')) { - let savedFrom = bubble.dataset.savedFrom; - let splitted = savedFrom.split('_'); - let peerId = +splitted[0]; - let msgId = +splitted[1]; + const savedFrom = bubble.dataset.savedFrom; + const splitted = savedFrom.split('_'); + const peerId = +splitted[0]; + const msgId = +splitted[1]; ////this.log('savedFrom', peerId, msgID); this.chat.appImManager.setInnerPeer(peerId, msgId); return; @@ -564,8 +564,23 @@ export default class ChatBubbles { new PopupForward([mid]); //appSidebarRight.forwardTab.open([mid]); return; - } else if(target.classList.contains('name')) { - let peerId = +target.dataset.peerId; + }/* else if(target.classList.contains('follow')) { + cancelEvent(e); + const savedFrom = target.dataset.follow; + const splitted = savedFrom.split('_'); + this.chat.appImManager.setInnerPeer(+splitted[0], splitted.length > 1 ? +splitted[1] : undefined); + + return; + } else if(target.classList.contains('mention')) { + cancelEvent(e); + const username = target.innerText; + this.appUsersManager.resolveUsername(username.slice(1)).then(peer => { + this.chat.appImManager.setInnerPeer(peer._ == 'user' ? peer.id : -peer.id); + }); + + return; + } */ else if(target.classList.contains('name')) { + const peerId = +target.dataset.peerId; if(peerId) { this.chat.appImManager.setInnerPeer(peerId); @@ -573,7 +588,7 @@ export default class ChatBubbles { return; } else if(target.tagName == "AVATAR-ELEMENT") { - let peerId = +target.getAttribute('peer'); + const peerId = +target.getAttribute('peer'); if(peerId) { this.chat.appImManager.setInnerPeer(peerId); @@ -1405,12 +1420,17 @@ export default class ChatBubbles { } else if(message.grouped_id && albumMustBeRenderedFull) { const t = this.appMessagesManager.getAlbumText(message.grouped_id); messageMessage = t.message; + //totalEntities = t.entities; totalEntities = t.totalEntities; } else if(messageMedia?.document?.type != 'sticker') { messageMessage = message.message; + //totalEntities = message.entities; totalEntities = message.totalEntities; } + /* let richText = RichTextProcessor.wrapRichText(messageMessage, { + entities: totalEntities + }); */ let richText = RichTextProcessor.wrapRichText(messageMessage, { entities: totalEntities }); diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 139512cf..0d4b429e 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -30,6 +30,7 @@ import appPollsManager from './appPollsManager'; import SetTransition from '../../components/singleTransition'; import { isSafari } from '../../helpers/userAgent'; import ChatDragAndDrop from '../../components/chat/dragAndDrop'; +import appMessagesIdsManager from './appMessagesIdsManager'; //console.log('appImManager included33!'); @@ -134,6 +135,38 @@ export class AppImManager { this.createNewChat(); this.chatsSelectTab(0); + window.addEventListener('hashchange', (e) => { + const hash = location.hash; + const splitted = hash.split('?'); + + const params: any = {}; + splitted[1].split('&').forEach(item => { + params[item.split('=')[0]] = decodeURIComponent(item.split('=')[1]); + }); + + this.log('hashchange', splitted[0], params); + + switch(splitted[0]) { + case '#/im': { + const p = params.p; + if(p[0] === '@') { + let postId = params.post !== undefined ? +params.post : undefined; + appUsersManager.resolveUsername(p).then(peer => { + const isUser = peer._ == 'user'; + const peerId = isUser ? peer.id : -peer.id; + if(postId) { + postId = appMessagesIdsManager.getFullMessageId(postId, -peerId); + } + + this.setInnerPeer(peerId, postId); + }); + } + } + } + + location.hash = ''; + }); + //apiUpdatesManager.attach(); } diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index e84068b4..5830abe9 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -1695,22 +1695,24 @@ export class AppMessagesManager { public getAlbumText(grouped_id: string) { const group = appMessagesManager.groupedMessagesStorage[grouped_id]; - let foundMessages = 0, message: string, totalEntities: MessageEntity[]; + let foundMessages = 0, message: string, totalEntities: MessageEntity[], entities: MessageEntity[]; for(const i in group) { const m = group[i]; if(m.message) { if(++foundMessages > 1) break; message = m.message; totalEntities = m.totalEntities; + entities = m.entities; } } if(foundMessages > 1) { message = undefined; totalEntities = undefined; + entities = undefined; } - return {message, totalEntities}; + return {message, entities, totalEntities}; } public getMidsByAlbum(grouped_id: string) { @@ -1946,9 +1948,14 @@ export class AppMessagesManager { } if(message.message && message.message.length && !message.totalEntities) { + //message.totalEntities = (message.entities || []).slice(); const myEntities = RichTextProcessor.parseEntities(message.message); const apiEntities = message.entities || []; - message.totalEntities = RichTextProcessor.mergeEntities(myEntities, apiEntities, !message.pending); + //message.totalEntities = RichTextProcessor.mergeEntitiesNew(myEntities, apiEntities, !message.pending); + message.totalEntities = RichTextProcessor.mergeEntities(apiEntities, myEntities, !message.pending); // ! only in this order, otherwise bold and emoji formatting won't work + /* message.totalEntities = RichTextProcessor.mergeEntities(apiEntities, apiEntities, !message.pending); + message.totalEntities = RichTextProcessor.mergeEntities(myEntities, message.totalEntities, !message.pending); */ + //message.totalEntities = RichTextProcessor.mergeEntities(myEntities, apiEntities, !message.pending); } //if(!options.isEdited) { diff --git a/src/lib/appManagers/appUsersManager.ts b/src/lib/appManagers/appUsersManager.ts index 0ce2d87f..8132ef6f 100644 --- a/src/lib/appManagers/appUsersManager.ts +++ b/src/lib/appManagers/appUsersManager.ts @@ -147,16 +147,21 @@ export class AppUsersManager { return this.contactsFillPromise || (this.contactsFillPromise = promise); } - public async resolveUsername(username: string) { + public resolveUsername(username: string) { + if(username[0] == '@') { + username = username.slice(1); + } + + username = username.toLowerCase(); if(this.usernames[username]) { - return this.users[this.usernames[username]]; + return Promise.resolve(this.users[this.usernames[username]]); } - return await apiManager.invokeApi('contacts.resolveUsername', {username}).then(resolvedPeer => { - this.saveApiUser(resolvedPeer.users[0] as User); + return apiManager.invokeApi('contacts.resolveUsername', {username}).then(resolvedPeer => { + this.saveApiUsers(resolvedPeer.users); appChatsManager.saveApiChats(resolvedPeer.chats); - return this.users[this.usernames[username]]; + return appPeersManager.getPeer(appPeersManager.getPeerId(resolvedPeer.peer)); }); } diff --git a/src/lib/richtextprocessor.ts b/src/lib/richtextprocessor.ts index d740613c..9ab79aaf 100644 --- a/src/lib/richtextprocessor.ts +++ b/src/lib/richtextprocessor.ts @@ -306,17 +306,18 @@ namespace RichTextProcessor { return newText; } - export function mergeEntities(currentEntities: MessageEntity[], newEntities: MessageEntity[], fromApi?: boolean) { + /* 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; - } */ + // if(fromApi && + // curEntity._ != 'messageEntityLinebreak' && + // curEntity._ != 'messageEntityEmoji') { + // continue; + // } + // console.log('s', curEntity, newEntities); const start = curEntity.offset; const end = start + curEntity.length; @@ -362,6 +363,14 @@ namespace RichTextProcessor { }); // console.log('merge', currentEntities, newEntities, totalEntities) return totalEntities; + } */ + + export function mergeEntities(currentEntities: MessageEntity[], newEntities: MessageEntity[], fromApi?: boolean) { + currentEntities = currentEntities.slice(); + const filtered = newEntities.filter(e => !currentEntities.find(_e => e._ == _e._ && e.offset == _e.offset && e.length == _e.length)); + currentEntities.push(...filtered); + currentEntities.sort((a, b) => a.offset - b.offset); + return currentEntities; } export function wrapRichNestedText(text: string, nested: MessageEntity[], options: any) { @@ -387,6 +396,245 @@ namespace RichTextProcessor { [_ in MessageEntity['_']]: true }>, + nested?: true, + contextHashtag?: string + }> = {}) { + if(!text || !text.length) { + return ''; + } + + const lol: { + part: string, + offset: number + }[] = []; + const entities = options.entities || parseEntities(text); + + const passEntities: typeof options.passEntities = options.passEntities || {}; + const contextSite = options.contextSite || 'Telegram'; + const contextExternal = contextSite != 'Telegram'; + + const insertPart = (entity: MessageEntity, startPart: string, endPart?: string) => { + lol.push({part: startPart, offset: entity.offset}); + + if(endPart) { + lol.unshift({part: endPart, offset: entity.offset + entity.length}); + } + }; + + for(const entity of entities) { + switch(entity._) { + case 'messageEntityBold': { + if(!options.noTextFormat) { + if(options.wrappingDraft) { + insertPart(entity, '', ''); + } else { + insertPart(entity, '', ''); + } + } + + break; + } + + case 'messageEntityItalic': { + if(!options.noTextFormat) { + if(options.wrappingDraft) { + insertPart(entity, '', ''); + } else { + insertPart(entity, '', ''); + } + } + + break; + } + + case 'messageEntityStrike': { + if(options.wrappingDraft) { + const styleName = isSafari ? 'text-decoration' : 'text-decoration-line'; + insertPart(entity, ``, ''); + } else { + insertPart(entity, '', ''); + } + + break; + } + + case 'messageEntityUnderline': { + if(options.wrappingDraft) { + const styleName = isSafari ? 'text-decoration' : 'text-decoration-line'; + insertPart(entity, ``, ''); + } else { + insertPart(entity, '', ''); + } + + break; + } + + case 'messageEntityCode': { + if(options.wrappingDraft) { + insertPart(entity, '', ''); + } else { + insertPart(entity, '', ''); + } + + break; + } + + case 'messageEntityPre': { + if(!options.noTextFormat) { + insertPart(entity, `
`, '
'); + } + + break; + } + + case 'messageEntityHighlight': { + insertPart(entity, '', ''); + break; + } + + case 'messageEntityBotCommand': { + if(!(options.noLinks || options.noCommands || contextExternal)) { + const entityText = text.substr(entity.offset, entity.length); + let command = entityText.substr(1); + let bot: string | boolean; + let atPos: number; + if((atPos = command.indexOf('@')) != -1) { + bot = command.substr(atPos + 1); + command = command.substr(0, atPos); + } else { + bot = options.fromBot; + } + + insertPart(entity, ``, ``); + } + + break; + } + + case 'messageEntityEmoji': { + if(!(options.wrappingDraft && emojiSupported)) { // * fix safari emoji + if(emojiSupported) { // ! contenteditable="false" нужен для поля ввода, иначе там будет меняться шрифт в Safari, или же рендерить смайлик напрямую, без контейнера + insertPart(entity, '', ''); + } else { + insertPart(entity, ``, ``); + } + } + + break; + } + + /* case 'messageEntityLinebreak': { + if(options.noLinebreaks) { + insertPart(entity, ' '); + } else { + insertPart(entity, '
'); + } + + break; + } */ + + case 'messageEntityUrl': + case 'messageEntityTextUrl': { + if(!(options.noLinks && !passEntities[entity._])) { + const entityText = text.substr(entity.offset, entity.length); + + 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)); + } + + const currentContext = url[0] === '#'; + + insertPart(entity, ``, ''); + } + + break; + } + + case 'messageEntityEmail': { + if(!options.noLinks) { + const entityText = text.substr(entity.offset, entity.length); + insertPart(entity, ``, ''); + } + + break; + } + + case 'messageEntityHashtag': { + const contextUrl = !options.noLinks && siteHashtags[contextSite]; + if(contextUrl) { + const entityText = text.substr(entity.offset, entity.length); + const hashtag = entityText.substr(1); + insertPart(entity, ``, ''); + } + + break; + } + + case 'messageEntityMentionName': { + if(!options.noLinks) { + insertPart(entity, ``, ''); + } + + break; + } + + case 'messageEntityMention': { + const contextUrl = !options.noLinks && siteMentions[contextSite]; + if(contextUrl) { + const entityText = text.substr(entity.offset, entity.length); + const username = entityText.substr(1); + + insertPart(entity, ``, ''); + } + + break; + } + } + } + + lol.sort((a, b) => a.offset - b.offset); + + let out = ''; + let usedLength = 0; + for(const {part, offset} of lol) { + if(offset > usedLength) { + out += encodeEntities(text.slice(usedLength, offset)); + usedLength = offset; + } + + out += part; + + + } + + if(usedLength < text.length) { + out += encodeEntities(text.slice(usedLength)); + } + + 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 }> = {}) { @@ -654,7 +902,7 @@ namespace RichTextProcessor { text = html.join(''); return text; - } + } */ /* export function wrapDraftText(text: string, options: any = {}) { if(!text || !text.length) { @@ -868,7 +1116,8 @@ namespace RichTextProcessor { default: if(path[1] && path[1].match(/^\d+$/)) { - url = 'tg://resolve?domain=' + path[0] + '&post=' + path[1]; + url = siteMentions['Telegram'].replace('{1}', path[0] + '&post=' + path[1]); + //url = 'tg://resolve?domain=' + path[0] + '&post=' + path[1]; } else if(path.length == 1) { var domainQuery = path[0].split('?'); var domain = domainQuery[0]; @@ -885,7 +1134,8 @@ namespace RichTextProcessor { } } - url = 'tg://resolve?domain=' + domain + (query ? '&' + query : ''); + url = siteMentions['Telegram'].replace('{1}', domain + (query ? '&' + query : '')); + //url = 'tg://resolve?domain=' + domain + (query ? '&' + query : ''); } } } else if((telescoPeMatch = url.match(/^https?:\/\/telesco\.pe\/([^/?]+)\/(\d+)/))) {