Bug 1132301: Part 4 - add a Share Link button in the Loop conversation window to share a room URL via the Social API. r=Standard8
☠☠ backed out by 3f3bb18b9f04 ☠ ☠
authorMike de Boer <mdeboer@mozilla.com>
Fri, 27 Mar 2015 14:31:58 +0100
changeset 264991 0e57fb772be5ffe2628824d30fc562a4cf75e571
parent 264990 d241102c104c2a9d1eef2a84efcae657c7f3d967
child 264992 ef364246350a5f0af2b57fcfac7fb23188e8384e
push id4718
push userraliiev@mozilla.com
push dateMon, 11 May 2015 18:39:53 +0000
treeherdermozilla-beta@c20c4ef55f08 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs1132301
milestone39.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 1132301: Part 4 - add a Share Link button in the Loop conversation window to share a room URL via the Social API. r=Standard8
browser/components/loop/content/css/contacts.css
browser/components/loop/content/css/panel.css
browser/components/loop/content/js/roomViews.js
browser/components/loop/content/js/roomViews.jsx
browser/components/loop/content/shared/css/common.css
browser/components/loop/content/shared/css/conversation.css
browser/components/loop/content/shared/img/icons-16x16.svg
browser/components/loop/content/shared/js/actions.js
browser/components/loop/content/shared/js/activeRoomStore.js
browser/components/loop/content/shared/js/mixins.js
browser/components/loop/content/shared/js/roomStore.js
browser/components/loop/standalone/content/img/logo.png
browser/components/loop/standalone/content/index.html
browser/components/loop/ui/ui-showcase.js
browser/components/loop/ui/ui-showcase.jsx
browser/locales/en-US/chrome/browser/loop/loop.properties
--- a/browser/components/loop/content/css/contacts.css
+++ b/browser/components/loop/content/css/contacts.css
@@ -189,22 +189,21 @@
 }
 
 .contact > .dropdown-menu-up {
   bottom: 10px;
   top: auto;
 }
 
 .contact > .dropdown-menu > .dropdown-menu-item > .icon {
-  display: inline-block;
   width: 20px;
   height: 10px;
   background-position: center left;
   background-size: 10px 10px;
-  background-repeat: no-repeat;
+  margin-top: 3px;
 }
 
 .contact > .dropdown-menu > .dropdown-menu-item > .icon-audio-call {
   background-image: url("../shared/img/icons-16x16.svg#audio");
 }
 
 .contact > .dropdown-menu > .dropdown-menu-item > .icon-video-call {
   background-image: url("../shared/img/icons-16x16.svg#video");
--- a/browser/components/loop/content/css/panel.css
+++ b/browser/components/loop/content/css/panel.css
@@ -450,54 +450,16 @@ body[dir=rtl] .rooms > div > .context > 
 }
 
 .button-close:hover,
 .button-close:hover:active {
   background-color: transparent;
   border: none;
 }
 
-/* Dropdown menu */
-
-.dropdown {
-  position: relative;
-}
-
-.dropdown-menu {
-  position: absolute;
-  bottom: 0;
-  left: 0;
-  background-color: #fdfdfd;
-  box-shadow: 0 1px 3px rgba(0,0,0,.3);
-  list-style: none;
-  padding: 5px;
-  border-radius: 2px;
-}
-
-body[dir=rtl] .dropdown-menu-item {
-  left: auto;
-  right: 10px;
-}
-
-.dropdown-menu-item {
-  text-align: start;
-  margin: .3em 0;
-  padding: .2em .5em;
-  cursor: pointer;
-  border: 1px solid transparent;
-  border-radius: 2px;
-  font-size: 1em;
-  white-space: nowrap;
-}
-
-.dropdown-menu-item:hover {
-  border: 1px solid #ccc;
-  background-color: #eee;
-}
-
 /* Spinner */
 
 @keyframes spinnerRotate {
   to { transform: rotate(360deg); }
 }
 
 .spinner {
   width: 16px;
@@ -729,21 +691,21 @@ body[dir=rtl] .generate-url-spinner {
      set by .dropdown-menu */
   top: auto;
   left: auto;
   bottom: -8px;
   right: 14px;
 }
 
 .settings-menu .icon {
-  display: inline-block;
   background-size: contain;
   width: 12px;
   height: 12px;
   margin-right: 1em;
+  margin-top: 2px;
 }
 
 .settings-menu .icon-settings {
   background: transparent url(../shared/img/svg/glyph-settings-16x16.svg) no-repeat center center;
 }
 
 .settings-menu .icon-tour {
   background: transparent url("../shared/img/icons-16x16.svg#tour") no-repeat center center;
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -50,21 +50,120 @@ loop.roomViews = (function(mozL10n) {
       var storeState = this.props.roomStore.getStoreState("activeRoom");
       return _.extend({
         // Used by the UI showcase.
         roomState: this.props.roomState || storeState.roomState
       }, storeState);
     }
   };
 
+  var SocialShareDropdown = React.createClass({displayName: "SocialShareDropdown",
+    mixins: [ActiveRoomStoreMixin],
+
+    propTypes: {
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      show: React.PropTypes.bool.isRequired
+    },
+
+    handleToolbarAddButtonClick: function(event) {
+      event.preventDefault();
+
+      this.props.dispatcher.dispatch(new sharedActions.AddSocialShareButton());
+    },
+
+    handleAddServiceClick: function(event) {
+      event.preventDefault();
+
+      this.props.dispatcher.dispatch(new sharedActions.AddSocialShareProvider());
+    },
+
+    handleProviderClick: function(event) {
+      event.preventDefault();
+
+      var origin = event.currentTarget.dataset.provider;
+      var provider = this.state.socialShareProviders.filter(function(provider) {
+        return provider.origin == origin;
+      })[0];
+
+      this.props.dispatcher.dispatch(new sharedActions.ShareRoomUrl({
+        provider: provider,
+        roomUrl: this.state.roomUrl,
+        previews: []
+      }));
+    },
+
+    render: function() {
+      // Don't render a thing when no data has been fetched yet.
+      if (!this.state.socialShareProviders) {
+        return null;
+      }
+
+      var cx = React.addons.classSet;
+      var shareDropdown = cx({
+        "share-service-dropdown": true,
+        "dropdown-menu": true,
+        "share-button-unavailable": !this.state.socialShareButtonAvailable,
+        "hide": !this.props.show
+      });
+
+      // When the button is not yet available, we offer to put it in the navbar
+      // for the user.
+      if (!this.state.socialShareButtonAvailable) {
+        return (
+          React.createElement("div", {className: shareDropdown}, 
+            React.createElement("div", {className: "share-panel-header"}, 
+              mozL10n.get("share_panel_header")
+            ), 
+            React.createElement("div", {className: "share-panel-body"}, 
+              
+                mozL10n.get("share_panel_body", {
+                  brandShortname: mozL10n.get("brandShortname"),
+                  clientSuperShortname: mozL10n.get("clientSuperShortname"),
+                })
+              
+            ), 
+            React.createElement("button", {className: "btn btn-info btn-toolbar-add", 
+                    onClick: this.handleToolbarAddButtonClick}, 
+              mozL10n.get("add_to_toolbar_button")
+            )
+          )
+        );
+      }
+
+      return (
+        React.createElement("ul", {className: shareDropdown}, 
+          React.createElement("li", {className: "dropdown-menu-item", onClick: this.handleAddServiceClick}, 
+            React.createElement("i", {className: "icon icon-add-share-service"}), 
+            React.createElement("span", null, mozL10n.get("share_add_service_button"))
+          ), 
+          this.state.socialShareProviders.length ? React.createElement("li", {className: "dropdown-menu-separator"}) : null, 
+          
+            this.state.socialShareProviders.map(function(provider, idx) {
+              return (
+                React.createElement("li", {className: "dropdown-menu-item", 
+                    key: "provider-" + idx, 
+                    "data-provider": provider.origin, 
+                    onClick: this.handleProviderClick}, 
+                  React.createElement("img", {className: "icon", src: provider.iconURL}), 
+                  React.createElement("span", null, provider.name)
+                )
+              );
+            }.bind(this))
+          
+        )
+      );
+    }
+  });
+
   /**
    * Desktop room invitation view (overlay).
    */
   var DesktopRoomInvitationView = React.createClass({displayName: "DesktopRoomInvitationView",
-    mixins: [ActiveRoomStoreMixin, React.addons.LinkedStateMixin],
+    mixins: [ActiveRoomStoreMixin, React.addons.LinkedStateMixin,
+             sharedMixins.DropdownMenuMixin],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
     },
 
     getInitialState: function() {
       return {
         copiedUrl: false,
@@ -112,16 +211,22 @@ loop.roomViews = (function(mozL10n) {
       event.preventDefault();
 
       this.props.dispatcher.dispatch(
         new sharedActions.CopyRoomUrl({roomUrl: this.state.roomUrl}));
 
       this.setState({copiedUrl: true});
     },
 
+    handleShareButtonClick: function(event) {
+      event.preventDefault();
+
+      this.toggleDropdownMenu();
+    },
+
     onRoomError: function() {
       // Only update the state if we're mounted, to avoid the problem where
       // stopListening doesn't nuke the active listeners during a event
       // processing.
       if (this.isMounted()) {
         this.setState({error: this.props.roomStore.getStoreState("error")});
       }
     },
@@ -140,24 +245,33 @@ loop.roomViews = (function(mozL10n) {
               onBlur: this.handleFormSubmit, 
               onKeyDown: this.handleTextareaKeyDown, 
               placeholder: mozL10n.get("rooms_name_this_room_label")})
           ), 
           React.createElement("p", null, mozL10n.get("invite_header_text")), 
           React.createElement("div", {className: "btn-group call-action-group"}, 
             React.createElement("button", {className: "btn btn-info btn-email", 
                     onClick: this.handleEmailButtonClick}, 
-              mozL10n.get("share_button2")
+              mozL10n.get("email_link_button")
             ), 
             React.createElement("button", {className: "btn btn-info btn-copy", 
                     onClick: this.handleCopyButtonClick}, 
               this.state.copiedUrl ? mozL10n.get("copied_url_button") :
                                       mozL10n.get("copy_url_button2")
+            ), 
+            React.createElement("button", {className: "btn btn-info btn-share", 
+                    ref: "anchor", 
+                    onClick: this.handleShareButtonClick}, 
+              mozL10n.get("share_button3")
             )
-          )
+          ), 
+          React.createElement(SocialShareDropdown, {dispatcher: this.props.dispatcher, 
+                               roomStore: this.props.roomStore, 
+                               show: this.state.showMenu, 
+                               ref: "menu"})
         )
       );
     }
   });
 
   /**
    * Desktop room conversation view.
    */
@@ -283,13 +397,14 @@ loop.roomViews = (function(mozL10n) {
           );
         }
       }
     }
   });
 
   return {
     ActiveRoomStoreMixin: ActiveRoomStoreMixin,
+    SocialShareDropdown: SocialShareDropdown,
     DesktopRoomConversationView: DesktopRoomConversationView,
     DesktopRoomInvitationView: DesktopRoomInvitationView
   };
 
 })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -50,21 +50,120 @@ loop.roomViews = (function(mozL10n) {
       var storeState = this.props.roomStore.getStoreState("activeRoom");
       return _.extend({
         // Used by the UI showcase.
         roomState: this.props.roomState || storeState.roomState
       }, storeState);
     }
   };
 
+  var SocialShareDropdown = React.createClass({
+    mixins: [ActiveRoomStoreMixin],
+
+    propTypes: {
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      show: React.PropTypes.bool.isRequired
+    },
+
+    handleToolbarAddButtonClick: function(event) {
+      event.preventDefault();
+
+      this.props.dispatcher.dispatch(new sharedActions.AddSocialShareButton());
+    },
+
+    handleAddServiceClick: function(event) {
+      event.preventDefault();
+
+      this.props.dispatcher.dispatch(new sharedActions.AddSocialShareProvider());
+    },
+
+    handleProviderClick: function(event) {
+      event.preventDefault();
+
+      var origin = event.currentTarget.dataset.provider;
+      var provider = this.state.socialShareProviders.filter(function(provider) {
+        return provider.origin == origin;
+      })[0];
+
+      this.props.dispatcher.dispatch(new sharedActions.ShareRoomUrl({
+        provider: provider,
+        roomUrl: this.state.roomUrl,
+        previews: []
+      }));
+    },
+
+    render: function() {
+      // Don't render a thing when no data has been fetched yet.
+      if (!this.state.socialShareProviders) {
+        return null;
+      }
+
+      var cx = React.addons.classSet;
+      var shareDropdown = cx({
+        "share-service-dropdown": true,
+        "dropdown-menu": true,
+        "share-button-unavailable": !this.state.socialShareButtonAvailable,
+        "hide": !this.props.show
+      });
+
+      // When the button is not yet available, we offer to put it in the navbar
+      // for the user.
+      if (!this.state.socialShareButtonAvailable) {
+        return (
+          <div className={shareDropdown}>
+            <div className="share-panel-header">
+              {mozL10n.get("share_panel_header")}
+            </div>
+            <div className="share-panel-body">
+              {
+                mozL10n.get("share_panel_body", {
+                  brandShortname: mozL10n.get("brandShortname"),
+                  clientSuperShortname: mozL10n.get("clientSuperShortname"),
+                })
+              }
+            </div>
+            <button className="btn btn-info btn-toolbar-add"
+                    onClick={this.handleToolbarAddButtonClick}>
+              {mozL10n.get("add_to_toolbar_button")}
+            </button>
+          </div>
+        );
+      }
+
+      return (
+        <ul className={shareDropdown}>
+          <li className="dropdown-menu-item" onClick={this.handleAddServiceClick}>
+            <i className="icon icon-add-share-service"></i>
+            <span>{mozL10n.get("share_add_service_button")}</span>
+          </li>
+          {this.state.socialShareProviders.length ? <li className="dropdown-menu-separator"/> : null}
+          {
+            this.state.socialShareProviders.map(function(provider, idx) {
+              return (
+                <li className="dropdown-menu-item"
+                    key={"provider-" + idx}
+                    data-provider={provider.origin}
+                    onClick={this.handleProviderClick}>
+                  <img className="icon" src={provider.iconURL}/>
+                  <span>{provider.name}</span>
+                </li>
+              );
+            }.bind(this))
+          }
+        </ul>
+      );
+    }
+  });
+
   /**
    * Desktop room invitation view (overlay).
    */
   var DesktopRoomInvitationView = React.createClass({
-    mixins: [ActiveRoomStoreMixin, React.addons.LinkedStateMixin],
+    mixins: [ActiveRoomStoreMixin, React.addons.LinkedStateMixin,
+             sharedMixins.DropdownMenuMixin],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
     },
 
     getInitialState: function() {
       return {
         copiedUrl: false,
@@ -112,16 +211,22 @@ loop.roomViews = (function(mozL10n) {
       event.preventDefault();
 
       this.props.dispatcher.dispatch(
         new sharedActions.CopyRoomUrl({roomUrl: this.state.roomUrl}));
 
       this.setState({copiedUrl: true});
     },
 
+    handleShareButtonClick: function(event) {
+      event.preventDefault();
+
+      this.toggleDropdownMenu();
+    },
+
     onRoomError: function() {
       // Only update the state if we're mounted, to avoid the problem where
       // stopListening doesn't nuke the active listeners during a event
       // processing.
       if (this.isMounted()) {
         this.setState({error: this.props.roomStore.getStoreState("error")});
       }
     },
@@ -140,24 +245,33 @@ loop.roomViews = (function(mozL10n) {
               onBlur={this.handleFormSubmit}
               onKeyDown={this.handleTextareaKeyDown}
               placeholder={mozL10n.get("rooms_name_this_room_label")} />
           </form>
           <p>{mozL10n.get("invite_header_text")}</p>
           <div className="btn-group call-action-group">
             <button className="btn btn-info btn-email"
                     onClick={this.handleEmailButtonClick}>
-              {mozL10n.get("share_button2")}
+              {mozL10n.get("email_link_button")}
             </button>
             <button className="btn btn-info btn-copy"
                     onClick={this.handleCopyButtonClick}>
               {this.state.copiedUrl ? mozL10n.get("copied_url_button") :
                                       mozL10n.get("copy_url_button2")}
             </button>
+            <button className="btn btn-info btn-share"
+                    ref="anchor"
+                    onClick={this.handleShareButtonClick}>
+              {mozL10n.get("share_button3")}
+            </button>
           </div>
+          <SocialShareDropdown dispatcher={this.props.dispatcher}
+                               roomStore={this.props.roomStore}
+                               show={this.state.showMenu}
+                               ref="menu"/>
         </div>
       );
     }
   });
 
   /**
    * Desktop room conversation view.
    */
@@ -283,13 +397,14 @@ loop.roomViews = (function(mozL10n) {
           );
         }
       }
     }
   });
 
   return {
     ActiveRoomStoreMixin: ActiveRoomStoreMixin,
+    SocialShareDropdown: SocialShareDropdown,
     DesktopRoomConversationView: DesktopRoomConversationView,
     DesktopRoomInvitationView: DesktopRoomInvitationView
   };
 
 })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/shared/css/common.css
+++ b/browser/components/loop/content/shared/css/common.css
@@ -11,16 +11,17 @@
 *, *:before, *:after {
   box-sizing: border-box;
 }
 
 body {
   font: message-box;
   font-size: 12px;
   background: #fbfbfb;
+  overflow: hidden;
 }
 
 img {
   border: none;
 }
 
 h1, h2, h3 {
   color: #666;
@@ -402,8 +403,58 @@ p {
 
 .firefox-logo {
   margin: 0 auto; /* horizontal block centering */
   width: 100px;
   height: 100px;
   background: transparent url(../img/firefox-logo.png) no-repeat center center;
   background-size: contain;
 }
+
+/* Dropdown menu */
+
+.dropdown {
+  position: relative;
+}
+
+.dropdown-menu {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  background-color: #fdfdfd;
+  box-shadow: 0 1px 3px rgba(0,0,0,.3);
+  list-style: none;
+  padding: 5px;
+  border-radius: 2px;
+}
+
+body[dir=rtl] .dropdown-menu-item {
+  left: auto;
+  right: 10px;
+}
+
+.dropdown-menu-item {
+  text-align: start;
+  margin: .3em 0;
+  padding: .2em .5em;
+  cursor: pointer;
+  border: 1px solid transparent;
+  border-radius: 2px;
+  font-size: 1em;
+  white-space: nowrap;
+}
+
+.dropdown-menu-item:hover {
+  border: 1px solid #ccc;
+  background-color: #eee;
+}
+
+.dropdown-menu-item > .icon {
+  background-repeat: no-repeat;
+  display: inline-block;
+}
+
+.dropdown-menu-separator {
+  height: 1px;
+  margin: 2px -2px 1px -2px;
+  border-top: 1px solid #dedede;
+  background-color: #fff;
+}
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -897,16 +897,73 @@ html, .fx-embedded, #main,
   border-radius: .5em;
 }
 
 .room-invitation-overlay .btn-group {
   position: absolute;
   bottom: 10px;
 }
 
+.share-service-dropdown {
+  color: #000;
+  text-align: start;
+  bottom: auto;
+  top: 0;
+}
+
+.share-service-dropdown.share-button-unavailable {
+  width: 230px;
+  padding: 8px;
+}
+
+.share-service-dropdown > .dropdown-menu-item > .icon {
+  width: 14px;
+  height: 14px;
+  margin-right: 4px;
+}
+
+.share-service-dropdown .share-panel-header {
+  background-image: url("../img/icons-16x16.svg#share-darkgrey");
+  background-size: 3em 3em;
+  background-repeat: no-repeat;
+  min-height: 3em;
+  font-weight: bold;
+  margin-bottom: 1em;
+  padding-left: 4.5em;
+}
+
+body[dir=rtl] .share-service-dropdown .share-panel-header {
+  background-position: top right;
+  padding-left: 0;
+  padding-right: 4.5em;
+}
+
+.share-service-dropdown .btn-toolbar-add {
+  padding: 4px 2px;
+  border-radius: 2px;
+  margin-top: 1em;
+  width: 100%;
+}
+
+.dropdown-menu-item > .icon-add-share-service {
+  background-image: url("../img/icons-16x16.svg#add");
+  background-repeat: no-repeat;
+  background-size: 12px 12px;
+  width: 12px;
+  height: 12px;
+}
+
+.dropdown-menu-item:hover > .icon-add-share-service {
+  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");
+}
+
 /* Standalone rooms */
 
 .standalone .room-conversation-wrapper {
   position: relative;
   height: 100%;
   background: #000;
 }
 
--- a/browser/components/loop/content/shared/img/icons-16x16.svg
+++ b/browser/components/loop/content/shared/img/icons-16x16.svg
@@ -28,21 +28,27 @@ use[id$="-active"] {
 use[id$="-red"] {
   fill: #d74345
 }
 
 use[id$="-white"] {
   fill: #fff;
 }
 
+use[id$="-darkgrey"] {
+  fill: #666;
+}
+
 use[id$="-disabled"] {
   fill: rgba(255,255,255,.6);
 }
 </style>
 <defs style="display:none">
+  <polygon id="add-shape" fill-rule="evenodd" clip-rule="evenodd" points="16,6.4 9.6,6.4 9.6,0 6.4,0 6.4,6.4 0,6.4 0,9.6
+    6.4,9.6 6.4,16 9.6,16 9.6,9.6 16,9.6"/>
   <path id="audio-shape" fill-rule="evenodd" clip-rule="evenodd" d="M11.429,6.857v2.286c0,1.894-1.535,3.429-3.429,3.429
     c-1.894,0-3.429-1.535-3.429-3.429V6.857H3.429v2.286c0,2.129,1.458,3.913,3.429,4.422v1.293H6.286
     c-0.746,0-1.379,0.477-1.615,1.143h6.658c-0.236-0.665-0.869-1.143-1.615-1.143H9.143v-1.293c1.971-0.508,3.429-2.292,3.429-4.422
     V6.857H11.429z M8,12c1.578,0,2.857-1.279,2.857-2.857V2.857C10.857,1.279,9.578,0,8,0C6.422,0,5.143,1.279,5.143,2.857v6.286
     C5.143,10.721,6.422,12,8,12z"/>
   <path id="block-shape" fill-rule="evenodd" clip-rule="evenodd" d="M8,0C3.582,0,0,3.582,0,8c0,4.418,3.582,8,8,8
     c4.418,0,8-3.582,8-8C16,3.582,12.418,0,8,0z M8,2.442c1.073,0,2.075,0.301,2.926,0.821l-7.673,7.673
     C2.718,10.085,2.408,9.079,2.408,8C2.408,4.931,4.911,2.442,8,2.442z M8,13.557c-1.073,0-2.075-0.301-2.926-0.821l7.673-7.673
@@ -79,16 +85,27 @@ use[id$="-disabled"] {
     l-0.209,0.596c-0.874-0.205-1.692-0.553-2.434-1.011l0.272-0.567c0.171-0.355-0.17-1.066-0.739-1.635
     c-0.568-0.568-1.279-0.909-1.635-0.738l-0.568,0.273c-0.46-0.741-0.79-1.566-0.998-2.439l0.584-0.205
     C0.969,9.547,1.231,8.804,1.231,8c0-0.804-0.262-1.548-0.634-1.678L0,6.112c0.206-0.874,0.565-1.685,1.025-2.427l0.554,0.266
     c0.355,0.171,1.066-0.17,1.635-0.738c0.569-0.568,0.909-1.28,0.739-1.635L3.686,1.025c0.742-0.46,1.553-0.818,2.427-1.024
     l0.209,0.596C6.453,0.969,7.197,1.23,8.001,1.23s1.548-0.262,1.678-0.634l0.209-0.596c0.874,0.205,1.692,0.553,2.434,1.011
     l-0.272,0.567c-0.171,0.355,0.17,1.066,0.738,1.635c0.569,0.568,1.279,0.909,1.635,0.738l0.568-0.273
     c0.46,0.741,0.79,1.566,0.998,2.438l-0.584,0.205C15.032,6.452,14.77,7.196,14.77,8z M8.001,3.661C5.604,3.661,3.661,5.603,3.661,8
     c0,2.397,1.943,4.34,4.339,4.34c2.397,0,4.339-1.943,4.339-4.34C12.34,5.603,10.397,3.661,8.001,3.661z"/>
+  <g id="share-shape">
+    <path fill="transparent" d="M11.704,12.375H7.556c-0.183,0-0.353-0.071-0.48-0.199c-0.124-0.125-0.191-0.29-0.19-0.464V10.09h-2.59
+      c-0.37,0-0.671-0.296-0.671-0.661V4.286c0-0.365,0.301-0.661,0.671-0.661h3.384L7.817,3.73l1.299,1.254v0.927h1.851
+      l1.408,1.359v4.444C12.375,12.079,12.074,12.375,11.704,12.375z M7.635,11.625h3.989V7.588l-0.961-0.927H7.635V11.625z
+      M4.375,9.34h2.5V6.561c0-0.365,0.301-0.661,0.671-0.661h0.82V5.302L7.405,4.375h-3.03V9.34z"/>
+    <polygon fill="transparent" points="10.222,8 10.222,6.857 11.407,8"/>
+    <polygon fill="transparent" points="6.963,5.714 6.963,4.571 8.148,5.714"/>
+    <path fill-rule="evenodd" clip-rule="evenodd" d="M8.999,10.654L8.69,10.6L8.999,16l2.56-3.754L8.999,10.654z
+      M8.658,10.041l0.341-0.043l6,2.898V0L1,10.998l4.55-0.569L8.999,16l-1.892-5.68l-0.283-0.05l0.256-0.032L7,9.998l6.999-8.003
+      L8.656,9.998L8.658,10.041z"/>
+  </g>
   <path id="tag-shape" fill-rule="evenodd" clip-rule="evenodd" d="M15.578,7.317L9.659,1.398
     C9.374,1.033,8.955,0.777,8.471,0.761L2.556,0C1.72-0.027-0.027,1.72,0,2.556l0.761,5.916c0.016,0.484,0.272,0.902,0.637,1.188
     l5.919,5.919c0.591,0.591,1.584,0.557,2.218-0.076l5.966-5.966C16.135,8.902,16.169,7.909,15.578,7.317z M4.222,4.163
     c-0.511,0.511-1.339,0.511-1.85,0c-0.511-0.511-0.511-1.339,0-1.85c0.511-0.511,1.339-0.511,1.85,0
     C4.733,2.823,4.733,3.652,4.222,4.163z"/>
   <path id="unblock-shape" fill-rule="evenodd" clip-rule="evenodd" d="M8,16c-4.418,0-8-3.582-8-8c0-4.418,3.582-8,8-8
     c4.418,0,8,3.582,8,8C16,12.418,12.418,16,8,16z M8,2.442C4.911,2.442,2.408,4.931,2.408,8c0,3.069,2.504,5.557,5.592,5.557
     S13.592,11.069,13.592,8C13.592,4.931,11.089,2.442,8,2.442z"/>
@@ -143,16 +160,19 @@ use[id$="-disabled"] {
   <g id="screenmute-shape">
     <path fill-rule="evenodd" clip-rule="evenodd" d="M13.55,4.73h-0.54l-8.13,8.13L4.2,13.53
       C4.46,13.82,4.84,14,5.25,14h8.3c0.8,0,1.45-0.68,1.45-1.52V6.24C15,5.41,14.35,4.73,13.55,4.73z"/>
     <path fill-rule="evenodd" clip-rule="evenodd" d="M14.21,2.72l-0.99-0.99l-1.15,1.15C11.83,2.36,11.33,2,10.75,2
       h-8.3C1.65,2,1,2.68,1,3.52v6.24c0,0.83,0.65,1.51,1.45,1.51h0.52V5.43c0-0.84,0.65-1.51,1.45-1.51h6.61l-0.81,
       0.81H5.25 c-0.8,0-1.45,0.68-1.45,1.51v4.91l-1.89,1.89l0.99,0.99l1-1l8.3-8.3L14.21,2.72z"/>
   </g>
 </defs>
+<use id="add"                 xlink:href="#add-shape"/>
+<use id="add-hover"           xlink:href="#add-shape"/>
+<use id="add-active"          xlink:href="#add-shape"/>
 <use id="audio"               xlink:href="#audio-shape"/>
 <use id="audio-hover"         xlink:href="#audio-shape"/>
 <use id="audio-active"        xlink:href="#audio-shape"/>
 <use id="block"               xlink:href="#block-shape"/>
 <use id="block-red"           xlink:href="#block-shape"/>
 <use id="block-hover"         xlink:href="#block-shape"/>
 <use id="block-active"        xlink:href="#block-shape"/>
 <use id="contacts"            xlink:href="#contacts-shape"/>
@@ -168,16 +188,17 @@ use[id$="-disabled"] {
 <use id="history-active"      xlink:href="#history-shape"/>
 <use id="leave"               xlink:href="#leave-shape"/>
 <use id="precall"             xlink:href="#precall-shape"/>
 <use id="precall-hover"       xlink:href="#precall-shape"/>
 <use id="precall-active"      xlink:href="#precall-shape"/>
 <use id="settings"            xlink:href="#settings-shape"/>
 <use id="settings-hover"      xlink:href="#settings-shape"/>
 <use id="settings-active"     xlink:href="#settings-shape"/>
+<use id="share-darkgrey"      xlink:href="#share-shape"/>
 <use id="tag"                 xlink:href="#tag-shape"/>
 <use id="tag-hover"           xlink:href="#tag-shape"/>
 <use id="tag-active"          xlink:href="#tag-shape"/>
 <use id="trash"               xlink:href="#trash-shape"/>
 <use id="unblock"             xlink:href="#unblock-shape"/>
 <use id="unblock-hover"       xlink:href="#unblock-shape"/>
 <use id="unblock-active"      xlink:href="#unblock-shape"/>
 <use id="video"               xlink:href="#video-shape"/>
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -353,16 +353,39 @@ loop.shared.actions = (function() {
      * Email a room url.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     EmailRoomUrl: Action.define("emailRoomUrl", {
       roomUrl: String
     }),
 
     /**
+     * Share a room url via the Social API.
+     * XXX: should move to some roomActions module - refs bug 1079284
+     */
+    ShareRoomUrl: Action.define("shareRoomUrl", {
+      provider: Object,
+      roomUrl: String
+    }),
+
+    /**
+     * Add the Social Share button to the browser toolbar.
+     * XXX: should move to some roomActions module - refs bug 1079284
+     */
+    AddSocialShareButton: Action.define("addSocialShareButton", {
+    }),
+
+    /**
+     * Open the share panel to add a Social share provider.
+     * XXX: should move to some roomActions module - refs bug 1079284
+     */
+    AddSocialShareProvider: Action.define("addSocialShareProvider", {
+    }),
+
+    /**
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     RoomFailure: Action.define("roomFailure", {
       error: Object,
       // True when the failures occurs in the join room request to the loop-server.
       failedJoinRequest: Boolean
     }),
 
@@ -371,32 +394,43 @@ loop.shared.actions = (function() {
      * XXX: should move to some roomActions module - refs bug 1079284
      *
      * @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
      */
     SetupRoomInfo: Action.define("setupRoomInfo", {
       // roomName: String - Optional.
       roomOwner: String,
       roomToken: String,
-      roomUrl: String
+      roomUrl: String,
+      socialShareButtonAvailable: Boolean,
+      socialShareProviders: Array
     }),
 
     /**
      * Updates the room information when it is received.
      * XXX: should move to some roomActions module - refs bug 1079284
      *
      * @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
      */
     UpdateRoomInfo: Action.define("updateRoomInfo", {
       // roomName: String - Optional.
       roomOwner: String,
       roomUrl: String
     }),
 
     /**
+     * Updates the Social API information when it is received.
+     * XXX: should move to some roomActions module - refs bug 1079284
+     */
+    UpdateSocialShareInfo: Action.define("updateSocialShareInfo", {
+      socialShareButtonAvailable: Boolean,
+      socialShareProviders: Array
+    }),
+
+    /**
      * Starts the process for the user to join the room.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     JoinRoom: Action.define("joinRoom", {
     }),
 
     /**
      * Signals the user has successfully joined the room on the loop-server.
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -71,17 +71,20 @@ loop.store.ActiveRoomStore = (function()
         // Tracks if the room has been used during this
         // session. 'Used' means at least one call has been placed
         // with it. Entering and leaving the room without seeing
         // anyone is not considered as 'used'
         used: false,
         localVideoDimensions: {},
         remoteVideoDimensions: {},
         screenSharingState: SCREEN_SHARE_STATES.INACTIVE,
-        receivingScreenShare: false
+        receivingScreenShare: false,
+        // Social API state.
+        socialShareButtonAvailable: false,
+        socialShareProviders: null
       };
     },
 
     /**
      * Handles a room failure.
      *
      * @param {sharedActions.RoomFailure} actionData
      */
@@ -127,17 +130,18 @@ loop.store.ActiveRoomStore = (function()
         "receivingScreenShare",
         "remotePeerDisconnected",
         "remotePeerConnected",
         "windowUnload",
         "leaveRoom",
         "feedbackComplete",
         "videoDimensionsChanged",
         "startScreenShare",
-        "endScreenShare"
+        "endScreenShare",
+        "updateSocialShareInfo"
       ]);
     },
 
     /**
      * Execute setupWindowData event action from the dispatcher. This gets
      * the room data from the mozLoop api, and dispatches an UpdateRoomInfo event.
      * It also dispatches JoinRoom as this action is only applicable to the desktop
      * client, and needs to auto-join.
@@ -167,17 +171,19 @@ loop.store.ActiveRoomStore = (function()
             }));
             return;
           }
 
           this.dispatchAction(new sharedActions.SetupRoomInfo({
             roomToken: actionData.roomToken,
             roomName: roomData.roomName,
             roomOwner: roomData.roomOwner,
-            roomUrl: roomData.roomUrl
+            roomUrl: roomData.roomUrl,
+            socialShareButtonAvailable: this._mozLoop.isSocialShareButtonAvailable(),
+            socialShareProviders: this._mozLoop.getSocialShareProviders()
           }));
 
           // For the conversation window, we need to automatically
           // join the room.
           this.dispatchAction(new sharedActions.JoinRoom());
         }.bind(this));
     },
 
@@ -232,40 +238,58 @@ loop.store.ActiveRoomStore = (function()
         return;
       }
 
       this.setStoreState({
         roomName: actionData.roomName,
         roomOwner: actionData.roomOwner,
         roomState: ROOM_STATES.READY,
         roomToken: actionData.roomToken,
-        roomUrl: actionData.roomUrl
+        roomUrl: actionData.roomUrl,
+        socialShareButtonAvailable: actionData.socialShareButtonAvailable,
+        socialShareProviders: actionData.socialShareProviders
       });
 
       this._onUpdateListener = this._handleRoomUpdate.bind(this);
       this._onDeleteListener = this._handleRoomDelete.bind(this);
+      this._onSocialShareUpdate = this._handleSocialShareUpdate.bind(this);
 
       this._mozLoop.rooms.on("update:" + actionData.roomToken, this._onUpdateListener);
       this._mozLoop.rooms.on("delete:" + actionData.roomToken, this._onDeleteListener);
+      window.addEventListener("LoopShareWidgetChanged", this._onSocialShareUpdate);
+      window.addEventListener("LoopSocialProvidersChanged", this._onSocialShareUpdate);
     },
 
     /**
      * Handles the updateRoomInfo action. Updates the room data.
      *
      * @param {sharedActions.UpdateRoomInfo} actionData
      */
     updateRoomInfo: function(actionData) {
       this.setStoreState({
         roomName: actionData.roomName,
         roomOwner: actionData.roomOwner,
         roomUrl: actionData.roomUrl
       });
     },
 
     /**
+     * Handles the updateSocialShareInfo action. Updates the room data with new
+     * Social API info.
+     *
+     * @param  {sharedActions.UpdateSocialShareInfo} actionData
+     */
+    updateSocialShareInfo: function(actionData) {
+      this.setStoreState({
+        socialShareButtonAvailable: actionData.socialShareButtonAvailable,
+        socialShareProviders: actionData.socialShareProviders
+      });
+    },
+
+    /**
      * Handles room updates notified by the mozLoop rooms API.
      *
      * @param {String} eventName The name of the event
      * @param {Object} roomData  The new roomData.
      */
     _handleRoomUpdate: function(eventName, roomData) {
       this.dispatchAction(new sharedActions.UpdateRoomInfo({
         roomName: roomData.roomName,
@@ -282,16 +306,27 @@ loop.store.ActiveRoomStore = (function()
      */
     _handleRoomDelete: function(eventName, roomData) {
       this._sdkDriver.forceDisconnectAll(function() {
         window.close();
       });
     },
 
     /**
+     * Handles an update of the position of the Share widget and changes to list
+     * of Social API providers, notified by the mozLoop API.
+     */
+    _handleSocialShareUpdate: function() {
+      this.dispatchAction(new sharedActions.UpdateSocialShareInfo({
+        socialShareButtonAvailable: this._mozLoop.isSocialShareButtonAvailable(),
+        socialShareProviders: this._mozLoop.getSocialShareProviders()
+      }));
+    },
+
+    /**
      * Handles the action to join to a room.
      */
     joinRoom: function() {
       // Reset the failure reason if necessary.
       if (this.getStoreState().failureReason) {
         this.setStoreState({failureReason: undefined});
       }
 
@@ -532,18 +567,22 @@ loop.store.ActiveRoomStore = (function()
       if (!this._onUpdateListener) {
         return;
       }
 
       // If we're closing the window, we can stop listening to updates.
       var roomToken = this.getStoreState().roomToken;
       this._mozLoop.rooms.off("update:" + roomToken, this._onUpdateListener);
       this._mozLoop.rooms.off("delete:" + roomToken, this._onDeleteListener);
+      window.removeEventListener("LoopShareWidgetChanged", this._onShareWidgetUpdate);
+      window.removeEventListener("LoopSocialProvidersChanged", this._onSocialProvidersUpdate);
       delete this._onUpdateListener;
       delete this._onDeleteListener;
+      delete this._onShareWidgetUpdate;
+      delete this._onSocialProvidersUpdate;
     },
 
     /**
      * Handles a room being left.
      */
     leaveRoom: function() {
       this._leaveRoom(ROOM_STATES.ENDED);
     },
--- a/browser/components/loop/content/shared/js/mixins.js
+++ b/browser/components/loop/content/shared/js/mixins.js
@@ -93,64 +93,87 @@ loop.shared.mixins = (function() {
     getInitialState: function() {
       return {showMenu: false};
     },
 
     _onBodyClick: function() {
       this.setState({showMenu: false});
     },
 
-    componentDidMount: function() {
-      this.documentBody.addEventListener("click", this._onBodyClick);
-      rootObject.addEventListener("blur", this.hideDropdownMenu);
-
-      var menu = this.refs.menu;
+    _correctMenuPosition: function() {
+      var menu = this.refs.menu && this.refs.menu.getDOMNode();
       if (!menu) {
         return;
       }
 
-      // Correct the position of the menu if necessary.
-      var menuNode = menu.getDOMNode();
-      if (!menuNode) {
-        return;
-      }
-      var menuNodeRect = menuNode.getBoundingClientRect();
+      // Correct the position of the menu only if necessary.
+      var x, y;
+      var menuNodeRect = menu.getBoundingClientRect();
+      var x = menuNodeRect.left;
+      var y = menuNodeRect.top;
+      // Amount of pixels that the dropdown needs to stay away from the edges of
+      // the page body.
+      var bodyMargin = 10;
       var bodyRect = {
-        height: this.documentBody.offsetHeight,
-        width: this.documentBody.offsetWidth
+        height: this.documentBody.offsetHeight - bodyMargin,
+        width: this.documentBody.offsetWidth - bodyMargin
       };
 
-      // First we check the vertical overflow.
-      var y = menuNodeRect.top + menuNodeRect.height;
-      if (y >= bodyRect.height) {
-        menuNode.style.marginTop = bodyRect.height - y + "px";
+      // If there's an anchor present, position it relative to it first.
+      var anchor = this.refs.anchor && this.refs.anchor.getDOMNode();
+      if (anchor) {
+        // XXXmikedeboer: at the moment we only support positioning centered above
+        //                anchor node. Please add more modes as necessary.
+        var anchorNodeRect = anchor.getBoundingClientRect();
+        // Because we're _correcting_ the position of the dropdown, we assume that
+        // the node is positioned absolute at 0,0 coordinates (top left).
+        x = anchorNodeRect.left - (menuNodeRect.width / 2) + (anchorNodeRect.width / 2);
+        y = anchorNodeRect.top - menuNodeRect.height - anchorNodeRect.height;
       }
 
-      // Then we check the horizontal overflow.
-      var x = menuNodeRect.left + menuNodeRect.width;
-      if (x >= bodyRect.width) {
-        menuNode.style.marginLeft = bodyRect.width - x + "px";
+      var overflowX = false;
+      var overflowY = false;
+      // Check the horizontal overflow.
+      if (x + menuNodeRect.width > bodyRect.width) {
+        // Anchor positioning is already relative, so don't subtract it again.
+        x = bodyRect.width - ((anchor ? 0 : x) + menuNodeRect.width);
+        overflowX = true;
       }
+      // Check the vertical overflow.
+      if (y + menuNodeRect.height > bodyRect.height) {
+        // Anchor positioning is already relative, so don't subtract it again.
+        y = bodyRect.height - ((anchor ? 0 : y) + menuNodeRect.height);
+        overflowY = true;
+      }
+
+      menu.style.marginLeft = (anchor || overflowX) ? x + "px" : "auto";
+      menu.style.marginTop = (anchor || overflowY) ? y + "px" : "auto";
+    },
+
+    componentDidMount: function() {
+      this.documentBody.addEventListener("click", this._onBodyClick);
+      rootObject.addEventListener("blur", this.hideDropdownMenu);
     },
 
     componentWillUnmount: function() {
       this.documentBody.removeEventListener("click", this._onBodyClick);
       rootObject.removeEventListener("blur", this.hideDropdownMenu);
     },
 
     showDropdownMenu: function() {
       this.setState({showMenu: true});
+      rootObject.setTimeout(this._correctMenuPosition, 0);
     },
 
     hideDropdownMenu: function() {
       this.setState({showMenu: false});
     },
 
     toggleDropdownMenu: function() {
-      this.setState({showMenu: !this.state.showMenu});
+      this[this.state.showMenu ? "hideDropdownMenu" : "showDropdownMenu"]();
     },
   };
 
   /**
    * Document visibility mixin. Allows defining the following hooks for when the
    * document visibility status changes:
    *
    * - {Function} onDocumentVisible For when the document becomes visible.
--- a/browser/components/loop/content/shared/js/roomStore.js
+++ b/browser/components/loop/content/shared/js/roomStore.js
@@ -85,28 +85,31 @@ loop.store = loop.store || {};
      */
     defaultExpiresIn: DEFAULT_EXPIRES_IN,
 
     /**
      * Registered actions.
      * @type {Array}
      */
     actions: [
+      "addSocialShareButton",
+      "addSocialShareProvider",
       "createRoom",
       "createdRoom",
       "createRoomError",
       "copyRoomUrl",
       "deleteRoom",
       "deleteRoomError",
       "emailRoomUrl",
       "getAllRooms",
       "getAllRoomsError",
       "openRoom",
       "renameRoom",
       "renameRoomError",
+      "shareRoomUrl",
       "updateRoomList"
     ],
 
     initialize: function(options) {
       if (!options.mozLoop) {
         throw new Error("Missing option mozLoop");
       }
       this._mozLoop = options.mozLoop;
@@ -337,16 +340,69 @@ loop.store = loop.store || {};
      * @param  {sharedActions.EmailRoomUrl} actionData The action data.
      */
     emailRoomUrl: function(actionData) {
       loop.shared.utils.composeCallUrlEmail(actionData.roomUrl);
       this._mozLoop.notifyUITour("Loop:RoomURLEmailed");
     },
 
     /**
+     * Share a room url.
+     *
+     * @param  {sharedActions.ShareRoomUrl} actionData The action data.
+     */
+    shareRoomUrl: function(actionData) {
+      var providerOrigin = new URL(actionData.provider.origin).hostname;
+      var shareTitle = "";
+      var shareBody = null;
+
+      switch (providerOrigin) {
+        case "mail.google.com":
+          shareTitle = mozL10n.get("share_email_subject5", {
+            clientShortname2: mozL10n.get("clientShortname2")
+          });
+          shareBody = mozL10n.get("share_email_body5", {
+            callUrl: actionData.roomUrl,
+            brandShortname: mozL10n.get("brandShortname"),
+            clientShortname2: mozL10n.get("clientShortname2"),
+            clientSuperShortname: mozL10n.get("clientSuperShortname"),
+            learnMoreUrl: this._mozLoop.getLoopPref("learnMoreUrl")
+          });
+        case "twitter.com":
+        default:
+          shareTitle = mozL10n.get("share_tweet", {
+            clientShortname2: mozL10n.get("clientShortname2")
+          });
+          break;
+      }
+
+      this._mozLoop.socialShareRoom(actionData.provider.origin, actionData.roomUrl,
+        shareTitle, shareBody);
+      this._mozLoop.notifyUITour("Loop:RoomURLShared");
+    },
+
+    /**
+     * Add the Social Share button to the browser toolbar.
+     *
+     * @param {sharedActions.AddSocialShareButton} actionData The action data.
+     */
+    addSocialShareButton: function(actionData) {
+      this._mozLoop.addSocialShareButton();
+    },
+
+    /**
+     * Open the share panel to add a Social share provider.
+     *
+     * @param {sharedActions.AddSocialShareProvider} actionData The action data.
+     */
+    addSocialShareProvider: function(actionData) {
+      this._mozLoop.addSocialShareProvider();
+    },
+
+    /**
      * Creates a new room.
      *
      * @param {sharedActions.DeleteRoom} actionData The action data.
      */
     deleteRoom: function(actionData) {
       this._mozLoop.rooms.delete(actionData.roomToken, function(err) {
         if (err) {
          this.dispatchAction(new sharedActions.DeleteRoomError({error: err}));
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..2de263f511e864a149b72d2f67589f072a3ec041
GIT binary patch
literal 3960
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
BztsQ$
--- a/browser/components/loop/standalone/content/index.html
+++ b/browser/components/loop/standalone/content/index.html
@@ -131,10 +131,14 @@
     <script type="text/javascript" src="js/webapp.js"></script>
 
     <script>
       // Wait for all the localization notes to load
       window.addEventListener('localized', function() {
         loop.webapp.init();
       }, false);
     </script>
+
+    <noscript>
+      <img src="img/logo.png" border="0" alt="Logo"/>
+    </noscript>
   </body>
 </html>
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -171,24 +171,24 @@
       "14x14": ["audio", "audio-active", "audio-disabled", "facemute",
         "facemute-active", "facemute-disabled", "hangup", "hangup-active",
         "hangup-disabled", "incoming", "incoming-active", "incoming-disabled",
         "link", "link-active", "link-disabled", "mute", "mute-active",
         "mute-disabled", "pause", "pause-active", "pause-disabled", "video",
         "video-white", "video-active", "video-disabled", "volume", "volume-active",
         "volume-disabled"
       ],
-      "16x16": ["audio", "audio-hover", "audio-active", "block", "block-red",
-        "block-hover", "block-active", "contacts", "contacts-hover", "contacts-active",
-        "copy", "checkmark", "google", "google-hover", "google-active", "history",
-        "history-hover", "history-active", "leave", "precall", "precall-hover",
+      "16x16": ["add", "add-hover", "add-active", "audio", "audio-hover", "audio-active",
+        "block", "block-red", "block-hover", "block-active", "contacts", "contacts-hover",
+        "contacts-active", "copy", "checkmark", "google", "google-hover", "google-active",
+        "history", "history-hover", "history-active", "leave", "precall", "precall-hover",
         "precall-active", "screen-white", "screenmute-white", "settings",
-        "settings-hover", "settings-active", "tag", "tag-hover", "tag-active",
-        "trash", "unblock", "unblock-hover", "unblock-active", "video", "video-hover",
-        "video-active", "tour"
+        "settings-hover", "settings-active", "share-darkgrey", "tag", "tag-hover",
+        "tag-active", "trash", "unblock", "unblock-hover", "unblock-active", "video",
+        "video-hover", "video-active", "tour"
       ]
     },
 
     render: function() {
       var icons = this.shapes[this.props.size].map(function(shapeId, i) {
         return (
           React.createElement("li", {key: this.props.size + "-" + i, className: "svg-icon-entry"}, 
             React.createElement("p", null, React.createElement(SVGIcon, {shapeId: shapeId, size: this.props.size})), 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -171,24 +171,24 @@
       "14x14": ["audio", "audio-active", "audio-disabled", "facemute",
         "facemute-active", "facemute-disabled", "hangup", "hangup-active",
         "hangup-disabled", "incoming", "incoming-active", "incoming-disabled",
         "link", "link-active", "link-disabled", "mute", "mute-active",
         "mute-disabled", "pause", "pause-active", "pause-disabled", "video",
         "video-white", "video-active", "video-disabled", "volume", "volume-active",
         "volume-disabled"
       ],
-      "16x16": ["audio", "audio-hover", "audio-active", "block", "block-red",
-        "block-hover", "block-active", "contacts", "contacts-hover", "contacts-active",
-        "copy", "checkmark", "google", "google-hover", "google-active", "history",
-        "history-hover", "history-active", "leave", "precall", "precall-hover",
+      "16x16": ["add", "add-hover", "add-active", "audio", "audio-hover", "audio-active",
+        "block", "block-red", "block-hover", "block-active", "contacts", "contacts-hover",
+        "contacts-active", "copy", "checkmark", "google", "google-hover", "google-active",
+        "history", "history-hover", "history-active", "leave", "precall", "precall-hover",
         "precall-active", "screen-white", "screenmute-white", "settings",
-        "settings-hover", "settings-active", "tag", "tag-hover", "tag-active",
-        "trash", "unblock", "unblock-hover", "unblock-active", "video", "video-hover",
-        "video-active", "tour"
+        "settings-hover", "settings-active", "share-darkgrey", "tag", "tag-hover",
+        "tag-active", "trash", "unblock", "unblock-hover", "unblock-active", "video",
+        "video-hover", "video-active", "tour"
       ]
     },
 
     render: function() {
       var icons = this.shapes[this.props.size].map(function(shapeId, i) {
         return (
           <li key={this.props.size + "-" + i} className="svg-icon-entry">
             <p><SVGIcon shapeId={shapeId} size={this.props.size} /></p>
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -43,18 +43,28 @@ problem_accessing_account=There Was A Pr
 ## the appropriate action.
 ## See https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#error for location
 retry_button=Retry
 
 share_email_subject5={{clientShortname2}} — Join the conversation
 ## LOCALIZATION NOTE (share_email_body4): In this item, don't translate the
 ## part between {{..}} and leave the \n\n part alone
 share_email_body5=Hello!\n\nJoin me for a video conversation on {{clientShortname2}}.\n\nIt's the easiest way to connect by video with anyone anywhere.  With {{clientSuperShortname}}, you don't have to download or install anything. Just click or paste this link into your {{brandShortname}}, Opera, or Chrome browser to join the conversation:\n\n{{callUrl}}\n\nIf you'd like to learn more about {{clientSuperShortname}} and how you can start your own free video conversations, visit {{learnMoreUrl}}\n\nTalk to you soon!
+## LOCALIZATION NOTE (share_tweeet): In this item, don't translate the part
+## between {{..}}. Please keep the text below 117 characters to make sure it fits
+## in a tweet.
+share_tweet=Join me for a video conversation on {{clientShortname2}}!
 
-share_button2=Email Link
+share_button3=Share Link
+share_add_service_button=Add a Service
+add_to_toolbar_button=Add the Share panel to my toolbar
+share_panel_header=Share the web with your friends!
+## LOCALIZATION NOTE (share_panel_body): In this item, don't translate the part
+## between {{..}}.
+share_panel_body={{brandShortname}}'s new Share panel allows you to quickly share links or {{clientSuperShortname}} invitations with your favourite social networks.
 copy_url_button2=Copy Link
 copied_url_button=Copied!
 
 panel_footer_signin_or_signup_link=Sign In or Sign Up
 
 settings_menu_item_account=Account
 settings_menu_item_settings=Settings
 settings_menu_item_signout=Sign Out