Browse Source

Supported server-side message entities

Also added basic markdown formatting for code

Closes #147
master
Igor Zhukov 9 years ago
parent
commit
edeba50f60
  1. 55
      app/js/directives.js
  2. 539
      app/js/services.js
  3. 5
      app/less/app.less
  4. 4
      app/partials/desktop/message.html
  5. 2
      app/partials/mobile/message.html

55
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() { .directive('myReplyMarkup', function() {
return { return {

539
app/js/services.js

@ -1736,6 +1736,12 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
selective: (apiMessage.reply_markup.flags & 4) > 0 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), inputPeer = AppPeersManager.getInputPeerByID(peerID),
flags = 0, flags = 0,
replyToMsgID = options.replyToMsgID, replyToMsgID = options.replyToMsgID,
entities = [],
message; message;
text = RichTextProcessor.parseMarkdown(text, entities);
if (historyStorage === undefined) { if (historyStorage === undefined) {
historyStorage = historiesStorage[peerID] = {count: null, history: [], pending: []}; historyStorage = historiesStorage[peerID] = {count: null, history: [], pending: []};
} }
@ -1777,6 +1786,7 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
message: text, message: text,
random_id: randomIDS, random_id: randomIDS,
reply_to_msg_id: replyToMsgID, reply_to_msg_id: replyToMsgID,
entities: entities,
pending: true pending: true
}; };
@ -1806,18 +1816,24 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
if (replyToMsgID) { if (replyToMsgID) {
flags |= 1; flags |= 1;
} }
if (entities.length) {
flags |= 8;
}
console.log(flags, entities);
MtpApiManager.invokeApi('messages.sendMessage', { MtpApiManager.invokeApi('messages.sendMessage', {
flags: flags, flags: flags,
peer: inputPeer, peer: inputPeer,
message: text, message: text,
random_id: randomID, random_id: randomID,
reply_to_msg_id: replyToMsgID reply_to_msg_id: replyToMsgID,
entities: entities
}, sentRequestOptions).then(function (updates) { }, sentRequestOptions).then(function (updates) {
if (updates._ == 'updateShortSentMessage') { if (updates._ == 'updateShortSentMessage') {
message.flags = updates.flags; message.flags = updates.flags;
message.date = updates.date; message.date = updates.date;
message.id = updates.id; message.id = updates.id;
message.media = updates.media; message.media = updates.media;
message.entities = updates.entities;
updates = { updates = {
_: 'updates', _: 'updates',
users: [], 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; 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 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 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 = { var siteHashtags = {
Telegram: '#/im?q=%23{1}', Telegram: '#/im?q=%23{1}',
Twitter: 'https://twitter.com/hashtag/{1}', Twitter: 'https://twitter.com/hashtag/{1}',
@ -4681,7 +4679,10 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
return { return {
wrapRichText: wrapRichText, wrapRichText: wrapRichText,
wrapPlainText: wrapPlainText wrapPlainText: wrapPlainText,
parseEntities: parseEntities,
parseMarkdown: parseMarkdown,
mergeEntities: mergeEntities
}; };
function getEmojiSpritesheetCoords(emojiCode) { function getEmojiSpritesheetCoords(emojiCode) {
@ -4699,71 +4700,44 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
return null; return null;
} }
function wrapRichText(text, options) { function parseEntities (text, options) {
if (!text || !text.length) {
return '';
}
options = options || {}; options = options || {};
var match, var match,
raw = text, raw = text,
html = [],
url, url,
contextSite = options.contextSite || 'Telegram', entities = [],
contextExternal = contextSite != 'Telegram', emojiCode,
emojiFound = false, emojiCoords,
emojiTitle, matchIndex,
emojiCoords; rawOffset = 0;
// var start = tsNow(); // var start = tsNow();
while ((match = raw.match(fullRegExp))) { while ((match = raw.match(fullRegExp))) {
html.push(encodeEntities(raw.substr(0, match.index))); matchIndex = rawOffset + match.index;
if (match[3]) { // telegram.me links if (match[3]) { // mentions
var contextUrl = !options.noLinks && siteMentions[contextSite]; entities.push({
if (contextUrl) { _: 'messageEntityMention',
var attr = ''; offset: matchIndex + match[1].length,
if (options.highlightUsername && length: match[2].length + match[3].length
options.highlightUsername.toLowerCase() == match[3].toLowerCase()) { });
attr = 'class="im_message_mymention"';
}
html.push(
match[1],
'<a ',
attr,
contextExternal ? ' target="_blank" ' : '',
' href="',
contextUrl.replace('{1}', encodeURIComponent(match[3])),
'">',
encodeEntities(match[2] + match[3]),
'</a>'
);
} else {
html.push(
match[1],
encodeEntities(match[2] + match[3])
);
}
} }
else if (match[4]) { // URL & e-mail else if (match[4]) {
if (!options.noLinks) { if (emailRegExp.test(match[4])) { // email
if (emailRegExp.test(match[4])) { entities.push({
html.push( _: 'messageEntityEmail',
'<a href="', offset: matchIndex,
encodeEntities('mailto:' + match[4]), length: match[4].length
'" target="_blank">', });
encodeEntities(match[4]),
'</a>'
);
} else { } else {
var url = false, var url = false,
protocol = match[5], protocol = match[5],
tld = match[6], tld = match[6],
excluded = ''; excluded = '';
if (tld) { if (tld) { // URL
if (!protocol && (tld.substr(0, 4) === 'xn--' || Config.TLD.indexOf(tld.toLowerCase()) !== -1)) { if (!protocol && (tld.substr(0, 4) === 'xn--' || Config.TLD.indexOf(tld.toLowerCase()) !== -1)) {
protocol = 'http://'; protocol = 'http://';
} }
@ -4800,109 +4774,353 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
} }
if (url) { if (url) {
html.push( entities.push({
'<a href="', _: 'messageEntityUrl',
encodeEntities(url), offset: matchIndex,
'" target="_blank">', length: match[4].length
encodeEntities(match[4]), });
'</a>', }
excluded }
); }
else if (match[7]) { // New line
entities.push({
_: 'messageEntityLinebreak',
offset: matchIndex,
length: 1
});
}
else if (match[8]) { // Emoji
if ((emojiCode = emojiMap[match[8]]) &&
(emojiCoords = getEmojiSpritesheetCoords(emojiCode))) {
if (options.extractUrlEmbed && entities.push({
!options.extractedUrlEmbed) { _: 'messageEntityEmoji',
options.extractedUrlEmbed = findExternalEmbed(url); offset: matchIndex,
length: match[0].length,
coords: emojiCoords,
title: emojiData[emojiCode][1][0]
});
} }
} else {
html.push(encodeEntities(match[0]));
} }
else if (match[10]) { // Hashtag
entities.push({
_: 'messageEntityHashtag',
offset: matchIndex + match[9].length,
length: match[10].length
});
} }
} else { else if (match[12]) { // Bot command
html.push(encodeEntities(match[0])); 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;
} }
else if (match[7]) { // New line
if (!options.noLinebreaks) { if (entities.length) {
html.push('<br/>'); console.log('parse entities', text, entities.slice());
} else {
html.push(' ');
} }
return entities;
} }
else if (match[8]) {
if ((emojiCode = emojiMap[match[8]]) &&
(emojiCoords = getEmojiSpritesheetCoords(emojiCode))) {
emojiTitle = encodeEntities(emojiData[emojiCode][1][0]); function parseMarkdown (text, entities) {
emojiFound = true; 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;
}
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( html.push(
'<span class="emoji emoji-', encodeEntities(text.substr(lastOffset, entity.offset - lastOffset))
emojiCoords.category,
'-',
(emojiIconSize * emojiCoords.column),
'-',
(emojiIconSize * emojiCoords.row),
'" ',
'title="',emojiTitle, '">',
':', emojiTitle, ':</span>'
); );
} else {
html.push(encodeEntities(match[8]));
} }
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"';
} }
else if (match[10]) {
var contextUrl = !options.noLinks && siteHashtags[contextSite] || options.contextHashtag;
if (contextUrl) {
html.push( html.push(
encodeEntities(match[9]),
'<a ', '<a ',
attr,
contextExternal ? ' target="_blank" ' : '', contextExternal ? ' target="_blank" ' : '',
' href="', ' href="',
contextUrl.replace('{1}', encodeURIComponent(match[10].substr(1))) contextUrl.replace('{1}', encodeURIComponent(username)),
'">',
encodeEntities(entityText),
'</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" ' : '',
'href="',
contextUrl.replace('{1}', encodeURIComponent(hashtag))
, ,
'">', '">',
encodeEntities(match[10]), encodeEntities(entityText),
'</a>' '</a>'
); );
} else { break;
case 'messageEntityEmail':
if (options.noLinks) {
skipEntity = true;
break;
}
html.push( html.push(
encodeEntities(match[9]), '<a href="',
encodeEntities(match[10]) encodeEntities('mailto:' + entityText),
'" target="_blank">',
encodeEntities(entityText),
'</a>'
); );
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] : '');
} }
} }
else if (match[12]) { // Bot commands
if (!options.noLinks &&
!options.noCommands &&
!contextExternal) {
var bot = match[13] || options.fromBot;
html.push( html.push(
encodeEntities(match[11]),
'<a href="', '<a href="',
encodeEntities('tg://bot_command?command=' + encodeURIComponent(match[12]) + (bot ? '&bot=' + encodeURIComponent(bot) : '')), encodeEntities(url),
'">', '" target="_blank">',
encodeEntities('/' + match[12] + (match[13] ? '@' + match[13] : '')), encodeEntities(entityText),
'</a>', '</a>'
encodeEntities(match[14])
); );
} else { break;
case 'messageEntityLinebreak':
html.push(options.noLinebreaks ? ' ' : '<br/>');
break;
case 'messageEntityEmoji':
html.push( html.push(
encodeEntities(match[0]) '<span class="emoji emoji-',
entity.coords.category,
'-',
(emojiIconSize * entity.coords.column),
'-',
(emojiIconSize * entity.coords.row),
'" ',
'title="', entity.title, '">',
':', entity.title, ':</span>'
); );
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;
} }
raw = raw.substr(match.index + match[0].length); html.push(
} '<a href="',
encodeEntities('tg://bot_command?command=' + encodeURIComponent(command) + (bot ? '&bot=' + encodeURIComponent(bot) : '')),
'">',
encodeEntities(entityText),
'</a>'
);
break;
html.push(encodeEntities(raw)); case 'messageEntityBold':
html.push(
'<strong>',
encodeEntities(entityText),
'</strong>'
);
break;
// var timeDiff = tsNow() - start; case 'messageEntityItalic':
// if (timeDiff > 1) { html.push(
// console.log(dT(), 'wrap text', text.length, timeDiff); '<em>',
// } encodeEntities(entityText),
'</em>'
);
break;
text = $sanitize(html.join('')); case 'messageEntityCode':
html.push(
'<code>',
encodeEntities(entityText),
'</code>'
);
break;
// console.log(3, text, html); case 'messageEntityPre':
html.push(
'<pre><code',(entity.language ? ' class="language-' + encodeEntities(entity.language) +'"' : ''),'>',
encodeEntities(entityText),
'</code></pre>'
);
break;
default:
skipEntity = true;
}
if (!skipEntity) {
lastOffset = entity.offset + entity.length;
}
}
html.push(encodeEntities(text.substr(lastOffset)));
text = $sanitize(html.join(''));
if (emojiFound) { if (emojiFound) {
text = text.replace(/\ufe0f|&#65039;|&#65533;|&#8205;/g, '', text); text = text.replace(/\ufe0f|&#65039;|&#65533;|&#8205;/g, '', text);
@ -4930,53 +5148,6 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils'])
return url; 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) { function wrapPlainText (text, options) {
if (emojiSupported) { if (emojiSupported) {
return text; return text;

5
app/less/app.less

@ -2049,6 +2049,11 @@ a.im_message_fwd_photo {
word-wrap: break-word; word-wrap: break-word;
line-height: 150%; line-height: 150%;
} }
.im_message_text pre {
margin-bottom: 0;
max-height: 300px;
overflow: auto;
}
.im_message_photo_caption, .im_message_photo_caption,
.im_message_video_caption { .im_message_video_caption {
clear: both; clear: both;

4
app/partials/desktop/message.html

@ -52,8 +52,8 @@
</div> </div>
</div> </div>
<div class="im_message_text" ng-if="::historyMessage.message.length || false" ng-bind-html="::historyMessage.richMessage" dir="auto"></div> <div class="im_message_text" ng-if="::historyMessage.message.length || false" my-message-text="::historyMessage" dir="auto"></div>
<!-- <div class="im_message_external_embed_wrap" ng-if="::historyMessage.richUrlEmbed || false" my-external-embed="historyMessage.richUrlEmbed"></div> -->
<div ng-if="::historyMessage.media || historyMessage.id < 0 ? true : false" class="im_message_media" ng-switch="historyMessage.media._"> <div ng-if="::historyMessage.media || historyMessage.id < 0 ? true : false" class="im_message_media" ng-switch="historyMessage.media._">
<div ng-switch-when="messageMediaPhoto" my-message-photo="historyMessage.media" message-id="historyMessage.id"></div> <div ng-switch-when="messageMediaPhoto" my-message-photo="historyMessage.media" message-id="historyMessage.id"></div>

2
app/partials/mobile/message.html

@ -62,7 +62,7 @@
</div> </div>
<div class="im_message_text" ng-if="::historyMessage.message.length || false" ng-bind-html="::historyMessage.richMessage" dir="auto"></div> <div class="im_message_text" ng-if="::historyMessage.message.length || false" my-message-text="::historyMessage" dir="auto"></div>
</div> </div>
</div> </div>

Loading…
Cancel
Save