Backed out changeset a8855a65a9c7 (Bug 955642) for tooltip regression. rs=bustage-fix DONTBUILD
authoraleth <aleth@instantbird.org>
Fri, 11 Nov 2016 19:36:49 +0100
changeset 20687 b10948d0a008d21ccf941acc2bca0f44973294fd
parent 20686 3bbc1b01ea13a5b280e635d0c3109e35bbc7aa46
child 20688 228b399d8933abaa10c96f7616f5f98fb452821e
push id12517
push useraleth@instantbird.org
push dateFri, 11 Nov 2016 18:38:00 +0000
treeherdercomm-central@b10948d0a008 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbustage-fix
bugs955642
backs outa8855a65a9c7acc41a30a5b323cad62ac6a44438
Backed out changeset a8855a65a9c7 (Bug 955642) for tooltip regression. rs=bustage-fix DONTBUILD
chat/protocols/twitter/twitter.js
--- a/chat/protocols/twitter/twitter.js
+++ b/chat/protocols/twitter/twitter.js
@@ -148,47 +148,38 @@ var GenericTwitterConversation = {
   systemMessage: function(aMessage, aIsError, aDate) {
     let flags = {system: true};
     if (aIsError)
       flags.error = true;
     if (aDate)
       flags.time = aDate;
     this.writeMessage("twitter.com", aMessage, flags);
   },
-  onSentCallback: function(aMsg, aData) {
+  onSentCallback: function(aData) {
     // The conversation may have been unitialized in the time it takes for
     // the async callback to fire.  Use `_observers` as a proxy for uninit'd.
     if (!this._observers)
       return;
 
     let tweet = JSON.parse(aData);
-    // The OTR extension requires that the protocol not modify the message
-    // (see the notes at `imIOutgoingMessage`).  That's the contract we made.
-    // Unfortunately, Twitter trims tweets and substitutes links.
-    tweet.text = aMsg;
     this.displayMessages([tweet]);
   },
-  prepareForDisplaying: function(aMsg) {
-    if (!this._tweets.has(aMsg.id))
-      return;
-    let tweet = this._tweets.get(aMsg.id)._tweet;
-    this._tweets.delete(aMsg.id);
-
-    let text = aMsg.displayMessage;
+  parseTweet: function(aTweet) {
+    let text = aTweet.text;
     let entities = {};
 
     // Handle retweets: retweeted_status contains the object for the original
     // tweet that is being retweeted.
     // If the retweet prefix ("RT @<username>: ") causes the tweet to be over
     // 140 characters, ellipses will be added. In this case, we want to get
     // the FULL text from the original tweet and update the entities to match.
     // Note: the truncated flag is not always set correctly by twitter, so we
     // always make use of the original tweet.
-    if ("retweeted_status" in tweet) {
-      let retweet = tweet["retweeted_status"];
+    if ("retweeted_status" in aTweet) {
+      let retweet = aTweet["retweeted_status"];
       let retweetText, retweetEntities = {};
 
       if ("extended_tweet" in retweet) {
         // Note that if an extended tweet is retweeted, only the
         // retweeted_status part will be extended, not the tweet itself.
         let extended = retweet.extended_tweet;
         retweetText = extended.full_text;
         if ("entities" in extended)
@@ -198,18 +189,16 @@ var GenericTwitterConversation = {
         retweetText = retweet.text;
         if ("entities" in retweet)
           retweetEntities = retweet.entities;
       }
 
       // We're going to take portions of the retweeted status and replace parts
       // of the original tweet, the retweeted status prepends the original
       // status with "RT @<username>: ", we need to keep the prefix.
-      // Note: this doesn't play nice with extensions that may have altered
-      // `text` to this point, but at least OTR doesn't act on `isChat`.
       let offset = text.indexOf(": ") + 2;
       text = text.slice(0, offset) + retweetText;
 
       // Keep any entities that refer to the prefix (we can refer directly to
       // aTweet for these since they are not edited).
       if ("entities" in aTweet) {
         for (let type in aTweet.entities) {
           let filteredEntities =
@@ -230,59 +219,105 @@ var GenericTwitterConversation = {
           retweetEntities[type].map(function(aEntity) {
             let entity = Object.create(aEntity);
             // Add the offset to the indices to account for the prefix.
             entity.indices = entity.indices.map(i => i + offset);
             return entity;
           })
         );
       }
-    } else if ("extended_tweet" in tweet) {
+    } else if ("extended_tweet" in aTweet) {
       // Bare bones extended tweet handling.
-      let extended = tweet.extended_tweet;
+      let extended = aTweet.extended_tweet;
       text = extended.full_text;
       if ("entities" in extended)
         entities = extended.entities;
     } else {
       // For non-retweets, we just want to use the entities that are given.
-      if ("entities" in tweet)
-        entities = tweet.entities;
+      if ("entities" in aTweet)
+        entities = aTweet.entities;
     }
 
     this._account.LOG("Tweet: " + text);
 
-    aMsg.displayMessage = twttr.txt.autoLink(text, {
-      usernameClass: "ib-person",
-      // Pass in the url entities so the t.co links are replaced.
-      urlEntities: tweet.entities.urls.map(function(u) {
-        let o = Object.assign(u);
-        // But remove the indices so they apply in the face of modifications.
-        delete o.indices;
-        return o;
-      })
-    });
+    if (Object.keys(entities).length) {
+      /* entArray is an array of entities ready to be replaced in the tweet,
+       * each entity contains:
+       *  - start: the start index of the entity inside the tweet,
+       *  - end: the end index of the entity inside the tweet,
+       *  - str: the string that should be replaced inside the tweet,
+       *  - href: the url (href attribute) of the created link tag,
+       *  - [optional] text: the text to display for the link,
+       *     The original string (str) will be used if this is not set.
+       *  - [optional] title: the title attribute for the link.
+       */
+      let entArray = [];
+      if ("hashtags" in entities && Array.isArray(entities.hashtags)) {
+        entArray = entArray.concat(entities.hashtags.map(h => ({
+          start: h.indices[0],
+          end: h.indices[1],
+          str: "#" + h.text,
+          href: "https://twitter.com/#!/search?q=%23" + h.text})));
+      }
+      if ("urls" in entities && Array.isArray(entities.urls)) {
+        entArray = entArray.concat(entities.urls.map(u => ({
+          start: u.indices[0],
+          end: u.indices[1],
+          str: u.url,
+          text: u.display_url || u.url,
+          href: u.expanded_url || u.url})));
+      }
+      if ("user_mentions" in entities &&
+          Array.isArray(entities.user_mentions)) {
+        entArray = entArray.concat(entities.user_mentions.map(um => ({
+          start: um.indices[0],
+          end: um.indices[1],
+          str: "@" + um.screen_name,
+          text: '@<span class="ib-person">' + um.screen_name + "</span>",
+          title: um.name,
+          href: "https://twitter.com/" + um.screen_name})));
+      }
+      entArray.sort((a, b) => a.start - b.start);
+      let offset = 0;
+      for (let entity of entArray) {
+        let str = text.substring(offset + entity.start, offset + entity.end);
+        if (str[0] == "\uFF20") // @ - unicode character similar to @
+          str = "@" + str.substring(1);
+        if (str[0] == "\uFF03") // # - unicode character similar to #
+          str = "#" + str.substring(1);
+        if (str.toLowerCase() != entity.str.toLowerCase())
+          continue;
 
-    GenericConversationPrototype.prepareForDisplaying.apply(this, arguments);
+        let html = "<a href=\"" + entity.href + "\"";
+        if ("title" in entity)
+          html += " title=\"" + entity.title + "\"";
+        html += ">" + ("text" in entity ? entity.text : entity.str) + "</a>";
+        text = text.slice(0, offset + entity.start) + html +
+               text.slice(offset + entity.end);
+        offset += html.length - (entity.end - entity.start);
+      }
+    }
+
+    return text;
   },
   displayTweet: function(aTweet, aUser) {
     let name = aUser.screen_name;
+    let text = this.parseTweet(aTweet);
 
     let flags = name == this.nick ? {outgoing: true} : {incoming: true};
     flags.time = Math.round(new Date(aTweet.created_at) / 1000);
     flags._iconURL = aUser.profile_image_url;
     if (aTweet.delayed)
       flags.delayed = true;
     if (aTweet.entities && aTweet.entities.user_mentions &&
         Array.isArray(aTweet.entities.user_mentions) &&
         aTweet.entities.user_mentions.some(mention => mention.screen_name == this.nick))
       flags.containsNick = true;
 
-    let tweet = new Tweet(aTweet, name, aTweet.text, flags);
-    this._tweets.set(tweet.id, tweet);
-    tweet.conversation = this;
+    (new Tweet(aTweet, name, text, flags)).conversation = this;
   },
   _parseError: function(aData) {
     let error = "";
     try {
       let data = JSON.parse(aData);
       if ("error" in data)
         error = data.error;
       else if ("errors" in data)
@@ -309,19 +344,16 @@ function TimelineConversation(aAccount)
     this._ensureParticipantExists(names.get(id_str));
 
   // If the user's info has already been received, update the timeline topic.
   if (aAccount._userInfo.has(aAccount.name)) {
     let userInfo = aAccount._userInfo.get(aAccount.name);
     if ("description" in userInfo)
       this.setTopic(userInfo.description, aAccount.name, true);
   }
-
-  // Store messages by message id.
-  this._tweets = new Map();
 }
 TimelineConversation.prototype = {
   __proto__: GenericConvChatPrototype,
   unInit: function() {
     delete this._account._timeline;
     GenericConvChatPrototype.unInit.call(this);
   },
   inReplyToStatusId: null,
@@ -353,18 +385,17 @@ TimelineConversation.prototype = {
                            aTweet.text), true);
     }, this);
   },
   sendMsg: function(aMsg) {
     if (this.getTweetLength(aMsg) > kMaxMessageLength) {
       this.systemMessage(_("error.tooLong"), true);
       throw Cr.NS_ERROR_INVALID_ARG;
     }
-    this._account.tweet(aMsg, this.inReplyToStatusId,
-                        this.onSentCallback.bind(this, aMsg),
+    this._account.tweet(aMsg, this.inReplyToStatusId, this.onSentCallback,
                         function(aException, aData) {
       let error = this._parseError(aData);
       this.systemMessage(_("error.general", error, aMsg), true);
     }, this);
     this.sendTyping("");
   },
   like: function(aTweet, aRemoveLike = false) {
     this._account.like(aTweet, aRemoveLike, function() {
@@ -427,25 +458,21 @@ TimelineConversation.prototype = {
       this._account.setUserDescription(aTopic);
   }
 };
 Object.assign(TimelineConversation.prototype, GenericTwitterConversation);
 
 function DirectMessageConversation(aAccount, aName)
 {
   this._init(aAccount, aName);
-
-  // Store messages by message id.
-  this._tweets = new Map();
 }
 DirectMessageConversation.prototype = {
   __proto__: GenericConvIMPrototype,
   sendMsg: function(aMsg) {
-    this._account.directMessage(aMsg, this.name,
-                                this.onSentCallback.bind(this, aMsg),
+    this._account.directMessage(aMsg, this.name, this.onSentCallback,
                                 function(aException, aData) {
       let error = this._parseError(aData);
       this.systemMessage(_("error.general", error, aMsg), true);
     }, this);
   },
   displayMessages: function(aMessages) {
     let account = this._account;
     for (let tweet of aMessages) {