Bug 1176280 - make links in Hello chat clickable, r=mikedeboer, r=gerv for license.html changes
authorDan Mosedale <dmose@meer.net>
Mon, 03 Aug 2015 14:53:16 -0700
changeset 287590 ece4d8f79cfc08491e31ad84acecd1c49a951b04
parent 287589 18f9c283b687558d67ad0e33d2236850c8d5c4d4
child 287591 d192b58f439bac3f982aeec3cad74b67972b648d
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-beta@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmikedeboer, gerv
bugs1176280
milestone42.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1176280 - make links in Hello chat clickable, r=mikedeboer, r=gerv for license.html changes
browser/components/loop/.eslintignore
browser/components/loop/content/conversation.html
browser/components/loop/content/shared/js/linkifiedTextView.js
browser/components/loop/content/shared/js/linkifiedTextView.jsx
browser/components/loop/content/shared/js/textChatView.js
browser/components/loop/content/shared/js/textChatView.jsx
browser/components/loop/content/shared/js/urlRegExps.js
browser/components/loop/jar.mn
browser/components/loop/standalone/content/index.html
browser/components/loop/test/karma/karma.coverage.shared_standalone.js
browser/components/loop/test/shared/index.html
browser/components/loop/test/shared/linkifiedTextView_test.js
browser/components/loop/test/shared/textChatView_test.js
browser/components/loop/ui/index.html
browser/components/loop/ui/ui-showcase.js
browser/components/loop/ui/ui-showcase.jsx
toolkit/content/license.html
--- a/browser/components/loop/.eslintignore
+++ b/browser/components/loop/.eslintignore
@@ -17,13 +17,14 @@ test/node_modules
 # These are generated react files that we don't need to check
 content/js/contacts.js
 content/js/conversation.js
 content/js/conversationViews.js
 content/js/panel.js
 content/js/roomViews.js
 content/js/feedbackViews.js
 content/shared/js/textChatView.js
+content/shared/js/linkifiedTextView.js
 content/shared/js/views.js
 standalone/content/js/fxOSMarketplace.js
 standalone/content/js/standaloneRoomViews.js
 standalone/content/js/webapp.js
 ui/ui-showcase.js
--- a/browser/components/loop/content/conversation.html
+++ b/browser/components/loop/content/conversation.html
@@ -34,16 +34,18 @@
     <script type="text/javascript" src="loop/shared/js/conversationStore.js"></script>
     <script type="text/javascript" src="loop/shared/js/roomStates.js"></script>
     <script type="text/javascript" src="loop/shared/js/fxOSActiveRoomStore.js"></script>
     <script type="text/javascript" src="loop/shared/js/activeRoomStore.js"></script>
     <script type="text/javascript" src="loop/shared/js/views.js"></script>
     <script type="text/javascript" src="loop/js/feedbackViews.js"></script>
     <script type="text/javascript" src="loop/shared/js/textChatStore.js"></script>
     <script type="text/javascript" src="loop/shared/js/textChatView.js"></script>
+    <script type="text/javascript" src="loop/shared/js/linkifiedTextView.js"></script>
+    <script type="text/javascript" src="loop/shared/js/urlRegExps.js"></script>
     <script type="text/javascript" src="loop/js/conversationViews.js"></script>
     <script type="text/javascript" src="loop/shared/js/websocket.js"></script>
     <script type="text/javascript" src="loop/js/conversationAppStore.js"></script>
     <script type="text/javascript" src="loop/js/client.js"></script>
     <script type="text/javascript" src="loop/js/conversationViews.js"></script>
     <script type="text/javascript" src="loop/js/roomStore.js"></script>
     <script type="text/javascript" src="loop/js/roomViews.js"></script>
     <script type="text/javascript" src="loop/js/conversation.js"></script>
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/js/linkifiedTextView.js
@@ -0,0 +1,114 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var loop = loop || {};
+loop.shared = loop.shared || {};
+loop.shared.views = loop.shared.views || {};
+loop.shared.views.LinkifiedTextView = (function(mozL10n) {
+  "use strict";
+
+  /**
+   * Given a rawText property, renderer a version of that text with any
+   * links starting with http://, https://, or ftp:// as actual clickable
+   * links inside a <p> container.
+   */
+  var LinkifiedTextView = React.createClass({displayName: "LinkifiedTextView",
+    propTypes: {
+      // Call this instead of allowing the default <a> click semantics, if
+      // given.  Also causes sendReferrer and suppressTarget attributes to be
+      // ignored.
+      linkClickHandler: React.PropTypes.func,
+      // The text to be linkified.
+      rawText: React.PropTypes.string.isRequired,
+      // Should the links send a referrer?  Defaults to false.
+      sendReferrer: React.PropTypes.bool,
+      // Should we suppress target="_blank" on the link? Defaults to false.
+      // Mostly for testing use.
+      suppressTarget: React.PropTypes.bool
+    },
+
+    mixins: [
+      React.addons.PureRenderMixin
+    ],
+
+    _handleClickEvent: function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+
+      this.props.linkClickHandler(e.currentTarget.href);
+    },
+
+    _generateLinkAttributes: function(href) {
+      var linkAttributes = {
+        href: href
+      };
+
+      if (this.props.linkClickHandler) {
+        linkAttributes.onClick = this._handleClickEvent;
+
+        // if this is specified, we short-circuit return to avoid unnecessarily
+        // creating target and rel attributes.
+        return linkAttributes;
+      }
+
+      if (!this.props.suppressTarget) {
+        linkAttributes.target = "_blank";
+      }
+
+      if (!this.props.sendReferrer) {
+        linkAttributes.rel = "noreferrer";
+      }
+
+      return linkAttributes;
+    },
+
+    /**                                                              a
+     * Parse the given string into an array of strings and React <a> elements
+     * in the order in which they should be rendered (i.e. FIFO).
+     *
+     * @param {String} s the raw string to be parsed
+     *
+     * @returns {Array} of strings and React <a> elements in order.
+     */
+    parseStringToElements: function(s) {
+      var elements = [];
+      var result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
+      var reactElementsCounter = 0; // For giving keys to each ReactElement.
+
+      while (result) {
+        // If there's text preceding the first link, push it onto the array
+        // and update the string pointer.
+        if (result.index) {
+          elements.push(s.substr(0, result.index));
+          s = s.substr(result.index);
+        }
+
+        // Push the first link itself, and advance the string pointer again.
+        elements.push(
+          React.createElement("a", React.__spread({},   this._generateLinkAttributes(result[0]) , 
+            {key: reactElementsCounter++}), 
+            result[0]
+          )
+        );
+        s = s.substr(result[0].length);
+
+        // Check for another link, and perhaps continue...
+        result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
+      }
+
+      if (s) {
+        elements.push(s);
+      }
+
+      return elements;
+    },
+
+    render: function () {
+      return ( React.createElement("p", null,  this.parseStringToElements(this.props.rawText) ) );
+    }
+  });
+
+  return LinkifiedTextView;
+
+})(navigator.mozL10n || document.mozL10n);
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/js/linkifiedTextView.jsx
@@ -0,0 +1,114 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var loop = loop || {};
+loop.shared = loop.shared || {};
+loop.shared.views = loop.shared.views || {};
+loop.shared.views.LinkifiedTextView = (function(mozL10n) {
+  "use strict";
+
+  /**
+   * Given a rawText property, renderer a version of that text with any
+   * links starting with http://, https://, or ftp:// as actual clickable
+   * links inside a <p> container.
+   */
+  var LinkifiedTextView = React.createClass({
+    propTypes: {
+      // Call this instead of allowing the default <a> click semantics, if
+      // given.  Also causes sendReferrer and suppressTarget attributes to be
+      // ignored.
+      linkClickHandler: React.PropTypes.func,
+      // The text to be linkified.
+      rawText: React.PropTypes.string.isRequired,
+      // Should the links send a referrer?  Defaults to false.
+      sendReferrer: React.PropTypes.bool,
+      // Should we suppress target="_blank" on the link? Defaults to false.
+      // Mostly for testing use.
+      suppressTarget: React.PropTypes.bool
+    },
+
+    mixins: [
+      React.addons.PureRenderMixin
+    ],
+
+    _handleClickEvent: function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+
+      this.props.linkClickHandler(e.currentTarget.href);
+    },
+
+    _generateLinkAttributes: function(href) {
+      var linkAttributes = {
+        href: href
+      };
+
+      if (this.props.linkClickHandler) {
+        linkAttributes.onClick = this._handleClickEvent;
+
+        // if this is specified, we short-circuit return to avoid unnecessarily
+        // creating target and rel attributes.
+        return linkAttributes;
+      }
+
+      if (!this.props.suppressTarget) {
+        linkAttributes.target = "_blank";
+      }
+
+      if (!this.props.sendReferrer) {
+        linkAttributes.rel = "noreferrer";
+      }
+
+      return linkAttributes;
+    },
+
+    /**                                                              a
+     * Parse the given string into an array of strings and React <a> elements
+     * in the order in which they should be rendered (i.e. FIFO).
+     *
+     * @param {String} s the raw string to be parsed
+     *
+     * @returns {Array} of strings and React <a> elements in order.
+     */
+    parseStringToElements: function(s) {
+      var elements = [];
+      var result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
+      var reactElementsCounter = 0; // For giving keys to each ReactElement.
+
+      while (result) {
+        // If there's text preceding the first link, push it onto the array
+        // and update the string pointer.
+        if (result.index) {
+          elements.push(s.substr(0, result.index));
+          s = s.substr(result.index);
+        }
+
+        // Push the first link itself, and advance the string pointer again.
+        elements.push(
+          <a { ...this._generateLinkAttributes(result[0]) }
+            key={reactElementsCounter++}>
+            {result[0]}
+          </a>
+        );
+        s = s.substr(result[0].length);
+
+        // Check for another link, and perhaps continue...
+        result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
+      }
+
+      if (s) {
+        elements.push(s);
+      }
+
+      return elements;
+    },
+
+    render: function () {
+      return ( <p>{ this.parseStringToElements(this.props.rawText) }</p> );
+    }
+  });
+
+  return LinkifiedTextView;
+
+})(navigator.mozL10n || document.mozL10n);
--- a/browser/components/loop/content/shared/js/textChatView.js
+++ b/browser/components/loop/content/shared/js/textChatView.js
@@ -51,19 +51,25 @@ loop.shared.views.chat = (function(mozL1
       var classes = React.addons.classSet({
         "text-chat-entry": true,
         "received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED,
         "sent": this.props.type === CHAT_MESSAGE_TYPES.SENT,
         "special": this.props.type === CHAT_MESSAGE_TYPES.SPECIAL,
         "room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
       });
 
+      var optionalProps = {};
+      if (navigator.mozLoop) {
+        optionalProps.linkClickHandler = navigator.mozLoop.openURL;
+      }
+
       return (
         React.createElement("div", {className: classes}, 
-          React.createElement("p", null, this.props.message), 
+          React.createElement(sharedViews.LinkifiedTextView, React.__spread({},  optionalProps, 
+            {rawText: this.props.message})), 
           React.createElement("span", {className: "text-chat-arrow"}), 
           this.props.showTimestamp ? this._renderTimestamp() : null
         )
       );
     }
   });
 
   var TextChatRoomName = React.createClass({displayName: "TextChatRoomName",
--- a/browser/components/loop/content/shared/js/textChatView.jsx
+++ b/browser/components/loop/content/shared/js/textChatView.jsx
@@ -51,19 +51,25 @@ loop.shared.views.chat = (function(mozL1
       var classes = React.addons.classSet({
         "text-chat-entry": true,
         "received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED,
         "sent": this.props.type === CHAT_MESSAGE_TYPES.SENT,
         "special": this.props.type === CHAT_MESSAGE_TYPES.SPECIAL,
         "room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
       });
 
+      var optionalProps = {};
+      if (navigator.mozLoop) {
+        optionalProps.linkClickHandler = navigator.mozLoop.openURL;
+      }
+
       return (
         <div className={classes}>
-          <p>{this.props.message}</p>
+          <sharedViews.LinkifiedTextView {...optionalProps}
+            rawText={this.props.message} />
           <span className="text-chat-arrow" />
           {this.props.showTimestamp ? this._renderTimestamp() : null}
         </div>
       );
     }
   });
 
   var TextChatRoomName = React.createClass({
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/js/urlRegExps.js
@@ -0,0 +1,72 @@
+// This is derived from Diego Perini's code,
+// currently available at https://gist.github.com/dperini/729294
+
+// Regular Expression for URL validation
+//
+// Original Author: Diego Perini
+// License: MIT
+//
+// Copyright (c) 2010-2013 Diego Perini (http://www.iport.it)
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+var loop = loop || {};
+loop.shared = loop.shared || {};
+loop.shared.urlRegExps = (function() {
+
+  "use strict";
+
+  // Some https://wiki.mozilla.org/Loop/Development/RegExpDebugging for tools
+  // if you need to debug changes to this:
+
+  var fullUrlMatch = new RegExp(
+    // Protocol identifier.
+    "(?:(?:https?|ftp)://)" +
+      // User:pass authentication.
+    "((?:\\S+(?::\\S*)?@)?" +
+    "(?:" +
+      // IP address dotted notation octets:
+      // excludes loopback network 0.0.0.0,
+      // excludes reserved space >= 224.0.0.0,
+      // excludes network & broadcast addresses,
+      // (first & last IP address of each class).
+    "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
+    "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
+    "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
+    "|" +
+      // Host name.
+    "(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)" +
+      // Domain name.
+    "(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*" +
+      // TLD identifier.
+    "(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))" +
+      // Port number.
+    "(?::\\d{2,5})?" +
+      // Resource path.
+    "(?:[/?#]\\S*)?)", "i");
+
+  return {
+    fullUrlMatch: fullUrlMatch
+  };
+
+})();
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -81,18 +81,20 @@ browser.jar:
   content/browser/loop/shared/js/roomStates.js          (content/shared/js/roomStates.js)
   content/browser/loop/shared/js/fxOSActiveRoomStore.js (content/shared/js/fxOSActiveRoomStore.js)
   content/browser/loop/shared/js/activeRoomStore.js     (content/shared/js/activeRoomStore.js)
   content/browser/loop/shared/js/dispatcher.js          (content/shared/js/dispatcher.js)
   content/browser/loop/shared/js/models.js              (content/shared/js/models.js)
   content/browser/loop/shared/js/mixins.js              (content/shared/js/mixins.js)
   content/browser/loop/shared/js/otSdkDriver.js         (content/shared/js/otSdkDriver.js)
   content/browser/loop/shared/js/views.js               (content/shared/js/views.js)
+  content/browser/loop/shared/js/linkifiedTextView.js   (content/shared/js/linkifiedTextView.js)
   content/browser/loop/shared/js/textChatStore.js       (content/shared/js/textChatStore.js)
   content/browser/loop/shared/js/textChatView.js        (content/shared/js/textChatView.js)
+  content/browser/loop/shared/js/urlRegExps.js          (content/shared/js/urlRegExps.js)
   content/browser/loop/shared/js/utils.js               (content/shared/js/utils.js)
   content/browser/loop/shared/js/validate.js            (content/shared/js/validate.js)
   content/browser/loop/shared/js/websocket.js           (content/shared/js/websocket.js)
 
   # Shared libs
 #ifdef DEBUG
   content/browser/loop/shared/libs/react-0.12.2.js    (content/shared/libs/react-0.12.2.js)
 #else
--- a/browser/components/loop/standalone/content/index.html
+++ b/browser/components/loop/standalone/content/index.html
@@ -139,19 +139,20 @@
     <script type="text/javascript" src="shared/js/dispatcher.js"></script>
     <script type="text/javascript" src="shared/js/websocket.js"></script>
     <script type="text/javascript" src="shared/js/otSdkDriver.js"></script>
     <script type="text/javascript" src="shared/js/store.js"></script>
     <script type="text/javascript" src="shared/js/roomStates.js"></script>
     <script type="text/javascript" src="shared/js/fxOSActiveRoomStore.js"></script>
     <script type="text/javascript" src="shared/js/activeRoomStore.js"></script>
     <script type="text/javascript" src="shared/js/views.js"></script>
-    <script type="text/javascript" src="shared/js/feedbackViews.js"></script>
     <script type="text/javascript" src="shared/js/textChatStore.js"></script>
     <script type="text/javascript" src="shared/js/textChatView.js"></script>
+    <script type="text/javascript" src="shared/js/urlRegExps.js"></script>
+    <script type="text/javascript" src="shared/js/linkifiedTextView.js"></script>
     <script type="text/javascript" src="js/standaloneAppStore.js"></script>
     <script type="text/javascript" src="js/standaloneClient.js"></script>
     <script type="text/javascript" src="js/standaloneMozLoop.js"></script>
     <script type="text/javascript" src="js/fxOSMarketplace.js"></script>
     <script type="text/javascript" src="js/standaloneRoomViews.js"></script>
     <script type="text/javascript" src="js/standaloneMetricsStore.js"></script>
     <script type="text/javascript" src="js/webapp.js"></script>
 
--- a/browser/components/loop/test/karma/karma.coverage.shared_standalone.js
+++ b/browser/components/loop/test/karma/karma.coverage.shared_standalone.js
@@ -29,16 +29,18 @@ module.exports = function(config) {
     "content/shared/js/otSdkDriver.js",
     "content/shared/js/roomStates.js",
     "content/shared/js/fxOSActiveRoomStore.js",
     "content/shared/js/activeRoomStore.js",
     "content/shared/js/conversationStore.js",
     "content/shared/js/views.js",
     "content/shared/js/textChatStore.js",
     "content/shared/js/textChatView.js",
+    "content/shared/js/urlRegExps.js",
+    "content/shared/js/linkifiedTextView.js",
     "standalone/content/js/multiplexGum.js",
     "standalone/content/js/standaloneAppStore.js",
     "standalone/content/js/standaloneClient.js",
     "standalone/content/js/standaloneMozLoop.js",
     "standalone/content/js/fxOSMarketplace.js",
     "standalone/content/js/standaloneRoomViews.js",
     "standalone/content/js/standaloneMetricsStore.js",
     "standalone/content/js/webapp.js",
--- a/browser/components/loop/test/shared/index.html
+++ b/browser/components/loop/test/shared/index.html
@@ -58,16 +58,18 @@
   <script src="../../content/shared/js/store.js"></script>
   <script src="../../content/shared/js/roomStates.js"></script>
   <script src="../../content/shared/js/fxOSActiveRoomStore.js"></script>
   <script src="../../content/shared/js/activeRoomStore.js"></script>
   <script src="../../content/shared/js/conversationStore.js"></script>
   <script src="../../content/shared/js/views.js"></script>
   <script src="../../content/shared/js/textChatStore.js"></script>
   <script src="../../content/shared/js/textChatView.js"></script>
+  <script src="../../content/shared/js/urlRegExps.js"></script>
+  <script src="../../content/shared/js/linkifiedTextView.js"></script>
 
   <!-- Test scripts -->
   <script src="models_test.js"></script>
   <script src="mixins_test.js"></script>
   <script src="utils_test.js"></script>
   <script src="crypto_test.js"></script>
   <script src="views_test.js"></script>
   <script src="websocket_test.js"></script>
@@ -75,16 +77,18 @@
   <script src="dispatcher_test.js"></script>
   <script src="activeRoomStore_test.js"></script>
   <script src="fxOSActiveRoomStore_test.js"></script>
   <script src="conversationStore_test.js"></script>
   <script src="otSdkDriver_test.js"></script>
   <script src="store_test.js"></script>
   <script src="textChatStore_test.js"></script>
   <script src="textChatView_test.js"></script>
+  <script src="linkifiedTextView_test.js"></script>
+
   <script>
     describe("Uncaught Error Check", function() {
       it("should load the tests without errors", function() {
         chai.expect(uncaughtError && uncaughtError.message).to.be.undefined;
       });
     });
 
     describe("Unexpected Warnings Check", function() {
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/shared/linkifiedTextView_test.js
@@ -0,0 +1,378 @@
+/*
+ * Many of these tests are ported from Autolinker.js:
+ *
+ * https://github.com/gregjacobs/Autolinker.js/blob/master/tests/AutolinkerSpec.js
+ *
+ * which is released under the MIT license.  Thanks to Greg Jacobs for his hard
+ * work there.
+ *
+ * The MIT License (MIT)
+ * Original Work Copyright (c) 2014 Gregory Jacobs (http://greg-jacobs.com)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to the
+ * following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+describe("loop.shared.views.LinkifiedTextView", function () {
+  "use strict";
+
+  var expect = chai.expect;
+  var LinkifiedTextView = loop.shared.views.LinkifiedTextView;
+  var TestUtils = React.addons.TestUtils;
+
+  describe("LinkifiedTextView", function () {
+    function renderToMarkup(string, extraProps) {
+      return React.renderToStaticMarkup(
+        React.createElement(
+          LinkifiedTextView,
+          _.extend({rawText: string}, extraProps)));
+    }
+
+    describe("#render", function() {
+      function testRender(testData) {
+        it(testData.desc, function() {
+          var markup = renderToMarkup(testData.rawText,
+            {suppressTarget: true, sendReferrer: true});
+
+          expect(markup).to.equal(testData.markup);
+        });
+      }
+
+      function testSkip(testData) {
+        it.skip(testData.desc, function() {
+          var markup = renderToMarkup(testData.rawText,
+            {suppressTarget: true, sendReferrer: true});
+
+          expect(markup).to.equal(testData.markup);
+        });
+      }
+
+      describe("this.props.suppressTarget", function() {
+        it("should make links w/o a target attr if suppressTarget is true",
+          function() {
+            var markup = renderToMarkup("http://example.com", {suppressTarget: true});
+
+            expect(markup).to.equal(
+              '<p><a href="http://example.com" rel="noreferrer">http://example.com</a></p>');
+          });
+
+        it("should make links with target=_blank if suppressTarget is not given",
+          function() {
+            var markup = renderToMarkup("http://example.com", {});
+
+            expect(markup).to.equal(
+              '<p><a href="http://example.com" target="_blank" rel="noreferrer">http://example.com</a></p>');
+          });
+      });
+
+      describe("this.props.sendReferrer", function() {
+        it("should make links w/o rel=noreferrer if sendReferrer is true",
+          function() {
+            var markup = renderToMarkup("http://example.com", {sendReferrer: true});
+
+            expect(markup).to.equal(
+              '<p><a href="http://example.com" target="_blank">http://example.com</a></p>');
+          });
+
+        it("should make links with rel=noreferrer if sendReferrer is not given",
+          function() {
+            var markup = renderToMarkup("http://example.com", {});
+
+            expect(markup).to.equal(
+              '<p><a href="http://example.com" target="_blank" rel="noreferrer">http://example.com</a></p>');
+          });
+      });
+
+      describe("this.props.linkClickHandler", function () {
+        function mountTestComponent(string, extraProps) {
+          return TestUtils.renderIntoDocument(
+            React.createElement(
+              LinkifiedTextView,
+              _.extend({rawText: string}, extraProps)));
+        }
+
+        it("should be called when a generated link is clicked", function () {
+          var fakeUrl = "http://example.com";
+          var linkClickHandler = sinon.stub();
+          var comp = mountTestComponent(fakeUrl, {linkClickHandler: linkClickHandler});
+
+          TestUtils.Simulate.click(comp.getDOMNode().querySelector("a"));
+
+          sinon.assert.calledOnce(linkClickHandler);
+        });
+
+        it("should cause sendReferrer and suppressTarget props to be ignored",
+          function() {
+            var fakeUrl = "http://example.com";
+            var linkClickHandler = function() {};
+
+            var markup = renderToMarkup("http://example.com", {
+              linkClickHandler: linkClickHandler,
+              sendReferrer: false,
+              suppressTarget: false
+            });
+
+            expect(markup).to.equal(
+              '<p><a href="http://example.com">http://example.com</a></p>');
+          });
+
+        describe("#_handleClickEvent", function () {
+          var fakeEvent;
+          var fakeUrl = "http://example.com";
+
+          beforeEach(function() {
+            fakeEvent = {
+              currentTarget: { href: fakeUrl },
+              preventDefault: sinon.stub(),
+              stopPropagation: sinon.stub()
+            };
+          });
+
+          it("should call preventDefault on the given event", function () {
+            function linkClickHandler() {}
+            var comp = mountTestComponent(
+              fakeUrl, {linkClickHandler: linkClickHandler});
+
+            comp._handleClickEvent(fakeEvent);
+
+            sinon.assert.calledOnce(fakeEvent.preventDefault);
+            sinon.assert.calledWithExactly(fakeEvent.stopPropagation);
+          });
+
+          it("should call stopPropagation on the given event", function () {
+            function linkClickHandler() {}
+            var comp = mountTestComponent(
+              fakeUrl, {linkClickHandler: linkClickHandler});
+
+            comp._handleClickEvent(fakeEvent);
+
+            sinon.assert.calledOnce(fakeEvent.stopPropagation);
+            sinon.assert.calledWithExactly(fakeEvent.stopPropagation);
+          });
+
+          it("should call this.props.linkClickHandler with event.currentTarget.href", function () {
+            var linkClickHandler = sinon.stub();
+            var comp = mountTestComponent(
+              fakeUrl, {linkClickHandler: linkClickHandler});
+
+            comp._handleClickEvent(fakeEvent);
+
+            sinon.assert.calledOnce(linkClickHandler);
+            sinon.assert.calledWithExactly(linkClickHandler, fakeUrl);
+          });
+        });
+      });
+
+      // Note that these are really integration tests with the parser and React.
+      // Since we're depending on that integration to provide us with security
+      // against various injection problems, it feels fairly important.  That
+      // said, these tests are not terribly robust in the face of markup changes
+      // in the code, and over time, some of them may want to be pushed down
+      // to only be unit tests against the parser or against
+      // parseStringToElements.  We may also want both unit and integration
+      // testing for some subset of these.
+      var tests = [
+        {
+          desc: "should only add a container to a string with no URLs",
+          rawText: "This is a test.",
+          markup: "<p>This is a test.</p>"
+        },
+        {
+          desc: "should linkify a string containing only a URL",
+          rawText: "http://example.com/",
+          markup: '<p><a href="http://example.com/">http://example.com/</a></p>'
+        },
+        {
+          desc: "should linkify a URL with text preceding it",
+          rawText: "This is a link to http://example.com",
+          markup: '<p>This is a link to <a href="http://example.com">http://example.com</a></p>'
+        },
+        {
+          desc: "should linkify a URL with text before and after",
+          rawText: "Look at http://example.com near the bottom",
+          markup: '<p>Look at <a href="http://example.com">http://example.com</a> near the bottom</p>'
+        },
+        {
+          desc: "should linkify an http URL",
+          rawText: "This is an http://example.com test",
+          markup: '<p>This is an <a href="http://example.com">http://example.com</a> test</p>'
+        },
+        {
+          desc: "should linkify an https URL",
+          rawText: "This is an https://example.com test",
+          markup: '<p>This is an <a href="https://example.com">https://example.com</a> test</p>'
+        },
+        {
+          desc: "should not linkify a data URL",
+          rawText: "This is an data:image/png;base64,iVBORw0KGgoAAA test",
+          markup: "<p>This is an data:image/png;base64,iVBORw0KGgoAAA test</p>"
+        },
+        {
+          desc: "should linkify URLs with a port number",
+          rawText: "Joe went to http://example.com:8000 today.",
+          markup: '<p>Joe went to <a href="http://example.com:8000">http://example.com:8000</a> today.</p>'
+        },
+        {
+          desc: "should linkify URLs with a port number and a trailing slash",
+          rawText: "Joe went to http://example.com:8000/ today.",
+          markup: '<p>Joe went to <a href="http://example.com:8000/">http://example.com:8000/</a> today.</p>'
+        },
+        {
+          desc: "should linkify URLs with a port number and a path",
+          rawText: "Joe went to http://example.com:8000/mysite/page today.",
+          markup: '<p>Joe went to <a href="http://example.com:8000/mysite/page">http://example.com:8000/mysite/page</a> today.</p>'
+        },
+        {
+          desc: "should linkify URLs with a port number and a query string",
+          rawText: "Joe went to http://example.com:8000?page=index today.",
+          markup: '<p>Joe went to <a href="http://example.com:8000?page=index">http://example.com:8000?page=index</a> today.</p>'
+        },
+        {
+          desc: "should linkify a URL with a port number and a hash string",
+          rawText: "Joe went to http://example.com:8000#page=index today.",
+          markup: '<p>Joe went to <a href="http://example.com:8000#page=index">http://example.com:8000#page=index</a> today.</p>'
+        },
+        {
+          desc: "should NOT include preceding ':' intros without a space",
+          rawText: "the link:http://example.com/",
+          markup: '<p>the link:<a href="http://example.com/">http://example.com/</a></p>'
+        },
+        {
+          desc: "should NOT autolink URLs with 'javascript:' URI scheme",
+          rawText: "do not link javascript:window.alert('hi') please",
+          markup: "<p>do not link javascript:window.alert(&#x27;hi&#x27;) please</p>"
+        },
+        {
+          desc: "should NOT autolink URLs with the 'JavAscriPt:' scheme",
+          rawText: "do not link JavAscriPt:window.alert('hi') please",
+          markup: "<p>do not link JavAscriPt:window.alert(&#x27;hi&#x27;) please</p>"
+        },
+        {
+          desc: "should NOT autolink possible URLs with the 'vbscript:' URI scheme",
+          rawText: "do not link vbscript:window.alert('hi') please",
+          markup: "<p>do not link vbscript:window.alert(&#x27;hi&#x27;) please</p>"
+        },
+        {
+          desc: "should NOT autolink URLs with the 'vBsCriPt:' URI scheme",
+          rawText: "do not link vBsCriPt:window.alert('hi') please",
+          markup: "<p>do not link vBsCriPt:window.alert(&#x27;hi&#x27;) please</p>"
+        },
+        {
+          desc: "should NOT autolink a string in the form of 'version:1.0'",
+          rawText: "version:1.0",
+          markup: "<p>version:1.0</p>"
+        },
+        {
+          desc: "should linkify an ftp URL",
+          rawText: "This is an ftp://example.com test",
+          markup: '<p>This is an <a href="ftp://example.com">ftp://example.com</a> test</p>'
+        },
+
+        // We don't want to include trailing dots in URLs, even though those
+        // are valid DNS names, as that should match user intent more of the
+        // time, as well as avoid this stuff:
+        //
+        // http://saynt2day.blogspot.it/2013/03/danger-of-trailing-dot-in-domain-name.html
+        //
+        {
+          desc: "should linkify 'http://example.com.', w/o a trailing dot",
+          rawText: "Joe went to http://example.com.",
+          markup: '<p>Joe went to <a href="http://example.com">http://example.com</a>.</p>'
+        },
+        // XXX several more tests like this we could port from Autolinkify.js
+        // see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
+        {
+          desc: "should exclude invalid chars after domain part",
+          rawText: "Joe went to http://www.example.com's today",
+          markup: '<p>Joe went to <a href="http://www.example.com">http://www.example.com</a>&#x27;s today</p>'
+        },
+        {
+          desc: "should not linkify protocol-relative URLs",
+          rawText: "//C/Programs",
+          markup: "<p>//C/Programs</p>"
+        },
+        // do a few tests to convince ourselves that, when our code is handled
+        // HTML in the input box, the integration of our code with React should
+        // cause that to rendered to appear as HTML source code, rather than
+        // getting injected into our real HTML DOM
+        {
+          desc: "should linkify simple HTML include an href properly escaped",
+          rawText: '<p>Joe went to <a href="http://www.example.com">example</a></p>',
+          markup: '<p>&lt;p&gt;Joe went to &lt;a href=&quot;<a href="http://www.example.com">http://www.example.com</a>&quot;&gt;example&lt;/a&gt;&lt;/p&gt;</p>'
+        },
+        {
+          desc: "should linkify HTML with nested tags and resource path properly escaped",
+          rawText: '<a href="http://example.com"><img src="http://example.com" /></a>',
+          markup: '<p>&lt;a href=&quot;<a href="http://example.com">http://example.com</a>&quot;&gt;&lt;img src=&quot;<a href="http://example.com">http://example.com</a>&quot; /&gt;&lt;/a&gt;</p>'
+         }
+      ];
+
+      var skippedTests = [
+
+        // XXX lots of tests similar to below we could port:
+        // see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
+        {
+          desc: "should link localhost URLs with an allowed URL scheme",
+          rawText: "Joe went to http://localhost today",
+          markup: '<p>Joe went to <a href="http://localhost">localhost</a></p> today'
+        },
+        // XXX lots of tests similar to below we could port:
+        // see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
+        {
+          desc: "should not include a ? if at the end of a URL",
+          rawText: "Did Joe go to http://example.com?",
+          markup: '<p>Did Joe go to <a href="http://example.com">http://example.com</a>?</p>'
+        },
+        {
+          desc: "should linkify 'check out http://example.com/monkey.', w/o trailing dots",
+          rawText: "check out http://example.com/monkey...",
+          markup: '<p>check out <a href="http://example.com/monkey">http://example.com/monkey</a>...</p>'
+        },
+        // another variant of eating too many trailing characters, it includes
+        // the trailing ", which it shouldn't.  Makes links inside pasted HTML
+        // source be slightly broken. Not key for our target users, I suspect,
+        // but still...
+        {
+          desc: "should linkify HTML with nested tags and a resource path properly escaped",
+          rawText: '<a href="http://example.com"><img src="http://example.com/someImage.jpg" /></a>',
+          markup: '<p>&lt;a href=&quot;<a href="http://example.com">http://example.com</a>&quot;&gt;&lt;img src=&quot;<a href="http://example.com/someImage.jpg&quot;">http://example.com/someImage.jpg&quot;</a> /&gt;&lt;/a&gt;</p>'
+        },
+        // XXX handle domains without schemes (bug 1186245)
+        // see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
+        {
+          desc: "should linkify a.museum (known TLD), but not abc.qqqq",
+          rawText: "a.museum should be linked, but abc.qqqq should not",
+          markup: '<p><a href="http://a.museum">a.museum</a> should be linked, but abc.qqqq should not</p>'
+        },
+        {
+          desc: "should linkify example.xyz (known TLD), but not example.etc (unknown TLD)",
+          rawText: "example.xyz should be linked, example.etc should not",
+          rawMarkup: '<><a href="http://example.xyz">example.xyz</a> should be linked, example.etc should not</p>'
+        }
+      ];
+
+      tests.forEach(testRender);
+
+      // XXX Over time, we'll want to make these pass..
+      // see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
+
+      skippedTests.forEach(testSkip);
+
+    });
+  });
+});
--- a/browser/components/loop/test/shared/textChatView_test.js
+++ b/browser/components/loop/test/shared/textChatView_test.js
@@ -245,16 +245,31 @@ describe("loop.shared.views.TextChatView
         type: CHAT_MESSAGE_TYPES.RECEIVED,
         contentType: CHAT_CONTENT_TYPES.TEXT,
         message: "foo"
       });
       var node = view.getDOMNode();
 
       expect(node.querySelector(".text-chat-entry-timestamp")).to.not.eql(null);
     });
+
+    // note that this is really an integration test to be sure that we don't
+    // inadvertently regress using LinkifiedTextView.
+    it("should linkify a URL starting with http", function (){
+      view = mountTestComponent({
+        showTimestamp: true,
+        timestamp: "2015-06-23T22:48:39.738Z",
+        type: CHAT_MESSAGE_TYPES.RECEIVED,
+        contentType: CHAT_CONTENT_TYPES.TEXT,
+        message: "Check out http://example.com and see what you think..."
+      });
+      var node = view.getDOMNode();
+
+      expect(node.querySelector("a")).to.not.eql(null);
+    });
   });
 
   describe("TextChatView", function() {
     var view, fakeServer;
 
     function mountTestComponent(extraProps) {
       var props = _.extend({
         dispatcher: dispatcher,
--- a/browser/components/loop/ui/index.html
+++ b/browser/components/loop/ui/index.html
@@ -53,16 +53,18 @@
     <script src="../content/shared/js/conversationStore.js"></script>
     <script src="../content/shared/js/roomStates.js"></script>
     <script src="../content/shared/js/fxOSActiveRoomStore.js"></script>
     <script src="../content/shared/js/activeRoomStore.js"></script>
     <script src="../content/shared/js/views.js"></script>
     <script src="../content/shared/js/textChatStore.js"></script>
     <script src="../content/js/feedbackViews.js"></script>
     <script src="../content/shared/js/textChatView.js"></script>
+    <script src="../content/shared/js/urlRegExps.js"></script>
+    <script src="../content/shared/js/linkifiedTextView.js"></script>
     <script src="../content/js/roomStore.js"></script>
     <script src="../content/js/roomViews.js"></script>
     <script src="../content/js/conversationViews.js"></script>
     <script src="../content/js/client.js"></script>
     <script src="../standalone/content/js/multiplexGum.js"></script>
     <script src="../standalone/content/js/webapp.js"></script>
     <script src="../standalone/content/js/standaloneRoomViews.js"></script>
     <script src="../standalone/content/js/fxOSMarketplace.js"></script>
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -81,19 +81,21 @@
     ]);
   };
 
   MockSDK.prototype = {
     setupStreamElements: function() {
       // Dummy function to stop warnings.
     },
 
-    sendTextChatMessage: function(message) {
+    sendTextChatMessage: function(actionData) {
       dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
-        message: message.message
+        contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
+        message: actionData.message,
+        receivedTimestamp: actionData.sentTimestamp
       }));
     }
   };
 
   var mockSDK = new MockSDK();
 
   /**
    * Every view that uses an activeRoomStore needs its own; if they shared
@@ -391,51 +393,46 @@
 
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Rheet!",
     sentTimestamp: "2015-06-23T22:21:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
-    message: "Hi there",
-    receivedTimestamp: "2015-06-23T22:21:45.590Z"
-  }));
-  dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
-    contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Hello",
     receivedTimestamp: "2015-06-23T23:24:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
+    message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
+    "linewrappingissuesifthecssiswrong",
+    sentTimestamp: "2015-06-23T22:23:45.590Z"
+  }));
+  dispatcher.dispatch(new sharedActions.SendTextChatMessage({
+    contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Check out this menu from DNA Pizza:" +
     " http://example.com/DNA/pizza/menu/lots-of-different-kinds-of-pizza/" +
     "%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%",
     sentTimestamp: "2015-06-23T22:23:45.590Z"
   }));
-  dispatcher.dispatch(new sharedActions.SendTextChatMessage({
-    contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
-    message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
-    "linewrappingissuesifthecssiswrong",
-    sentTimestamp: "2015-06-23T22:23:45.590Z"
-  }));
   dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "That avocado monkey-brains pie sounds tasty!",
     receivedTimestamp: "2015-06-23T22:25:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "What time should we meet?",
     sentTimestamp: "2015-06-23T22:27:45.590Z"
   }));
-  dispatcher.dispatch(new sharedActions.SendTextChatMessage({
+  dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
-    message: "Cool",
-    sentTimestamp: "2015-06-23T22:27:45.590Z"
+    message: "8:00 PM",
+    receivedTimestamp: "2015-06-23T22:27:45.590Z"
   }));
 
   loop.store.StoreMixin.register({
     activeRoomStore: activeRoomStore,
     conversationStore: conversationStores[0],
     textChatStore: textChatStore
   });
 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -81,19 +81,21 @@
     ]);
   };
 
   MockSDK.prototype = {
     setupStreamElements: function() {
       // Dummy function to stop warnings.
     },
 
-    sendTextChatMessage: function(message) {
+    sendTextChatMessage: function(actionData) {
       dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
-        message: message.message
+        contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
+        message: actionData.message,
+        receivedTimestamp: actionData.sentTimestamp
       }));
     }
   };
 
   var mockSDK = new MockSDK();
 
   /**
    * Every view that uses an activeRoomStore needs its own; if they shared
@@ -391,51 +393,46 @@
 
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Rheet!",
     sentTimestamp: "2015-06-23T22:21:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
-    message: "Hi there",
-    receivedTimestamp: "2015-06-23T22:21:45.590Z"
-  }));
-  dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
-    contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Hello",
     receivedTimestamp: "2015-06-23T23:24:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
+    message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
+    "linewrappingissuesifthecssiswrong",
+    sentTimestamp: "2015-06-23T22:23:45.590Z"
+  }));
+  dispatcher.dispatch(new sharedActions.SendTextChatMessage({
+    contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Check out this menu from DNA Pizza:" +
     " http://example.com/DNA/pizza/menu/lots-of-different-kinds-of-pizza/" +
     "%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%",
     sentTimestamp: "2015-06-23T22:23:45.590Z"
   }));
-  dispatcher.dispatch(new sharedActions.SendTextChatMessage({
-    contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
-    message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
-    "linewrappingissuesifthecssiswrong",
-    sentTimestamp: "2015-06-23T22:23:45.590Z"
-  }));
   dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "That avocado monkey-brains pie sounds tasty!",
     receivedTimestamp: "2015-06-23T22:25:45.590Z"
   }));
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "What time should we meet?",
     sentTimestamp: "2015-06-23T22:27:45.590Z"
   }));
-  dispatcher.dispatch(new sharedActions.SendTextChatMessage({
+  dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
-    message: "Cool",
-    sentTimestamp: "2015-06-23T22:27:45.590Z"
+    message: "8:00 PM",
+    receivedTimestamp: "2015-06-23T22:27:45.590Z"
   }));
 
   loop.store.StoreMixin.register({
     activeRoomStore: activeRoomStore,
     conversationStore: conversationStores[0],
     textChatStore: textChatStore
   });
 
--- a/toolkit/content/license.html
+++ b/toolkit/content/license.html
@@ -130,16 +130,17 @@
       <li><a href="about:license#snappy">Snappy License</a></li>
 #ifdef USE_STLPORT
       <li><a href="about:license#stlport">STLport License</a></li>
 #endif
       <li><a href="about:license#sunsoft">SunSoft License</a></li>
       <li><a href="about:license#superfasthash">SuperFastHash License</a></li>
       <li><a href="about:license#unicode">Unicode License</a></li>
       <li><a href="about:license#ucal">University of California License</a></li>
+      <li><a href="about:license#url-validation">URL Validation Regexp License</a></li>
       <li><a href="about:license#hunspell-en-US">US English Spellchecking Dictionary Licenses</a></li>
       <li><a href="about:license#v8">V8 License</a></li>
       <li><a href="about:license#vtune">VTune License</a></li>
       <li><a href="about:license#webrtc">WebRTC License</a></li>
       <li><a href="about:license#x264">x264 License</a></li>
       <li><a href="about:license#xiph">Xiph.org Foundation License</a></li>
     </ul>
 
@@ -4192,16 +4193,53 @@ HOWEVER CAUSED AND ON ANY THEORY OF LIAB
 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 SUCH DAMAGE.
 </pre>
 
 
     <hr>
 
+    <h1><a id="url-validation"></a>URL Validation Regexp License</h1>
+
+    <p>This license applies to the following file:</p>
+
+    <ul>
+      <li class="path">
+        browser/components/loop/content/shared/js/urlRegExps.js
+      </li>
+    </ul>
+
+<pre>
+Copyright (c) 2010-2013 Diego Perini (http://www.iport.it)
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+</pre>
+
+    <hr>
+
     <h1><a id="hunspell-en-US"></a>US English Spellchecking Dictionary Licenses</h1>
 
     <p>These licenses apply to certain files in the directory
       <span class="path">extensions/spellcheck/locales/en-US/hunspell/</span>. (This
       code only ships in some localized versions of this product.)</p>
 
 <pre>
 Different parts of the US English dictionary (SCOWL) are subject to the