Bug 1184917 - Implement the refreshed design for 'Edit' conversation toolbar button. r=mdeboer
authorMarina Rodriguez Iglesias <marina.rodrigueziglesias@telefonica.com>
Tue, 25 Aug 2015 08:23:00 -0400
changeset 287736 c1d5f91e676ecb4b2d7ec3bf7b09a66d69110f55
parent 287735 c7df9c9e5d970bbd649f0a159332aca06993a14a
child 287737 f0aa386a944f2c2e963bab24d62cedda2e241579
push id4738
push usersteffen.wilberg@web.de
push dateTue, 25 Aug 2015 19:49:56 +0000
reviewersmdeboer
bugs1184917
milestone43.0a1
Bug 1184917 - Implement the refreshed design for 'Edit' conversation toolbar button. r=mdeboer
browser/components/loop/content/js/conversationViews.js
browser/components/loop/content/js/conversationViews.jsx
browser/components/loop/content/js/roomViews.js
browser/components/loop/content/js/roomViews.jsx
browser/components/loop/content/shared/css/conversation.css
browser/components/loop/content/shared/js/views.js
browser/components/loop/content/shared/js/views.jsx
browser/components/loop/standalone/content/js/standaloneRoomViews.js
browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
browser/components/loop/test/desktop-local/conversationViews_test.js
browser/components/loop/test/desktop-local/roomViews_test.js
browser/components/loop/test/shared/views_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/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -578,16 +578,17 @@ loop.conversationViews = (function(mozL1
       // easy configurability for the ui-showcase.
       conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       // The poster URLs are for UI-showcase testing and development.
       localPosterUrl: React.PropTypes.string,
       // This is used from the props rather than the state to make it easier for
       // the ui-showcase.
       mediaConnected: React.PropTypes.bool,
+      mozLoop: React.PropTypes.object,
       remotePosterUrl: React.PropTypes.string,
       remoteVideoEnabled: React.PropTypes.bool,
       // local
       video: React.PropTypes.object
     },
 
     getDefaultProps: function() {
       return {
@@ -675,16 +676,26 @@ loop.conversationViews = (function(mozL1
       }
 
       // We're not yet connected, but we don't want to show the avatar, and in
       // the common case, we'll just transition to the video.
       return true;
     },
 
     render: function() {
+      // 'visible' and 'enabled' are true by default.
+      var settingsMenuItems = [
+        {
+          id: "edit",
+          visible: false,
+          enabled: false
+        },
+        { id: "feedback" },
+        { id: "help" }
+      ];
       return (
         React.createElement("div", {className: "desktop-call-wrapper"}, 
           React.createElement(sharedViews.MediaLayoutView, {
             dispatcher: this.props.dispatcher, 
             displayScreenShare: false, 
             isLocalLoading: this._isLocalLoading(), 
             isRemoteLoading: this._isRemoteLoading(), 
             isScreenShareLoading: false, 
@@ -697,19 +708,20 @@ loop.conversationViews = (function(mozL1
             renderRemoteVideo: this.shouldRenderRemoteVideo(), 
             screenSharePosterUrl: null, 
             screenShareVideoObject: this.state.screenShareVideoObject, 
             showContextRoomName: false, 
             useDesktopPaths: true}, 
             React.createElement(loop.shared.views.ConversationToolbar, {
               audio: this.props.audio, 
               dispatcher: this.props.dispatcher, 
-              edit: { visible: false, enabled: false}, 
               hangup: this.hangup, 
+              mozLoop: this.props.mozLoop, 
               publishStream: this.publishStream, 
+              settingsMenuItems: settingsMenuItems, 
               video: this.props.video})
           )
         )
       );
     }
   });
 
   /**
@@ -802,16 +814,17 @@ loop.conversationViews = (function(mozL1
             outgoing: this.state.outgoing}));
         }
         case CALL_STATES.ONGOING: {
           return (React.createElement(OngoingConversationView, {
             audio: { enabled: !this.state.audioMuted, visible: true}, 
             conversationStore: this.getStore(), 
             dispatcher: this.props.dispatcher, 
             mediaConnected: this.state.mediaConnected, 
+            mozLoop: this.props.mozLoop, 
             remoteSrcVideoObject: this.state.remoteSrcVideoObject, 
             remoteVideoEnabled: this.state.remoteVideoEnabled, 
             video: { enabled: !this.state.videoMuted, visible: true}})
           );
         }
         case CALL_STATES.FINISHED: {
           this.play("terminated");
 
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -578,16 +578,17 @@ loop.conversationViews = (function(mozL1
       // easy configurability for the ui-showcase.
       conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       // The poster URLs are for UI-showcase testing and development.
       localPosterUrl: React.PropTypes.string,
       // This is used from the props rather than the state to make it easier for
       // the ui-showcase.
       mediaConnected: React.PropTypes.bool,
+      mozLoop: React.PropTypes.object,
       remotePosterUrl: React.PropTypes.string,
       remoteVideoEnabled: React.PropTypes.bool,
       // local
       video: React.PropTypes.object
     },
 
     getDefaultProps: function() {
       return {
@@ -675,16 +676,26 @@ loop.conversationViews = (function(mozL1
       }
 
       // We're not yet connected, but we don't want to show the avatar, and in
       // the common case, we'll just transition to the video.
       return true;
     },
 
     render: function() {
+      // 'visible' and 'enabled' are true by default.
+      var settingsMenuItems = [
+        {
+          id: "edit",
+          visible: false,
+          enabled: false
+        },
+        { id: "feedback" },
+        { id: "help" }
+      ];
       return (
         <div className="desktop-call-wrapper">
           <sharedViews.MediaLayoutView
             dispatcher={this.props.dispatcher}
             displayScreenShare={false}
             isLocalLoading={this._isLocalLoading()}
             isRemoteLoading={this._isRemoteLoading()}
             isScreenShareLoading={false}
@@ -697,19 +708,20 @@ loop.conversationViews = (function(mozL1
             renderRemoteVideo={this.shouldRenderRemoteVideo()}
             screenSharePosterUrl={null}
             screenShareVideoObject={this.state.screenShareVideoObject}
             showContextRoomName={false}
             useDesktopPaths={true}>
             <loop.shared.views.ConversationToolbar
               audio={this.props.audio}
               dispatcher={this.props.dispatcher}
-              edit={{ visible: false, enabled: false }}
               hangup={this.hangup}
+              mozLoop={this.props.mozLoop}
               publishStream={this.publishStream}
+              settingsMenuItems={settingsMenuItems}
               video={this.props.video} />
           </sharedViews.MediaLayoutView>
         </div>
       );
     }
   });
 
   /**
@@ -802,16 +814,17 @@ loop.conversationViews = (function(mozL1
             outgoing={this.state.outgoing} />);
         }
         case CALL_STATES.ONGOING: {
           return (<OngoingConversationView
             audio={{ enabled: !this.state.audioMuted, visible: true }}
             conversationStore={this.getStore()}
             dispatcher={this.props.dispatcher}
             mediaConnected={this.state.mediaConnected}
+            mozLoop={this.props.mozLoop}
             remoteSrcVideoObject={this.state.remoteSrcVideoObject}
             remoteVideoEnabled={this.state.remoteVideoEnabled}
             video={{ enabled: !this.state.videoMuted, visible: true }} />
           );
         }
         case CALL_STATES.FINISHED: {
           this.play("terminated");
 
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -719,16 +719,26 @@ loop.roomViews = (function(mozL10n) {
           );
         }
         case ROOM_STATES.ENDED: {
           // When conversation ended we either display a feedback form or
           // close the window. This is decided in the AppControllerView.
           return null;
         }
         default: {
+          var settingsMenuItems = [
+            {
+              id: "edit",
+              enabled: !this.state.showEditContext,
+              visible: this.state.contextEnabled,
+              onClick: this.handleEditContextClick
+            },
+            { id: "feedback" },
+            { id: "help" }
+          ];
           return (
             React.createElement("div", {className: "room-conversation-wrapper desktop-room-wrapper"}, 
               React.createElement(sharedViews.MediaLayoutView, {
                 dispatcher: this.props.dispatcher, 
                 displayScreenShare: false, 
                 isLocalLoading: this._isLocalLoading(), 
                 isRemoteLoading: this._isRemoteLoading(), 
                 isScreenShareLoading: false, 
@@ -741,21 +751,21 @@ loop.roomViews = (function(mozL10n) {
                 renderRemoteVideo: this.shouldRenderRemoteVideo(), 
                 screenSharePosterUrl: null, 
                 screenShareVideoObject: this.state.screenShareVideoObject, 
                 showContextRoomName: false, 
                 useDesktopPaths: true}, 
                 React.createElement(sharedViews.ConversationToolbar, {
                   audio: {enabled: !this.state.audioMuted, visible: true}, 
                   dispatcher: this.props.dispatcher, 
-                  edit: { visible: this.state.contextEnabled, enabled: !this.state.showEditContext}, 
                   hangup: this.leaveRoom, 
-                  onEditClick: this.handleEditContextClick, 
+                  mozLoop: this.props.mozLoop, 
                   publishStream: this.publishStream, 
                   screenShare: screenShareData, 
+                  settingsMenuItems: settingsMenuItems, 
                   video: {enabled: !this.state.videoMuted, visible: true}}), 
                 React.createElement(DesktopRoomInvitationView, {
                   dispatcher: this.props.dispatcher, 
                   error: this.state.error, 
                   mozLoop: this.props.mozLoop, 
                   onAddContextClick: this.handleAddContextClick, 
                   onEditContextClose: this.handleEditContextClose, 
                   roomData: roomData, 
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -719,16 +719,26 @@ loop.roomViews = (function(mozL10n) {
           );
         }
         case ROOM_STATES.ENDED: {
           // When conversation ended we either display a feedback form or
           // close the window. This is decided in the AppControllerView.
           return null;
         }
         default: {
+          var settingsMenuItems = [
+            {
+              id: "edit",
+              enabled: !this.state.showEditContext,
+              visible: this.state.contextEnabled,
+              onClick: this.handleEditContextClick
+            },
+            { id: "feedback" },
+            { id: "help" }
+          ];
           return (
             <div className="room-conversation-wrapper desktop-room-wrapper">
               <sharedViews.MediaLayoutView
                 dispatcher={this.props.dispatcher}
                 displayScreenShare={false}
                 isLocalLoading={this._isLocalLoading()}
                 isRemoteLoading={this._isRemoteLoading()}
                 isScreenShareLoading={false}
@@ -741,21 +751,21 @@ loop.roomViews = (function(mozL10n) {
                 renderRemoteVideo={this.shouldRenderRemoteVideo()}
                 screenSharePosterUrl={null}
                 screenShareVideoObject={this.state.screenShareVideoObject}
                 showContextRoomName={false}
                 useDesktopPaths={true}>
                 <sharedViews.ConversationToolbar
                   audio={{enabled: !this.state.audioMuted, visible: true}}
                   dispatcher={this.props.dispatcher}
-                  edit={{ visible: this.state.contextEnabled, enabled: !this.state.showEditContext }}
                   hangup={this.leaveRoom}
-                  onEditClick={this.handleEditContextClick}
+                  mozLoop={this.props.mozLoop}
                   publishStream={this.publishStream}
                   screenShare={screenShareData}
+                  settingsMenuItems={settingsMenuItems}
                   video={{enabled: !this.state.videoMuted, visible: true}} />
                 <DesktopRoomInvitationView
                   dispatcher={this.props.dispatcher}
                   error={this.state.error}
                   mozLoop={this.props.mozLoop}
                   onAddContextClick={this.handleAddContextClick}
                   onEditContextClose={this.handleEditContextClose}
                   roomData={roomData}
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -198,38 +198,38 @@ html[dir="rtl"] .conversation-toolbar-bt
   content: url("../img/svg/video-mute.svg");
 }
 
 .btn-mute-video.muted:hover:after,
 .btn-mute-video.muted:active:after {
   content: url("../img/svg/video-mute-hover.svg");
 }
 
-.btn-mute-edit {
+.btn-settings {
   width: 28px;
   height: 28px;
   background-size: 28px;
   background-image: url("../img/svg/settings.svg");
 }
 
-.btn-mute-edit:hover,
-.btn-mute-edit:active {
+.btn-settings:hover,
+.btn-settings:active {
   background-image: url("../img/svg/settings-hover.svg");
 }
 
 .btn-screen-share {
   background-image: url("../img/svg/sharing.svg");
 }
 
 .btn-screen-share:hover,
 .btn-screen-share:active {
   background-image: url("../img/svg/sharing-hover.svg");
 }
 
-.btn-mute-edit.muted,
+.btn-settings.muted,
 .btn-screen-share.active {
   opacity: 1;
 }
 
 .btn-screen-share.disabled {
   background-image: url("../img/svg/sharing-mute.svg");
 }
 
@@ -380,16 +380,27 @@ html[dir="rtl"] .conversation-toolbar-bt
 }
 
 .conversation-window-dropdown > li {
   padding: .2rem;
   font-size: 1rem;
   white-space: nowrap;
 }
 
+.settings-menu.dropdown-menu {
+  left: auto;
+  bottom: 3.1rem;
+  right: 14px;
+}
+
+html[dir="rtl"] .settings-menu.dropdown-menu {
+  left: 14px;
+  right: auto;
+}
+
 /* Expired call url page */
 
 .expired-url-info {
   width: 400px;
   margin: 0 auto;
 }
 
 .promote-firefox {
@@ -1223,16 +1234,22 @@ html[dir="rtl"] .room-context-btn-close 
 
   .media-wrapper > .text-chat-view,
   .media-wrapper.showing-local-streams > .text-chat-view,
   .media-wrapper.showing-local-streams.receiving-screen-share > .text-chat-view {
     /* The remaining 30% that the .focus-stream doesn't use. */
     height: 30%;
   }
 
+  .media-wrapper.receiving-screen-share > .remote > .conversation-toolbar,
+  .media-wrapper.showing-local-streams.receiving-screen-share  > .remote > .conversation-toolbar {
+    bottom: calc(30% + 1.5rem);
+  }
+
+
   .desktop-call-wrapper > .media-layout > .media-wrapper > .text-chat-view,
   .desktop-room-wrapper > .media-layout > .media-wrapper > .text-chat-view {
     /* This is temp, to echo the .media-wrapper > .text-chat-view above */
     height: 30%;
   }
 
   .media-wrapper.receiving-screen-share > .screen {
     order: 1;
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -176,66 +176,200 @@ loop.shared.views = (function(_, mozL10n
             )
           )
         )
       );
     }
   });
 
   /**
+   * Settings control button.
+   */
+  var SettingsControlButton = React.createClass({displayName: "SettingsControlButton",
+    propTypes: {
+      menuItems: React.PropTypes.array,
+      mozLoop: React.PropTypes.object
+    },
+
+    mixins: [
+      sharedMixins.DropdownMenuMixin(),
+      React.addons.PureRenderMixin
+    ],
+
+    /**
+     * Show or hide the settings menu
+     */
+    handleClick: function(event) {
+      event.preventDefault();
+      this.toggleDropdownMenu();
+    },
+
+    /**
+     * Return the function that Show or hide the edit context edition form
+     */
+    getHandleToggleEdit: function(editItem) {
+      return function _handleToglleEdit(event) {
+          event.preventDefault();
+          if (editItem.onClick) {
+            editItem.onClick(!editItem.enabled);
+          }
+        };
+    },
+
+    /**
+     * Load on the browser the help (support) url from prefs
+     */
+    handleHelpEntry: function(event) {
+      event.preventDefault();
+      var helloSupportUrl = this.props.mozLoop.getLoopPref("support_url");
+      this.props.mozLoop.openURL(helloSupportUrl);
+    },
+
+    /**
+     * Load on the browser the feedback url from prefs
+     */
+    handleSubmitFeedback: function(event) {
+      event.preventDefault();
+      var helloFeedbackUrl = this.props.mozLoop.getLoopPref("feedback.formURL");
+      this.props.mozLoop.openURL(helloFeedbackUrl);
+    },
+
+    /**
+     * Recover the needed info for generating an specific menu Item
+     */
+    getItemInfo: function(menuItem) {
+      var cx = React.addons.classSet;
+      switch (menuItem.id) {
+        case "feedback":
+          return {
+            cssClasses: "dropdown-menu-item",
+            handler: this.handleSubmitFeedback,
+            label: mozL10n.get("feedback_request_button")
+          };
+        case "help":
+          return {
+            cssClasses: "dropdown-menu-item",
+            handler: this.handleHelpEntry,
+            label: mozL10n.get("help_label")
+          };
+        case "edit":
+          return {
+            cssClasses: cx({
+              "dropdown-menu-item": true,
+              "entry-settings-edit": true,
+              "hide": !menuItem.visible
+            }),
+            handler: this.getHandleToggleEdit(menuItem),
+            label: mozL10n.get(menuItem.enabled ?
+              "conversation_settings_menu_edit_context" :
+              "conversation_settings_menu_hide_context"),
+            scope: "local",
+            type: "edit"
+          };
+        default:
+          console.error("Invalid menu item", menuItem);
+          return null;
+       }
+    },
+
+    /**
+     * Generate a menu item after recover its info
+     */
+    generateMenuItem: function(menuItem) {
+      var itemInfo = this.getItemInfo(menuItem);
+      if (!itemInfo) {
+        return null;
+      }
+      return (
+        React.createElement("li", {className: itemInfo.cssClasses, 
+            key: menuItem.id, 
+            onClick: itemInfo.handler, 
+            scope: itemInfo.scope || "", 
+            type: itemInfo.type || ""}, 
+          itemInfo.label
+        )
+        );
+    },
+
+    render: function() {
+      if (!this.props.menuItems || !this.props.menuItems.length) {
+        return null;
+      }
+      var menuItemRows = this.props.menuItems.map(this.generateMenuItem)
+        .filter(function(item) { return item; });
+
+      if (!menuItemRows || !menuItemRows.length) {
+        return null;
+      }
+
+      var cx = React.addons.classSet;
+      var settingsDropdownMenuClasses = cx({
+        "settings-menu": true,
+        "dropdown-menu": true,
+        "hide": !this.state.showMenu
+      });
+      return (
+        React.createElement("div", null, 
+          React.createElement("button", {className: "btn btn-settings transparent-button", 
+             onClick: this.toggleDropdownMenu, 
+             ref: "menu-button", 
+             title: mozL10n.get("settings_menu_button_tooltip")}), 
+          React.createElement("ul", {className: settingsDropdownMenuClasses, ref: "menu"}, 
+            menuItemRows
+          )
+        )
+      );
+    }
+  });
+
+  /**
    * Conversation controls.
    */
   var ConversationToolbar = React.createClass({displayName: "ConversationToolbar",
     getDefaultProps: function() {
       return {
         video: {enabled: true, visible: true},
         audio: {enabled: true, visible: true},
-        edit: {enabled: false, visible: false},
         screenShare: {state: SCREEN_SHARE_STATES.INACTIVE, visible: false},
+        settingsMenuItems: null,
         enableHangup: true
       };
     },
 
     getInitialState: function() {
       return {
         idle: false
       };
     },
 
     propTypes: {
       audio: React.PropTypes.object.isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      edit: React.PropTypes.object.isRequired,
       enableHangup: React.PropTypes.bool,
       hangup: React.PropTypes.func.isRequired,
       hangupButtonLabel: React.PropTypes.string,
-      onEditClick: React.PropTypes.func,
+      mozLoop: React.PropTypes.object,
       publishStream: React.PropTypes.func.isRequired,
       screenShare: React.PropTypes.object,
+      settingsMenuItems: React.PropTypes.array,
       video: React.PropTypes.object.isRequired
     },
 
     handleClickHangup: function() {
       this.props.hangup();
     },
 
     handleToggleVideo: function() {
       this.props.publishStream("video", !this.props.video.enabled);
     },
 
     handleToggleAudio: function() {
       this.props.publishStream("audio", !this.props.audio.enabled);
     },
 
-    handleToggleEdit: function() {
-      if (this.props.onEditClick) {
-        this.props.onEditClick(!this.props.edit.enabled);
-      }
-    },
-
     componentDidMount: function() {
       this.userActivity = false;
       this.startIdleCountDown();
       document.body.addEventListener("mousemove", this._onBodyMouseMove);
     },
 
     componentWillUnmount: function() {
       clearTimeout(this.inactivityTimeout);
@@ -321,32 +455,26 @@ loop.shared.views = (function(_, mozL10n
                 React.createElement(MediaControlButton, {action: this.handleToggleVideo, 
                                     enabled: this.props.video.enabled, 
                                     scope: "local", type: "video", 
                                     visible: this.props.video.visible}), 
                 React.createElement(MediaControlButton, {action: this.handleToggleAudio, 
                                     enabled: this.props.audio.enabled, 
                                     scope: "local", type: "audio", 
                                     visible: this.props.audio.visible})
-
             )
           ), 
           React.createElement("li", {className: "conversation-toolbar-btn-box"}, 
             React.createElement(ScreenShareControlButton, {dispatcher: this.props.dispatcher, 
                                       state: this.props.screenShare.state, 
                                       visible: this.props.screenShare.visible})
           ), 
           React.createElement("li", {className: "conversation-toolbar-btn-box btn-edit-entry"}, 
-            React.createElement(MediaControlButton, {action: this.handleToggleEdit, 
-                                enabled: this.props.edit.enabled, 
-                                scope: "local", 
-                                title: mozL10n.get(this.props.edit.enabled ?
-                                  "context_edit_tooltip" : "context_hide_tooltip"), 
-                                type: "edit", 
-                                visible: this.props.edit.visible})
+            React.createElement(SettingsControlButton, {menuItems: this.props.settingsMenuItems, 
+                                   mozLoop: this.props.mozLoop})
           )
         )
       );
     }
   });
 
   /**
    * Conversation view.
@@ -359,16 +487,17 @@ loop.shared.views = (function(_, mozL10n
     ],
 
     propTypes: {
       audio: React.PropTypes.object,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       initiate: React.PropTypes.bool,
       isDesktop: React.PropTypes.bool,
       model: React.PropTypes.object.isRequired,
+      mozLoop: React.PropTypes.object,
       sdk: React.PropTypes.object.isRequired,
       video: React.PropTypes.object
     },
 
     getDefaultProps: function() {
       return {
         initiate: true,
         isDesktop: false,
@@ -554,16 +683,17 @@ loop.shared.views = (function(_, mozL10n
           React.createElement("div", {className: "conversation in-call"}, 
             React.createElement("div", {className: "media nested"}, 
               React.createElement("div", {className: "video_wrapper remote_wrapper"}, 
                 React.createElement("div", {className: "video_inner remote focus-stream"}, 
                   React.createElement(ConversationToolbar, {
                     audio: this.state.audio, 
                     dispatcher: this.props.dispatcher, 
                     hangup: this.hangup, 
+                    mozLoop: this.props.mozLoop, 
                     publishStream: this.publishStream, 
                     video: this.state.video})
                 )
               ), 
               React.createElement("div", {className: localStreamClasses})
 
             )
           )
@@ -1171,12 +1301,13 @@ loop.shared.views = (function(_, mozL10n
     Checkbox: Checkbox,
     ContextUrlView: ContextUrlView,
     ConversationView: ConversationView,
     ConversationToolbar: ConversationToolbar,
     MediaControlButton: MediaControlButton,
     MediaLayoutView: MediaLayoutView,
     MediaView: MediaView,
     LoadingView: LoadingView,
+    SettingsControlButton: SettingsControlButton,
     ScreenShareControlButton: ScreenShareControlButton,
     NotificationListView: NotificationListView
   };
 })(_, navigator.mozL10n || document.mozL10n);
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -176,66 +176,200 @@ loop.shared.views = (function(_, mozL10n
             </li>
           </ul>
         </div>
       );
     }
   });
 
   /**
+   * Settings control button.
+   */
+  var SettingsControlButton = React.createClass({
+    propTypes: {
+      menuItems: React.PropTypes.array,
+      mozLoop: React.PropTypes.object
+    },
+
+    mixins: [
+      sharedMixins.DropdownMenuMixin(),
+      React.addons.PureRenderMixin
+    ],
+
+    /**
+     * Show or hide the settings menu
+     */
+    handleClick: function(event) {
+      event.preventDefault();
+      this.toggleDropdownMenu();
+    },
+
+    /**
+     * Return the function that Show or hide the edit context edition form
+     */
+    getHandleToggleEdit: function(editItem) {
+      return function _handleToglleEdit(event) {
+          event.preventDefault();
+          if (editItem.onClick) {
+            editItem.onClick(!editItem.enabled);
+          }
+        };
+    },
+
+    /**
+     * Load on the browser the help (support) url from prefs
+     */
+    handleHelpEntry: function(event) {
+      event.preventDefault();
+      var helloSupportUrl = this.props.mozLoop.getLoopPref("support_url");
+      this.props.mozLoop.openURL(helloSupportUrl);
+    },
+
+    /**
+     * Load on the browser the feedback url from prefs
+     */
+    handleSubmitFeedback: function(event) {
+      event.preventDefault();
+      var helloFeedbackUrl = this.props.mozLoop.getLoopPref("feedback.formURL");
+      this.props.mozLoop.openURL(helloFeedbackUrl);
+    },
+
+    /**
+     * Recover the needed info for generating an specific menu Item
+     */
+    getItemInfo: function(menuItem) {
+      var cx = React.addons.classSet;
+      switch (menuItem.id) {
+        case "feedback":
+          return {
+            cssClasses: "dropdown-menu-item",
+            handler: this.handleSubmitFeedback,
+            label: mozL10n.get("feedback_request_button")
+          };
+        case "help":
+          return {
+            cssClasses: "dropdown-menu-item",
+            handler: this.handleHelpEntry,
+            label: mozL10n.get("help_label")
+          };
+        case "edit":
+          return {
+            cssClasses: cx({
+              "dropdown-menu-item": true,
+              "entry-settings-edit": true,
+              "hide": !menuItem.visible
+            }),
+            handler: this.getHandleToggleEdit(menuItem),
+            label: mozL10n.get(menuItem.enabled ?
+              "conversation_settings_menu_edit_context" :
+              "conversation_settings_menu_hide_context"),
+            scope: "local",
+            type: "edit"
+          };
+        default:
+          console.error("Invalid menu item", menuItem);
+          return null;
+       }
+    },
+
+    /**
+     * Generate a menu item after recover its info
+     */
+    generateMenuItem: function(menuItem) {
+      var itemInfo = this.getItemInfo(menuItem);
+      if (!itemInfo) {
+        return null;
+      }
+      return (
+        <li className={itemInfo.cssClasses}
+            key={menuItem.id}
+            onClick={itemInfo.handler}
+            scope={itemInfo.scope || ""}
+            type={itemInfo.type || ""} >
+          {itemInfo.label}
+        </li>
+        );
+    },
+
+    render: function() {
+      if (!this.props.menuItems || !this.props.menuItems.length) {
+        return null;
+      }
+      var menuItemRows = this.props.menuItems.map(this.generateMenuItem)
+        .filter(function(item) { return item; });
+
+      if (!menuItemRows || !menuItemRows.length) {
+        return null;
+      }
+
+      var cx = React.addons.classSet;
+      var settingsDropdownMenuClasses = cx({
+        "settings-menu": true,
+        "dropdown-menu": true,
+        "hide": !this.state.showMenu
+      });
+      return (
+        <div>
+          <button className="btn btn-settings transparent-button"
+             onClick={this.toggleDropdownMenu}
+             ref="menu-button"
+             title={mozL10n.get("settings_menu_button_tooltip")} />
+          <ul className={settingsDropdownMenuClasses} ref="menu">
+            {menuItemRows}
+          </ul>
+        </div>
+      );
+    }
+  });
+
+  /**
    * Conversation controls.
    */
   var ConversationToolbar = React.createClass({
     getDefaultProps: function() {
       return {
         video: {enabled: true, visible: true},
         audio: {enabled: true, visible: true},
-        edit: {enabled: false, visible: false},
         screenShare: {state: SCREEN_SHARE_STATES.INACTIVE, visible: false},
+        settingsMenuItems: null,
         enableHangup: true
       };
     },
 
     getInitialState: function() {
       return {
         idle: false
       };
     },
 
     propTypes: {
       audio: React.PropTypes.object.isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      edit: React.PropTypes.object.isRequired,
       enableHangup: React.PropTypes.bool,
       hangup: React.PropTypes.func.isRequired,
       hangupButtonLabel: React.PropTypes.string,
-      onEditClick: React.PropTypes.func,
+      mozLoop: React.PropTypes.object,
       publishStream: React.PropTypes.func.isRequired,
       screenShare: React.PropTypes.object,
+      settingsMenuItems: React.PropTypes.array,
       video: React.PropTypes.object.isRequired
     },
 
     handleClickHangup: function() {
       this.props.hangup();
     },
 
     handleToggleVideo: function() {
       this.props.publishStream("video", !this.props.video.enabled);
     },
 
     handleToggleAudio: function() {
       this.props.publishStream("audio", !this.props.audio.enabled);
     },
 
-    handleToggleEdit: function() {
-      if (this.props.onEditClick) {
-        this.props.onEditClick(!this.props.edit.enabled);
-      }
-    },
-
     componentDidMount: function() {
       this.userActivity = false;
       this.startIdleCountDown();
       document.body.addEventListener("mousemove", this._onBodyMouseMove);
     },
 
     componentWillUnmount: function() {
       clearTimeout(this.inactivityTimeout);
@@ -321,32 +455,26 @@ loop.shared.views = (function(_, mozL10n
                 <MediaControlButton action={this.handleToggleVideo}
                                     enabled={this.props.video.enabled}
                                     scope="local" type="video"
                                     visible={this.props.video.visible}/>
                 <MediaControlButton action={this.handleToggleAudio}
                                     enabled={this.props.audio.enabled}
                                     scope="local" type="audio"
                                     visible={this.props.audio.visible}/>
-
             </div>
           </li>
           <li className="conversation-toolbar-btn-box">
             <ScreenShareControlButton dispatcher={this.props.dispatcher}
                                       state={this.props.screenShare.state}
                                       visible={this.props.screenShare.visible} />
           </li>
           <li className="conversation-toolbar-btn-box btn-edit-entry">
-            <MediaControlButton action={this.handleToggleEdit}
-                                enabled={this.props.edit.enabled}
-                                scope="local"
-                                title={mozL10n.get(this.props.edit.enabled ?
-                                  "context_edit_tooltip" : "context_hide_tooltip")}
-                                type="edit"
-                                visible={this.props.edit.visible} />
+            <SettingsControlButton menuItems={this.props.settingsMenuItems}
+                                   mozLoop={this.props.mozLoop} />
           </li>
         </ul>
       );
     }
   });
 
   /**
    * Conversation view.
@@ -359,16 +487,17 @@ loop.shared.views = (function(_, mozL10n
     ],
 
     propTypes: {
       audio: React.PropTypes.object,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       initiate: React.PropTypes.bool,
       isDesktop: React.PropTypes.bool,
       model: React.PropTypes.object.isRequired,
+      mozLoop: React.PropTypes.object,
       sdk: React.PropTypes.object.isRequired,
       video: React.PropTypes.object
     },
 
     getDefaultProps: function() {
       return {
         initiate: true,
         isDesktop: false,
@@ -554,16 +683,17 @@ loop.shared.views = (function(_, mozL10n
           <div className="conversation in-call">
             <div className="media nested">
               <div className="video_wrapper remote_wrapper">
                 <div className="video_inner remote focus-stream">
                   <ConversationToolbar
                     audio={this.state.audio}
                     dispatcher={this.props.dispatcher}
                     hangup={this.hangup}
+                    mozLoop={this.props.mozLoop}
                     publishStream={this.publishStream}
                     video={this.state.video} />
                 </div>
               </div>
               <div className={localStreamClasses}></div>
 
             </div>
           </div>
@@ -1171,12 +1301,13 @@ loop.shared.views = (function(_, mozL10n
     Checkbox: Checkbox,
     ContextUrlView: ContextUrlView,
     ConversationView: ConversationView,
     ConversationToolbar: ConversationToolbar,
     MediaControlButton: MediaControlButton,
     MediaLayoutView: MediaLayoutView,
     MediaView: MediaView,
     LoadingView: LoadingView,
+    SettingsControlButton: SettingsControlButton,
     ScreenShareControlButton: ScreenShareControlButton,
     NotificationListView: NotificationListView
   };
 })(_, navigator.mozL10n || document.mozL10n);
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -519,17 +519,16 @@ loop.standaloneRoomViews = (function(moz
               isFirefox: this.props.isFirefox, 
               joinRoom: this.joinRoom, 
               roomState: this.state.roomState, 
               roomUsed: this.state.used}), 
             React.createElement(sharedViews.ConversationToolbar, {
               audio: {enabled: !this.state.audioMuted,
                       visible: this._roomIsActive()}, 
               dispatcher: this.props.dispatcher, 
-              edit: { visible: false, enabled: false}, 
               enableHangup: this._roomIsActive(), 
               hangup: this.leaveRoom, 
               hangupButtonLabel: mozL10n.get("rooms_leave_button_label"), 
               publishStream: this.publishStream, 
               video: {enabled: !this.state.videoMuted,
                       visible: this._roomIsActive()}})
           ), 
           React.createElement(loop.fxOSMarketplaceViews.FxOSHiddenMarketplaceView, {
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -519,17 +519,16 @@ loop.standaloneRoomViews = (function(moz
               isFirefox={this.props.isFirefox}
               joinRoom={this.joinRoom}
               roomState={this.state.roomState}
               roomUsed={this.state.used} />
             <sharedViews.ConversationToolbar
               audio={{enabled: !this.state.audioMuted,
                       visible: this._roomIsActive()}}
               dispatcher={this.props.dispatcher}
-              edit={{ visible: false, enabled: false }}
               enableHangup={this._roomIsActive()}
               hangup={this.leaveRoom}
               hangupButtonLabel={mozL10n.get("rooms_leave_button_label")}
               publishStream={this.publishStream}
               video={{enabled: !this.state.videoMuted,
                       visible: this._roomIsActive()}} />
           </sharedViews.MediaLayoutView>
           <loop.fxOSMarketplaceViews.FxOSHiddenMarketplaceView
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -474,16 +474,17 @@ describe("loop.conversationViews", funct
     });
   });
 
   describe("OngoingConversationView", function() {
     function mountTestComponent(extraProps) {
       var props = _.extend({
         conversationStore: conversationStore,
         dispatcher: dispatcher,
+        mozLoop: {},
         matchMedia: window.matchMedia
       }, extraProps);
       return TestUtils.renderIntoDocument(
         React.createElement(loop.conversationViews.OngoingConversationView, props));
     }
 
     it("should dispatch a setupStreamElements action when the view is created",
       function() {
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -600,26 +600,26 @@ describe("loop.roomViews", function () {
 
     describe("Edit Context", function() {
       it("should show the form when the edit button is clicked", function() {
         view = mountTestComponent();
         var node = view.getDOMNode();
 
         expect(node.querySelector(".room-context")).to.eql(null);
 
-        var editButton = node.querySelector(".btn-mute-edit");
+        var editButton = node.querySelector(".settings-menu > li.entry-settings-edit");
         React.addons.TestUtils.Simulate.click(editButton);
 
         expect(view.getDOMNode().querySelector(".room-context")).to.not.eql(null);
       });
 
       it("should hide the form when the edit button is clicked again", function() {
         view = mountTestComponent();
 
-        var editButton = view.getDOMNode().querySelector(".btn-mute-edit");
+        var editButton = view.getDOMNode().querySelector(".settings-menu > li.entry-settings-edit");
         React.addons.TestUtils.Simulate.click(editButton);
 
         // Click again.
         React.addons.TestUtils.Simulate.click(editButton);
 
         expect(view.getDOMNode().querySelector(".room-context")).to.eql(null);
       });
     });
--- a/browser/components/loop/test/shared/views_test.js
+++ b/browser/components/loop/test/shared/views_test.js
@@ -247,22 +247,169 @@ describe("loop.shared.views", function()
         TestUtils.Simulate.click(comp.getDOMNode().querySelector(".btn-screen-share"));
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.EndScreenShare({}));
       });
   });
 
+  describe("SettingsControlButton", function() {
+    var fakeMozLoop;
+    var support_url = "https://support.com";
+    var feedback_url = "https://feedback.com";
+
+    beforeEach(function() {
+      fakeMozLoop = {
+        openURL: sandbox.stub(),
+        setLoopPref: sandbox.stub(),
+        getLoopPref: function (prefName) {
+          switch (prefName) {
+            case "support_url":
+              return support_url;
+            case "feedback.formURL":
+              return feedback_url;
+            default:
+              return prefName;
+          }
+        }
+      };
+    });
+
+    function mountTestComponent(props) {
+      props = _.extend({
+        mozLoop: fakeMozLoop
+      }, props);
+
+      return TestUtils.renderIntoDocument(
+        React.createElement(sharedViews.SettingsControlButton, props));
+    }
+
+    it("should render a visible button", function() {
+      var settingsMenuItems = [{ id: "feedback" }];
+      var comp = mountTestComponent({ menuItems: settingsMenuItems} );
+
+      var node = comp.getDOMNode().querySelector(".btn-settings");
+      expect(node.classList.contains("hide")).eql(false);
+    });
+
+    it("should not render anything", function() {
+      var comp = mountTestComponent();
+      expect(comp.getDOMNode()).to.eql(null);
+    });
+
+    it("should not show an indefined menu option", function() {
+      var settingsMenuItems = [
+        { id: "not Defined" },
+        { id: "help" }
+      ];
+      var comp = mountTestComponent({ menuItems: settingsMenuItems} );
+      var menuItems = comp.getDOMNode().querySelectorAll(".settings-menu > li");
+      expect(menuItems).to.have.length.of(1);
+    });
+
+    it("should not render anythin if not exists any valid item to show", function() {
+      var settingsMenuItems = [
+        { id: "not Defined" },
+        { id: "another wrong menu item" }
+      ];
+      var comp = mountTestComponent({ menuItems: settingsMenuItems} );
+      expect(comp.getDOMNode()).to.eql(null);
+    });
+
+    it("should show the settings dropdown on click", function() {
+      var settingsMenuItems = [{ id: "feedback" }];
+      var comp = mountTestComponent({ menuItems: settingsMenuItems} );
+
+      expect(comp.state.showMenu).eql(false);
+      TestUtils.Simulate.click(comp.getDOMNode().querySelector(".btn-settings"));
+
+      expect(comp.state.showMenu).eql(true);
+    });
+
+    it("should show edit Context on menu when the option is enabled", function() {
+      var settingsMenuItems = [
+        {
+          id: "edit",
+          enabled: true,
+          visible: true,
+          onClick: function() {}
+        }
+      ];
+      var comp = mountTestComponent({ menuItems: settingsMenuItems} );
+
+      var node = comp.getDOMNode().querySelector(".settings-menu > li.entry-settings-edit");
+      expect(node.classList.contains("hide")).eql(false);
+    });
+
+    it("should hide edit Context on menu when the option is not visible", function() {
+      var settingsMenuItems = [
+        {
+          id: "edit",
+          enabled: false,
+          visible: false,
+          onClick: function() {}
+        }
+      ];
+      var comp = mountTestComponent({ menuItems: settingsMenuItems} );
+
+      var node = comp.getDOMNode().querySelector(".settings-menu > li.entry-settings-edit");
+      expect(node.classList.contains("hide")).eql(true);
+    });
+
+    it("should call onClick method when the edit context menu item is clicked", function() {
+      var onClickCalled = false;
+      var settingsMenuItems = [
+        {
+          id: "edit",
+          enabled: true,
+          visible: true,
+          onClick: sandbox.stub()
+        }
+      ];
+      var comp = mountTestComponent({ menuItems: settingsMenuItems} );
+
+      TestUtils.Simulate.click(comp.getDOMNode().querySelector(".settings-menu > li.entry-settings-edit"));
+      sinon.assert.calledOnce(settingsMenuItems[0].onClick);
+    });
+
+    it("should open a tab to the feedback url when the feedback menu item is clicked", function() {
+      var settingsMenuItems = [
+        { id: "feedback" },
+        { id: "help" }
+      ];
+      var comp = mountTestComponent({ menuItems: settingsMenuItems} );
+
+      TestUtils.Simulate.click(comp.getDOMNode().querySelector(".settings-menu > li:first-child"));
+
+      sinon.assert.calledOnce(fakeMozLoop.openURL);
+      sinon.assert.calledWithExactly(fakeMozLoop.openURL, feedback_url);
+    });
+
+    it("should open a tab to the support url when the support menu item is clicked", function() {
+      var settingsMenuItems = [
+        { id: "feedback" },
+        { id: "help" }
+      ];
+      var comp = mountTestComponent({ menuItems: settingsMenuItems} );
+
+      TestUtils.Simulate.click(comp.getDOMNode().querySelector(".settings-menu > li:last-child"));
+
+      sinon.assert.calledOnce(fakeMozLoop.openURL);
+      sinon.assert.calledWithExactly(fakeMozLoop.openURL, support_url);
+    });
+  });
+
   describe("ConversationToolbar", function() {
     var clock, hangup, publishStream;
 
     function mountTestComponent(props) {
       props = _.extend({
-        dispatcher: dispatcher
+        dispatcher: dispatcher,
+        mozLoop: {}
       }, props || {});
       return TestUtils.renderIntoDocument(
         React.createElement(sharedViews.ConversationToolbar, props));
     }
 
     beforeEach(function() {
       hangup = sandbox.stub();
       publishStream = sandbox.stub();
@@ -402,17 +549,18 @@ describe("loop.shared.views", function()
     });
   });
 
   describe("ConversationView", function() {
     var fakeSDK, fakeSessionData, fakeSession, fakePublisher, model, fakeAudio;
 
     function mountTestComponent(props) {
       props = _.extend({
-        dispatcher: dispatcher
+        dispatcher: dispatcher,
+        mozLoop: {}
       }, props || {});
       return TestUtils.renderIntoDocument(
         React.createElement(sharedViews.ConversationView, props));
     }
 
     beforeEach(function() {
       fakeAudio = {
         play: sinon.spy(),
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -1023,38 +1023,41 @@
               React.createElement(FramedExample, {dashed: true, 
                              height: 56, 
                              summary: "Default", 
                              width: 300}, 
                 React.createElement("div", {className: "fx-embedded"}, 
                   React.createElement(ConversationToolbar, {audio: { enabled: true, visible: true}, 
                                        hangup: noop, 
                                        publishStream: noop, 
+                                       settingsMenuItems: [{ id: "feedback" }], 
                                        video: { enabled: true, visible: true}})
                 )
               ), 
               React.createElement(FramedExample, {dashed: true, 
                              height: 56, 
                              summary: "Video muted", 
                              width: 300}, 
                 React.createElement("div", {className: "fx-embedded"}, 
                   React.createElement(ConversationToolbar, {audio: { enabled: true, visible: true}, 
                                        hangup: noop, 
                                        publishStream: noop, 
+                                       settingsMenuItems: [{ id: "feedback" }], 
                                        video: { enabled: false, visible: true}})
                 )
               ), 
               React.createElement(FramedExample, {dashed: true, 
                              height: 56, 
                              summary: "Audio muted", 
                              width: 300}, 
                 React.createElement("div", {className: "fx-embedded"}, 
                   React.createElement(ConversationToolbar, {audio: { enabled: false, visible: true}, 
                                        hangup: noop, 
                                        publishStream: noop, 
+                                       settingsMenuItems: [{ id: "feedback" }], 
                                        video: { enabled: true, visible: true}})
                 )
               )
             )
           ), 
 
           React.createElement(Section, {name: "PendingConversationView (Desktop)"}, 
             React.createElement(FramedExample, {dashed: true, 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -1023,38 +1023,41 @@
               <FramedExample dashed={true}
                              height={56}
                              summary="Default"
                              width={300}>
                 <div className="fx-embedded">
                   <ConversationToolbar audio={{ enabled: true, visible: true }}
                                        hangup={noop}
                                        publishStream={noop}
+                                       settingsMenuItems={[{ id: "feedback" }]}
                                        video={{ enabled: true, visible: true }} />
                 </div>
               </FramedExample>
               <FramedExample dashed={true}
                              height={56}
                              summary="Video muted"
                              width={300}>
                 <div className="fx-embedded">
                   <ConversationToolbar audio={{ enabled: true, visible: true }}
                                        hangup={noop}
                                        publishStream={noop}
+                                       settingsMenuItems={[{ id: "feedback" }]}
                                        video={{ enabled: false, visible: true }} />
                 </div>
               </FramedExample>
               <FramedExample dashed={true}
                              height={56}
                              summary="Audio muted"
                              width={300}>
                 <div className="fx-embedded">
                   <ConversationToolbar audio={{ enabled: false, visible: true }}
                                        hangup={noop}
                                        publishStream={noop}
+                                       settingsMenuItems={[{ id: "feedback" }]}
                                        video={{ enabled: true, visible: true }} />
                 </div>
               </FramedExample>
             </div>
           </Section>
 
           <Section name="PendingConversationView (Desktop)">
             <FramedExample dashed={true}
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -352,18 +352,19 @@ infobar_menuitem_dontshowagain_accesskey
 context_inroom_label=Let's talk about:
 ## LOCALIZATION_NOTE (context_edit_activate_label): {{title}} will be replaced
 ## by the title of the active tab, also known as the title of an HTML document.
 ## The quotes around the title are intentional.
 context_edit_activate_label=Talk about "{{title}}"
 context_edit_name_placeholder=Conversation Name
 context_edit_comments_placeholder=Comments
 context_add_some_label=Add some context
-context_edit_tooltip=Edit Context
-context_hide_tooltip=Hide Context
 context_show_tooltip=Show Context
 context_save_label2=Save
 context_link_modified=This link was modified.
 context_learn_more_link_label=Learn more.
+conversation_settings_menu_edit_context=Edit Context
+conversation_settings_menu_hide_context=Hide Context
+
 
 # Text chat strings
 
 chat_textbox_placeholder=Type hereā€¦