Bug 1171925 - Allow the entire area of Loop's context to be clicked; don't show hover effects in the panel. r=dmose
authorMark Banner <standard8@mozilla.com>
Tue, 11 Aug 2015 21:53:27 +0100
changeset 289939 7726e996f847be791c4a61cf8c0a053bb61d45f4
parent 289938 d85445174174fb2f74b30296643b41c1c99a0499
child 289940 7a05d1df460f39762e5b1aab6b176c30ee22af95
push id5245
push userraliiev@mozilla.com
push dateThu, 29 Oct 2015 11:30:51 +0000
treeherdermozilla-beta@dac831dc1bd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdmose
bugs1171925
milestone43.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 1171925 - Allow the entire area of Loop's context to be clicked; don't show hover effects in the panel. r=dmose
browser/components/loop/content/shared/css/common.css
browser/components/loop/content/shared/css/conversation.css
browser/components/loop/content/shared/js/textChatView.js
browser/components/loop/content/shared/js/textChatView.jsx
browser/components/loop/content/shared/js/views.js
browser/components/loop/content/shared/js/views.jsx
browser/components/loop/test/shared/textChatView_test.js
browser/components/loop/test/shared/views_test.js
browser/components/loop/ui/ui-showcase.js
browser/components/loop/ui/ui-showcase.jsx
--- a/browser/components/loop/content/shared/css/common.css
+++ b/browser/components/loop/content/shared/css/common.css
@@ -545,20 +545,18 @@ html[dir="rtl"] .context-content {
   border: 1px solid #5cccee;
   border-radius: 4px;
   background: #fff;
   padding: .8em;
   /* Use the flex row mode to position the elements next to each other. */
   display: flex;
   flex-flow: row nowrap;
   line-height: 1.1em;
-}
-
-.context-wrapper:hover {
-  background-color: #dbf7ff;
+  /* No underline for the text in the context view. */
+  text-decoration: none;
 }
 
 .context-wrapper > .context-preview {
   float: left;
   /* 16px is standard height/width for a favicon */
   width: 16px;
   max-height: 16px;
   margin-right: .8em;
@@ -566,23 +564,32 @@ html[dir="rtl"] .context-content {
 }
 
 html[dir="rtl"] .context-wrapper > .context-preview {
   float: left;
   margin-left: .8em;
   margin-right: 0;
 }
 
-.context-wrapper > .context-description {
+.context-wrapper > .context-info {
   flex: 0 1 auto;
   display: block;
   color: black;
   /* 16px for the preview, plus its .8em margin */
   max-width: calc(100% - 16px - .8em);
   word-wrap: break-word;
 }
 
-.context-wrapper > .context-description > .context-url {
+.context-wrapper > .context-info > .context-url {
   display: block;
   color: #00a9dc;
   font-weight: 700;
   clear: both;
 }
+
+.clicks-allowed.context-wrapper:hover {
+  background-color: #dbf7ff;
+}
+
+/* Only underline the url, not the associated text */
+.clicks-allowed.context-wrapper:hover > .context-info > .context-url {
+  text-decoration: underline;
+}
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -894,19 +894,19 @@ body[platform="win"] .share-service-drop
   background-image: url("../img/icons-16x16.svg#add-hover");
 }
 
 .dropdown-menu-item:hover:active > .icon-add-share-service {
   background-image: url("../img/icons-16x16.svg#add-active");
 }
 
 .context-url-view-wrapper {
-  padding-left: 1em;
-  padding-right: 1em;
-  padding-bottom: 0.5em;
+  /* 18px for indent of .text-chat-arrow, 1px for border of .text-chat-entry > p,
+     0.5rem for padding of .text-chat-entry > p */
+  padding: calc(18px - 1px - 0.5rem);
   margin-bottom: 0.5em;
   background-color: #E8F6FE;
 }
 
 .room-context {
   background: rgba(0,0,0,.8);
   border-top: 2px solid #444;
   border-bottom: 2px solid #444;
@@ -1396,38 +1396,16 @@ html[dir="rtl"] .room-context-btn-close 
 
 .standalone .room-conversation-wrapper .room-inner-info-area a.btn {
   padding: .5em 3em .3em 3em;
   border-radius: 3px;
   font-weight: normal;
   max-width: 400px;
 }
 
-.standalone-context-url {
-  color: #fff;
-  /* Try and keep clear of local video */
-  height: 40%;
-}
-
-.standalone-context-url.screen-share-active {
-  /* Try and keep clear of remote video when screensharing */
-  height: 15%;
-}
-
-.standalone-context-url > img {
-  margin: 1em auto;
-  width: 16px;
-  height: 16px;
-}
-
-.standalone-context-url-description-wrapper {
-  /* So that we can use max-height for the image */
-  height: 20%;
-}
-
 .standalone .room-conversation .media {
   background: #000;
 }
 
 .standalone .room-conversation .video_wrapper.remote_wrapper {
   background-color: #4e4e4e;
   width: calc(75% - 10px); /* Take the left margin into account. */
 }
@@ -1475,22 +1453,24 @@ html[dir="rtl"] .room-context-btn-close 
   align-items: flex-start;
 }
 
 html[dir="rtl"] .text-chat-entry {
   margin-right: auto;
   margin-left: .2em;
 }
 
+/* If you change this entry, check it doesn't affect the "special" text
+   chat entries as well (.speical, .room-name, .context-url-view-wrapper */
 .text-chat-entry > p {
   position: relative;
   z-index: 10;
   /* Drop the default margins from the 'p' element. */
   margin: 0;
-  padding: .7em;
+  padding: .5rem;
   /* leave some room for the chat bubble arrow */
   max-width: 80%;
   border-width: 1px;
   border-style: solid;
   border-color: #5cccee;
   background: #fff;
   word-wrap: break-word;
   flex: 0 1 auto;
@@ -1642,36 +1622,41 @@ html[dir="rtl"] .text-chat-entry.receive
   margin-left: -10px;
 }
 
 .text-chat-entry.special.room-name {
   color: black;
   font-weight: bold;
   text-align: start;
   background-color: #E8F6FE;
-  padding-bottom: 0;
   margin-bottom: 0;
+  margin-right: 0;
 }
 
 .text-chat-entry.special.room-name p {
   background: #E8F6FE;
+  max-width: 100%;
+  /* 18px for indent of .text-chat-arrow, 1px for border of .text-chat-entry > p,
+   0.5rem for padding of .text-chat-entry > p */
+  padding: calc(18px - 1px - 0.5rem);
+  padding-bottom: 0px;
 }
 
 .text-chat-entry.special > p {
   border: none;
 }
 
 .text-chat-box {
   margin: auto;
 }
 
 .text-chat-box > form > input {
   width: 100%;
   height: 40px;
-  padding: 0 .5em .5em;
+  padding: 0 .4rem .4rem;
   font-size: 1.1em;
   border: 0;
   border-top: 1px solid #d8d8d8;
 }
 
 .text-chat-box > form > input::-webkit-input-placeholder {
   font-size: 1.1em;
   color: #999;
@@ -1698,22 +1683,16 @@ html[dir="rtl"] .text-chat-entry.receive
 }
 
 /* turn the visible border blue as a visual indicator of focus */
 .text-chat-box > form > input:focus {
   border-top: 1px solid #66c9f2;
 }
 
 @media screen and (max-width:640px) {
-  .standalone-context-url {
-    /* XXX We haven't got UX for standalone yet, so temporarily not displaying
-       on narrow window widths. See bug 1153827. */
-    display: none;
-  }
-
   /* Rooms specific responsive styling */
   .standalone .room-conversation {
     background: #000;
   }
 
   .standalone .room-conversation-wrapper .room-inner-info-area {
     right: 0;
     margin: auto;
@@ -1744,38 +1723,46 @@ html[dir="rtl"] .text-chat-entry.receive
     /* This forces the remote video stream to fit within wrapper's height */
     min-height: 0px;
   }
 }
 
 /* e.g. very narrow widths similar to conversation window */
 @media screen and (max-width:300px) {
   .text-chat-view {
-    flex: 0 0 auto;
     display: flex;
     flex-flow: column nowrap;
     /* 120px max-height of .text-chat-entries plus 40px of .text-chat-box */
     max-height: 160px;
     /* 60px min-height of .text-chat-entries plus 40px of .text-chat-box */
     min-height: 100px;
     /* The !important is to override the values defined above which have more
        specificity when we fix bug 1184559, we should be able to remove it,
-       but this should be tests first. */
+       but this should be tested first. */
     height: auto !important;
+    /* Let the view be the minimum size it needs to be - don't flex to take up
+       more. */
+    flex: 0 0 auto !important;
   }
 
   .text-chat-entries {
     /* The !important is to override the values defined above which have more
        specificity when we fix bug 1184559, we should be able to remove it,
        but this should be tests first. */
     flex: 1 1 auto !important;
     max-height: 120px;
     min-height: 60px;
   }
 
+  .text-chat-view.text-chat-disabled {
+    /* When we don't have text chat enabled, limit the view to the same height
+       as the entries, to avoid unnecessary whitespace */
+    max-height: 120px;
+  }
+
   .text-chat-entries-empty.text-chat-disabled {
     display: none;
   }
 
   /* When the text chat entries are not present, then hide the entries view
      and just show the chat box. */
   .text-chat-entries-empty {
     max-height: 40px;
--- a/browser/components/loop/content/shared/js/textChatView.js
+++ b/browser/components/loop/content/shared/js/textChatView.js
@@ -111,39 +111,49 @@ loop.shared.views.chat = (function(mozL1
     },
 
     getInitialState: function() {
       return {
         receivedMessageCount: 0
       };
     },
 
+    _hasChatMessages: function() {
+      return this.props.messageList.some(function(message) {
+        return message.contentType === CHAT_CONTENT_TYPES.TEXT;
+      });
+    },
+
     componentWillUpdate: function() {
       var node = this.getDOMNode();
       if (!node) {
         return;
       }
-      // Scroll only if we're right at the bottom of the display.
-      this.shouldScroll = node.scrollHeight === node.scrollTop + node.clientHeight;
+      // Scroll only if we're right at the bottom of the display, or if we've
+      // not had any chat messages so far.
+      this.shouldScroll = !this._hasChatMessages() ||
+        node.scrollHeight === node.scrollTop + node.clientHeight;
     },
 
     componentWillReceiveProps: function(nextProps) {
       var receivedMessageCount = nextProps.messageList.filter(function(message) {
         return message.type === CHAT_MESSAGE_TYPES.RECEIVED;
       }).length;
 
       // If the number of received messages has increased, we play a sound.
       if (receivedMessageCount > this.state.receivedMessageCount) {
         this.play("message");
         this.setState({receivedMessageCount: receivedMessageCount});
       }
     },
 
     componentDidUpdate: function() {
-      if (this.shouldScroll) {
+      // Don't scroll if we haven't got any chat messages yet - e.g. for context
+      // display, we want to display starting at the top.
+      if (this.shouldScroll && this._hasChatMessages()) {
         // This ensures the paint is complete.
         window.requestAnimationFrame(function() {
           try {
             var node = this.getDOMNode();
             node.scrollTop = node.scrollHeight - node.clientHeight;
           } catch (ex) {
             console.error("TextChatEntriesView.componentDidUpdate exception", ex);
           }
@@ -365,46 +375,46 @@ loop.shared.views.chat = (function(mozL1
     },
 
     getInitialState: function() {
       return this.getStoreState();
     },
 
     render: function() {
       var messageList;
-      var hasNonSpecialMessages;
 
       if (this.props.showRoomName) {
         messageList = this.state.messageList;
-        hasNonSpecialMessages = messageList.some(function(item) {
-          return item.type !== CHAT_MESSAGE_TYPES.SPECIAL;
-        });
       } else {
         messageList = this.state.messageList.filter(function(item) {
           return item.type !== CHAT_MESSAGE_TYPES.SPECIAL ||
             item.contentType !== CHAT_CONTENT_TYPES.ROOM_NAME;
         });
-        hasNonSpecialMessages = !!messageList.length;
       }
 
+      // Only show the placeholder if we've sent messages.
+      var hasSentMessages = messageList.some(function(item) {
+        return item.type === CHAT_MESSAGE_TYPES.SENT;
+      });
+
       var textChatViewClasses = React.addons.classSet({
         "text-chat-view": true,
         "text-chat-disabled": !this.state.textChatEnabled,
         "text-chat-entries-empty": !messageList.length
       });
 
       return (
         React.createElement("div", {className: textChatViewClasses}, 
           React.createElement(TextChatEntriesView, {
             dispatcher: this.props.dispatcher, 
             messageList: messageList, 
             useDesktopPaths: this.props.useDesktopPaths}), 
           React.createElement(TextChatInputView, {
             dispatcher: this.props.dispatcher, 
-            showPlaceholder: !hasNonSpecialMessages, 
+            showPlaceholder: !hasSentMessages, 
             textChatEnabled: this.state.textChatEnabled})
         )
       );
     }
   });
 
   return {
     TextChatEntriesView: TextChatEntriesView,
--- a/browser/components/loop/content/shared/js/textChatView.jsx
+++ b/browser/components/loop/content/shared/js/textChatView.jsx
@@ -111,39 +111,49 @@ loop.shared.views.chat = (function(mozL1
     },
 
     getInitialState: function() {
       return {
         receivedMessageCount: 0
       };
     },
 
+    _hasChatMessages: function() {
+      return this.props.messageList.some(function(message) {
+        return message.contentType === CHAT_CONTENT_TYPES.TEXT;
+      });
+    },
+
     componentWillUpdate: function() {
       var node = this.getDOMNode();
       if (!node) {
         return;
       }
-      // Scroll only if we're right at the bottom of the display.
-      this.shouldScroll = node.scrollHeight === node.scrollTop + node.clientHeight;
+      // Scroll only if we're right at the bottom of the display, or if we've
+      // not had any chat messages so far.
+      this.shouldScroll = !this._hasChatMessages() ||
+        node.scrollHeight === node.scrollTop + node.clientHeight;
     },
 
     componentWillReceiveProps: function(nextProps) {
       var receivedMessageCount = nextProps.messageList.filter(function(message) {
         return message.type === CHAT_MESSAGE_TYPES.RECEIVED;
       }).length;
 
       // If the number of received messages has increased, we play a sound.
       if (receivedMessageCount > this.state.receivedMessageCount) {
         this.play("message");
         this.setState({receivedMessageCount: receivedMessageCount});
       }
     },
 
     componentDidUpdate: function() {
-      if (this.shouldScroll) {
+      // Don't scroll if we haven't got any chat messages yet - e.g. for context
+      // display, we want to display starting at the top.
+      if (this.shouldScroll && this._hasChatMessages()) {
         // This ensures the paint is complete.
         window.requestAnimationFrame(function() {
           try {
             var node = this.getDOMNode();
             node.scrollTop = node.scrollHeight - node.clientHeight;
           } catch (ex) {
             console.error("TextChatEntriesView.componentDidUpdate exception", ex);
           }
@@ -365,46 +375,46 @@ loop.shared.views.chat = (function(mozL1
     },
 
     getInitialState: function() {
       return this.getStoreState();
     },
 
     render: function() {
       var messageList;
-      var hasNonSpecialMessages;
 
       if (this.props.showRoomName) {
         messageList = this.state.messageList;
-        hasNonSpecialMessages = messageList.some(function(item) {
-          return item.type !== CHAT_MESSAGE_TYPES.SPECIAL;
-        });
       } else {
         messageList = this.state.messageList.filter(function(item) {
           return item.type !== CHAT_MESSAGE_TYPES.SPECIAL ||
             item.contentType !== CHAT_CONTENT_TYPES.ROOM_NAME;
         });
-        hasNonSpecialMessages = !!messageList.length;
       }
 
+      // Only show the placeholder if we've sent messages.
+      var hasSentMessages = messageList.some(function(item) {
+        return item.type === CHAT_MESSAGE_TYPES.SENT;
+      });
+
       var textChatViewClasses = React.addons.classSet({
         "text-chat-view": true,
         "text-chat-disabled": !this.state.textChatEnabled,
         "text-chat-entries-empty": !messageList.length
       });
 
       return (
         <div className={textChatViewClasses}>
           <TextChatEntriesView
             dispatcher={this.props.dispatcher}
             messageList={messageList}
             useDesktopPaths={this.props.useDesktopPaths} />
           <TextChatInputView
             dispatcher={this.props.dispatcher}
-            showPlaceholder={!hasNonSpecialMessages}
+            showPlaceholder={!hasSentMessages}
             textChatEnabled={this.state.textChatEnabled} />
         </div>
       );
     }
   });
 
   return {
     TextChatEntriesView: TextChatEntriesView,
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -808,28 +808,35 @@ loop.shared.views = (function(_, mozL10n
       var thumbnail = this.props.thumbnail;
 
       if (!thumbnail) {
         thumbnail = this.props.useDesktopPaths ?
           "loop/shared/img/icons-16x16.svg#globe" :
           "shared/img/icons-16x16.svg#globe";
       }
 
+      var wrapperClasses = React.addons.classSet({
+        "context-wrapper": true,
+        "clicks-allowed": this.props.allowClick
+      });
+
       return (
         React.createElement("div", {className: "context-content"}, 
           this.renderContextTitle(), 
-          React.createElement("div", {className: "context-wrapper"}, 
+          React.createElement("a", {className: wrapperClasses, 
+             href: this.props.allowClick ? this.props.url : null, 
+             onClick: this.handleLinkClick, 
+             rel: "noreferrer", 
+             target: "_blank"}, 
             React.createElement("img", {className: "context-preview", src: thumbnail}), 
-            React.createElement("span", {className: "context-description"}, 
+            React.createElement("span", {className: "context-info"}, 
               this.props.description, 
-              React.createElement("a", {className: "context-url", 
-                 href: this.props.allowClick ? this.props.url : null, 
-                 onClick: this.handleLinkClick, 
-                 rel: "noreferrer", 
-                 target: "_blank"}, hostname)
+              React.createElement("span", {className: "context-url"}, 
+                hostname
+              )
             )
           )
         )
       );
     }
   });
 
   /**
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -808,30 +808,37 @@ loop.shared.views = (function(_, mozL10n
       var thumbnail = this.props.thumbnail;
 
       if (!thumbnail) {
         thumbnail = this.props.useDesktopPaths ?
           "loop/shared/img/icons-16x16.svg#globe" :
           "shared/img/icons-16x16.svg#globe";
       }
 
+      var wrapperClasses = React.addons.classSet({
+        "context-wrapper": true,
+        "clicks-allowed": this.props.allowClick
+      });
+
       return (
         <div className="context-content">
           {this.renderContextTitle()}
-          <div className="context-wrapper">
+          <a className={wrapperClasses}
+             href={this.props.allowClick ? this.props.url : null}
+             onClick={this.handleLinkClick}
+             rel="noreferrer"
+             target="_blank">
             <img className="context-preview" src={thumbnail} />
-            <span className="context-description">
+            <span className="context-info">
               {this.props.description}
-              <a className="context-url"
-                 href={this.props.allowClick ? this.props.url : null}
-                 onClick={this.handleLinkClick}
-                 rel="noreferrer"
-                 target="_blank">{hostname}</a>
+              <span className="context-url">
+                {hostname}
+              </span>
             </span>
-          </div>
+          </a>
         </div>
       );
     }
   });
 
   /**
    * Renders a media element for display. This also handles displaying an avatar
    * instead of the video, and attaching a video stream to the video element.
--- a/browser/components/loop/test/shared/textChatView_test.js
+++ b/browser/components/loop/test/shared/textChatView_test.js
@@ -47,16 +47,28 @@ describe("loop.shared.views.TextChatView
         useDesktopPaths: false
       };
 
       return TestUtils.renderIntoDocument(
         React.createElement(loop.shared.views.chat.TextChatEntriesView,
           _.extend(basicProps, extraProps)));
     }
 
+    function mountAsRealComponent(extraProps, container) {
+      var basicProps = {
+        dispatcher: dispatcher,
+        messageList: [],
+        useDesktopPaths: false
+      };
+
+      return React.render(
+        React.createElement(loop.shared.views.chat.TextChatEntriesView,
+          _.extend(basicProps, extraProps)), container);
+    }
+
     beforeEach(function() {
       store.setStoreState({ textChatEnabled: true });
     });
 
     it("should render message entries when message were sent/ received", function() {
       view = mountTestComponent({
         messageList: [{
           type: CHAT_MESSAGE_TYPES.RECEIVED,
@@ -203,16 +215,139 @@ describe("loop.shared.views.TextChatView
           receivedTimestamp: "2015-06-25T17:53:55.357Z"
         }]
       });
       node = view.getDOMNode();
 
       expect(node.querySelectorAll(".text-chat-entry-timestamp").length)
           .to.eql(1);
     });
+
+    describe("Scrolling", function() {
+      var fixtures;
+
+      beforeEach(function() {
+        sandbox.stub(window, "requestAnimationFrame", function(callback) {
+          callback();
+        });
+
+        fixtures = document.querySelector("#fixtures");
+        // If we're running code coverage in Karma, we might not have
+        // a fixtures element already.
+        if (!fixtures) {
+          fixtures = document.body.appendChild(document.createElement("div"));
+          fixtures.id = "fixtures";
+        }
+
+        // We're using scrolling, so we need to mount as a real one.
+        view = mountAsRealComponent({}, fixtures);
+        sandbox.stub(view, "play");
+
+        // We need some basic styling to ensure scrolling.
+        view.getDOMNode().style.overflow = "scroll";
+        view.getDOMNode().style["max-height"] = "4ch";
+      });
+
+      afterEach(function() {
+        React.unmountComponentAtNode(fixtures);
+      });
+
+      it("should scroll when a text message is added", function() {
+        var messageList = [{
+          type: CHAT_MESSAGE_TYPES.RECEIVED,
+          contentType: CHAT_CONTENT_TYPES.TEXT,
+          message: "Hello!",
+          receivedTimestamp: "2015-06-25T17:53:55.357Z"
+        }];
+
+        view.setProps({ messageList: messageList });
+
+        node = view.getDOMNode();
+
+        expect(node.scrollTop).eql(node.scrollHeight - node.clientHeight);
+      });
+
+      it("should not scroll when a context tile is added", function() {
+        var messageList = [{
+          type: CHAT_MESSAGE_TYPES.SPECIAL,
+          contentType: CHAT_CONTENT_TYPES.CONTEXT,
+          message: "Awesome!",
+          extraData: {
+            location: "http://invalid.com"
+          }
+        }];
+
+        view.setProps({ messageList: messageList });
+
+        node = view.getDOMNode();
+
+        expect(node.scrollTop).eql(0);
+      });
+
+      it("should scroll when a message is received after a context tile", function() {
+        // The context tile.
+        var messageList = [{
+          type: CHAT_MESSAGE_TYPES.SPECIAL,
+          contentType: CHAT_CONTENT_TYPES.CONTEXT,
+          message: "Awesome!",
+          extraData: {
+            location: "http://invalid.com"
+          }
+        }];
+
+        view.setProps({ messageList: messageList });
+
+        // Now add a message. Don't use the same list as this is a shared object,
+        // that messes with React.
+        var messageList1 = [
+          messageList[0], {
+            type: CHAT_MESSAGE_TYPES.RECEIVED,
+            contentType: CHAT_CONTENT_TYPES.TEXT,
+            message: "Hello!",
+            receivedTimestamp: "2015-06-25T17:53:55.357Z"
+          }
+        ];
+
+        view.setProps({ messageList: messageList1 });
+
+        node = view.getDOMNode();
+
+        expect(node.scrollTop).eql(node.scrollHeight - node.clientHeight);
+
+      });
+
+      it("should not scroll when receiving a message and the scroll is not at the bottom", function() {
+        node = view.getDOMNode();
+
+        var messageList = [{
+          type: CHAT_MESSAGE_TYPES.RECEIVED,
+          contentType: CHAT_CONTENT_TYPES.TEXT,
+          message: "Hello!",
+          receivedTimestamp: "2015-06-25T17:53:55.357Z"
+        }];
+
+        view.setProps({ messageList: messageList });
+
+        node.scrollTop = 0;
+
+        // Don't use the same list as this is a shared object, that messes with React.
+        var messageList1 = [
+          messageList[0], {
+            type: CHAT_MESSAGE_TYPES.RECEIVED,
+            contentType: CHAT_CONTENT_TYPES.TEXT,
+            message: "Hello!",
+            receivedTimestamp: "2015-06-25T17:53:55.357Z"
+          }
+        ];
+
+        view.setProps({ messageList: messageList1 });
+
+        expect(node.scrollTop).eql(0);
+      });
+    });
   });
 
   describe("TextChatEntry", function() {
     var view;
 
     function mountTestComponent(extraProps) {
       var props = _.extend({
         contentType: CHAT_CONTENT_TYPES.TEXT,
@@ -280,16 +415,20 @@ describe("loop.shared.views.TextChatView
       return TestUtils.renderIntoDocument(
         React.createElement(loop.shared.views.chat.TextChatView, props));
     }
 
     beforeEach(function() {
       // Fake server to catch all XHR requests.
       fakeServer = sinon.fakeServer.create();
       store.setStoreState({ textChatEnabled: true });
+
+      sandbox.stub(navigator.mozL10n, "get", function(string) {
+        return string;
+      });
     });
 
     afterEach(function() {
       fakeServer.restore();
     });
 
     it("should add a disabled class when text chat is disabled", function() {
       view = mountTestComponent();
@@ -477,10 +616,39 @@ describe("loop.shared.views.TextChatView
 
       TestUtils.Simulate.keyDown(entryNode, {
         key: "Enter",
         which: 13
       });
 
       sinon.assert.notCalled(dispatcher.dispatch);
     });
+
+    it("should show a placeholder when no messages have been sent", function() {
+      view = mountTestComponent();
+
+      store.receivedTextChatMessage({
+        contentType: CHAT_CONTENT_TYPES.TEXT,
+        message: "Foo",
+        sentTimestamp: "1970-01-01T00:03:00.000Z",
+        receivedTimestamp: "1970-01-01T00:03:00.000Z"
+      });
+
+      var textBox = view.getDOMNode().querySelector(".text-chat-box input");
+
+      expect(textBox.placeholder).contain("placeholder");
+    });
+
+    it("should not show a placeholder when messages have been sent", function() {
+      view = mountTestComponent();
+
+      store.sendTextChatMessage({
+        contentType: CHAT_CONTENT_TYPES.TEXT,
+        message: "Foo",
+        sentTimestamp: "2015-06-25T17:53:55.357Z"
+      });
+
+      var textBox = view.getDOMNode().querySelector(".text-chat-box input");
+
+      expect(textBox.placeholder).not.contain("placeholder");
+    });
   });
 });
--- a/browser/components/loop/test/shared/views_test.js
+++ b/browser/components/loop/test/shared/views_test.js
@@ -845,16 +845,38 @@ describe("loop.shared.views", function()
         dispatcher: dispatcher,
         showContextTitle: false,
         useDesktopPaths: false
       }, extraProps);
       return TestUtils.renderIntoDocument(
         React.createElement(sharedViews.ContextUrlView, props));
     }
 
+    it("should set a clicks-allowed class if clicks are allowed", function() {
+      view = mountTestComponent({
+        allowClick: true,
+        url: "http://wonderful.invalid"
+      });
+
+      var wrapper = view.getDOMNode().querySelector(".context-wrapper");
+
+      expect(wrapper.classList.contains("clicks-allowed")).eql(true);
+    });
+
+    it("should not set a clicks-allowed class if clicks are not allowed", function() {
+      view = mountTestComponent({
+        allowClick: false,
+        url: "http://wonderful.invalid"
+      });
+
+      var wrapper = view.getDOMNode().querySelector(".context-wrapper");
+
+      expect(wrapper.classList.contains("clicks-allowed")).eql(false);
+    });
+
     it("should display nothing if the url is invalid", function() {
       view = mountTestComponent({
         url: "fjrTykyw"
       });
 
       expect(view.getDOMNode()).eql(null);
     });
 
@@ -895,36 +917,49 @@ describe("loop.shared.views", function()
     });
 
     it("should set the href on the link if clicks are allowed", function() {
       view = mountTestComponent({
         allowClick: true,
         url: "http://wonderful.invalid"
       });
 
-      expect(view.getDOMNode().querySelector(".context-url").href)
+      expect(view.getDOMNode().querySelector(".context-wrapper").href)
         .eql("http://wonderful.invalid/");
     });
 
     it("should dispatch an action to record link clicks", function() {
       view = mountTestComponent({
         allowClick: true,
         url: "http://wonderful.invalid"
       });
 
-      var linkNode = view.getDOMNode().querySelector(".context-url");
+      var linkNode = view.getDOMNode().querySelector(".context-wrapper");
 
       TestUtils.Simulate.click(linkNode);
 
       sinon.assert.calledOnce(dispatcher.dispatch);
       sinon.assert.calledWith(dispatcher.dispatch,
         new sharedActions.RecordClick({
           linkInfo: "Shared URL"
         }));
     });
+
+    it("should not dispatch an action if clicks are not allowed", function() {
+      view = mountTestComponent({
+        allowClick: false,
+        url: "http://wonderful.invalid"
+      });
+
+      var linkNode = view.getDOMNode().querySelector(".context-wrapper");
+
+      TestUtils.Simulate.click(linkNode);
+
+      sinon.assert.notCalled(dispatcher.dispatch);
+    });
   });
 
   describe("MediaView", function() {
     var view;
 
     function mountTestComponent(props) {
       props = _.extend({
         isLoading: false
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -382,17 +382,17 @@
   }
 
   // Update the text chat store with the room info.
   textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({
     roomName: "A Very Long Conversation Name",
     roomOwner: "fake",
     roomUrl: "http://showcase",
     urls: [{
-      description: "A wonderful page!",
+      description: "1171925 - Clicking the title or favicon for context (in the conversation/standalone windows) should appear to be part of the link and open the webpage",
       location: "http://wonderful.invalid"
       // use the fallback thumbnail
     }]
   }));
 
   textChatStore.setStoreState({textChatEnabled: true});
 
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -382,17 +382,17 @@
   }
 
   // Update the text chat store with the room info.
   textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({
     roomName: "A Very Long Conversation Name",
     roomOwner: "fake",
     roomUrl: "http://showcase",
     urls: [{
-      description: "A wonderful page!",
+      description: "1171925 - Clicking the title or favicon for context (in the conversation/standalone windows) should appear to be part of the link and open the webpage",
       location: "http://wonderful.invalid"
       // use the fallback thumbnail
     }]
   }));
 
   textChatStore.setStoreState({textChatEnabled: true});
 
   dispatcher.dispatch(new sharedActions.SendTextChatMessage({