author | Carsten "Tomcat" Book <cbook@mozilla.com> |
Mon, 28 Jul 2014 15:32:36 +0200 | |
changeset 196316 | 70b3fc807a70c47833d1707762c63c8fe27edbf1 |
parent 196315 | a4dcfbebcb588b9893900152eb512c3810880d68 (current diff) |
parent 196250 | b70f46a09115e84657f389eda7db7d62c25021bb (diff) |
child 196335 | d77f6a96ff960d0755cafa0e1dd976d9d285d311 |
push id | 46844 |
push user | cbook@mozilla.com |
push date | Mon, 28 Jul 2014 14:30:47 +0000 |
treeherder | mozilla-inbound@7dd701896de8 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 34.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
|
--- a/CLOBBER +++ b/CLOBBER @@ -17,9 +17,9 @@ # # Modifying this file will now automatically clobber the buildbot machines \o/ # # Are you updating CLOBBER because you think it's needed for your WebIDL # changes to stick? As of bug 928195, this shouldn't be necessary! Please # don't change CLOBBER for WebIDL changes any more. -Bug 1016529 - Updating Android SDK to include Google Play Services +Multiple bugs that happened to trigger bug 1042115.
--- a/Makefile.in +++ b/Makefile.in @@ -195,16 +195,17 @@ MAKE_SYM_STORE_PATH := $(DIST)/bin endif DUMP_SYMS_BIN ?= $(DIST)/host/bin/dump_syms endif ifeq (,$(filter-out Linux SunOS,$(OS_ARCH))) MAKE_SYM_STORE_ARGS := -c --vcs-info DUMP_SYMS_BIN ?= $(DIST)/host/bin/dump_syms MAKE_SYM_STORE_PATH := $(DIST)/bin endif +MAKE_SYM_STORE_ARGS += --install-manifest=$(DEPTH)/_build_manifests/install/dist_include,$(DIST)/include SYM_STORE_SOURCE_DIRS := $(topsrcdir) ifndef JS_STANDALONE include $(topsrcdir)/toolkit/mozapps/installer/package-name.mk ifdef MOZ_SYMBOLS_EXTRA_BUILDID EXTRA_BUILDID := -$(MOZ_SYMBOLS_EXTRA_BUILDID)
--- a/browser/base/content/aboutneterror/info.svg +++ b/browser/base/content/aboutneterror/info.svg @@ -1,7 +1,7 @@ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100"> -<g fill="#85898C"> - <circle cx="50" cy="50" r="44" style="stroke: #85898C; stroke-width: 11; fill: transparent;"/> +<g fill="#424E5A"> + <circle cx="50" cy="50" r="44" style="stroke: #424E5A; stroke-width: 11; fill: transparent;"/> <circle cx="50" cy="24.6" r="6.4"/> <rect x="45" y="39.9" width="10.1" height="41.8"/> </g> </svg>
--- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -5599,17 +5599,17 @@ var OfflineApps = { this.offlineAppRequested(event.originalTarget.defaultView); } }, ///////////////////////////////////////////////////////////////////////////// // OfflineApps Implementation Methods // XXX: _getBrowserWindowForContentWindow and _getBrowserForContentWindow - // were taken from browser/components/feeds/src/WebContentConverter. + // were taken from browser/components/feeds/WebContentConverter. _getBrowserWindowForContentWindow: function(aContentWindow) { return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsIDocShellTreeItem) .rootTreeItem .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindow) .wrappedJSObject;
--- a/browser/components/build/moz.build +++ b/browser/components/build/moz.build @@ -14,18 +14,18 @@ SOURCES += [ LIBRARY_NAME = 'browsercomps' IS_COMPONENT = True LOCAL_INCLUDES += [ '../about', '../dirprovider', - '../feeds/src', - '../migration/src', - '../shell/src', + '../feeds', + '../migration', + '../shell', ] USE_LIBS += [ 'mozalloc', 'xpcomglue_s', 'xul', ]
rename from browser/components/customizableui/src/CustomizableUI.jsm rename to browser/components/customizableui/CustomizableUI.jsm
rename from browser/components/customizableui/src/CustomizableWidgets.jsm rename to browser/components/customizableui/CustomizableWidgets.jsm
rename from browser/components/customizableui/src/CustomizeMode.jsm rename to browser/components/customizableui/CustomizeMode.jsm
rename from browser/components/customizableui/src/DragPositionManager.jsm rename to browser/components/customizableui/DragPositionManager.jsm
rename from browser/components/customizableui/src/PanelWideWidgetTracker.jsm rename to browser/components/customizableui/PanelWideWidgetTracker.jsm
rename from browser/components/customizableui/src/ScrollbarSampler.jsm rename to browser/components/customizableui/ScrollbarSampler.jsm
rename from browser/components/customizableui/src/logging.js rename to browser/components/customizableui/logging.js
--- a/browser/components/customizableui/moz.build +++ b/browser/components/customizableui/moz.build @@ -1,12 +1,31 @@ # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- # vim: set filetype=python: # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. PARALLEL_DIRS += [ 'content', - 'src', ] BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] + +EXTRA_JS_MODULES += [ + 'DragPositionManager.jsm', + 'ScrollbarSampler.jsm', +] + +DEFINES['E10S_TESTING_ONLY'] = CONFIG['E10S_TESTING_ONLY'] + +if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'cocoa'): + DEFINES['CAN_DRAW_IN_TITLEBAR'] = 1 + +if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'gtk2', 'gtk3'): + DEFINES['MENUBAR_CAN_AUTOHIDE'] = 1 + +EXTRA_PP_JS_MODULES += [ + 'CustomizableUI.jsm', + 'CustomizableWidgets.jsm', + 'CustomizeMode.jsm', + 'PanelWideWidgetTracker.jsm', +]
deleted file mode 100644 --- a/browser/components/customizableui/src/moz.build +++ /dev/null @@ -1,25 +0,0 @@ -# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -EXTRA_JS_MODULES += [ - 'DragPositionManager.jsm', - 'ScrollbarSampler.jsm', -] - -DEFINES['E10S_TESTING_ONLY'] = CONFIG['E10S_TESTING_ONLY'] - -if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'cocoa'): - DEFINES['CAN_DRAW_IN_TITLEBAR'] = 1 - -if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'gtk2', 'gtk3'): - DEFINES['MENUBAR_CAN_AUTOHIDE'] = 1 - -EXTRA_PP_JS_MODULES += [ - 'CustomizableUI.jsm', - 'CustomizableWidgets.jsm', - 'CustomizeMode.jsm', - 'PanelWideWidgetTracker.jsm', -]
rename from browser/components/downloads/src/BrowserDownloads.manifest rename to browser/components/downloads/BrowserDownloads.manifest
rename from browser/components/downloads/src/DownloadsCommon.jsm rename to browser/components/downloads/DownloadsCommon.jsm
rename from browser/components/downloads/src/DownloadsLogger.jsm rename to browser/components/downloads/DownloadsLogger.jsm
rename from browser/components/downloads/src/DownloadsStartup.js rename to browser/components/downloads/DownloadsStartup.js
rename from browser/components/downloads/src/DownloadsTaskbar.jsm rename to browser/components/downloads/DownloadsTaskbar.jsm
rename from browser/components/downloads/src/DownloadsUI.js rename to browser/components/downloads/DownloadsUI.js
--- a/browser/components/downloads/moz.build +++ b/browser/components/downloads/moz.build @@ -1,17 +1,22 @@ # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- # vim: set filetype=python: # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -DIRS += ['src'] +XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini'] +BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini'] + +JAR_MANIFESTS += ['jar.mn'] -XPCSHELL_TESTS_MANIFESTS += [ - 'test/unit/xpcshell.ini', +EXTRA_COMPONENTS += [ + 'BrowserDownloads.manifest', + 'DownloadsStartup.js', + 'DownloadsUI.js', ] -BROWSER_CHROME_MANIFESTS += [ - 'test/browser/browser.ini', +EXTRA_JS_MODULES += [ + 'DownloadsCommon.jsm', + 'DownloadsLogger.jsm', + 'DownloadsTaskbar.jsm', ] - -JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
deleted file mode 100644 --- a/browser/components/downloads/src/moz.build +++ /dev/null @@ -1,18 +0,0 @@ -# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -EXTRA_COMPONENTS += [ - 'BrowserDownloads.manifest', - 'DownloadsStartup.js', - 'DownloadsUI.js', -] - -EXTRA_JS_MODULES += [ - 'DownloadsCommon.jsm', - 'DownloadsLogger.jsm', - 'DownloadsTaskbar.jsm', -] -
rename from browser/components/feeds/src/BrowserFeeds.manifest rename to browser/components/feeds/BrowserFeeds.manifest
rename from browser/components/feeds/src/FeedConverter.js rename to browser/components/feeds/FeedConverter.js
rename from browser/components/feeds/src/FeedWriter.js rename to browser/components/feeds/FeedWriter.js
rename from browser/components/feeds/src/WebContentConverter.js rename to browser/components/feeds/WebContentConverter.js
--- a/browser/components/feeds/moz.build +++ b/browser/components/feeds/moz.build @@ -1,21 +1,41 @@ # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- # vim: set filetype=python: # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -DIRS += ['public', 'src'] +XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini'] +MOCHITEST_CHROME_MANIFESTS += ['test/chrome/chrome.ini'] +MOCHITEST_MANIFESTS += ['test/mochitest.ini'] + +JAR_MANIFESTS += ['jar.mn'] -XPCSHELL_TESTS_MANIFESTS += [ - 'test/unit/xpcshell.ini', +XPIDL_SOURCES += [ + 'nsIFeedResultService.idl', + 'nsIWebContentConverterRegistrar.idl', +] + +XPIDL_MODULE = 'browser-feeds' + +SOURCES += [ + 'nsFeedSniffer.cpp', ] -MOCHITEST_CHROME_MANIFESTS += [ - 'test/chrome/chrome.ini', +EXTRA_COMPONENTS += [ + 'BrowserFeeds.manifest', + 'FeedConverter.js', + 'WebContentConverter.js', ] -MOCHITEST_MANIFESTS += [ - 'test/mochitest.ini' +EXTRA_PP_COMPONENTS += [ + 'FeedWriter.js', ] -JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file +FINAL_LIBRARY = 'browsercomps' + +for var in ('MOZ_APP_NAME', 'MOZ_MACBUNDLE_NAME'): + DEFINES[var] = CONFIG[var] + +LOCAL_INCLUDES += [ + '../build', +]
rename from browser/components/feeds/src/nsFeedSniffer.cpp rename to browser/components/feeds/nsFeedSniffer.cpp
rename from browser/components/feeds/src/nsFeedSniffer.h rename to browser/components/feeds/nsFeedSniffer.h
rename from browser/components/feeds/public/nsIFeedResultService.idl rename to browser/components/feeds/nsIFeedResultService.idl
rename from browser/components/feeds/public/nsIWebContentConverterRegistrar.idl rename to browser/components/feeds/nsIWebContentConverterRegistrar.idl
deleted file mode 100644 --- a/browser/components/feeds/public/moz.build +++ /dev/null @@ -1,13 +0,0 @@ -# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -XPIDL_SOURCES += [ - 'nsIFeedResultService.idl', - 'nsIWebContentConverterRegistrar.idl', -] - -XPIDL_MODULE = 'browser-feeds' -
deleted file mode 100644 --- a/browser/components/feeds/src/moz.build +++ /dev/null @@ -1,29 +0,0 @@ -# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -SOURCES += [ - 'nsFeedSniffer.cpp', -] - -EXTRA_COMPONENTS += [ - 'BrowserFeeds.manifest', - 'FeedConverter.js', - 'WebContentConverter.js', -] - -EXTRA_PP_COMPONENTS += [ - 'FeedWriter.js', -] - -FINAL_LIBRARY = 'browsercomps' - -for var in ('MOZ_APP_NAME', 'MOZ_MACBUNDLE_NAME'): - DEFINES[var] = CONFIG[var] - -LOCAL_INCLUDES += [ - '../../build', -] -
--- a/browser/components/loop/content/js/client.js +++ b/browser/components/loop/content/js/client.js @@ -77,17 +77,25 @@ loop.Client = (function($) { * Ensures the client is registered with the push server. * * Callback parameters: * - err null on successful registration, non-null otherwise. * * @param {Function} cb Callback(err) */ _ensureRegistered: function(cb) { - this.mozLoop.ensureRegistered(cb); + this.mozLoop.ensureRegistered(function(error) { + if (error) { + console.log("Error registering with Loop server, code: " + error); + cb(error); + return; + } else { + cb(null); + } + }); }, /** * Internal handler for requesting a call url from the server. * * Callback parameters: * - err null on successful registration, non-null otherwise. * - callUrlData an object of the obtained call url data if successful: @@ -121,33 +129,71 @@ loop.Client = (function($) { } catch (err) { console.log("Error requesting call info", err); cb(err); } }); }, /** + * Block call URL based on the token identifier + * + * @param {string} token Conversation identifier used to block the URL + * @param {function} cb Callback function used for handling an error + * response. XXX The incoming call panel does not + * exist after the block button is clicked therefore + * it does not make sense to display an error. + **/ + deleteCallUrl: function(token, cb) { + this._ensureRegistered(function(err) { + if (err) { + cb(err); + return; + } + + this._deleteCallUrlInternal(token, cb); + }.bind(this)); + }, + + _deleteCallUrlInternal: function(token, cb) { + this.mozLoop.hawkRequest("/call-url/" + token, "DELETE", null, + (error, responseText) => { + if (error) { + this._failureHandler(cb, error); + return; + } + + try { + cb(null); + + this.mozLoop.noteCallUrlExpiry((new Date()).getTime() / 1000); + } catch (err) { + console.log("Error deleting call info", err); + cb(err); + } + }); + }, + + /** * Requests a call URL from the Loop server. It will note the * expiry time for the url with the mozLoop api. * * Callback parameters: * - err null on successful registration, non-null otherwise. * - callUrlData an object of the obtained call url data if successful: * -- call_url: The url of the call * -- expiresAt: The amount of hours until expiry of the url * * @param {String} simplepushUrl a registered Simple Push URL * @param {string} nickname the nickname of the future caller * @param {Function} cb Callback(err, callUrlData) */ requestCallUrl: function(nickname, cb) { this._ensureRegistered(function(err) { if (err) { - console.log("Error registering with Loop server, code: " + err); cb(err); return; } this._requestCallUrlInternal(nickname, cb); }.bind(this)); },
--- a/browser/components/loop/content/js/conversation.js +++ b/browser/components/loop/content/js/conversation.js @@ -22,16 +22,37 @@ loop.conversation = (function(OT, mozL10 var router; var IncomingCallView = React.createClass({displayName: 'IncomingCallView', propTypes: { model: React.PropTypes.func.isRequired }, + getInitialState: function() { + return {showDeclineMenu: false}; + }, + + componentDidMount: function() { + window.addEventListener('click', this.clickHandler); + window.addEventListener('blur', this._hideDeclineMenu); + }, + + componentWillUnmount: function() { + window.removeEventListener('click', this.clickHandler); + window.removeEventListener('blur', this._hideDeclineMenu); + }, + + clickHandler: function(e) { + var target = e.target; + if (!target.classList.contains('btn-chevron')) { + this._hideDeclineMenu(); + } + }, + /** * Used for adding different styles to the panel * @returns {String} Corresponds to the client platform * */ _getTargetPlatform: function() { var platform="unknown_platform"; if (navigator.platform.indexOf("Win") !== -1) { @@ -50,29 +71,66 @@ loop.conversation = (function(OT, mozL10 _handleAccept: function() { this.props.model.trigger("accept"); }, _handleDecline: function() { this.props.model.trigger("decline"); }, + _handleDeclineBlock: function(e) { + this.props.model.trigger("declineAndBlock"); + /* Prevent event propagation + * stop the click from reaching parent element */ + return false; + }, + + _toggleDeclineMenu: function() { + var currentState = this.state.showDeclineMenu; + this.setState({showDeclineMenu: !currentState}); + }, + + _hideDeclineMenu: function() { + this.setState({showDeclineMenu: false}); + }, + render: function() { /* jshint ignore:start */ - var btnClassAccept = "btn btn-error btn-decline"; - var btnClassDecline = "btn btn-success btn-accept"; + var btnClassAccept = "btn btn-success btn-accept"; + var btnClassBlock = "btn btn-error btn-block"; + var btnClassDecline = "btn btn-error btn-decline"; var conversationPanelClass = "incoming-call " + this._getTargetPlatform(); + var cx = React.addons.classSet; + var declineDropdownMenuClasses = cx({ + "native-dropdown-menu": true, + "decline-block-menu": true, + "visually-hidden": !this.state.showDeclineMenu + }); return ( - React.DOM.div( {className:conversationPanelClass}, - React.DOM.h2(null, __("incoming_call")), - React.DOM.div( {className:"button-group"}, - React.DOM.button( {className:btnClassAccept, onClick:this._handleDecline}, - __("incoming_call_decline_button") - ), - React.DOM.button( {className:btnClassDecline, onClick:this._handleAccept}, + React.DOM.div({className: conversationPanelClass}, + React.DOM.h2(null, __("incoming_call")), + React.DOM.div({className: "button-group incoming-call-action-group"}, + React.DOM.div({className: "button-chevron-menu-group"}, + React.DOM.div({className: "button-group-chevron"}, + React.DOM.div({className: "button-group"}, + React.DOM.button({className: btnClassDecline, onClick: this._handleDecline}, + __("incoming_call_decline_button") + ), + React.DOM.div({className: "btn-chevron", + onClick: this._toggleDeclineMenu} + ) + ), + React.DOM.ul({className: declineDropdownMenuClasses}, + React.DOM.li({className: "btn-block", onClick: this._handleDeclineBlock}, + __("incoming_call_decline_and_block_button") + ) + ) + ) + ), + React.DOM.button({className: btnClassAccept, onClick: this._handleAccept}, __("incoming_call_answer_button") ) ) ) ); /* jshint ignore:end */ } }); @@ -111,17 +169,18 @@ loop.conversation = (function(OT, mozL10 * @type {loop.shared.router.BaseConversationRouter} */ var ConversationRouter = loop.desktopRouter.DesktopConversationRouter.extend({ routes: { "incoming/:version": "incoming", "call/accept": "accept", "call/decline": "decline", "call/ongoing": "conversation", - "call/ended": "ended" + "call/ended": "ended", + "call/declineAndBlock": "declineAndBlock" }, /** * @override {loop.shared.router.BaseConversationRouter.startCall} */ startCall: function() { this.navigate("call/ongoing", {trigger: true}); }, @@ -143,16 +202,19 @@ loop.conversation = (function(OT, mozL10 window.navigator.mozLoop.startAlerting(); this._conversation.set({loopVersion: loopVersion}); this._conversation.once("accept", function() { this.navigate("call/accept", {trigger: true}); }.bind(this)); this._conversation.once("decline", function() { this.navigate("call/decline", {trigger: true}); }.bind(this)); + this._conversation.once("declineAndBlock", function() { + this.navigate("call/declineAndBlock", {trigger: true}); + }.bind(this)); this.loadReactComponent(loop.conversation.IncomingCallView({ model: this._conversation })); }, /** * Accepts an incoming call. */ @@ -169,16 +231,34 @@ loop.conversation = (function(OT, mozL10 */ decline: function() { window.navigator.mozLoop.stopAlerting(); // XXX For now, we just close the window window.close(); }, /** + * Decline and block an incoming call + * @note: + * - loopToken is the callUrl identifier. It gets set in the panel + * after a callUrl is received + */ + declineAndBlock: function() { + window.navigator.mozLoop.stopAlerting(); + var token = navigator.mozLoop.getLoopCharPref('loopToken'); + var client = new loop.Client(); + client.deleteCallUrl(token, function(error) { + // XXX The conversation window will be closed when this cb is triggered + // figure out if there is a better way to report the error to the user + console.log(error); + }); + window.close(); + }, + + /** * conversation is the route when the conversation is active. The start * route should be navigated to first. */ conversation: function() { if (!this._conversation.isSessionReady()) { console.error("Error: navigated to conversation route without " + "the start route to initialise the call first"); this._notifier.errorL10n("cannot_start_call_session_not_ready");
--- a/browser/components/loop/content/js/conversation.jsx +++ b/browser/components/loop/content/js/conversation.jsx @@ -22,16 +22,37 @@ loop.conversation = (function(OT, mozL10 var router; var IncomingCallView = React.createClass({ propTypes: { model: React.PropTypes.func.isRequired }, + getInitialState: function() { + return {showDeclineMenu: false}; + }, + + componentDidMount: function() { + window.addEventListener('click', this.clickHandler); + window.addEventListener('blur', this._hideDeclineMenu); + }, + + componentWillUnmount: function() { + window.removeEventListener('click', this.clickHandler); + window.removeEventListener('blur', this._hideDeclineMenu); + }, + + clickHandler: function(e) { + var target = e.target; + if (!target.classList.contains('btn-chevron')) { + this._hideDeclineMenu(); + } + }, + /** * Used for adding different styles to the panel * @returns {String} Corresponds to the client platform * */ _getTargetPlatform: function() { var platform="unknown_platform"; if (navigator.platform.indexOf("Win") !== -1) { @@ -50,29 +71,66 @@ loop.conversation = (function(OT, mozL10 _handleAccept: function() { this.props.model.trigger("accept"); }, _handleDecline: function() { this.props.model.trigger("decline"); }, + _handleDeclineBlock: function(e) { + this.props.model.trigger("declineAndBlock"); + /* Prevent event propagation + * stop the click from reaching parent element */ + return false; + }, + + _toggleDeclineMenu: function() { + var currentState = this.state.showDeclineMenu; + this.setState({showDeclineMenu: !currentState}); + }, + + _hideDeclineMenu: function() { + this.setState({showDeclineMenu: false}); + }, + render: function() { /* jshint ignore:start */ - var btnClassAccept = "btn btn-error btn-decline"; - var btnClassDecline = "btn btn-success btn-accept"; + var btnClassAccept = "btn btn-success btn-accept"; + var btnClassBlock = "btn btn-error btn-block"; + var btnClassDecline = "btn btn-error btn-decline"; var conversationPanelClass = "incoming-call " + this._getTargetPlatform(); + var cx = React.addons.classSet; + var declineDropdownMenuClasses = cx({ + "native-dropdown-menu": true, + "decline-block-menu": true, + "visually-hidden": !this.state.showDeclineMenu + }); return ( <div className={conversationPanelClass}> <h2>{__("incoming_call")}</h2> - <div className="button-group"> - <button className={btnClassAccept} onClick={this._handleDecline}> - {__("incoming_call_decline_button")} - </button> - <button className={btnClassDecline} onClick={this._handleAccept}> + <div className="button-group incoming-call-action-group"> + <div className="button-chevron-menu-group"> + <div className="button-group-chevron"> + <div className="button-group"> + <button className={btnClassDecline} onClick={this._handleDecline}> + {__("incoming_call_decline_button")} + </button> + <div className="btn-chevron" + onClick={this._toggleDeclineMenu}> + </div> + </div> + <ul className={declineDropdownMenuClasses}> + <li className="btn-block" onClick={this._handleDeclineBlock}> + {__("incoming_call_decline_and_block_button")} + </li> + </ul> + </div> + </div> + <button className={btnClassAccept} onClick={this._handleAccept}> {__("incoming_call_answer_button")} </button> </div> </div> ); /* jshint ignore:end */ } }); @@ -111,17 +169,18 @@ loop.conversation = (function(OT, mozL10 * @type {loop.shared.router.BaseConversationRouter} */ var ConversationRouter = loop.desktopRouter.DesktopConversationRouter.extend({ routes: { "incoming/:version": "incoming", "call/accept": "accept", "call/decline": "decline", "call/ongoing": "conversation", - "call/ended": "ended" + "call/ended": "ended", + "call/declineAndBlock": "declineAndBlock" }, /** * @override {loop.shared.router.BaseConversationRouter.startCall} */ startCall: function() { this.navigate("call/ongoing", {trigger: true}); }, @@ -143,16 +202,19 @@ loop.conversation = (function(OT, mozL10 window.navigator.mozLoop.startAlerting(); this._conversation.set({loopVersion: loopVersion}); this._conversation.once("accept", function() { this.navigate("call/accept", {trigger: true}); }.bind(this)); this._conversation.once("decline", function() { this.navigate("call/decline", {trigger: true}); }.bind(this)); + this._conversation.once("declineAndBlock", function() { + this.navigate("call/declineAndBlock", {trigger: true}); + }.bind(this)); this.loadReactComponent(loop.conversation.IncomingCallView({ model: this._conversation })); }, /** * Accepts an incoming call. */ @@ -169,16 +231,34 @@ loop.conversation = (function(OT, mozL10 */ decline: function() { window.navigator.mozLoop.stopAlerting(); // XXX For now, we just close the window window.close(); }, /** + * Decline and block an incoming call + * @note: + * - loopToken is the callUrl identifier. It gets set in the panel + * after a callUrl is received + */ + declineAndBlock: function() { + window.navigator.mozLoop.stopAlerting(); + var token = navigator.mozLoop.getLoopCharPref('loopToken'); + var client = new loop.Client(); + client.deleteCallUrl(token, function(error) { + // XXX The conversation window will be closed when this cb is triggered + // figure out if there is a better way to report the error to the user + console.log(error); + }); + window.close(); + }, + + /** * conversation is the route when the conversation is active. The start * route should be navigated to first. */ conversation: function() { if (!this._conversation.isSessionReady()) { console.error("Error: navigated to conversation route without " + "the start route to initialise the call first"); this._notifier.errorL10n("cannot_start_call_session_not_ready");
--- a/browser/components/loop/content/js/panel.js +++ b/browser/components/loop/content/js/panel.js @@ -72,32 +72,32 @@ loop.panel = (function(_, mozL10n) { 'dnd-menu': true, 'hide': !this.state.showMenu }); var availabilityText = this.state.doNotDisturb ? __("display_name_dnd_status") : __("display_name_available_status"); return ( - React.DOM.div( {className:"footer component-spacer"}, - React.DOM.div( {className:"do-not-disturb"}, - 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"}, - React.DOM.i( {className:"status status-available"}), + React.DOM.div({className: "footer component-spacer"}, + React.DOM.div({className: "do-not-disturb"}, + 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"}, + 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"}, - React.DOM.i( {className:"status status-dnd"}), + ), + React.DOM.li({onClick: this.changeAvailability("do-not-disturb"), + className: "dnd-menu-item dnd-make-unavailable"}, + React.DOM.i({className: "status status-dnd"}), React.DOM.span(null, __("display_name_dnd_status")) ) ) ) ) ); } }); @@ -110,36 +110,36 @@ loop.panel = (function(_, mozL10n) { render: function() { var tosHTML = __("legal_text_and_links", { "terms_of_use_url": "https://accounts.firefox.com/legal/terms", "privacy_notice_url": "www.mozilla.org/privacy/" }); if (this.state.seenToS == "unseen") { navigator.mozLoop.setLoopCharPref('seenToS', 'seen'); - return React.DOM.p( {className:"terms-service", - dangerouslySetInnerHTML:{__html: tosHTML}}); + return React.DOM.p({className: "terms-service", + dangerouslySetInnerHTML: {__html: tosHTML}}); } else { - return React.DOM.div(null ); + return React.DOM.div(null); } } }); var PanelLayout = React.createClass({displayName: 'PanelLayout', propTypes: { summary: React.PropTypes.string.isRequired }, render: function() { return ( - React.DOM.div( {className:"component-spacer share generate-url"}, - React.DOM.div( {className:"description"}, - React.DOM.p( {className:"description-content"}, this.props.summary) - ), - React.DOM.div( {className:"action"}, + React.DOM.div({className: "component-spacer share generate-url"}, + React.DOM.div({className: "description"}, + React.DOM.p({className: "description-content"}, this.props.summary) + ), + React.DOM.div({className: "action"}, this.props.children ) ) ); } }); var CallUrlResult = React.createClass({displayName: 'CallUrlResult', @@ -150,57 +150,67 @@ loop.panel = (function(_, mozL10n) { callUrl: '' }; }, /** * Returns a random 5 character string used to identify * the conversation. * XXX this will go away once the backend changes + * @note: + * - When we get back a callUrl we use setLoopCharPref to store the token + * (the last fragment of the URL) so that it can be used to ignore&block + * the call. The preference is used by the conversation router. */ conversationIdentifier: function() { return Math.random().toString(36).substring(5); }, componentDidMount: function() { this.setState({pending: true}); this.props.client.requestCallUrl(this.conversationIdentifier(), this._onCallUrlReceived); }, _onCallUrlReceived: function(err, callUrlData) { - // XXX this initializer is a bug, as it will cause - // setState to set the callUrl to false if one is not returned. - // Should decide on an implement correct behavior and state - // (eg set widget as disabled, state.callUrl == '') - // - var callUrl = false; - this.props.notifier.clear(); if (err) { this.props.notifier.errorL10n("unable_retrieve_url"); + this.setState({pending: false}); } else { - callUrl = callUrlData.callUrl || callUrlData.call_url; + try { + var callUrl = new window.URL(callUrlData.callUrl || + callUrlData.call_url); + // XXX the current server vers does not implement the callToken field + // but it exists in the API. This workaround should be removed in the future + var token = callUrlData.callToken || + callUrl.pathname.split('/').pop(); + + navigator.mozLoop.setLoopCharPref('loopToken', token); + this.setState({pending: false, callUrl: callUrl.href}); + } catch(e) { + console.log(e); + this.props.notifier.errorL10n("unable_retrieve_url"); + this.setState({pending: false}); + } } - - this.setState({pending: false, callUrl: callUrl}); }, render: function() { // XXX setting elem value from a state (in the callUrl input) // makes it immutable ie read only but that is fine in our case. // readOnly attr will suppress a warning regarding this issue // from the react lib. var cx = React.addons.classSet; return ( - PanelLayout( {summary:__("share_link_header_text")}, - React.DOM.div( {className:"invite"}, - React.DOM.input( {type:"url", value:this.state.callUrl, readOnly:"true", - className:cx({'pending': this.state.pending})} ) + PanelLayout({summary: __("share_link_header_text")}, + React.DOM.div({className: "invite"}, + React.DOM.input({type: "url", value: this.state.callUrl, readOnly: "true", + className: cx({'pending': this.state.pending})}) ) ) ); } }); /** * Panel view. @@ -209,20 +219,20 @@ loop.panel = (function(_, mozL10n) { propTypes: { notifier: React.PropTypes.object.isRequired, client: React.PropTypes.object.isRequired }, render: function() { return ( React.DOM.div(null, - CallUrlResult( {client:this.props.client, - notifier:this.props.notifier} ), - ToSView(null ), - AvailabilityDropdown(null ) + CallUrlResult({client: this.props.client, + notifier: this.props.notifier}), + ToSView(null), + AvailabilityDropdown(null) ) ); } }); var PanelRouter = loop.desktopRouter.DesktopRouter.extend({ /** * DOM document object. @@ -279,18 +289,18 @@ loop.panel = (function(_, mozL10n) { /** * Resets this router to its initial state. */ reset: function() { this._notifier.clear(); var client = new loop.Client({ baseServerUrl: navigator.mozLoop.serverUrl }); - this.loadReactComponent(PanelView( {client:client, - notifier:this._notifier} )); + this.loadReactComponent(PanelView({client: client, + notifier: this._notifier})); } }); /** * Panel initialisation. */ function init() { // Do the initial L10n setup, we do this before anything
--- a/browser/components/loop/content/js/panel.jsx +++ b/browser/components/loop/content/js/panel.jsx @@ -150,44 +150,54 @@ loop.panel = (function(_, mozL10n) { callUrl: '' }; }, /** * Returns a random 5 character string used to identify * the conversation. * XXX this will go away once the backend changes + * @note: + * - When we get back a callUrl we use setLoopCharPref to store the token + * (the last fragment of the URL) so that it can be used to ignore&block + * the call. The preference is used by the conversation router. */ conversationIdentifier: function() { return Math.random().toString(36).substring(5); }, componentDidMount: function() { this.setState({pending: true}); this.props.client.requestCallUrl(this.conversationIdentifier(), this._onCallUrlReceived); }, _onCallUrlReceived: function(err, callUrlData) { - // XXX this initializer is a bug, as it will cause - // setState to set the callUrl to false if one is not returned. - // Should decide on an implement correct behavior and state - // (eg set widget as disabled, state.callUrl == '') - // - var callUrl = false; - this.props.notifier.clear(); if (err) { this.props.notifier.errorL10n("unable_retrieve_url"); + this.setState({pending: false}); } else { - callUrl = callUrlData.callUrl || callUrlData.call_url; + try { + var callUrl = new window.URL(callUrlData.callUrl || + callUrlData.call_url); + // XXX the current server vers does not implement the callToken field + // but it exists in the API. This workaround should be removed in the future + var token = callUrlData.callToken || + callUrl.pathname.split('/').pop(); + + navigator.mozLoop.setLoopCharPref('loopToken', token); + this.setState({pending: false, callUrl: callUrl.href}); + } catch(e) { + console.log(e); + this.props.notifier.errorL10n("unable_retrieve_url"); + this.setState({pending: false}); + } } - - this.setState({pending: false, callUrl: callUrl}); }, render: function() { // XXX setting elem value from a state (in the callUrl input) // makes it immutable ie read only but that is fine in our case. // readOnly attr will suppress a warning regarding this issue // from the react lib. var cx = React.addons.classSet;
--- a/browser/components/loop/content/shared/css/common.css +++ b/browser/components/loop/content/shared/css/common.css @@ -48,16 +48,20 @@ img { .cf:after { clear: both; } .hide { display: none; } +.visually-hidden { + visibility: hidden; +} + .tc { text-align: center; } .full-width { width: 100%; } @@ -68,82 +72,134 @@ img { background: #a5a; border: none; color: #fff; text-decoration: none; height: 26px; padding: 0 0.5em; border-radius: 2px; cursor: pointer; + font-size: .9em; + text-align: center; } .btn-info { - background: #0096dd; + background-color: #0096dd; border: 1px solid #0095dd; } .btn-info:hover { - background: #008acb; + background-color: #008acb; border: 1px solid #008acb; } .btn-info:active { - background: #006b9d; + background-color: #006b9d; border: 1px solid #006b9d; } .btn-success { - background: #74bf43; + background-color: #74bf43; border: 1px solid #74bf43; } .btn-success:hover { - background: #6cb23e; + background-color: #6cb23e; border: 1px solid #6cb23e; } .btn-success:active { - background: #64a43a; + background-color: #64a43a; border: 1px solid #64a43a; } .btn-warning { - background: #f0ad4e; + background-color: #f0ad4e; } -.btn-error { - background: #d74345; +.btn-error, +.btn-error + .btn-chevron { + background-color: #d74345; border: 1px solid #d74345; } - .btn-error:hover { - background: #c53436; + .btn-error:hover, + .btn-error + .btn-chevron:hover { + background-color: #c53436; border: 1px solid #c53436; } - .btn-error:active { - background: #ae2325; + .btn-error:active, + .btn-error + .btn-chevron:active { + background-color: #ae2325; border: 1px solid #ae2325; } +.btn-chevron { + width: 26px; + height: 26px; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; +} + +/* Groups together a button and a chevron */ +.button-group-chevron { + display: flex; + flex-direction: column; + flex: 1; +} + +/* Groups together a button-group-chevron + * and the dropdown menu */ +.button-chevron-menu-group { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + flex: 1; +} + +.button-group-chevron .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + flex: 2; +} + + .btn + .btn-chevron, + .btn + .btn-chevron:hover, + .btn + .btn-chevron:active { + border-left: 1px solid rgba(255,255,255,.4); + background-image: url("../img/dropdown-inverse.png"); + background-repeat: no-repeat; + background-position: center; + background-size: 10px; + } + +@media (min-resolution: 2dppx) { + .btn-chevron { + background-image: url(../img/dropdown-inverse@2x.png); + background-position: center; + background-size: 10px; + background-repeat: no-repeat; + } +} + .disabled, button[disabled] { cursor: not-allowe