Refactored text formatting
Handle following by t.me links Handle following by @username
This commit is contained in:
parent
20d0adc406
commit
bdefbcfa41
@ -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
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -147,16 +147,21 @@ export class AppUsersManager {
|
||||
return this.contactsFillPromise || (this.contactsFillPromise = promise);
|
||||
}
|
||||
|
||||
public async resolveUsername(username: string) {
|
||||
if(this.usernames[username]) {
|
||||
return this.users[this.usernames[username]];
|
||||
public resolveUsername(username: string) {
|
||||
if(username[0] == '@') {
|
||||
username = username.slice(1);
|
||||
}
|
||||
|
||||
return await apiManager.invokeApi('contacts.resolveUsername', {username}).then(resolvedPeer => {
|
||||
this.saveApiUser(resolvedPeer.users[0] as User);
|
||||
username = username.toLowerCase();
|
||||
if(this.usernames[username]) {
|
||||
return Promise.resolve(this.users[this.usernames[username]]);
|
||||
}
|
||||
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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, '<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 {
|
||||
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+)/))) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user