diff --git a/app/js/directives.js b/app/js/directives.js
index 7cd32640..e7bc807a 100755
--- a/app/js/directives.js
+++ b/app/js/directives.js
@@ -420,6 +420,61 @@ angular.module('myApp.directives', ['myApp.filters'])
})
+ .directive('myMessageText', function(AppMessagesManager, AppUsersManager, RichTextProcessor) {
+ return {
+ link: link,
+ scope: {
+ message: '=myMessageText'
+ }
+ };
+
+ function updateHtml (message, element) {
+ var entities = message.totalEntities;
+ var fromUser = AppUsersManager.getUser(message.from_id);
+ var fromBot = fromUser.pFlags.bot && fromUser.username || false;
+ var withBot = (fromBot ||
+ message.to_id && (
+ message.to_id.chat_id ||
+ message.to_id.user_id && AppUsersManager.isBot(message.to_id.user_id)
+ )
+ );
+
+ var options = {
+ noCommands: !withBot,
+ fromBot: fromBot,
+ entities: entities
+ };
+ if (message.flags & 16) {
+ var user = AppUsersManager.getSelf();
+ if (user) {
+ options.highlightUsername = user.username;
+ }
+ }
+ var html = RichTextProcessor.wrapRichText(message.message, options);
+ // console.log('dd', entities, html);
+
+ element.html(html.valueOf());
+ }
+
+ function link ($scope, element, attrs) {
+ var message = $scope.message;
+ var msgID = message.id;
+ // var msgID = $scope.$eval(attrs.myMessageText);
+ // var message = AppMessagesManager.getMessage(msgID);
+
+ updateHtml(message, element);
+
+ if (message.pending) {
+ var unlink = $scope.$on('messages_pending', function () {
+ if (message.id != msgID) {
+ updateHtml(message, element);
+ unlink();
+ }
+ })
+ }
+ }
+ })
+
.directive('myReplyMarkup', function() {
return {
diff --git a/app/js/services.js b/app/js/services.js
index 82c73c12..1a8e9559 100755
--- a/app/js/services.js
+++ b/app/js/services.js
@@ -1736,6 +1736,12 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
selective: (apiMessage.reply_markup.flags & 4) > 0
};
}
+
+ if (apiMessage.message && apiMessage.message.length) {
+ var myEntities = RichTextProcessor.parseEntities(apiMessage.message);
+ var apiEntities = apiMessage.entities || [];
+ apiMessage.totalEntities = RichTextProcessor.mergeEntities(myEntities, apiEntities, !apiMessage.pending);
+ }
});
}
@@ -1751,8 +1757,11 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
inputPeer = AppPeersManager.getInputPeerByID(peerID),
flags = 0,
replyToMsgID = options.replyToMsgID,
+ entities = [],
message;
+ text = RichTextProcessor.parseMarkdown(text, entities);
+
if (historyStorage === undefined) {
historyStorage = historiesStorage[peerID] = {count: null, history: [], pending: []};
}
@@ -1777,6 +1786,7 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
message: text,
random_id: randomIDS,
reply_to_msg_id: replyToMsgID,
+ entities: entities,
pending: true
};
@@ -1806,18 +1816,24 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
if (replyToMsgID) {
flags |= 1;
}
+ if (entities.length) {
+ flags |= 8;
+ }
+ console.log(flags, entities);
MtpApiManager.invokeApi('messages.sendMessage', {
flags: flags,
peer: inputPeer,
message: text,
random_id: randomID,
- reply_to_msg_id: replyToMsgID
+ reply_to_msg_id: replyToMsgID,
+ entities: entities
}, sentRequestOptions).then(function (updates) {
if (updates._ == 'updateShortSentMessage') {
message.flags = updates.flags;
message.date = updates.date;
message.id = updates.id;
message.media = updates.media;
+ message.entities = updates.entities;
updates = {
_: 'updates',
users: [],
@@ -2467,26 +2483,6 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
}
}
- if (message.message && message.message.length) {
- var options = {
- noCommands: !withBot,
- fromBot: fromBot
- };
- if (!Config.Navigator.mobile) {
- options.extractUrlEmbed = true;
- }
- if (message.flags & 16) {
- var user = AppUsersManager.getSelf();
- if (user) {
- options.highlightUsername = user.username;
- }
- }
- message.richMessage = RichTextProcessor.wrapRichText(message.message, options);
- if (options.extractedUrlEmbed) {
- message.richUrlEmbed = options.extractedUrlEmbed;
- }
- }
-
return messagesForHistory[msgID] = message;
}
@@ -4665,6 +4661,8 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
var soundcloudRegExp = /^https?:\/\/(?:soundcloud\.com|snd\.sc)\/([a-zA-Z0-9%\-\_]+)\/([a-zA-Z0-9%\-\_]+)/i;
var spotifyRegExp = /(https?:\/\/(open\.spotify\.com|play\.spotify\.com|spoti\.fi)\/(.+)|spotify:(.+))/i;
+ var markdownRegExp = /(^|\n)```(.{0,16})\n([\s\S]+?)\n```(\n|$)|(^|\s)`([^\n]+?)`/;
+
var siteHashtags = {
Telegram: '#/im?q=%23{1}',
Twitter: 'https://twitter.com/hashtag/{1}',
@@ -4681,7 +4679,10 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
return {
wrapRichText: wrapRichText,
- wrapPlainText: wrapPlainText
+ wrapPlainText: wrapPlainText,
+ parseEntities: parseEntities,
+ parseMarkdown: parseMarkdown,
+ mergeEntities: mergeEntities
};
function getEmojiSpritesheetCoords(emojiCode) {
@@ -4699,210 +4700,427 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
return null;
}
- function wrapRichText(text, options) {
- if (!text || !text.length) {
- return '';
- }
-
+ function parseEntities (text, options) {
options = options || {};
var match,
raw = text,
- html = [],
url,
- contextSite = options.contextSite || 'Telegram',
- contextExternal = contextSite != 'Telegram',
- emojiFound = false,
- emojiTitle,
- emojiCoords;
+ entities = [],
+ emojiCode,
+ emojiCoords,
+ matchIndex,
+ rawOffset = 0;
// var start = tsNow();
while ((match = raw.match(fullRegExp))) {
- html.push(encodeEntities(raw.substr(0, match.index)));
+ matchIndex = rawOffset + match.index;
- if (match[3]) { // telegram.me links
- var contextUrl = !options.noLinks && siteMentions[contextSite];
- if (contextUrl) {
- var attr = '';
- if (options.highlightUsername &&
- options.highlightUsername.toLowerCase() == match[3].toLowerCase()) {
- attr = 'class="im_message_mymention"';
- }
- html.push(
- match[1],
- '',
- encodeEntities(match[2] + match[3]),
- ''
- );
- } else {
- html.push(
- match[1],
- encodeEntities(match[2] + match[3])
- );
- }
+ if (match[3]) { // mentions
+ entities.push({
+ _: 'messageEntityMention',
+ offset: matchIndex + match[1].length,
+ length: match[2].length + match[3].length
+ });
}
- else if (match[4]) { // URL & e-mail
- if (!options.noLinks) {
- if (emailRegExp.test(match[4])) {
- html.push(
- '',
- encodeEntities(match[4]),
- ''
- );
- } else {
- var url = false,
- protocol = match[5],
- tld = match[6],
- excluded = '';
-
- if (tld) {
- if (!protocol && (tld.substr(0, 4) === 'xn--' || Config.TLD.indexOf(tld.toLowerCase()) !== -1)) {
- protocol = 'http://';
- }
-
- if (protocol) {
- var balanced = checkBrackets(match[4]);
+ else if (match[4]) {
+ if (emailRegExp.test(match[4])) { // email
+ entities.push({
+ _: 'messageEntityEmail',
+ offset: matchIndex,
+ length: match[4].length
+ });
+ } else {
+ var url = false,
+ protocol = match[5],
+ tld = match[6],
+ excluded = '';
+
+ if (tld) { // URL
+ if (!protocol && (tld.substr(0, 4) === 'xn--' || Config.TLD.indexOf(tld.toLowerCase()) !== -1)) {
+ protocol = 'http://';
+ }
- if (balanced.length !== match[4].length) {
- excluded = match[4].substring(balanced.length);
- match[4] = balanced;
- }
+ if (protocol) {
+ var balanced = checkBrackets(match[4]);
- url = (match[5] ? '' : protocol) + match[4];
+ if (balanced.length !== match[4].length) {
+ excluded = match[4].substring(balanced.length);
+ match[4] = balanced;
}
- var tgMeMatch;
- if (tld == 'me' &&
- (tgMeMatch = url.match(/^https?:\/\/telegram\.me\/(.+)/))) {
- var path = tgMeMatch[1].split('/');
- switch (path[0]) {
- case 'joinchat':
- url = 'tg://join?invite=' + path[1];
- break;
- case 'addstickers':
- url = 'tg://addstickers?set=' + path[1];
- break;
- default:
- var domainQuery = path[0].split('?');
- url = 'tg://resolve?domain=' + domainQuery[0] + (domainQuery[1] ? '&' + domainQuery[1] : '');
- }
- }
- } else { // IP address
- url = (match[5] ? '' : 'http://') + match[4];
+ url = (match[5] ? '' : protocol) + match[4];
}
- if (url) {
- html.push(
- '',
- encodeEntities(match[4]),
- '',
- excluded
- );
-
- if (options.extractUrlEmbed &&
- !options.extractedUrlEmbed) {
- options.extractedUrlEmbed = findExternalEmbed(url);
+ var tgMeMatch;
+ if (tld == 'me' &&
+ (tgMeMatch = url.match(/^https?:\/\/telegram\.me\/(.+)/))) {
+ var path = tgMeMatch[1].split('/');
+ switch (path[0]) {
+ case 'joinchat':
+ url = 'tg://join?invite=' + path[1];
+ break;
+ case 'addstickers':
+ url = 'tg://addstickers?set=' + path[1];
+ break;
+ default:
+ var domainQuery = path[0].split('?');
+ url = 'tg://resolve?domain=' + domainQuery[0] + (domainQuery[1] ? '&' + domainQuery[1] : '');
}
- } else {
- html.push(encodeEntities(match[0]));
}
+ } else { // IP address
+ url = (match[5] ? '' : 'http://') + match[4];
+ }
+
+ if (url) {
+ entities.push({
+ _: 'messageEntityUrl',
+ offset: matchIndex,
+ length: match[4].length
+ });
}
- } else {
- html.push(encodeEntities(match[0]));
}
}
else if (match[7]) { // New line
- if (!options.noLinebreaks) {
- html.push('
');
- } else {
- html.push(' ');
- }
+ entities.push({
+ _: 'messageEntityLinebreak',
+ offset: matchIndex,
+ length: 1
+ });
}
- else if (match[8]) {
+ else if (match[8]) { // Emoji
if ((emojiCode = emojiMap[match[8]]) &&
(emojiCoords = getEmojiSpritesheetCoords(emojiCode))) {
- emojiTitle = encodeEntities(emojiData[emojiCode][1][0]);
- emojiFound = true;
- html.push(
- '',
- ':', emojiTitle, ':'
- );
- } else {
- html.push(encodeEntities(match[8]));
+ entities.push({
+ _: 'messageEntityEmoji',
+ offset: matchIndex,
+ length: match[0].length,
+ coords: emojiCoords,
+ title: emojiData[emojiCode][1][0]
+ });
+ }
+ }
+ else if (match[10]) { // Hashtag
+ entities.push({
+ _: 'messageEntityHashtag',
+ offset: matchIndex + match[9].length,
+ length: match[10].length
+ });
+ }
+ else if (match[12]) { // Bot command
+ entities.push({
+ _: 'messageEntityBotCommand',
+ offset: matchIndex + match[11].length,
+ length: 1 + match[12].length + (match[13] ? 1 + match[13].length : 0)
+ });
+ }
+ raw = raw.substr(match.index + match[0].length);
+ rawOffset += match.index + match[0].length;
+ }
+
+ if (entities.length) {
+ console.log('parse entities', text, entities.slice());
+ }
+
+ return entities;
+ }
+
+ function parseMarkdown (text, entities) {
+ if (text.indexOf('`') == -1) {
+ return text;
+ }
+ var raw = text;
+ var match;
+ var newText = [];
+ while (match = raw.match(markdownRegExp)) {
+ newText.push(raw.substr(0, match.index));
+
+ if (match[3]) { // pre
+ newText.push(match[1] + match[3] + match[4]);
+ entities.push({
+ _: 'messageEntityPre',
+ language: match[2] || '',
+ offset: match.index + match[1].length,
+ length: match[3].length
+ })
+ } else { // code
+ newText.push(match[5] + match[6]);
+ entities.push({
+ _: 'messageEntityCode',
+ offset: match.index + match[5].length,
+ length: match[6].length
+ })
+ }
+ raw = raw.substr(match.index + match[0].length);
+ }
+ newText.push(raw);
+ return newText.join('');
+ }
+
+ function mergeEntities (currentEntities, newEntities, fromApi) {
+ var totalEntities = newEntities.slice();
+
+ var i, len = currentEntities.length;
+ var j, len2 = newEntities.length;
+ var startJ = 0;
+ var curEntity, newEntity;
+ var start, end, cStart, cEnd, bad;
+ for (i = 0; i < len; i++) {
+ curEntity = currentEntities[i];
+ if (fromApi &&
+ curEntity._ != 'messageEntityLinebreak' &&
+ curEntity._ != 'messageEntityEmoji') {
+ continue;
+ }
+ // console.log('s', curEntity, newEntities);
+ start = curEntity.offset;
+ end = start + curEntity.length;
+ bad = false;
+ for (j = startJ; j < len2; j++) {
+ newEntity = newEntities[j];
+ cStart = newEntity.offset;
+ cEnd = cStart + newEntity.length;
+ if (cStart <= start) {
+ startJ = j;
+ }
+ if (start >= cStart && start < cEnd ||
+ end > cStart && end <= cEnd) {
+ // console.log('bad', curEntity, newEntity);
+ bad = true;
+ break;
}
+ if (cStart >= end) {
+ break;
+ }
+ }
+ if (bad) {
+ continue;
}
- else if (match[10]) {
- var contextUrl = !options.noLinks && siteHashtags[contextSite] || options.contextHashtag;
- if (contextUrl) {
+ totalEntities.push(curEntity);
+ }
+
+ totalEntities.sort(function (a, b) {
+ return a.offset - b.offset;
+ });
+
+ // console.log('merge', currentEntities, newEntities, totalEntities);
+
+ return totalEntities;
+ }
+
+ function wrapRichText (text, options) {
+ if (!text || !text.length) {
+ return '';
+ }
+
+ options = options || {};
+
+ var entities = options.entities,
+ contextSite = options.contextSite || 'Telegram',
+ contextExternal = contextSite != 'Telegram',
+ emojiFound = false;
+
+ if (entities === undefined) {
+ entities = parseEntities(text, options);
+ }
+
+ var i = 0;
+ var len = entities.length;
+ var entity;
+ var entityText;
+ var skipEntity;
+ var url;
+ var html = [];
+ var lastOffset = 0;
+ for (i = 0; i < len; i++) {
+ entity = entities[i];
+ if (entity.offset > lastOffset) {
+ html.push(
+ encodeEntities(text.substr(lastOffset, entity.offset - lastOffset))
+ );
+ }
+ else if (entity.offset < lastOffset) {
+ continue;
+ }
+ skipEntity = false;
+ 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(
+ '',
+ encodeEntities(entityText),
+ ''
+ );
+ break;
+
+ case 'messageEntityHashtag':
+ var contextUrl = !options.noLinks && siteHashtags[contextSite];
+ if (!contextUrl) {
+ skipEntity = true;
+ break;
+ }
+ var hashtag = entityText.substr(1);
html.push(
- encodeEntities(match[9]),
'',
- encodeEntities(match[10]),
+ encodeEntities(entityText),
''
);
- } else {
+ break;
+
+ case 'messageEntityEmail':
+ if (options.noLinks) {
+ skipEntity = true;
+ break;
+ }
+ html.push(
+ '',
+ encodeEntities(entityText),
+ ''
+ );
+ break;
+
+ case 'messageEntityUrl':
+ case 'messageEntityTextUrl':
+ if (options.noLinks) {
+ skipEntity = true;
+ break;
+ }
+ var url = entity.url || entityText;
+ if (!url.match(/^https?:\/\//i)) {
+ url = 'http://' + url;
+ }
+ var tgMeMatch;
+ if ((tgMeMatch = url.match(/^https?:\/\/telegram\.me\/(.+)/))) {
+ var path = tgMeMatch[1].split('/');
+ switch (path[0]) {
+ case 'joinchat':
+ url = 'tg://join?invite=' + path[1];
+ break;
+ case 'addstickers':
+ url = 'tg://addstickers?set=' + path[1];
+ break;
+ default:
+ var domainQuery = path[0].split('?');
+ url = 'tg://resolve?domain=' + domainQuery[0] + (domainQuery[1] ? '&' + domainQuery[1] : '');
+ }
+ }
html.push(
- encodeEntities(match[9]),
- encodeEntities(match[10])
+ '',
+ encodeEntities(entityText),
+ ''
);
- }
- }
- else if (match[12]) { // Bot commands
- if (!options.noLinks &&
- !options.noCommands &&
- !contextExternal) {
- var bot = match[13] || options.fromBot;
+ break;
+
+ case 'messageEntityLinebreak':
+ html.push(options.noLinebreaks ? ' ' : '
');
+ break;
+
+ case 'messageEntityEmoji':
+ html.push(
+ '',
+ ':', entity.title, ':'
+ );
+ emojiFound = true;
+ break;
+
+ case 'messageEntityBotCommand':
+ if (options.noLinks || options.noCommands || contextExternal) {
+ skipEntity = true;
+ break;
+ }
+ var command = entityText;
+ var bot, atPos;
+ if ((atPos = command.indexOf('@')) != -1) {
+ bot = command.substr(atPos);
+ command = command.substr(0, atPos);
+ } else {
+ bot = options.fromBot;
+ }
html.push(
- encodeEntities(match[11]),
'',
- encodeEntities('/' + match[12] + (match[13] ? '@' + match[13] : '')),
- '',
- encodeEntities(match[14])
+ encodeEntities(entityText),
+ ''
);
- } else {
+ break;
+
+ case 'messageEntityBold':
html.push(
- encodeEntities(match[0])
+ '',
+ encodeEntities(entityText),
+ ''
);
- }
- }
- raw = raw.substr(match.index + match[0].length);
- }
+ break;
- html.push(encodeEntities(raw));
+ case 'messageEntityItalic':
+ html.push(
+ '',
+ encodeEntities(entityText),
+ ''
+ );
+ break;
- // var timeDiff = tsNow() - start;
- // if (timeDiff > 1) {
- // console.log(dT(), 'wrap text', text.length, timeDiff);
- // }
+ case 'messageEntityCode':
+ html.push(
+ '',
+ encodeEntities(entityText),
+ '
'
+ );
+ break;
- text = $sanitize(html.join(''));
+ case 'messageEntityPre':
+ html.push(
+ '
',
+ encodeEntities(entityText),
+ '
'
+ );
+ break;
- // console.log(3, text, html);
+ default:
+ skipEntity = true;
+ }
+ if (!skipEntity) {
+ lastOffset = entity.offset + entity.length;
+ }
+ }
+ html.push(encodeEntities(text.substr(lastOffset)));
+
+ text = $sanitize(html.join(''));
if (emojiFound) {
text = text.replace(/\ufe0f|️|�|/g, '', text);
@@ -4930,53 +5148,6 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
return url;
}
- function findExternalEmbed(url) {
- var embedUrlMatches,
- result;
-
- if (embedUrlMatches = url.match(youtubeRegExp)) {
- return ['youtube', embedUrlMatches[1]];
- }
- if (embedUrlMatches = url.match(vimeoRegExp)) {
- return ['vimeo', embedUrlMatches[1]];
- }
- else if (embedUrlMatches = url.match(instagramRegExp)) {
- return ['instagram', embedUrlMatches[1]];
- }
- else if (embedUrlMatches = url.match(vineRegExp)) {
- return ['vine', embedUrlMatches[1]];
- }
- else if (embedUrlMatches = url.match(soundcloudRegExp)) {
- var badFolders = 'explore,upload,pages,terms-of-use,mobile,jobs,imprint'.split(',');
- var badSubfolders = 'sets'.split(',');
- if (badFolders.indexOf(embedUrlMatches[1]) == -1 &&
- badSubfolders.indexOf(embedUrlMatches[2]) == -1) {
- return ['soundcloud', embedUrlMatches[0]];
- }
- }
- else if (embedUrlMatches = url.match(spotifyRegExp)) {
- return ['spotify', embedUrlMatches[3].replace('/', ':')];
- }
-
- if (!Config.Modes.chrome_packed) { // Need external JS
- if (embedUrlMatches = url.match(twitterRegExp)) {
- return ['twitter', embedUrlMatches[0]];
- }
- else if (embedUrlMatches = url.match(facebookRegExp)) {
- if (embedUrlMatches[2]!= undefined){
- return ['facebook', "https://www.facebook.com/"+embedUrlMatches[2]+"/posts/"+embedUrlMatches[1]];
- }
- return ['facebook', embedUrlMatches[0]];
- }
- // Sorry, GPlus widget has no `xfbml.render` like callback and is too wide.
- // else if (embedUrlMatches = url.match(gplusRegExp)) {
- // return ['gplus', embedUrlMatches[0]];
- // }
- }
-
- return false;
- }
-
function wrapPlainText (text, options) {
if (emojiSupported) {
return text;
diff --git a/app/less/app.less b/app/less/app.less
index ebfa5efa..6223d3f5 100644
--- a/app/less/app.less
+++ b/app/less/app.less
@@ -2049,6 +2049,11 @@ a.im_message_fwd_photo {
word-wrap: break-word;
line-height: 150%;
}
+.im_message_text pre {
+ margin-bottom: 0;
+ max-height: 300px;
+ overflow: auto;
+}
.im_message_photo_caption,
.im_message_video_caption {
clear: both;
diff --git a/app/partials/desktop/message.html b/app/partials/desktop/message.html
index 5ef09e7e..fc3c5017 100644
--- a/app/partials/desktop/message.html
+++ b/app/partials/desktop/message.html
@@ -52,8 +52,8 @@
-
-
+
+
-
+