Browse Source

Refactored text formatting

Handle following by t.me links
Handle following by @username
master
Eduard Kuzmenko 4 years ago
parent
commit
bdefbcfa41
  1. 38
      src/components/chat/bubbles.ts
  2. 33
      src/lib/appManagers/appImManager.ts
  3. 13
      src/lib/appManagers/appMessagesManager.ts
  4. 15
      src/lib/appManagers/appUsersManager.ts
  5. 268
      src/lib/richtextprocessor.ts

38
src/components/chat/bubbles.ts

@ -548,14 +548,14 @@ export default class ChatBubbles { @@ -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 { @@ -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 { @@ -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 { @@ -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
});

33
src/lib/appManagers/appImManager.ts

@ -30,6 +30,7 @@ import appPollsManager from './appPollsManager'; @@ -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 { @@ -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();
}

13
src/lib/appManagers/appMessagesManager.ts

@ -1695,22 +1695,24 @@ export class AppMessagesManager { @@ -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 { @@ -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) {

15
src/lib/appManagers/appUsersManager.ts

@ -147,16 +147,21 @@ export class AppUsersManager { @@ -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));
});
}

268
src/lib/richtextprocessor.ts

@ -306,17 +306,18 @@ namespace RichTextProcessor { @@ -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 { @@ -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 { @@ -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, '<span style="font-weight: bold;">', '</span>');
} else {
insertPart(entity, '<strong>', '</strong>');
}
}
break;
}
case 'messageEntityItalic': {
if(!options.noTextFormat) {
if(options.wrappingDraft) {
insertPart(entity, '<span style="font-style: italic;">', '</span>');
} else {
insertPart(entity, '<em>', '</em>');
}
}
break;
}
case 'messageEntityStrike': {
if(options.wrappingDraft) {
const styleName = isSafari ? 'text-decoration' : 'text-decoration-line';
insertPart(entity, `<span style="${styleName}: line-through;">`, '</span>');
} else {
insertPart(entity, '<del>', '</del>');
}
break;
}
case 'messageEntityUnderline': {
if(options.wrappingDraft) {
const styleName = isSafari ? 'text-decoration' : 'text-decoration-line';
insertPart(entity, `<span style="${styleName}: underline;">`, '</span>');
} else {
insertPart(entity, '<u>', '</u>');
}
break;
}
case 'messageEntityCode': {
if(options.wrappingDraft) {
insertPart(entity, '<span style="font-family: monospace;">', '</span>');
} else {
insertPart(entity, '<code>', '</code>');
}
break;
}
case 'messageEntityPre': {
if(!options.noTextFormat) {
insertPart(entity, `<pre><code${entity.language ? ' class="language-' + encodeEntities(entity.language) + '"' : ''}>`, '</code></pre>');
}
break;
}
case 'messageEntityHighlight': {
insertPart(entity, '<i>', '</i>');
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, `<a href="${encodeEntities('tg://bot_command?command=' + encodeURIComponent(command) + (bot ? '&bot=' + encodeURIComponent(bot) : ''))}">`, `</a>`);
}
break;
}
case 'messageEntityEmoji': {
if(!(options.wrappingDraft && emojiSupported)) { // * fix safari emoji
if(emojiSupported) { // ! contenteditable="false" нужен для поля ввода, иначе там будет меняться шрифт в Safari, или же рендерить смайлик напрямую, без контейнера
insertPart(entity, '<span class="emoji">', '</span>');
} else {
insertPart(entity, `<img src="assets/img/emoji/${entity.unicode}.png" alt="`, `" class="emoji">`);
}
}
break;
}
/* case 'messageEntityLinebreak': {
if(options.noLinebreaks) {
insertPart(entity, ' ');
} else {
insertPart(entity, '<br/>');
}
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, `<a href="${encodeEntities(url)}"${currentContext ? '' : ' target="_blank" rel="noopener noreferrer"'}>`, '</a>');
}
break;
}
case 'messageEntityEmail': {
if(!options.noLinks) {
const entityText = text.substr(entity.offset, entity.length);
insertPart(entity, `<a href="${encodeEntities('mailto:' + entityText)}" target="_blank" rel="noopener noreferrer">`, '</a>');
}
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, `<a href="${contextUrl.replace('{1}', encodeURIComponent(hashtag))}"${contextExternal ? ' target="_blank" rel="noopener noreferrer"' : ''}>`, '</a>');
}
break;
}
case 'messageEntityMentionName': {
if(!options.noLinks) {
insertPart(entity, `<a href="#/im?p=u${encodeURIComponent(entity.user_id)}" class="follow" data-follow="${entity.user_id}">`, '</a>');
}
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, `<a class="mention" href="${contextUrl.replace('{1}', encodeURIComponent(username))}"${contextExternal ? ' target="_blank" rel="noopener noreferrer"' : ''}>`, '</a>');
}
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 { @@ -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 { @@ -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 { @@ -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+)/))) {

Loading…
Cancel
Save