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 cad808749b13 ☠ ☠
authorMike de Boer <mdeboer@mozilla.com>
Thu, 09 Apr 2015 16:29:16 +0200
changeset 257362 a2075595f6fdc242347b49954a4b84d7469bd43a
parent 257361 a2b8f0ddd7384a9a545271ec05927634bee85b2f
child 257363 c99243d545e26e3bb05ecac0b7a40cd10ff78453
push id8007
push userraliiev@mozilla.com
push dateMon, 11 May 2015 19:23:16 +0000
treeherdermozilla-aurora@e2ce1aac996e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs1132301
milestone40.0a1
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/roomStore.js
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/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
--- 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/roomStore.js
+++ b/browser/components/loop/content/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}));
--- 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")});
       }
     },
@@ -146,18 +251,27 @@ loop.roomViews = (function(mozL10n) {
             React.createElement("button", {className: "btn btn-info btn-email", 
                     onClick: this.handleEmailButtonClick}, 
               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.
    */
@@ -287,13 +401,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")});
       }
     },
@@ -147,17 +252,26 @@ loop.roomViews = (function(mozL10n) {
                     onClick={this.handleEmailButtonClick}>
               {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.
    */
@@ -287,13 +401,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
@@ -355,16 +355,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
     }),
 
@@ -373,33 +396,44 @@ 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", {
       // context: Object - Optional.
       // 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
@@ -78,17 +78,20 @@ loop.store.ActiveRoomStore = (function()
         used: false,
         localVideoDimensions: {},
         remoteVideoDimensions: {},
         screenSharingState: SCREEN_SHARE_STATES.INACTIVE,
         receivingScreenShare: false,
         // The roomCryptoKey to decode the context data if necessary.
         roomCryptoKey: null,
         // Room information failed to be obtained for a reason. See ROOM_INFO_FAILURES.
-        roomInfoFailure: null
+        roomInfoFailure: null,
+        // Social API state.
+        socialShareButtonAvailable: false,
+        socialShareProviders: null
       };
     },
 
     /**
      * Handles a room failure.
      *
      * @param {sharedActions.RoomFailure} actionData
      */
@@ -134,17 +137,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.
@@ -174,17 +178,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));
     },
 
@@ -287,24 +293,29 @@ 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) {
@@ -312,16 +323,29 @@ loop.store.ActiveRoomStore = (function()
         roomInfoFailure: actionData.roomInfoFailure,
         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,
@@ -338,16 +362,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});
       }
 
@@ -588,18 +623,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,96 @@ 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;
       }
+
+      if (anchor || overflowX) {
+        menu.style.marginLeft = x + "px";
+      } else if (!menu.style.marginLeft) {
+        menu.style.marginLeft = "auto";
+      }
+
+      if (anchor || overflowY) {
+        menu.style.marginTop =  y + "px";
+      } else if (!menu.style.marginLeft) {
+        menu.style.marginTop = "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.
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
@@ -132,10 +132,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>