Bug 1047144: Add a gear menu to the Loop panel. r=Standard8
authorNicolas Perriault <nperriault@gmail.com>
Mon, 01 Sep 2014 18:11:50 +0100
changeset 224402 05cbe9a31ad3c94e3782bc626d8e26b3b1e7fb8c
parent 224401 0efafd8c0a2430845f21f3d3a6e70325ff40685c
child 224403 1230a7a5c04b360610db9cc067efbe7bc7811c68
push id3979
push userraliiev@mozilla.com
push dateMon, 13 Oct 2014 16:35:44 +0000
treeherdermozilla-beta@30f2cc610691 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs1047144
milestone34.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 1047144: Add a gear menu to the Loop panel. r=Standard8
browser/components/loop/content/js/panel.js
browser/components/loop/content/js/panel.jsx
browser/components/loop/content/shared/css/panel.css
browser/components/loop/content/shared/img/svg/glyph-account-16x16.svg
browser/components/loop/content/shared/img/svg/glyph-settings-16x16.svg
browser/components/loop/content/shared/img/svg/glyph-signin-16x16.svg
browser/components/loop/content/shared/img/svg/glyph-signout-16x16.svg
browser/components/loop/jar.mn
browser/components/loop/standalone/content/css/webapp.css
browser/components/loop/test/desktop-local/panel_test.js
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/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -17,32 +17,55 @@ loop.panel = (function(_, mozL10n) {
 
   /**
    * Panel router.
    * @type {loop.desktopRouter.DesktopRouter}
    */
   var router;
 
   /**
-   * Availability drop down menu subview.
+   * Dropdown menu mixin.
+   * @type {Object}
    */
-  var AvailabilityDropdown = React.createClass({displayName: 'AvailabilityDropdown',
+  var DropdownMenuMixin = {
     getInitialState: function() {
-      return {
-        doNotDisturb: navigator.mozLoop.doNotDisturb,
-        showMenu: false
-      };
+      return {showMenu: false};
+    },
+
+    _onBodyClick: function() {
+      this.setState({showMenu: false});
+    },
+
+    componentDidMount: function() {
+      document.body.addEventListener("click", this._onBodyClick);
+    },
+
+    componentWillUnmount: function() {
+      document.body.removeEventListener("click", this._onBodyClick);
     },
 
     showDropdownMenu: function() {
       this.setState({showMenu: true});
     },
 
     hideDropdownMenu: function() {
       this.setState({showMenu: false});
+    }
+  };
+
+  /**
+   * Availability drop down menu subview.
+   */
+  var AvailabilityDropdown = React.createClass({displayName: 'AvailabilityDropdown',
+    mixins: [DropdownMenuMixin],
+
+    getInitialState: function() {
+      return {
+        doNotDisturb: navigator.mozLoop.doNotDisturb
+      };
     },
 
     // XXX target event can either be the li, the span or the i tag
     // this makes it easier to figure out the target by making a
     // closure with the desired status already passed in.
     changeAvailability: function(newAvailabilty) {
       return function(event) {
         // Note: side effect!
@@ -64,38 +87,38 @@ loop.panel = (function(_, mozL10n) {
       // XXX https://github.com/facebook/react/issues/310 for === htmlFor
       var cx = React.addons.classSet;
       var availabilityStatus = cx({
         'status': true,
         'status-dnd': this.state.doNotDisturb,
         'status-available': !this.state.doNotDisturb
       });
       var availabilityDropdown = cx({
-        'dnd-menu': true,
+        'dropdown-menu': true,
         'hide': !this.state.showMenu
       });
       var availabilityText = this.state.doNotDisturb ?
                               __("display_name_dnd_status") :
                               __("display_name_available_status");
 
       return (
-        React.DOM.div({className: "do-not-disturb"}, 
+        React.DOM.div({className: "dropdown"}, 
           React.DOM.p({className: "dnd-status", onClick: this.showDropdownMenu}, 
             React.DOM.span(null, availabilityText), 
             React.DOM.i({className: availabilityStatus})
           ), 
           React.DOM.ul({className: availabilityDropdown, 
               onMouseLeave: this.hideDropdownMenu}, 
             React.DOM.li({onClick: this.changeAvailability("available"), 
-                className: "dnd-menu-item dnd-make-available"}, 
+                className: "dropdown-menu-item dnd-make-available"}, 
               React.DOM.i({className: "status status-available"}), 
               React.DOM.span(null, __("display_name_available_status"))
             ), 
             React.DOM.li({onClick: this.changeAvailability("do-not-disturb"), 
-                className: "dnd-menu-item dnd-make-unavailable"}, 
+                className: "dropdown-menu-item dnd-make-unavailable"}, 
               React.DOM.i({className: "status status-dnd"}), 
               React.DOM.span(null, __("display_name_dnd_status"))
             )
           )
         )
       );
     }
   });
@@ -124,16 +147,103 @@ loop.panel = (function(_, mozL10n) {
         return React.DOM.p({className: "terms-service", 
                   dangerouslySetInnerHTML: {__html: tosHTML}});
       } else {
         return React.DOM.div(null);
       }
     }
   });
 
+  /**
+   * Panel settings (gear) menu entry.
+   */
+  var SettingsDropdownEntry = React.createClass({displayName: 'SettingsDropdownEntry',
+    propTypes: {
+      onClick: React.PropTypes.func.isRequired,
+      label: React.PropTypes.string.isRequired,
+      icon: React.PropTypes.string,
+      displayed: React.PropTypes.bool
+    },
+
+    getDefaultProps: function() {
+      return {displayed: true};
+    },
+
+    render: function() {
+      if (!this.props.displayed) {
+        return null;
+      }
+      return (
+        React.DOM.li({onClick: this.props.onClick, className: "dropdown-menu-item"}, 
+          this.props.icon ?
+            React.DOM.i({className: "icon icon-" + this.props.icon}) :
+            null, 
+          React.DOM.span(null, this.props.label)
+        )
+      );
+    }
+  });
+
+  /**
+   * Panel settings (gear) menu.
+   */
+  var SettingsDropdown = React.createClass({displayName: 'SettingsDropdown',
+    mixins: [DropdownMenuMixin],
+
+    handleClickSettingsEntry: function() {
+      // XXX to be implemented
+    },
+
+    handleClickAccountEntry: function() {
+      // XXX to be implemented
+    },
+
+    handleClickAuthEntry: function() {
+      if (this._isSignedIn()) {
+        // XXX to be implemented - bug 979845
+        navigator.mozLoop.logOutFromFxA();
+      } else {
+        navigator.mozLoop.logInToFxA();
+      }
+    },
+
+    _isSignedIn: function() {
+      // XXX to be implemented - bug 979845
+      return !!navigator.mozLoop.loggedInToFxA;
+    },
+
+    render: function() {
+      var cx = React.addons.classSet;
+      return (
+        React.DOM.div({className: "settings-menu dropdown"}, 
+          React.DOM.a({className: "btn btn-settings", onClick: this.showDropdownMenu, 
+             title: __("settings_menu_button_tooltip")}), 
+          React.DOM.ul({className: cx({"dropdown-menu": true, hide: !this.state.showMenu}), 
+              onMouseLeave: this.hideDropdownMenu}, 
+            SettingsDropdownEntry({label: __("settings_menu_item_settings"), 
+                                   onClick: this.handleClickSettingsEntry, 
+                                   icon: "settings"}), 
+            SettingsDropdownEntry({label: __("settings_menu_item_account"), 
+                                   onClick: this.handleClickAccountEntry, 
+                                   icon: "account", 
+                                   displayed: this._isSignedIn()}), 
+            SettingsDropdownEntry({label: this._isSignedIn() ?
+                                          __("settings_menu_item_signout") :
+                                          __("settings_menu_item_signin"), 
+                                   onClick: this.handleClickAuthEntry, 
+                                   icon: this._isSignedIn() ? "signout" : "signin"})
+          )
+        )
+      );
+    }
+  });
+
+  /**
+   * Panel layout.
+   */
   var PanelLayout = React.createClass({displayName: 'PanelLayout',
     propTypes: {
       summary: React.PropTypes.string.isRequired
     },
 
     render: function() {
       return (
         React.DOM.div({className: "share generate-url"}, 
@@ -255,42 +365,59 @@ loop.panel = (function(_, mozL10n) {
             )
           )
         )
       );
     }
   });
 
   /**
+   * FxA sign in/up link component.
+   */
+  var AuthLink = React.createClass({displayName: 'AuthLink',
+    handleSignUpLinkClick: function() {
+      navigator.mozLoop.logInToFxA();
+    },
+
+    render: function() {
+      if (navigator.mozLoop.loggedInToFxA) { // XXX to be implemented
+        return null;
+      }
+      return (
+        React.DOM.p({className: "signin-link"}, 
+          React.DOM.a({href: "#", onClick: this.handleSignUpLinkClick}, 
+            __("panel_footer_signin_or_signup_link")
+          )
+        )
+      );
+    }
+  });
+
+  /**
    * Panel view.
    */
   var PanelView = React.createClass({displayName: 'PanelView',
     propTypes: {
       notifier: React.PropTypes.object.isRequired,
       client: React.PropTypes.object.isRequired,
       // Mostly used for UI components showcase and unit tests
       callUrl: React.PropTypes.string
     },
 
-    handleSignUpLinkClick: function() {
-      navigator.mozLoop.logInToFxA();
-    },
-
     render: function() {
       return (
         React.DOM.div(null, 
           CallUrlResult({client: this.props.client, 
                          notifier: this.props.notifier, 
                          callUrl: this.props.callUrl}), 
           ToSView(null), 
           React.DOM.div({className: "footer"}, 
             AvailabilityDropdown(null), 
-            React.DOM.a({className: "signin-link", href: "#", onClick: this.handleSignUpLinkClick}, 
-              __("panel_footer_signin_or_signup_link")
-            )
+            AuthLink(null), 
+            SettingsDropdown(null)
           )
         )
       );
     }
   });
 
   var PanelRouter = loop.desktopRouter.DesktopRouter.extend({
     /**
@@ -382,11 +509,12 @@ loop.panel = (function(_, mozL10n) {
   }
 
   return {
     init: init,
     AvailabilityDropdown: AvailabilityDropdown,
     CallUrlResult: CallUrlResult,
     PanelView: PanelView,
     PanelRouter: PanelRouter,
+    SettingsDropdown: SettingsDropdown,
     ToSView: ToSView
   };
 })(_, document.mozL10n);
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -17,32 +17,55 @@ loop.panel = (function(_, mozL10n) {
 
   /**
    * Panel router.
    * @type {loop.desktopRouter.DesktopRouter}
    */
   var router;
 
   /**
-   * Availability drop down menu subview.
+   * Dropdown menu mixin.
+   * @type {Object}
    */
-  var AvailabilityDropdown = React.createClass({
+  var DropdownMenuMixin = {
     getInitialState: function() {
-      return {
-        doNotDisturb: navigator.mozLoop.doNotDisturb,
-        showMenu: false
-      };
+      return {showMenu: false};
+    },
+
+    _onBodyClick: function() {
+      this.setState({showMenu: false});
+    },
+
+    componentDidMount: function() {
+      document.body.addEventListener("click", this._onBodyClick);
+    },
+
+    componentWillUnmount: function() {
+      document.body.removeEventListener("click", this._onBodyClick);
     },
 
     showDropdownMenu: function() {
       this.setState({showMenu: true});
     },
 
     hideDropdownMenu: function() {
       this.setState({showMenu: false});
+    }
+  };
+
+  /**
+   * Availability drop down menu subview.
+   */
+  var AvailabilityDropdown = React.createClass({
+    mixins: [DropdownMenuMixin],
+
+    getInitialState: function() {
+      return {
+        doNotDisturb: navigator.mozLoop.doNotDisturb
+      };
     },
 
     // XXX target event can either be the li, the span or the i tag
     // this makes it easier to figure out the target by making a
     // closure with the desired status already passed in.
     changeAvailability: function(newAvailabilty) {
       return function(event) {
         // Note: side effect!
@@ -64,38 +87,38 @@ loop.panel = (function(_, mozL10n) {
       // XXX https://github.com/facebook/react/issues/310 for === htmlFor
       var cx = React.addons.classSet;
       var availabilityStatus = cx({
         'status': true,
         'status-dnd': this.state.doNotDisturb,
         'status-available': !this.state.doNotDisturb
       });
       var availabilityDropdown = cx({
-        'dnd-menu': true,
+        'dropdown-menu': true,
         'hide': !this.state.showMenu
       });
       var availabilityText = this.state.doNotDisturb ?
                               __("display_name_dnd_status") :
                               __("display_name_available_status");
 
       return (
-        <div className="do-not-disturb">
+        <div className="dropdown">
           <p className="dnd-status" onClick={this.showDropdownMenu}>
             <span>{availabilityText}</span>
             <i className={availabilityStatus}></i>
           </p>
           <ul className={availabilityDropdown}
               onMouseLeave={this.hideDropdownMenu}>
             <li onClick={this.changeAvailability("available")}
-                className="dnd-menu-item dnd-make-available">
+                className="dropdown-menu-item dnd-make-available">
               <i className="status status-available"></i>
               <span>{__("display_name_available_status")}</span>
             </li>
             <li onClick={this.changeAvailability("do-not-disturb")}
-                className="dnd-menu-item dnd-make-unavailable">
+                className="dropdown-menu-item dnd-make-unavailable">
               <i className="status status-dnd"></i>
               <span>{__("display_name_dnd_status")}</span>
             </li>
           </ul>
         </div>
       );
     }
   });
@@ -124,16 +147,103 @@ loop.panel = (function(_, mozL10n) {
         return <p className="terms-service"
                   dangerouslySetInnerHTML={{__html: tosHTML}}></p>;
       } else {
         return <div />;
       }
     }
   });
 
+  /**
+   * Panel settings (gear) menu entry.
+   */
+  var SettingsDropdownEntry = React.createClass({
+    propTypes: {
+      onClick: React.PropTypes.func.isRequired,
+      label: React.PropTypes.string.isRequired,
+      icon: React.PropTypes.string,
+      displayed: React.PropTypes.bool
+    },
+
+    getDefaultProps: function() {
+      return {displayed: true};
+    },
+
+    render: function() {
+      if (!this.props.displayed) {
+        return null;
+      }
+      return (
+        <li onClick={this.props.onClick} className="dropdown-menu-item">
+          {this.props.icon ?
+            <i className={"icon icon-" + this.props.icon}></i> :
+            null}
+          <span>{this.props.label}</span>
+        </li>
+      );
+    }
+  });
+
+  /**
+   * Panel settings (gear) menu.
+   */
+  var SettingsDropdown = React.createClass({
+    mixins: [DropdownMenuMixin],
+
+    handleClickSettingsEntry: function() {
+      // XXX to be implemented
+    },
+
+    handleClickAccountEntry: function() {
+      // XXX to be implemented
+    },
+
+    handleClickAuthEntry: function() {
+      if (this._isSignedIn()) {
+        // XXX to be implemented - bug 979845
+        navigator.mozLoop.logOutFromFxA();
+      } else {
+        navigator.mozLoop.logInToFxA();
+      }
+    },
+
+    _isSignedIn: function() {
+      // XXX to be implemented - bug 979845
+      return !!navigator.mozLoop.loggedInToFxA;
+    },
+
+    render: function() {
+      var cx = React.addons.classSet;
+      return (
+        <div className="settings-menu dropdown">
+          <a className="btn btn-settings" onClick={this.showDropdownMenu}
+             title={__("settings_menu_button_tooltip")} />
+          <ul className={cx({"dropdown-menu": true, hide: !this.state.showMenu})}
+              onMouseLeave={this.hideDropdownMenu}>
+            <SettingsDropdownEntry label={__("settings_menu_item_settings")}
+                                   onClick={this.handleClickSettingsEntry}
+                                   icon="settings" />
+            <SettingsDropdownEntry label={__("settings_menu_item_account")}
+                                   onClick={this.handleClickAccountEntry}
+                                   icon="account"
+                                   displayed={this._isSignedIn()} />
+            <SettingsDropdownEntry label={this._isSignedIn() ?
+                                          __("settings_menu_item_signout") :
+                                          __("settings_menu_item_signin")}
+                                   onClick={this.handleClickAuthEntry}
+                                   icon={this._isSignedIn() ? "signout" : "signin"} />
+          </ul>
+        </div>
+      );
+    }
+  });
+
+  /**
+   * Panel layout.
+   */
   var PanelLayout = React.createClass({
     propTypes: {
       summary: React.PropTypes.string.isRequired
     },
 
     render: function() {
       return (
         <div className="share generate-url">
@@ -255,42 +365,59 @@ loop.panel = (function(_, mozL10n) {
             </p>
           </div>
         </PanelLayout>
       );
     }
   });
 
   /**
+   * FxA sign in/up link component.
+   */
+  var AuthLink = React.createClass({
+    handleSignUpLinkClick: function() {
+      navigator.mozLoop.logInToFxA();
+    },
+
+    render: function() {
+      if (navigator.mozLoop.loggedInToFxA) { // XXX to be implemented
+        return null;
+      }
+      return (
+        <p className="signin-link">
+          <a href="#" onClick={this.handleSignUpLinkClick}>
+            {__("panel_footer_signin_or_signup_link")}
+          </a>
+        </p>
+      );
+    }
+  });
+
+  /**
    * Panel view.
    */
   var PanelView = React.createClass({
     propTypes: {
       notifier: React.PropTypes.object.isRequired,
       client: React.PropTypes.object.isRequired,
       // Mostly used for UI components showcase and unit tests
       callUrl: React.PropTypes.string
     },
 
-    handleSignUpLinkClick: function() {
-      navigator.mozLoop.logInToFxA();
-    },
-
     render: function() {
       return (
         <div>
           <CallUrlResult client={this.props.client}
                          notifier={this.props.notifier}
                          callUrl={this.props.callUrl} />
           <ToSView />
           <div className="footer">
             <AvailabilityDropdown />
-            <a className="signin-link" href="#" onClick={this.handleSignUpLinkClick}>
-              {__("panel_footer_signin_or_signup_link")}
-            </a>
+            <AuthLink />
+            <SettingsDropdown />
           </div>
         </div>
       );
     }
   });
 
   var PanelRouter = loop.desktopRouter.DesktopRouter.extend({
     /**
@@ -382,11 +509,12 @@ loop.panel = (function(_, mozL10n) {
   }
 
   return {
     init: init,
     AvailabilityDropdown: AvailabilityDropdown,
     CallUrlResult: CallUrlResult,
     PanelView: PanelView,
     PanelRouter: PanelRouter,
+    SettingsDropdown: SettingsDropdown,
     ToSView: ToSView
   };
 })(_, document.mozL10n);
--- a/browser/components/loop/content/shared/css/panel.css
+++ b/browser/components/loop/content/shared/css/panel.css
@@ -69,67 +69,69 @@
 }
 
 /* Specific cases */
 
 .panel #messages .alert {
   margin-bottom: 0;
 }
 
+/* Dropdown menu (shared styles) */
+
+.dropdown {
+  position: relative;
+}
+
+.dropdown-menu {
+  position: absolute;
+  top: -28px;
+  left: 0;
+  background: #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: #eee;
+}
+
 /* DnD menu */
 
 .dnd-status {
   border: 1px solid transparent;
   padding: 2px 4px;
   font-size: .9em;
   cursor: pointer;
   border-radius: 3px;
 }
 
 .dnd-status:hover {
   border: 1px solid #DDD;
   background: #F1F1F1;
 }
 
-.do-not-disturb {
-  position: relative;
-}
-
-.dnd-menu {
-  position: absolute;
-  top: -28px;
-  left: 0;
-  background: #fdfdfd;
-  box-shadow: 0 1px 3px rgba(0,0,0,.3);
-  list-style: none;
-  padding: 5px;
-  border-radius: 2px;
-}
-
-body[dir=rtl] .dnd-menu {
-  left: auto;
-  right: 10px;
-}
-
-.dnd-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;
-}
-
-.dnd-menu-item:hover {
-  border: 1px solid #ccc;
-  background: #eee;
-}
-
 /* Status badges -- Available/Unavailable */
 
 .status {
   display: inline-block;
   width: 8px;
   height: 8px;
   margin: 0 5px;
   border-radius: 50%;
@@ -141,22 +143,78 @@ body[dir=rtl] .dnd-menu {
 
 .status-dnd {
   border: 1px solid #888;
 }
 
 /* Sign in/up link */
 
 .signin-link {
-  display: none; /* XXX This should be removed as soon bugs 1047144 & 979845 land */
-  line-height: 100%;
+  display: none; /* XXX This should be displayed as soon bug 979845 lands */
+  flex: 2 1 auto;
+  margin-top: 14px;
+  border-right: 1px solid #aaa;
+  padding-right: 1em;
+  margin-right: 1em;
+  text-align: right;
+}
+
+.signin-link a {
   font-size: .9em;
   text-decoration: none;
   color: #888;
-  margin-top: 16px;
+}
+
+/* Settings (gear) menu */
+
+.btn-settings {
+  display: none; /* XXX This should be displayed as soon bug 979845 lands */
+  background: transparent url(../img/svg/glyph-settings-16x16.svg) no-repeat center center;
+  background-size: contain;
+  width: 12px;
+  height: 12px;
+}
+
+.footer .btn-settings {
+  margin-top: 17px; /* used to align the gear icon with the availability dropdown menu inner text */
+  opacity: .6;      /* used to "grey" the icon a little */
+}
+
+.settings-menu .dropdown-menu {
+  /* The panel can't have dropdown menu overflowing its iframe boudaries;
+     let's anchor it from the bottom-right, while resetting the top & left values
+     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;
+}
+
+.settings-menu .icon-settings {
+  background: transparent url(../img/svg/glyph-settings-16x16.svg) no-repeat center center;
+}
+
+.settings-menu .icon-account {
+  background: transparent url(../img/svg/glyph-account-16x16.svg) no-repeat center center;
+}
+
+.settings-menu .icon-signin {
+  background: transparent url(../img/svg/glyph-signin-16x16.svg) no-repeat center center;
+}
+
+.settings-menu .icon-signout {
+  background: transparent url(../img/svg/glyph-signout-16x16.svg) no-repeat center center;
 }
 
 /* Terms of Service */
 
 .terms-service {
   padding: 3px 10px 10px;
   background: #FFF;
   text-align: center;
@@ -181,9 +239,8 @@ body[dir=rtl] .dnd-menu {
   align-items: flex-start;
   font-size: 1em;
   border-top: 1px solid #D1D1D1;
   background: #EAEAEA;
   color: #7F7F7F;
   padding: 14px;
   margin-top: 14px;
 }
-
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/svg/glyph-account-16x16.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
+<g id="Contacts">
+	<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M8,6.526c1.802,0,3.263-1.461,3.263-3.263
+		C11.263,1.461,9.802,0,8,0C6.198,0,4.737,1.461,4.737,3.263C4.737,5.066,6.198,6.526,8,6.526z M14.067,11.421c0,0,0-0.001,0-0.001
+		c0-1.676-1.397-3.119-3.419-3.807L8.001,10.26L5.354,7.613C3.331,8.3,1.933,9.744,1.933,11.42v0.001H1.93
+		c0,1.679,0.328,3.246,0.896,4.579h10.348c0.568-1.333,0.896-2.9,0.896-4.579H14.067z"/>
+</g>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/svg/glyph-settings-16x16.svg
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
+<path id="Setting" fill-rule="evenodd" clip-rule="evenodd" fill="#131311" d="M14.77,8c0,0.804,0.262,1.548,0.634,1.678L16,9.887
+	c-0.205,0.874-0.553,1.692-1.011,2.434l-0.567-0.272c-0.355-0.171-1.066,0.17-1.635,0.738c-0.569,0.569-0.909,1.279-0.738,1.635
+	l0.273,0.568c-0.741,0.46-1.566,0.79-2.438,0.998l-0.205-0.584c-0.13-0.372-0.874-0.634-1.678-0.634s-1.548,0.262-1.678,0.634
+	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"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/svg/glyph-signin-16x16.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
+<g id="Outgoing_14x14_1_">
+	<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M9.921,8.415c0.105-0.11,0.146-0.265,0.13-0.432
+		c0.016-0.166-0.025-0.321-0.13-0.429L9.305,6.938l-2.6-2.65C6.402,3.973,5.973,3.906,5.748,4.139L5.238,4.68
+		c-0.225,0.233-0.16,0.679,0.144,0.995L6.44,6.754H0.608C0.272,6.754,0,7.026,0,7.361l0,1.215c0,0.335,0.272,0.607,0.608,0.607H6.47
+		l-1.136,1.155c-0.305,0.313-0.369,0.756-0.144,0.987L5.7,11.861c0.225,0.233,0.654,0.166,0.959-0.149l2.619-2.663L9.921,8.415z"/>
+</g>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M14,0H5.558c-0.331,0-0.6,0.269-0.6,0.6v0.8
+	c0,0.331,0.269,0.6,0.6,0.6H12.5C13.328,2,14,2.672,14,3.5v9c0,0.828-0.672,1.5-1.5,1.5H5.558c-0.331,0-0.6,0.269-0.6,0.6v0.8
+	c0,0.331,0.269,0.6,0.6,0.6H14c1.105,0,2-0.895,2-2V2C16,0.895,15.105,0,14,0z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/svg/glyph-signout-16x16.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M14,0H5.558c-0.331,0-0.6,0.269-0.6,0.6v0.8
+	c0,0.331,0.269,0.6,0.6,0.6H12.5C13.328,2,14,2.672,14,3.5v9c0,0.828-0.672,1.5-1.5,1.5H5.558c-0.331,0-0.6,0.269-0.6,0.6v0.8
+	c0,0.331,0.269,0.6,0.6,0.6H14c1.105,0,2-0.895,2-2V2C16,0.895,15.105,0,14,0z"/>
+<g id="Outgoing_14x14_2_">
+	<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M0.133,7.585c-0.105,0.11-0.146,0.265-0.13,0.432
+		c-0.016,0.166,0.025,0.321,0.13,0.429l0.616,0.615l2.6,2.65c0.304,0.315,0.732,0.382,0.958,0.149l0.51-0.541
+		c0.225-0.233,0.16-0.679-0.144-0.995L3.615,9.246h5.832c0.335,0,0.608-0.272,0.608-0.607V7.424c0-0.335-0.272-0.607-0.608-0.607
+		H3.585L4.72,5.662c0.305-0.313,0.369-0.756,0.144-0.987L4.355,4.139C4.13,3.906,3.701,3.973,3.396,4.287L0.777,6.951L0.133,7.585z"
+		/>
+</g>
+</svg>
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -33,18 +33,22 @@ browser.jar:
   content/browser/loop/shared/img/facemute-14x14.png            (content/shared/img/facemute-14x14.png)
   content/browser/loop/shared/img/facemute-14x14@2x.png         (content/shared/img/facemute-14x14@2x.png)
   content/browser/loop/shared/img/hangup-inverse-14x14.png      (content/shared/img/hangup-inverse-14x14.png)
   content/browser/loop/shared/img/hangup-inverse-14x14@2x.png   (content/shared/img/hangup-inverse-14x14@2x.png)
   content/browser/loop/shared/img/mute-inverse-14x14.png        (content/shared/img/mute-inverse-14x14.png)
   content/browser/loop/shared/img/mute-inverse-14x14@2x.png     (content/shared/img/mute-inverse-14x14@2x.png)
   content/browser/loop/shared/img/video-inverse-14x14.png       (content/shared/img/video-inverse-14x14.png)
   content/browser/loop/shared/img/video-inverse-14x14@2x.png    (content/shared/img/video-inverse-14x14@2x.png)
-  content/browser/loop/shared/img/dropdown-inverse.png       (content/shared/img/dropdown-inverse.png)
-  content/browser/loop/shared/img/dropdown-inverse@2x.png    (content/shared/img/dropdown-inverse@2x.png)
+  content/browser/loop/shared/img/dropdown-inverse.png          (content/shared/img/dropdown-inverse.png)
+  content/browser/loop/shared/img/dropdown-inverse@2x.png       (content/shared/img/dropdown-inverse@2x.png)
+  content/browser/loop/shared/img/svg/glyph-settings-16x16.svg  (content/shared/img/svg/glyph-settings-16x16.svg)
+  content/browser/loop/shared/img/svg/glyph-account-16x16.svg   (content/shared/img/svg/glyph-account-16x16.svg)
+  content/browser/loop/shared/img/svg/glyph-signin-16x16.svg    (content/shared/img/svg/glyph-signin-16x16.svg)
+  content/browser/loop/shared/img/svg/glyph-signout-16x16.svg   (content/shared/img/svg/glyph-signout-16x16.svg)
 
   # Shared scripts
   content/browser/loop/shared/js/feedbackApiClient.js (content/shared/js/feedbackApiClient.js)
   content/browser/loop/shared/js/models.js            (content/shared/js/models.js)
   content/browser/loop/shared/js/router.js            (content/shared/js/router.js)
   content/browser/loop/shared/js/views.js             (content/shared/js/views.js)
   content/browser/loop/shared/js/utils.js             (content/shared/js/utils.js)
   content/browser/loop/shared/js/websocket.js         (content/shared/js/websocket.js)
--- a/browser/components/loop/standalone/content/css/webapp.css
+++ b/browser/components/loop/standalone/content/css/webapp.css
@@ -50,18 +50,16 @@ body,
 
 .container-box {
   display: flex;
   flex-direction: column;
   width: 100%;
   align-content: center;
 }
 
-.footer,
-.footer a,
 .terms-service,
 .terms-service a {
   font-size: .6rem;
   font-weight: 400;
   color: #adadad;
 }
 
   .terms-service a {
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -212,28 +212,100 @@ describe("loop.panel", function() {
       };
 
       view = TestUtils.renderIntoDocument(loop.panel.PanelView({
         notifier: notifier,
         client: fakeClient
       }));
     });
 
-    describe("FxA sign in/up link", function() {
+    describe("AuthLink", function() {
       it("should trigger the FxA sign in/up process when clicking the link",
         function() {
+          navigator.mozLoop.loggedInToFxA = false;
           navigator.mozLoop.logInToFxA = sandbox.stub();
 
           TestUtils.Simulate.click(
-            view.getDOMNode().querySelector(".signin-link"));
+            view.getDOMNode().querySelector(".signin-link a"));
 
           sinon.assert.calledOnce(navigator.mozLoop.logInToFxA);
         });
       });
 
+    describe("SettingsDropdown", function() {
+      var view;
+
+      beforeEach(function() {
+        navigator.mozLoop.logInToFxA = sandbox.stub();
+        navigator.mozLoop.logOutFromFxA = sandbox.stub();
+      });
+
+      it("should show a signin entry when user is not authenticated",
+        function() {
+          navigator.mozLoop.loggedInToFxA = false;
+
+          var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
+
+          expect(view.getDOMNode().querySelectorAll(".icon-signout"))
+            .to.have.length.of(0);
+          expect(view.getDOMNode().querySelectorAll(".icon-signin"))
+            .to.have.length.of(1);
+        });
+
+      it("should show a signout entry when user is authenticated", function() {
+        navigator.mozLoop.loggedInToFxA = true;
+
+        var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
+
+        expect(view.getDOMNode().querySelectorAll(".icon-signout"))
+          .to.have.length.of(1);
+        expect(view.getDOMNode().querySelectorAll(".icon-signin"))
+          .to.have.length.of(0);
+      });
+
+      it("should show an account entry when user is authenticated", function() {
+        navigator.mozLoop.loggedInToFxA = true;
+
+        var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
+
+        expect(view.getDOMNode().querySelectorAll(".icon-account"))
+          .to.have.length.of(1);
+      });
+
+      it("should hide any account entry when user is not authenticated",
+        function() {
+          navigator.mozLoop.loggedInToFxA = false;
+
+          var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
+
+          expect(view.getDOMNode().querySelectorAll(".icon-account"))
+            .to.have.length.of(0);
+        });
+
+      it("should sign in the user on click when unauthenticated", function() {
+        navigator.mozLoop.loggedInToFxA = false;
+        var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
+
+        TestUtils.Simulate.click(
+          view.getDOMNode().querySelector(".icon-signin"));
+
+        sinon.assert.calledOnce(navigator.mozLoop.logInToFxA);
+      });
+
+      it("should sign out the user on click when authenticated", function() {
+        navigator.mozLoop.loggedInToFxA = true;
+        var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
+
+        TestUtils.Simulate.click(
+          view.getDOMNode().querySelector(".icon-signout"));
+
+        sinon.assert.calledOnce(navigator.mozLoop.logOutFromFxA);
+      });
+    });
+
     describe("#render", function() {
       it("should render a ToSView", function() {
         TestUtils.findRenderedComponentWithType(view, loop.panel.ToSView);
       });
     });
   });
 
   describe("loop.panel.CallUrlResult", function() {
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -29,30 +29,36 @@
   function returnTrue() {
     return true;
   }
 
   function returnFalse() {
     return false;
   }
 
+  function noop(){}
+
   // Feedback API client configured to send data to the stage input server,
   // which is available at https://input.allizom.org
   var stageFeedbackApiClient = new loop.FeedbackAPIClient(
     "https://input.allizom.org/api/v1/feedback", {
       product: "Loop"
     }
   );
 
   var mockClient = {
-    requestCallUrl: function() {}
+    requestCallUrl: noop,
+    requestCallUrlInfo: noop
   };
 
   var mockConversationModel = new loop.shared.models.ConversationModel({}, {sdk: {}});
 
+  // Fake notifier
+  var mockNotifier = {};
+
   var Example = React.createClass({displayName: 'Example',
     render: function() {
       var cx = React.addons.classSet;
       return (
         React.DOM.div({className: "example"}, 
           React.DOM.h3(null, this.props.summary), 
           React.DOM.div({className: cx({comp: true, dashed: this.props.dashed}), 
                style: this.props.style || {}}, 
@@ -100,98 +106,125 @@
     render: function() {
       return (
         ShowCase(null, 
           Section({name: "PanelView"}, 
             React.DOM.p({className: "note"}, 
               React.DOM.strong(null, "Note:"), " 332px wide."
             ), 
             Example({summary: "Call URL retrieved", dashed: "true", style: {width: "332px"}}, 
-              PanelView({callUrl: "http://invalid.example.url/", client: mockClient})
+              PanelView({client: mockClient, notifier: mockNotifier, 
+                         callUrl: "http://invalid.example.url/"})
             ), 
             Example({summary: "Pending call url retrieval", dashed: "true", style: {width: "332px"}}, 
-              PanelView({client: mockClient})
+              PanelView({client: mockClient, notifier: mockNotifier})
             )
           ), 
 
           Section({name: "IncomingCallView"}, 
             Example({summary: "Default", dashed: "true", style: {width: "280px"}}, 
-              IncomingCallView(null)
+              IncomingCallView({model: mockConversationModel})
             )
           ), 
 
           Section({name: "ConversationToolbar"}, 
             React.DOM.h3(null, "Desktop Conversation Window"), 
             React.DOM.div({className: "conversation-window"}, 
               Example({summary: "Default (260x265)", dashed: "true"}, 
-                ConversationToolbar({video: {enabled: true}, audio: {enabled: true}})
+                ConversationToolbar({video: {enabled: true}, 
+                                     audio: {enabled: true}, 
+                                     hangup: noop, 
+                                     publishStream: noop})
               ), 
               Example({summary: "Video muted"}, 
-                ConversationToolbar({video: {enabled: false}, audio: {enabled: true}})
+                ConversationToolbar({video: {enabled: false}, 
+                                     audio: {enabled: true}, 
+                                     hangup: noop, 
+                                     publishStream: noop})
               ), 
               Example({summary: "Audio muted"}, 
-                ConversationToolbar({video: {enabled: true}, audio: {enabled: false}})
+                ConversationToolbar({video: {enabled: true}, 
+                                     audio: {enabled: false}, 
+                                     hangup: noop, 
+                                     publishStream: noop})
               )
             ), 
 
             React.DOM.h3(null, "Standalone"), 
             React.DOM.div({className: "standalone"}, 
               Example({summary: "Default"}, 
-                ConversationToolbar({video: {enabled: true}, audio: {enabled: true}})
+                ConversationToolbar({video: {enabled: true}, 
+                                     audio: {enabled: true}, 
+                                     hangup: noop, 
+                                     publishStream: noop})
               ), 
               Example({summary: "Video muted"}, 
-                ConversationToolbar({video: {enabled: false}, audio: {enabled: true}})
+                ConversationToolbar({video: {enabled: false}, 
+                                     audio: {enabled: true}, 
+                                     hangup: noop, 
+                                     publishStream: noop})
               ), 
               Example({summary: "Audio muted"}, 
-                ConversationToolbar({video: {enabled: true}, audio: {enabled: false}})
+                ConversationToolbar({video: {enabled: true}, 
+                                     audio: {enabled: false}, 
+                                     hangup: noop, 
+                                     publishStream: noop})
               )
             )
           ), 
 
           Section({name: "StartConversationView"}, 
-
             Example({summary: "Start conversation view", dashed: "true"}, 
               React.DOM.div({className: "standalone"}, 
                 StartConversationView({model: mockConversationModel, 
-                  client: mockClient})
+                                       client: mockClient, 
+                                       notifier: mockNotifier})
               )
             )
-
           ), 
 
           Section({name: "ConversationView"}, 
-
             Example({summary: "Desktop conversation window", dashed: "true", 
                      style: {width: "260px", height: "265px"}}, 
               React.DOM.div({className: "conversation-window"}, 
-                ConversationView({video: {enabled: true}, audio: {enabled: true}, 
-                                  model: mockConversationModel})
+                ConversationView({sdk: {}, 
+                                  model: mockConversationModel, 
+                                  video: {enabled: true}, 
+                                  audio: {enabled: true}})
               )
             ), 
             Example({summary: "Standalone version"}, 
               React.DOM.div({className: "standalone"}, 
-                ConversationView({video: {enabled: true}, audio: {enabled: true}, 
-                                  model: mockConversationModel})
+                ConversationView({sdk: {}, 
+                                  model: mockConversationModel, 
+                                  video: {enabled: true}, 
+                                  audio: {enabled: true}})
               )
+            ), 
+            Example({summary: "Default"}, 
+              ConversationView({sdk: {}, 
+                                model: mockConversationModel, 
+                                video: {enabled: true}, 
+                                audio: {enabled: true}})
             )
           ), 
 
           Section({name: "FeedbackView"}, 
             React.DOM.p({className: "note"}, 
               React.DOM.strong(null, "Note:"), " For the useable demo, you can access submitted data at ", 
               React.DOM.a({href: "https://input.allizom.org/"}, "input.allizom.org"), "."
             ), 
             Example({summary: "Default (useable demo)", dashed: "true", style: {width: "280px"}}, 
               FeedbackView({feedbackApiClient: stageFeedbackApiClient})
             ), 
             Example({summary: "Detailed form", dashed: "true", style: {width: "280px"}}, 
-              FeedbackView({step: "form"})
+              FeedbackView({feedbackApiClient: stageFeedbackApiClient, step: "form"})
             ), 
             Example({summary: "Thank you!", dashed: "true", style: {width: "280px"}}, 
-              FeedbackView({step: "finished"})
+              FeedbackView({feedbackApiClient: stageFeedbackApiClient, step: "finished"})
             )
           ), 
 
           Section({name: "CallUrlExpiredView"}, 
             Example({summary: "Firefox User"}, 
               CallUrlExpiredView({helper: {isFirefox: returnTrue}})
             ), 
             Example({summary: "Non-Firefox User"}, 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -29,30 +29,36 @@
   function returnTrue() {
     return true;
   }
 
   function returnFalse() {
     return false;
   }
 
+  function noop(){}
+
   // Feedback API client configured to send data to the stage input server,
   // which is available at https://input.allizom.org
   var stageFeedbackApiClient = new loop.FeedbackAPIClient(
     "https://input.allizom.org/api/v1/feedback", {
       product: "Loop"
     }
   );
 
   var mockClient = {
-    requestCallUrl: function() {}
+    requestCallUrl: noop,
+    requestCallUrlInfo: noop
   };
 
   var mockConversationModel = new loop.shared.models.ConversationModel({}, {sdk: {}});
 
+  // Fake notifier
+  var mockNotifier = {};
+
   var Example = React.createClass({
     render: function() {
       var cx = React.addons.classSet;
       return (
         <div className="example">
           <h3>{this.props.summary}</h3>
           <div className={cx({comp: true, dashed: this.props.dashed})}
                style={this.props.style || {}}>
@@ -100,98 +106,125 @@
     render: function() {
       return (
         <ShowCase>
           <Section name="PanelView">
             <p className="note">
               <strong>Note:</strong> 332px wide.
             </p>
             <Example summary="Call URL retrieved" dashed="true" style={{width: "332px"}}>
-              <PanelView callUrl="http://invalid.example.url/" client={mockClient} />
+              <PanelView client={mockClient} notifier={mockNotifier}
+                         callUrl="http://invalid.example.url/" />
             </Example>
             <Example summary="Pending call url retrieval" dashed="true" style={{width: "332px"}}>
-              <PanelView client={mockClient} />
+              <PanelView client={mockClient} notifier={mockNotifier} />
             </Example>
           </Section>
 
           <Section name="IncomingCallView">
             <Example summary="Default" dashed="true" style={{width: "280px"}}>
-              <IncomingCallView />
+              <IncomingCallView model={mockConversationModel} />
             </Example>
           </Section>
 
           <Section name="ConversationToolbar">
             <h3>Desktop Conversation Window</h3>
             <div className="conversation-window">
               <Example summary="Default (260x265)" dashed="true">
-                <ConversationToolbar video={{enabled: true}} audio={{enabled: true}} />
+                <ConversationToolbar video={{enabled: true}}
+                                     audio={{enabled: true}}
+                                     hangup={noop}
+                                     publishStream={noop} />
               </Example>
               <Example summary="Video muted">
-                <ConversationToolbar video={{enabled: false}} audio={{enabled: true}} />
+                <ConversationToolbar video={{enabled: false}}
+                                     audio={{enabled: true}}
+                                     hangup={noop}
+                                     publishStream={noop} />
               </Example>
               <Example summary="Audio muted">
-                <ConversationToolbar video={{enabled: true}} audio={{enabled: false}} />
+                <ConversationToolbar video={{enabled: true}}
+                                     audio={{enabled: false}}
+                                     hangup={noop}
+                                     publishStream={noop} />
               </Example>
             </div>
 
             <h3>Standalone</h3>
             <div className="standalone">
               <Example summary="Default">
-                <ConversationToolbar video={{enabled: true}} audio={{enabled: true}} />
+                <ConversationToolbar video={{enabled: true}}
+                                     audio={{enabled: true}}
+                                     hangup={noop}
+                                     publishStream={noop} />
               </Example>
               <Example summary="Video muted">
-                <ConversationToolbar video={{enabled: false}} audio={{enabled: true}} />
+                <ConversationToolbar video={{enabled: false}}
+                                     audio={{enabled: true}}
+                                     hangup={noop}
+                                     publishStream={noop} />
               </Example>
               <Example summary="Audio muted">
-                <ConversationToolbar video={{enabled: true}} audio={{enabled: false}} />
+                <ConversationToolbar video={{enabled: true}}
+                                     audio={{enabled: false}}
+                                     hangup={noop}
+                                     publishStream={noop} />
               </Example>
             </div>
           </Section>
 
           <Section name="StartConversationView">
-
             <Example summary="Start conversation view" dashed="true">
               <div className="standalone">
                 <StartConversationView model={mockConversationModel}
-                  client={mockClient} />
+                                       client={mockClient}
+                                       notifier={mockNotifier} />
               </div>
             </Example>
-
           </Section>
 
           <Section name="ConversationView">
-
             <Example summary="Desktop conversation window" dashed="true"
                      style={{width: "260px", height: "265px"}}>
               <div className="conversation-window">
-                <ConversationView video={{enabled: true}} audio={{enabled: true}}
-                                  model={mockConversationModel} />
+                <ConversationView sdk={{}}
+                                  model={mockConversationModel}
+                                  video={{enabled: true}}
+                                  audio={{enabled: true}} />
               </div>
             </Example>
             <Example summary="Standalone version">
               <div className="standalone">
-                <ConversationView video={{enabled: true}} audio={{enabled: true}}
-                                  model={mockConversationModel} />
+                <ConversationView sdk={{}}
+                                  model={mockConversationModel}
+                                  video={{enabled: true}}
+                                  audio={{enabled: true}} />
               </div>
             </Example>
+            <Example summary="Default">
+              <ConversationView sdk={{}}
+                                model={mockConversationModel}
+                                video={{enabled: true}}
+                                audio={{enabled: true}} />
+            </Example>
           </Section>
 
           <Section name="FeedbackView">
             <p className="note">
               <strong>Note:</strong> For the useable demo, you can access submitted data at&nbsp;
               <a href="https://input.allizom.org/">input.allizom.org</a>.
             </p>
             <Example summary="Default (useable demo)" dashed="true" style={{width: "280px"}}>
               <FeedbackView feedbackApiClient={stageFeedbackApiClient} />
             </Example>
             <Example summary="Detailed form" dashed="true" style={{width: "280px"}}>
-              <FeedbackView step="form" />
+              <FeedbackView feedbackApiClient={stageFeedbackApiClient} step="form" />
             </Example>
             <Example summary="Thank you!" dashed="true" style={{width: "280px"}}>
-              <FeedbackView step="finished" />
+              <FeedbackView feedbackApiClient={stageFeedbackApiClient} step="finished" />
             </Example>
           </Section>
 
           <Section name="CallUrlExpiredView">
             <Example summary="Firefox User">
               <CallUrlExpiredView helper={{isFirefox: returnTrue}} />
             </Example>
             <Example summary="Non-Firefox User">
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -21,16 +21,22 @@ share_email_subject3=You have been invit
 ## part between {{..}} and leave the \r\n\r\n part alone
 share_email_body3=To accept this invitation, just copy or click this link to start your conversation:\r\n\r\n{{callUrl}}
 share_button=Email
 copy_url_button=Copy
 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
+settings_menu_item_signin=Sign In
+settings_menu_button_tooltip=Settings
+
 # Contact Strings (Panel)
 
 ## LOCALIZATION NOTE(contacts_search_placeholder): This is the placeholder text for
 ## the search field at https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
 contacts_search_placesholder=Search…
 
 ## LOCALIZATION NOTE (new_contact_button): This is the button to open the
 ## new contact sub-panel.