Merge f-t to m-c, a=merge

This commit is contained in:
Phil Ringnalda 2015-09-18 21:46:16 -07:00
commit 45d1b8a22f
101 changed files with 2345 additions and 857 deletions

View File

@ -1879,7 +1879,7 @@ pref("browser.polaris.enabled", false);
pref("privacy.trackingprotection.ui.enabled", false);
#endif
pref("privacy.trackingprotection.introCount", 0);
pref("privacy.trackingprotection.introURL", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/tracking-protection-pbm");
pref("privacy.trackingprotection.introURL", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/tracking-protection/start/");
#ifndef RELEASE_BUILD
// At the moment, autostart.2 is used, while autostart.1 is unused.

View File

@ -134,6 +134,9 @@ loop.conversation = (function(mozL10n) {
mozLoop: navigator.mozLoop
});
// expose for functional tests
loop.conversation._sdkDriver = sdkDriver;
// Create the stores.
var conversationAppStore = new loop.store.ConversationAppStore({
dispatcher: dispatcher,

View File

@ -134,6 +134,9 @@ loop.conversation = (function(mozL10n) {
mozLoop: navigator.mozLoop
});
// expose for functional tests
loop.conversation._sdkDriver = sdkDriver;
// Create the stores.
var conversationAppStore = new loop.store.ConversationAppStore({
dispatcher: dispatcher,

View File

@ -457,17 +457,37 @@ html[dir="rtl"] .room-failure > .settings-control {
white-space: nowrap;
}
.screen-share-menu.dropdown-menu,
.settings-menu.dropdown-menu {
left: auto;
bottom: 3.1rem;
}
.screen-share-menu.dropdown-menu {
/*offset dropdown menu to be above menu button*/
right: 40px;
}
.settings-menu.dropdown-menu {
/*offset dropdown menu to be above menu button*/
right: 14px;
}
html[dir="rtl"] .screen-share-menu.dropdown-menu,
html[dir="rtl"] .settings-menu.dropdown-menu {
left: 14px;
right: auto;
}
html[dir="rtl"] .screen-share-menu.dropdown-menu {
/*offset dropdown menu to be above menu button*/
left: 40px;
}
html[dir="rtl"] .settings-menu.dropdown-menu {
/*offset dropdown menu to be above menu button*/
left: 14px;
}
.settings-menu.dropdown-menu.menu-below {
top: 11.5rem;
bottom: auto;

View File

@ -150,12 +150,12 @@ loop.shared.views = (function(_, mozL10n) {
"disabled": this.props.state === SCREEN_SHARE_STATES.PENDING
});
var dropdownMenuClasses = cx({
"native-dropdown-menu": true,
"conversation-window-dropdown": true,
"hide": !this.state.showMenu,
"visually-hidden": true
"screen-share-menu": true,
"dropdown-menu": true,
"hide": !this.state.showMenu
});
var windowSharingClasses = cx({
"dropdown-menu-item": true,
"disabled": this.state.windowSharingDisabled
});
@ -168,7 +168,7 @@ loop.shared.views = (function(_, mozL10n) {
isActive ? null : React.createElement("span", {className: "chevron"})
),
React.createElement("ul", {className: dropdownMenuClasses, ref: "menu"},
React.createElement("li", {onClick: this._handleShareTabs},
React.createElement("li", {className: "dropdown-menu-item", onClick: this._handleShareTabs},
mozL10n.get("share_tabs_button_title2")
),
React.createElement("li", {className: windowSharingClasses, onClick: this._handleShareWindows},

View File

@ -150,12 +150,12 @@ loop.shared.views = (function(_, mozL10n) {
"disabled": this.props.state === SCREEN_SHARE_STATES.PENDING
});
var dropdownMenuClasses = cx({
"native-dropdown-menu": true,
"conversation-window-dropdown": true,
"hide": !this.state.showMenu,
"visually-hidden": true
"screen-share-menu": true,
"dropdown-menu": true,
"hide": !this.state.showMenu
});
var windowSharingClasses = cx({
"dropdown-menu-item": true,
"disabled": this.state.windowSharingDisabled
});
@ -168,7 +168,7 @@ loop.shared.views = (function(_, mozL10n) {
{isActive ? null : <span className="chevron"/>}
</button>
<ul className={dropdownMenuClasses} ref="menu">
<li onClick={this._handleShareTabs}>
<li className="dropdown-menu-item" onClick={this._handleShareTabs}>
{mozL10n.get("share_tabs_button_title2")}
</li>
<li className={windowSharingClasses} onClick={this._handleShareWindows}>

View File

@ -169,7 +169,7 @@ describe("loop.shared.views", function() {
}));
TestUtils.Simulate.click(comp.getDOMNode().querySelector(
".conversation-window-dropdown > li"));
".screen-share-menu > li"));
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
@ -186,7 +186,7 @@ describe("loop.shared.views", function() {
}));
TestUtils.Simulate.click(comp.getDOMNode().querySelector(
".conversation-window-dropdown > li:last-child"));
".screen-share-menu > li:last-child"));
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
@ -201,7 +201,7 @@ describe("loop.shared.views", function() {
state: SCREEN_SHARE_STATES.INACTIVE
}));
var node = comp.getDOMNode().querySelector(".conversation-window-dropdown > li:last-child");
var node = comp.getDOMNode().querySelector(".screen-share-menu > li:last-child");
expect(node.classList.contains("disabled")).eql(false);
});
@ -216,7 +216,7 @@ describe("loop.shared.views", function() {
state: SCREEN_SHARE_STATES.INACTIVE
}));
var node = comp.getDOMNode().querySelector(".conversation-window-dropdown > li:last-child");
var node = comp.getDOMNode().querySelector(".screen-share-menu > li:last-child");
expect(node.classList.contains("disabled")).eql(true);
});
@ -231,7 +231,7 @@ describe("loop.shared.views", function() {
state: SCREEN_SHARE_STATES.INACTIVE
}));
var node = comp.getDOMNode().querySelector(".conversation-window-dropdown > li:last-child");
var node = comp.getDOMNode().querySelector(".screen-share-menu > li:last-child");
expect(node.classList.contains("disabled")).eql(true);
});

View File

@ -335,6 +335,7 @@
<method name="handleSearchCommand">
<parameter name="aEvent"/>
<parameter name="aEngine"/>
<parameter name="aForceNewTab"/>
<body><![CDATA[
var textBox = this._textbox;
var textValue = textBox.value;
@ -347,6 +348,11 @@
return;
where = whereToOpenLink(aEvent, false, true);
}
else if (aForceNewTab) {
where = "tab";
if (Services.prefs.getBoolPref("browser.tabs.loadInBackground"))
where += "-background";
}
else {
var newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
if (((aEvent instanceof KeyboardEvent) && aEvent.altKey) ^ newTabPref)
@ -380,6 +386,8 @@
} else if (aEvent instanceof XULCommandEvent) {
if (target.getAttribute("anonid") == "paste-and-search") {
source = "paste";
} else if (target.getAttribute("anonid") == "search-one-offs-context-open-in-new-tab") {
source = "oneoff-context";
}
}
@ -1007,7 +1015,7 @@
<resources>
<stylesheet src="chrome://browser/skin/searchbar.css"/>
</resources>
<content ignorekeys="true" level="top" consumeoutsideclicks="never">
<content ignorekeys="true" level="top" consumeoutsideclicks="never" context="_child">
<xul:hbox anonid="searchbar-engine" xbl:inherits="showonlysettings"
class="search-panel-header search-panel-current-engine">
<xul:image class="searchbar-engine-image" xbl:inherits="src"/>
@ -1049,6 +1057,14 @@
oncommand="showSettings();"
class="search-setting-button search-panel-header"
label="&changeSearchSettings.button;"/>
<xul:menupopup anonid="search-one-offs-context-menu">
<xul:menuitem anonid="search-one-offs-context-open-in-new-tab"
label="&searchInNewTab.label;"
accesskey="&searchInNewTab.accesskey;"/>
<xul:menuitem anonid="search-one-offs-context-set-default"
label="&searchSetAsDefault.label;"
accesskey="&searchSetAsDefault.accesskey;"/>
</xul:menupopup>
</content>
<implementation>
<!-- Popup rollup is triggered by native events before the mousedown event
@ -1056,6 +1072,9 @@
false after the mousedown event has been triggered to detect what
caused rollup. -->
<field name="_isHiding">false</field>
<!-- When a context menu is opened on a one-off button, this is set to the
engine of that button for use with the context menu actions. -->
<field name="_contextEngine">null</field>
<field name="_bundle">null</field>
<property name="bundle" readonly="true">
<getter>
@ -1101,12 +1120,41 @@
BrowserSearch.searchBar._textbox.closePopup();
]]></body>
</method>
<constructor><![CDATA[
// Prevent popup events from the context menu from reaching the autocomplete
// binding (or other listeners).
let menu = document.getAnonymousElementByAttribute(this, "anonid", "search-one-offs-context-menu");
let listener = aEvent => aEvent.stopPropagation();
menu.addEventListener("popupshowing", listener);
menu.addEventListener("popuphiding", listener);
menu.addEventListener("popupshown", aEvent => {
this._ignoreMouseEvents = true;
aEvent.stopPropagation();
});
menu.addEventListener("popuphidden", aEvent => {
this._ignoreMouseEvents = false;
aEvent.stopPropagation();
});
]]></constructor>
</implementation>
<handlers>
<handler event="popuphidden"><![CDATA[
Services.tm.mainThread.dispatch(function() {
document.getElementById("searchbar").textbox.selectedButton = null;
}, Ci.nsIThread.DISPATCH_NORMAL);
this._contextEngine = null;
]]></handler>
<handler event="contextmenu"><![CDATA[
let target = event.originalTarget;
// Prevent the context menu from appearing except on the one off buttons.
if (!target.classList.contains("searchbar-engine-one-off-item") ||
target.classList.contains("dummy")) {
event.preventDefault();
return;
}
this._contextEngine = target.engine;
]]></handler>
<handler event="popupshowing"><![CDATA[
@ -1321,6 +1369,10 @@
if (target.localName != "button")
return;
// Ignore mouse events when the context menu is open.
if (this._ignoreMouseEvents)
return;
if ((target.classList.contains("searchbar-engine-one-off-item") &&
!target.classList.contains("dummy")) ||
target.classList.contains("addengine-item") ||
@ -1336,6 +1388,10 @@
if (target.localName != "button")
return;
// Don't deselect the current button if the context menu is open.
if (this._ignoreMouseEvents)
return;
let textbox = document.getElementById("searchbar").textbox;
if (textbox.selectedButton == target)
textbox.selectedButton = null;
@ -1351,6 +1407,14 @@
if (!engine)
return;
// For some reason, if the context menu had been opened prior to the
// click, the suggestions popup won't be closed after loading the search
// in the current tab - so we hide it manually. Some focusing magic
// that happens when a search is loaded ensures that the popup is opened
// again if it needs to be, so we don't need to worry about which cases
// require manual hiding.
this.hidePopup();
let searchbar = document.getElementById("searchbar");
searchbar.handleSearchCommand(event, engine);
]]></handler>
@ -1373,6 +1437,29 @@
target.getAttribute("image"), false,
installCallback);
}
let anonid = target.getAttribute("anonid");
if (anonid == "search-one-offs-context-open-in-new-tab") {
let searchbar = document.getElementById("searchbar");
searchbar.handleSearchCommand(event, this._contextEngine, true);
}
if (anonid == "search-one-offs-context-set-default") {
let currentEngine = Services.search.currentEngine;
// Make the target button of the context menu reflect the current
// search engine first. Doing this as opposed to rebuilding all the
// one-off buttons avoids flicker.
let button = document.getElementById("searchbar-engine-one-off-item-" +
this._contextEngine.name.replace(/ /g, '-'));
button.id = "searchbar-engine-one-off-item-" + currentEngine.name.replace(/ /g, '-');
let uri = "chrome://browser/skin/search-engine-placeholder.png";
if (currentEngine.iconURI)
uri = PlacesUtils.getImageURLForResolution(window, currentEngine.iconURI.spec);
button.setAttribute("image", uri);
button.setAttribute("tooltiptext", currentEngine.name);
button.engine = currentEngine;
Services.search.currentEngine = this._contextEngine;
}
]]></handler>
<handler event="popuphiding"><![CDATA[

View File

@ -3,43 +3,15 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const constants = require('../constants');
const promise = require('promise');
const { rdpInvoke, asPaused } = require('../utils');
const constants = require("../constants");
const { rdpInvoke, asPaused } = require("../utils");
const { reportException } = require("devtools/toolkit/DevToolsUtils");
const FETCH_EVENT_LISTENERS_DELAY = 200; // ms
const initialState = {
activeEventNames: [],
listeners: [],
fetchingListeners: false,
};
function update(state = initialState, action, emitChange) {
switch(action.type) {
case constants.UPDATE_EVENT_BREAKPOINTS:
state.activeEventNames = action.eventNames;
emitChange('activeEventNames', state.activeEventNames);
break;
case constants.FETCH_EVENT_LISTENERS:
if (action.status === "begin") {
state.fetchingListeners = true;
}
else if (action.status === "done") {
state.fetchingListeners = false;
state.listeners = action.listeners;
emitChange('listeners', state.listeners);
}
break;
}
return state;
};
function fetchEventListeners() {
return (dispatch, getState) => {
// Make sure we're not sending a batch of closely repeated requests.
// Make sure we"re not sending a batch of closely repeated requests.
// This can easily happen whenever new sources are fetched.
setNamedTimeout("event-listeners-fetch", FETCH_EVENT_LISTENERS_DELAY, () => {
// In case there is still a request of listeners going on (it
@ -81,7 +53,7 @@ const _getListeners = Task.async(function*() {
const response = yield rdpInvoke(gThreadClient, gThreadClient.eventListeners);
// Make sure all the listeners are sorted by the event type, since
// they're not guaranteed to be clustered together.
// they"re not guaranteed to be clustered together.
response.listeners.sort((a, b) => a.type > b.type ? 1 : -1);
// Add all the listeners in the debugger view event linsteners container.
@ -94,8 +66,8 @@ const _getListeners = Task.async(function*() {
} else if (listener.function.class == "Function") {
definitionSite = yield _getDefinitionSite(listener.function);
if (!definitionSite) {
// We don't know where this listener comes from so don't show it in
// the UI as breaking on it doesn't work (bug 942899).
// We don"t know where this listener comes from so don"t show it in
// the UI as breaking on it doesn"t work (bug 942899).
continue;
}
@ -141,7 +113,4 @@ function updateEventBreakpoints(eventNames) {
}
}
module.exports = {
update: update,
actions: { updateEventBreakpoints, fetchEventListeners }
}
module.exports = { updateEventBreakpoints, fetchEventListeners };

View File

@ -0,0 +1,37 @@
/* 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/. */
"use strict";
const constants = require('../constants');
const FETCH_EVENT_LISTENERS_DELAY = 200; // ms
const initialState = {
activeEventNames: [],
listeners: [],
fetchingListeners: false,
};
function update(state = initialState, action, emit) {
switch(action.type) {
case constants.UPDATE_EVENT_BREAKPOINTS:
state.activeEventNames = action.eventNames;
emit("@redux:activeEventNames", state.activeEventNames);
break;
case constants.FETCH_EVENT_LISTENERS:
if (action.status === "begin") {
state.fetchingListeners = true;
}
else if (action.status === "done") {
state.fetchingListeners = false;
state.listeners = action.listeners;
emit("@redux:listeners", state.listeners);
}
break;
}
return state;
}
module.exports = update;

View File

@ -5,4 +5,4 @@
const eventListeners = require('./event-listeners');
module.exports = { eventListeners };
exports.eventListeners = eventListeners;

View File

@ -3,26 +3,25 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const actions = require('../stores/event-listeners').actions;
const bindActionCreators = require('devtools/shared/fluxify/bindActionCreators');
const actions = require('../actions/event-listeners');
const { bindActionCreators } = require('devtools/shared/vendor/redux');
/**
* Functions handling the event listeners UI.
*/
function EventListenersView(dispatcher, DebuggerController) {
function EventListenersView(store, DebuggerController) {
dumpn("EventListenersView was instantiated");
this.actions = bindActionCreators(actions, dispatcher.dispatch);
this.getState = () => dispatcher.getState().eventListeners;
this.Breakpoints = DebuggerController.Breakpoints;
dispatcher.onChange({
"eventListeners": { "listeners": this.renderListeners }
}, this);
this.actions = bindActionCreators(actions, store.dispatch);
this.getState = () => store.getState().eventListeners;
this._onCheck = this._onCheck.bind(this);
this._onClick = this._onClick.bind(this);
this._onListeners = this._onListeners.bind(this);
this.Breakpoints = DebuggerController.Breakpoints;
this.controller = DebuggerController;
this.controller.on("@redux:listeners", this._onListeners);
}
EventListenersView.prototype = Heritage.extend(WidgetMethods, {
@ -53,8 +52,10 @@ EventListenersView.prototype = Heritage.extend(WidgetMethods, {
destroy: function() {
dumpn("Destroying the EventListenersView");
this.controller.off("@redux:listeners", this._onListeners);
this.widget.removeEventListener("check", this._onCheck, false);
this.widget.removeEventListener("click", this._onClick, false);
this.controller = this.Breakpoints = null;
},
renderListeners: function(listeners) {
@ -285,6 +286,13 @@ EventListenersView.prototype = Heritage.extend(WidgetMethods, {
}
},
/**
* Called when listeners change.
*/
_onListeners: function(_, listeners) {
this.renderListeners(listeners);
},
_eventCheckboxTooltip: "",
_onSelectorString: "",
_inSourceString: "",

View File

@ -1280,7 +1280,7 @@ SourceScripts.prototype = {
// Make sure the events listeners are up to date.
if (DebuggerView.instrumentsPaneTab == "events-tab") {
dispatcher.dispatch(actions.fetchEventListeners());
store.dispatch(actions.fetchEventListeners());
}
// Signal that a new source has been added.
@ -2055,6 +2055,7 @@ var Prefs = new ViewHelpers.Prefs("devtools", {
* Convenient way of emitting events from the panel window.
*/
EventEmitter.decorate(this);
EventEmitter.decorate(DebuggerController);
/**
* Preliminary setup for the DebuggerController object.

View File

@ -35,16 +35,19 @@ const RESIZE_REFRESH_RATE = 50; // ms
const PROMISE_DEBUGGER_URL =
"chrome://browser/content/devtools/promisedebugger/promise-debugger.xhtml";
const createDispatcher = require('devtools/shared/create-dispatcher')();
const stores = require('./content/stores/index');
const dispatcher = createDispatcher(stores);
const waitUntilService = require('devtools/shared/fluxify/waitUntilService');
const debuggerControllerEmit = DebuggerController.emit.bind(DebuggerController);
const createStore = require("devtools/shared/redux/create-store")();
const { combineEmittingReducers } = require("devtools/shared/redux/reducers");
const reducers = require("./content/reducers/index");
const store = createStore(combineEmittingReducers(reducers, debuggerControllerEmit));
const { NAME: WAIT_UNTIL_NAME } = require("devtools/shared/redux/middleware/wait-service");
const services = {
WAIT_UNTIL: waitUntilService.name
WAIT_UNTIL: WAIT_UNTIL_NAME
};
const EventListenersView = require('./content/views/event-listeners-view');
const actions = require('./content/stores/event-listeners').actions;
const actions = require('./content/actions/event-listeners');
/**
* Object defining the debugger view components.
@ -626,7 +629,7 @@ var DebuggerView = {
*/
_onInstrumentsPaneTabSelect: function() {
if (this._instrumentsPane.selectedTab.id == "events-tab") {
dispatcher.dispatch(actions.fetchEventListeners());
store.dispatch(actions.fetchEventListeners());
}
},
@ -928,4 +931,4 @@ ResultsPanelContainer.prototype = Heritage.extend(WidgetMethods, {
top: 0
});
DebuggerView.EventListeners = new EventListenersView(dispatcher, DebuggerController);
DebuggerView.EventListeners = new EventListenersView(store, DebuggerController);

View File

@ -18,9 +18,13 @@ EXTRA_JS_MODULES.devtools.debugger.content.views += [
'content/views/event-listeners-view.js'
]
EXTRA_JS_MODULES.devtools.debugger.content.stores += [
'content/stores/event-listeners.js',
'content/stores/index.js'
EXTRA_JS_MODULES.devtools.debugger.content.reducers += [
'content/reducers/event-listeners.js',
'content/reducers/index.js'
]
EXTRA_JS_MODULES.devtools.debugger.content.actions += [
'content/actions/event-listeners.js',
]
BROWSER_CHROME_MANIFESTS += ['test/mochitest/browser.ini']

View File

@ -13,7 +13,7 @@ function test() {
let gDebugger = aPanel.panelWin;
let gView = gDebugger.DebuggerView;
let gEvents = gView.EventListeners;
let gDispatcher = gDebugger.dispatcher;
let gStore = gDebugger.store;
let constants = gDebugger.require('./content/constants');
Task.spawn(function*() {
@ -26,7 +26,7 @@ function test() {
function testFetchOnFocus() {
return Task.spawn(function*() {
let fetched = afterDispatch(gDispatcher, constants.FETCH_EVENT_LISTENERS);
let fetched = afterDispatch(gStore, constants.FETCH_EVENT_LISTENERS);
gView.toggleInstrumentsPane({ visible: true, animated: false }, 1);
is(gView.instrumentsPaneHidden, false,
@ -45,7 +45,7 @@ function test() {
function testFetchOnReloadWhenFocused() {
return Task.spawn(function*() {
let fetched = afterDispatch(gDispatcher, constants.FETCH_EVENT_LISTENERS);
let fetched = afterDispatch(gStore, constants.FETCH_EVENT_LISTENERS);
let reloading = once(gDebugger.gTarget, "will-navigate");
let reloaded = waitForSourcesAfterReload();
@ -76,7 +76,7 @@ function test() {
function testFetchOnReloadWhenNotFocused() {
return Task.spawn(function*() {
gDispatcher.dispatch({
gStore.dispatch({
type: gDebugger.services.WAIT_UNTIL,
predicate: action => {
return (action.type === constants.FETCH_EVENT_LISTENERS ||

View File

@ -12,13 +12,13 @@ function test() {
let gDebugger = aPanel.panelWin;
let gView = gDebugger.DebuggerView;
let gEvents = gView.EventListeners;
let gDispatcher = gDebugger.dispatcher;
let gStore = gDebugger.store;
let constants = gDebugger.require('./content/constants');
Task.spawn(function*() {
yield waitForSourceShown(aPanel, ".html");
let fetched = afterDispatch(gDispatcher, constants.FETCH_EVENT_LISTENERS);
let fetched = afterDispatch(gStore, constants.FETCH_EVENT_LISTENERS);
gView.toggleInstrumentsPane({ visible: true, animated: false }, 1);
yield fetched;

View File

@ -14,14 +14,14 @@ function test() {
let gView = gDebugger.DebuggerView;
let gController = gDebugger.DebuggerController
let gEvents = gView.EventListeners;
let gDispatcher = gDebugger.dispatcher;
let getState = gDispatcher.getState;
let gStore = gDebugger.store;
let getState = gStore.getState;
let constants = gDebugger.require('./content/constants');
Task.spawn(function*() {
yield waitForSourceShown(aPanel, ".html");
let fetched = afterDispatch(gDispatcher, constants.FETCH_EVENT_LISTENERS);
let fetched = afterDispatch(gStore, constants.FETCH_EVENT_LISTENERS);
gView.toggleInstrumentsPane({ visible: true, animated: false }, 1);
yield fetched;
@ -34,7 +34,7 @@ function test() {
testEventGroup("mouseEvents", false);
testEventArrays("change,click,keydown,keyup", "");
let updated = afterDispatch(gDispatcher, constants.UPDATE_EVENT_BREAKPOINTS);
let updated = afterDispatch(gStore, constants.UPDATE_EVENT_BREAKPOINTS);
EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(0), gDebugger);
yield updated;
@ -47,7 +47,7 @@ function test() {
testEventGroup("mouseEvents", false);
testEventArrays("change,click,keydown,keyup", "change");
updated = afterDispatch(gDispatcher, constants.UPDATE_EVENT_BREAKPOINTS);
updated = afterDispatch(gStore, constants.UPDATE_EVENT_BREAKPOINTS);
EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(0), gDebugger);
yield updated;

View File

@ -15,14 +15,14 @@ function test() {
let gView = gDebugger.DebuggerView;
let gController = gDebugger.DebuggerController
let gEvents = gView.EventListeners;
let gDispatcher = gDebugger.dispatcher;
let getState = gDispatcher.getState;
let gStore = gDebugger.store;
let getState = gStore.getState;
let constants = gDebugger.require('./content/constants');
Task.spawn(function*() {
yield waitForSourceShown(aPanel, ".html");
let fetched = afterDispatch(gDispatcher, constants.FETCH_EVENT_LISTENERS);
let fetched = afterDispatch(gStore, constants.FETCH_EVENT_LISTENERS);
gView.toggleInstrumentsPane({ visible: true, animated: false }, 1);
yield fetched;
@ -35,7 +35,7 @@ function test() {
testEventGroup("mouseEvents", false);
testEventArrays("change,click,keydown,keyup", "");
let updated = afterDispatch(gDispatcher, constants.UPDATE_EVENT_BREAKPOINTS);
let updated = afterDispatch(gStore, constants.UPDATE_EVENT_BREAKPOINTS);
EventUtils.sendMouseEvent({ type: "click" }, getGroupCheckboxNode("interactionEvents"), gDebugger);
yield updated;
@ -48,7 +48,7 @@ function test() {
testEventGroup("mouseEvents", false);
testEventArrays("change,click,keydown,keyup", "change");
updated = afterDispatch(gDispatcher, constants.UPDATE_EVENT_BREAKPOINTS);
updated = afterDispatch(gStore, constants.UPDATE_EVENT_BREAKPOINTS);
EventUtils.sendMouseEvent({ type: "click" }, getGroupCheckboxNode("interactionEvents"), gDebugger);
yield updated;
@ -61,7 +61,7 @@ function test() {
testEventGroup("mouseEvents", false);
testEventArrays("change,click,keydown,keyup", "");
updated = afterDispatch(gDispatcher, constants.UPDATE_EVENT_BREAKPOINTS);
updated = afterDispatch(gStore, constants.UPDATE_EVENT_BREAKPOINTS);
EventUtils.sendMouseEvent({ type: "click" }, getGroupCheckboxNode("keyboardEvents"), gDebugger);
yield updated;
@ -74,7 +74,7 @@ function test() {
testEventGroup("mouseEvents", false);
testEventArrays("change,click,keydown,keyup", "keydown,keyup");
updated = afterDispatch(gDispatcher, constants.UPDATE_EVENT_BREAKPOINTS);
updated = afterDispatch(gStore, constants.UPDATE_EVENT_BREAKPOINTS);
EventUtils.sendMouseEvent({ type: "click" }, getGroupCheckboxNode("keyboardEvents"), gDebugger);
yield updated;

View File

@ -15,14 +15,14 @@ function test() {
let gController = gDebugger.DebuggerController
let gEvents = gView.EventListeners;
let gBreakpoints = gController.Breakpoints;
let gDispatcher = gDebugger.dispatcher;
let getState = gDispatcher.getState;
let gStore = gDebugger.store;
let getState = gStore.getState;
let constants = gDebugger.require('./content/constants');
Task.spawn(function*() {
yield waitForSourceShown(aPanel, ".html");
let fetched = afterDispatch(gDispatcher, constants.FETCH_EVENT_LISTENERS);
let fetched = afterDispatch(gStore, constants.FETCH_EVENT_LISTENERS);
gView.toggleInstrumentsPane({ visible: true, animated: false }, 1);
yield fetched;
@ -35,7 +35,7 @@ function test() {
testEventGroup("mouseEvents", false);
testEventArrays("change,click,keydown,keyup", "");
let updated = afterDispatch(gDispatcher, constants.UPDATE_EVENT_BREAKPOINTS);
let updated = afterDispatch(gStore, constants.UPDATE_EVENT_BREAKPOINTS);
EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(0), gDebugger);
EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(1), gDebugger);
EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(2), gDebugger);
@ -51,7 +51,7 @@ function test() {
testEventArrays("change,click,keydown,keyup", "change,click,keydown");
reload(aPanel);
yield afterDispatch(gDispatcher, constants.FETCH_EVENT_LISTENERS);
yield afterDispatch(gStore, constants.FETCH_EVENT_LISTENERS);
testEventItem(0, true);
testEventItem(1, true);
@ -62,7 +62,7 @@ function test() {
testEventGroup("mouseEvents", false);
testEventArrays("change,click,keydown,keyup", "change,click,keydown");
updated = afterDispatch(gDispatcher, constants.UPDATE_EVENT_BREAKPOINTS);
updated = afterDispatch(gStore, constants.UPDATE_EVENT_BREAKPOINTS);
EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(0), gDebugger);
EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(1), gDebugger);
EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(2), gDebugger);
@ -78,7 +78,7 @@ function test() {
testEventArrays("change,click,keydown,keyup", "");
reload(aPanel);
yield afterDispatch(gDispatcher, constants.FETCH_EVENT_LISTENERS);
yield afterDispatch(gStore, constants.FETCH_EVENT_LISTENERS);
testEventItem(0, false);
testEventItem(1, false);

View File

@ -13,15 +13,15 @@ function test() {
let gDebugger = aPanel.panelWin;
let gView = gDebugger.DebuggerView;
let gEvents = gView.EventListeners;
let gDispatcher = gDebugger.dispatcher;
let getState = gDispatcher.getState;
let gStore = gDebugger.store;
let getState = gStore.getState;
let constants = gDebugger.require('./content/constants');
Task.spawn(function*() {
yield waitForSourceShown(aPanel, ".html");
yield callInTab(gTab, "addBodyClickEventListener");
let fetched = afterDispatch(gDispatcher, constants.FETCH_EVENT_LISTENERS);
let fetched = afterDispatch(gStore, constants.FETCH_EVENT_LISTENERS);
gView.toggleInstrumentsPane({ visible: true, animated: false }, 1);
yield fetched;
yield ensureThreadClientState(aPanel, "attached");
@ -31,7 +31,7 @@ function test() {
is(gView.instrumentsPaneTab, "events-tab",
"The events tab should be selected.");
let updated = afterDispatch(gDispatcher, constants.UPDATE_EVENT_BREAKPOINTS);
let updated = afterDispatch(gStore, constants.UPDATE_EVENT_BREAKPOINTS);
EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(1), gDebugger);
yield updated;
yield ensureThreadClientState(aPanel, "attached");

View File

@ -31,13 +31,13 @@ add_task(function* () {
let [,, panel, win] = yield initDebugger(tab);
let gDebugger = panel.panelWin;
let gDispatcher = gDebugger.dispatcher;
let gStore = gDebugger.store;
let constants = gDebugger.require('./content/constants');
let eventListeners = gDebugger.require('./content/stores/event-listeners');
let fetched = afterDispatch(gDispatcher, constants.FETCH_EVENT_LISTENERS);
let actions = gDebugger.require('./content/actions/event-listeners');
let fetched = afterDispatch(gStore, constants.FETCH_EVENT_LISTENERS);
info("Scheduling event listener fetch.");
gDispatcher.dispatch(eventListeners.actions.fetchEventListeners());
gStore.dispatch(actions.fetchEventListeners());
info("Waiting for updated event listeners to arrive.");
yield fetched;

View File

@ -1192,10 +1192,10 @@ function source(sourceClient) {
return rdpInvoke(sourceClient, sourceClient.source);
}
function afterDispatch(dispatcher, type) {
function afterDispatch(store, type) {
info("Waiting on dispatch: " + type);
return new Promise(resolve => {
dispatcher.dispatch({
store.dispatch({
// Normally we would use `services.WAIT_UNTIL`, but use the
// internal name here so tests aren't forced to always pass it
// in

View File

@ -3,6 +3,8 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
browser.jar:
content/browser/devtools/d3.js (shared/vendor/d3.js)
content/browser/devtools/dagre-d3.js (shared/vendor/dagre-d3.js)
content/browser/devtools/widgets.css (shared/widgets/widgets.css)
content/browser/devtools/widgets/VariablesView.xul (shared/widgets/VariablesView.xul)
content/browser/devtools/markup-view.xhtml (markupview/markup-view.xhtml)
@ -84,9 +86,7 @@ browser.jar:
content/browser/devtools/canvasdebugger.js (canvasdebugger/canvasdebugger.js)
content/browser/devtools/canvasdebugger/snapshotslist.js (canvasdebugger/snapshotslist.js)
content/browser/devtools/canvasdebugger/callslist.js (canvasdebugger/callslist.js)
content/browser/devtools/d3.js (shared/d3.js)
content/browser/devtools/webaudioeditor.xul (webaudioeditor/webaudioeditor.xul)
content/browser/devtools/dagre-d3.js (webaudioeditor/lib/dagre-d3.js)
content/browser/devtools/webaudioeditor/includes.js (webaudioeditor/includes.js)
content/browser/devtools/webaudioeditor/models.js (webaudioeditor/models.js)
content/browser/devtools/webaudioeditor/controller.js (webaudioeditor/controller.js)

View File

@ -23,6 +23,7 @@ catch(e) {
};
}
const VENDOR_CONTENT_URL = "resource:///modules/devtools/shared/vendor";
/*
* Create a loader to be used in a browser environment. This evaluates
@ -55,8 +56,8 @@ function BrowserLoader(baseURI, window) {
let dynamicPaths = {};
if (appConstants.DEBUG_JS_MODULES) {
// Load in the dev version of React
dynamicPaths["devtools/shared/content/react"] =
"resource:///modules/devtools/shared/content/react-dev.js";
dynamicPaths["devtools/shared/vendor/react"] =
"resource:///modules/devtools/vendor/react-dev.js";
}
const opts = {
@ -67,8 +68,9 @@ function BrowserLoader(baseURI, window) {
invisibleToDebugger: loaderOptions.invisibleToDebugger,
require: (id, require) => {
const uri = require.resolve(id);
if (!uri.startsWith(baseURI) &&
!uri.startsWith("resource:///modules/devtools/shared/content")) {
!uri.startsWith(VENDOR_CONTENT_URL)) {
return devtools.require(uri);
}
return require(uri);

View File

@ -1,28 +0,0 @@
/* 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/. */
"use strict";
function bindActionCreator(actionCreator, dispatch) {
return (...args) => dispatch(actionCreator(...args));
}
/**
* Wraps action creator functions into a function that automatically
* dispatches the created action with `dispatch`. Normally action
* creators simply return actions, but wrapped functions will
* automatically dispatch.
*
* @param {object} actionCreators
* An object of action creator functions (names as keys)
* @param {function} dispatch
*/
function bindActionCreators(actionCreators, dispatch) {
let actions = {};
for (let k of Object.keys(actionCreators)) {
actions[k] = bindActionCreator(actionCreators[k], dispatch);
}
return actions;
}
module.exports = bindActionCreators;

View File

@ -1,247 +0,0 @@
/* 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/. */
"use strict";
const { entries, compose } = require("devtools/toolkit/DevToolsUtils");
/**
* A store creator that creates a dispatch function that runs the
* provided middlewares before actually dispatching. This allows
* simple middleware to augment the kinds of actions that can
* be dispatched. This would be used like this:
* `createDispatcher = applyMiddleware([fooMiddleware, ...])(createDispatcher)`
*
* Middlewares are simple functions that are provided `dispatch` and
* `getState` functions. They create functions that accept actions and
* can re-dispatch them in any way they want. A common scenario is
* asynchronously dispatching multiple actions. Here is a full
* middleware:
*
* function thunkMiddleware({ dispatch, getState }) {
* return next => action => {
* return typeof action === 'function' ?
* action(dispatch, getState) :
* next(action);
* }
* }
*
* `next` is essentially a `dispatch` function, but it calls the next
* middelware in the chain (or the real `dispatch` function). Using
* this middleware, you can return a function that gives you a
* dispatch function:
*
* function actionCreator(timeout) {
* return (dispatch, getState) => {
* dispatch({ type: TIMEOUT, status: "start" });
* setTimeout(() => dispatch({ type: TIMEOUT, status: "end" }),
* timeout);
* }
* }
*
*/
function applyMiddleware(...middlewares) {
return next => stores => {
const dispatcher = next(stores);
let dispatch = dispatcher.dispatch;
const api = {
getState: dispatcher.getState,
dispatch: action => dispatch(action)
};
const chain = middlewares.map(middleware => middleware(api));
dispatch = compose(...chain)(dispatcher.dispatch);
return Object.assign({}, dispatcher, { dispatch: dispatch });
}
}
/*
* The heart of the system. This creates a dispatcher which is the
* interface between views and stores. Views can use a dispatcher
* instance to dispatch actions, which know nothing about the stores.
* Actions are broadcasted to all registered stores, and stores can
* handle the action and update their state. The dispatcher gives
* stores an `emitChange` function, which signifies that a piece of
* state has changed. The dispatcher will notify all views that are
* listening to that piece of state, registered with `onChange`.
*
* Views generally are stateless, pure components (eventually React
* components). They simply take state and render it.
*
* Stores make up the entire app state, and are all grouped together
* into a single app state atom, returned by the dispatcher's
* `getState` function. The shape of the app state is determined by
* the `stores` object passed in to the dispatcher, so if
* `{ foo: fooStore }` was passed to `createDispatcher` the app state
* would be `{ foo: fooState }`
*
* Actions are just JavaScript object with a `type` property and any
* other fields pertinent to the action. Action creators should
* generally be used to create actions, which are just functions that
* return the action object. Additionally, the `bindActionCreators`
* module provides a function for automatically binding action
* creators to a dispatch function, so calling them automatically
* dispatches. For example:
*
* ```js
* // Manually dispatch
* dispatcher.dispatch({ type: constants.ADD_ITEM, item: item });
* // Using an action creator
* dispatcher.dispatch(addItem(item));
*
* // Using an action creator bound to dispatch
* actions = bindActionCreators({ addItem: addItem });
* actions.addItem(item);
* ```
*
* Our system expects stores to exist as an `update` function. You
* should define an update function in a module, and optionally
* any action creators that are useful to go along with it. Here is
* an example store file:
*
* ```js
* const initialState = { items: [] };
* function update(state = initialState, action, emitChange) {
* if (action.type === constants.ADD_ITEM) {
* state.items.push(action.item);
* emitChange("items", state.items);
* }
*
* return state;
* }
*
* function addItem(item) {
* return {
* type: constants.ADD_ITEM,
* item: item
* };
* }
*
* module.exports = {
* update: update,
* actions: { addItem }
* }
* ```
*
* Lastly, "constants" are simple strings that specify action names.
* Usually the entire set of available action types are specified in
* a `constants.js` file, so they are available globally. Use
* variables that are the same name as the string, for example
* `const ADD_ITEM = "ADD_ITEM"`.
*
* This entire system was inspired by Redux, which hopefully we will
* eventually use once things get cleaned up enough. You should read
* its docs, and keep in mind that it calls stores "reducers" and the
* dispatcher instance is called a single store.
* http://rackt.github.io/redux/
*/
function createDispatcher(stores) {
const state = {};
const listeners = {};
let enqueuedChanges = [];
let isDispatching = false;
// Validate the stores to make sure they have the right shape,
// and accumulate the initial state
entries(stores).forEach(([name, store]) => {
if (!store || typeof store.update !== "function") {
throw new Error("Error creating dispatcher: store \"" + name +
"\" does not have an `update` function");
}
state[name] = store.update(undefined, {});
});
function getState() {
return state;
}
function emitChange(storeName, dataName, payload) {
enqueuedChanges.push([storeName, dataName, payload]);
}
function onChange(paths, view) {
entries(paths).forEach(([storeName, data]) => {
if (!stores[storeName]) {
throw new Error("Error adding onChange handler to store: store " +
"\"" + storeName + "\" does not exist");
}
if (!listeners[storeName]) {
listeners[storeName] = [];
}
if (typeof data == 'function') {
listeners[storeName].push(data.bind(view));
}
else {
entries(data).forEach(([watchedName, handler]) => {
listeners[storeName].push((payload, dataName, storeName) => {
if (dataName === watchedName) {
handler.call(view, payload, dataName, storeName);
}
});
});
}
});
}
/**
* Flush any enqueued state changes from the dispatch cycle. Listeners
* are not immediately notified of changes, only after dispatching
* is completed, to ensure that all state is consistent (in the case
* of multiple stores changes at once).
*/
function flushChanges() {
enqueuedChanges.forEach(([storeName, dataName, payload]) => {
if (listeners[storeName]) {
listeners[storeName].forEach(listener => {
listener(payload, dataName, storeName);
});
}
});
enqueuedChanges = [];
}
function dispatch(action) {
if (isDispatching) {
throw new Error('Cannot dispatch in the middle of a dispatch');
}
if (!action.type) {
throw new Error(
'action type is null, ' +
'did you make a typo when publishing this action? ' +
JSON.stringify(action, null, 2)
);
}
isDispatching = true;
try {
entries(stores).forEach(([name, store]) => {
state[name] = store.update(
state[name],
action,
emitChange.bind(null, name)
);
});
}
finally {
isDispatching = false;
}
flushChanges();
}
return {
getState,
dispatch,
onChange
};
}
module.exports = {
createDispatcher: createDispatcher,
applyMiddleware: applyMiddleware
};

View File

@ -1,28 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
const CC = Components.Constructor;
const { require } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const createDispatcher = require('devtools/shared/create-dispatcher')({ log: true });
const waitUntilService = require('devtools/shared/fluxify/waitUntilService');
const services = {
WAIT_UNTIL: waitUntilService.name
};
const Services = require("Services");
const { waitForTick, waitForTime } = require("devtools/toolkit/DevToolsUtils");
var loadSubScript = Cc[
'@mozilla.org/moz/jssubscript-loader;1'
].getService(Ci.mozIJSSubScriptLoader).loadSubScript;
function getFileUrl(name, allowMissing=false) {
let file = do_get_file(name, allowMissing);
return Services.io.newFileURI(file).spec;
}

View File

@ -1,48 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// This file should be loaded with `loadSubScript` because it contains
// stateful stores that need a new instance per test.
const NumberStore = {
update: (state = 1, action, emitChange) => {
switch(action.type) {
case constants.ADD_NUMBER: {
const newState = state + action.value;
emitChange('number', newState);
return newState;
}
case constants.DOUBLE_NUMBER: {
const newState = state * 2;
emitChange('number', newState);
return newState;
}}
return state;
},
constants: {
ADD_NUMBER: 'ADD_NUMBER',
DOUBLE_NUMBER: 'DOUBLE_NUMBER'
}
};
const itemsInitialState = {
list: []
};
const ItemStore = {
update: (state = itemsInitialState, action, emitChange) => {
switch(action.type) {
case constants.ADD_ITEM:
state.list.push(action.item);
emitChange('list', state.list);
return state;
}
return state;
},
constants: {
ADD_ITEM: 'ADD_ITEM'
}
}

View File

@ -1,101 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
loadSubScript(getFileUrl("stores-for-testing.js"));
const constants = Object.assign(
{},
NumberStore.constants,
ItemStore.constants
);
const stores = { number: NumberStore,
items: ItemStore };
function addNumber(num) {
return {
type: constants.ADD_NUMBER,
value: num
}
}
function addItem(item) {
return {
type: constants.ADD_ITEM,
item: item
};
}
// Tests
function run_test() {
testInitialValue();
testDispatch();
testEmitChange();
run_next_test();
}
function testInitialValue() {
do_print("Testing initial value");
const dispatcher = createDispatcher(stores);
equal(dispatcher.getState().number, 1);
}
function testDispatch() {
do_print("Testing dispatch");
const dispatcher = createDispatcher(stores);
dispatcher.dispatch(addNumber(5));
equal(dispatcher.getState().number, 6);
dispatcher.dispatch(addNumber(2));
equal(dispatcher.getState().number, 8);
// It should ignore unknown action types
dispatcher.dispatch({ type: "FOO" });
equal(dispatcher.getState().number, 8);
}
function testEmitChange() {
do_print("Testing change emittters");
const dispatcher = createDispatcher(stores);
let listenerRan = false;
const numberView = {
x: 3,
renderNumber: function(num) {
ok(this.x, 3, "listener ran in context of view");
ok(num, 10);
listenerRan = true;
}
}
// Views can listen to changes in state by specifying which part of
// the state to listen to and giving a handler function. The
// function will be run with the view as `this`.
dispatcher.onChange({
"number": numberView.renderNumber
}, numberView);
dispatcher.dispatch(addNumber(9));
ok(listenerRan, "number listener actually ran");
listenerRan = false;
const itemsView = {
renderList: function(items) {
ok(items.length, 1);
ok(items[0].name = "james");
listenerRan = true;
}
}
// You can listen to deeper sections of the state by nesting objects
// to specify the path to that state. You can do this 1 level deep;
// you cannot arbitrarily nest state listeners.
dispatcher.onChange({
"items": {
"list": itemsView.renderList
}
}, itemsView);
dispatcher.dispatch(addItem({ name: "james" }));
ok(listenerRan, "items listener actually ran");
}

View File

@ -1,114 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
loadSubScript(getFileUrl('stores-for-testing.js'));
const constants = NumberStore.constants;
const stores = { number: NumberStore };
function run_test() {
run_next_test();
}
add_task(function* testThunkDispatch() {
do_print("Testing thunk dispatch");
// The thunk middleware allows you to return a function from an
// action creator which takes `dispatch` and `getState` functions as
// arguments. This allows the action creator to fire multiple
// actions manually with `dispatch`, possibly asynchronously.
function addNumberLater(num) {
return dispatch => {
// Just do it in the next tick, no need to wait too long
waitForTick().then(() => {
dispatch({
type: constants.ADD_NUMBER,
value: num
});
});
};
}
function addNumber(num) {
return (dispatch, getState) => {
dispatch({
type: constants.ADD_NUMBER,
value: getState().number > 10 ? (num * 2) : num
});
};
}
const dispatcher = createDispatcher(stores);
equal(dispatcher.getState().number, 1);
dispatcher.dispatch(addNumberLater(5));
equal(dispatcher.getState().number, 1, "state should not have changed");
yield waitForTick();
equal(dispatcher.getState().number, 6, "state should have changed");
dispatcher.dispatch(addNumber(5));
equal(dispatcher.getState().number, 11);
dispatcher.dispatch(addNumber(2));
// 2 * 2 should have actually been added because the state is
// greater than 10 (the action creator changes the value based on
// the current state)
equal(dispatcher.getState().number, 15);
});
add_task(function* testWaitUntilService() {
do_print("Testing waitUntil service");
// The waitUntil service allows you to queue functions to be run at a
// later time, depending on a predicate. As actions comes through
// the system, you predicate will be called with each action. Once
// your predicate returns true, the queued function will be run and
// removed from the pending queue.
function addWhenDoubled(num) {
return {
type: services.WAIT_UNTIL,
predicate: action => action.type === constants.DOUBLE_NUMBER,
run: (dispatch, getState, action) => {
ok(action.type, constants.DOUBLE_NUMBER);
ok(getState(), 10);
dispatch({
type: constants.ADD_NUMBER,
value: 2
});
}
};
}
function addWhenGreaterThan(threshold, num) {
return (dispatch, getState) => {
dispatch({
type: services.WAIT_UNTIL,
predicate: () => getState().number > threshold,
run: () => {
dispatch({
type: constants.ADD_NUMBER,
value: num
});
}
});
}
}
const dispatcher = createDispatcher(stores);
// Add a pending action that adds 2 after the number is doubled
equal(dispatcher.getState().number, 1);
dispatcher.dispatch(addWhenDoubled(2));
equal(dispatcher.getState().number, 1);
dispatcher.dispatch({ type: constants.DOUBLE_NUMBER });
// Note how the pending function we added ran synchronously. It
// should have added 2 after doubling 1, so 1 * 2 + 2 = 4
equal(dispatcher.getState().number, 4);
// Add a pending action that adds 5 once the number is greater than 10
dispatcher.dispatch(addWhenGreaterThan(10, 5));
equal(dispatcher.getState().number, 4);
dispatcher.dispatch({ type: constants.ADD_NUMBER, value: 10 });
// Again, the pending function we added ran synchronously. It should
// have added 5 more after 10 was added, since the number was
// greater than 10.
equal(dispatcher.getState().number, 19);
});

View File

@ -1,12 +0,0 @@
[DEFAULT]
tags = devtools
head = head.js
tail =
firefox-appdir = browser
skip-if = toolkit == 'android' || toolkit == 'gonk'
support-files =
stores-for-testing.js
[test_dispatcher.js]
[test_middlewares.js]

View File

@ -7,6 +7,11 @@
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
DIRS += [
'redux',
'vendor',
]
EXTRA_JS_MODULES.devtools += [
'AppCacheUtils.jsm',
'Curl.jsm',
@ -32,7 +37,6 @@ EXTRA_JS_MODULES.devtools += [
EXTRA_JS_MODULES.devtools.shared += [
'autocomplete-popup.js',
'browser-loader.js',
'create-dispatcher.js',
'devices.js',
'doorhanger.js',
'frame-script-utils.js',
@ -49,15 +53,6 @@ EXTRA_JS_MODULES.devtools.shared += [
'undo.js'
]
EXTRA_JS_MODULES.devtools.shared.content += [
'content/react.js'
]
if CONFIG['DEBUG_JS_MODULES']:
EXTRA_JS_MODULES.devtools.shared.content += [
'content/react-dev.js'
]
EXTRA_JS_MODULES.devtools.shared.widgets += [
'widgets/BarGraphWidget.js',
'widgets/CubicBezierPresets.js',
@ -74,5 +69,3 @@ EXTRA_JS_MODULES.devtools.shared.widgets += [
'widgets/Tooltip.js',
'widgets/TreeWidget.js',
]
DIRS += ['fluxify']

View File

@ -3,11 +3,10 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const fluxify = require('./fluxify/dispatcher');
const thunkMiddleware = require('./fluxify/thunkMiddleware');
const logMiddleware = require('./fluxify/logMiddleware');
const waitUntilService = require('./fluxify/waitUntilService')
const { compose } = require('devtools/toolkit/DevToolsUtils');
const { createStore, applyMiddleware } = require("devtools/shared/vendor/redux");
const { thunk } = require("./middleware/thunk");
const { waitUntilService } = require("./middleware/wait-service");
const { log } = require("./middleware/log");
/**
* This creates a dispatcher with all the standard middleware in place
@ -16,16 +15,21 @@ const { compose } = require('devtools/toolkit/DevToolsUtils');
*
* @param {object} opts - boolean configuration flags
* - log: log all dispatched actions to console
* - middleware: array of middleware to be included in the redux store
*/
module.exports = (opts={}) => {
const middleware = [
thunkMiddleware,
waitUntilService.service
thunk,
waitUntilService
];
if (opts.log) {
middleware.push(logMiddleware);
middleware.push(log);
}
return fluxify.applyMiddleware(...middleware)(fluxify.createDispatcher);
if (opts.middleware) {
opts.middleware.forEach(fn => middleware.push(fn));
}
return applyMiddleware(...middleware)(createStore);
}

View File

@ -7,11 +7,11 @@
* A middleware that logs all actions coming through the system
* to the console.
*/
function logMiddleware({ dispatch, getState }) {
function log({ dispatch, getState }) {
return next => action => {
console.log('[DISPATCH]', JSON.stringify(action));
console.log("[DISPATCH]", JSON.stringify(action));
next(action);
}
}
module.exports = logMiddleware;
exports.log = log;

View File

@ -9,12 +9,11 @@
* allowing the action to create multiple actions (most likely
* asynchronously).
*/
function thunkMiddleware({ dispatch, getState }) {
function thunk({ dispatch, getState }) {
return next => action => {
return typeof action === "function"
? action(dispatch, getState)
: next(action);
}
}
module.exports = thunkMiddleware;
exports.thunk = thunk;

View File

@ -3,8 +3,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const NAME = "@@service/waitUntil";
/**
* A middleware which acts like a service, because it is stateful
* and "long-running" in the background. It provides the ability
@ -13,7 +11,7 @@ const NAME = "@@service/waitUntil";
* it as a thunk that blocks until the condition is met. Example:
*
* ```js
* const services = { WAIT_UNTIL: require('waitUntilService').name };
* const services = { WAIT_UNTIL: require('wait-service').NAME };
*
* { type: services.WAIT_UNTIL,
* predicate: action => action.type === constants.ADD_ITEM,
@ -25,6 +23,8 @@ const NAME = "@@service/waitUntil";
* }
* ```
*/
const NAME = exports.NAME = "@@service/waitUntil";
function waitUntilService({ dispatch, getState }) {
let pending = [];
@ -62,8 +62,4 @@ function waitUntilService({ dispatch, getState }) {
}
}
}
module.exports = {
service: waitUntilService,
name: NAME
};
exports.waitUntilService = waitUntilService;

View File

@ -0,0 +1,16 @@
# -*- 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.devtools.shared.redux += [
'create-store.js',
'reducers.js',
]
EXTRA_JS_MODULES.devtools.shared.redux.middleware += [
'middleware/log.js',
'middleware/thunk.js',
'middleware/wait-service.js',
]

View File

@ -0,0 +1,33 @@
/* 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/. */
"use strict";
const { combineReducers } = require("devtools/shared/vendor/redux");
/**
* Function that takes a hash of reducers, like `combineReducers`,
* and an `emit` function and returns a function to be used as a reducer
* for a Redux store. This allows all reducers defined here to receive
* a third argument, the `emit` function, for event-based subscriptions
* from within reducers.
*
* @param {Object} reducers
* @param {Function} emit
* @return {Function}
*/
function combineEmittingReducers (reducers, emit) {
// Wrap each reducer with a wrapper function that calls
// the reducer with a third argument, an `emit` function.
// Use this rather than a new custom top level reducer that would ultimately
// have to replicate redux's `combineReducers` so we only pass in correct state,
// the error checking, and other edge cases.
function wrapReduce (newReducers, key) {
newReducers[key] = (state, action) => reducers[key](state, action, emit);
return newReducers;
}
return combineReducers(Object.keys(reducers).reduce(wrapReduce, Object.create(null)));
}
exports.combineEmittingReducers = combineEmittingReducers;

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Dan Abramov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -4,12 +4,12 @@
# 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.devtools.shared.fluxify += [
'bindActionCreators.js',
'dispatcher.js',
'logMiddleware.js',
'thunkMiddleware.js',
'waitUntilService.js'
EXTRA_JS_MODULES.devtools.shared.vendor += [
'react.js',
'redux.js',
]
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
if CONFIG['DEBUG_JS_MODULES']:
EXTRA_JS_MODULES.devtools.shared.vendor += [
'content/react-dev.js'
]

610
browser/devtools/shared/vendor/redux.js vendored Normal file
View File

@ -0,0 +1,610 @@
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else if(typeof exports === 'object')
exports["Redux"] = factory();
else
root["Redux"] = factory();
})(this, function() {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ exports: {},
/******/ id: moduleId,
/******/ loaded: false
/******/ };
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/ // Load entry module and return exports
/******/ return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
exports.__esModule = true;
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
var _createStore = __webpack_require__(1);
var _createStore2 = _interopRequireDefault(_createStore);
var _utilsCombineReducers = __webpack_require__(7);
var _utilsCombineReducers2 = _interopRequireDefault(_utilsCombineReducers);
var _utilsBindActionCreators = __webpack_require__(6);
var _utilsBindActionCreators2 = _interopRequireDefault(_utilsBindActionCreators);
var _utilsApplyMiddleware = __webpack_require__(5);
var _utilsApplyMiddleware2 = _interopRequireDefault(_utilsApplyMiddleware);
var _utilsCompose = __webpack_require__(2);
var _utilsCompose2 = _interopRequireDefault(_utilsCompose);
exports.createStore = _createStore2['default'];
exports.combineReducers = _utilsCombineReducers2['default'];
exports.bindActionCreators = _utilsBindActionCreators2['default'];
exports.applyMiddleware = _utilsApplyMiddleware2['default'];
exports.compose = _utilsCompose2['default'];
/***/ },
/* 1 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
exports.__esModule = true;
exports['default'] = createStore;
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
var _utilsIsPlainObject = __webpack_require__(3);
var _utilsIsPlainObject2 = _interopRequireDefault(_utilsIsPlainObject);
/**
* These are private action types reserved by Redux.
* For any unknown actions, you must return the current state.
* If the current state is undefined, you must return the initial state.
* Do not reference these action types directly in your code.
*/
var ActionTypes = {
INIT: '@@redux/INIT'
};
exports.ActionTypes = ActionTypes;
/**
* Creates a Redux store that holds the state tree.
* The only way to change the data in the store is to call `dispatch()` on it.
*
* There should only be a single store in your app. To specify how different
* parts of the state tree respond to actions, you may combine several reducers
* into a single reducer function by using `combineReducers`.
*
* @param {Function} reducer A function that returns the next state tree, given
* the current state tree and the action to handle.
*
* @param {any} [initialState] The initial state. You may optionally specify it
* to hydrate the state from the server in universal apps, or to restore a
* previously serialized user session.
* If you use `combineReducers` to produce the root reducer function, this must be
* an object with the same shape as `combineReducers` keys.
*
* @returns {Store} A Redux store that lets you read the state, dispatch actions
* and subscribe to changes.
*/
function createStore(reducer, initialState) {
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.');
}
var currentReducer = reducer;
var currentState = initialState;
var listeners = [];
var isDispatching = false;
/**
* Reads the state tree managed by the store.
*
* @returns {any} The current state tree of your application.
*/
function getState() {
return currentState;
}
/**
* Adds a change listener. It will be called any time an action is dispatched,
* and some part of the state tree may potentially have changed. You may then
* call `getState()` to read the current state tree inside the callback.
*
* @param {Function} listener A callback to be invoked on every dispatch.
* @returns {Function} A function to remove this change listener.
*/
function subscribe(listener) {
listeners.push(listener);
return function unsubscribe() {
var index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
}
/**
* Dispatches an action. It is the only way to trigger a state change.
*
* The `reducer` function, used to create the store, will be called with the
* current state tree and the given `action`. Its return value will
* be considered the **next** state of the tree, and the change listeners
* will be notified.
*
* The base implementation only supports plain object actions. If you want to
* dispatch a Promise, an Observable, a thunk, or something else, you need to
* wrap your store creating function into the corresponding middleware. For
* example, see the documentation for the `redux-thunk` package. Even the
* middleware will eventually dispatch plain object actions using this method.
*
* @param {Object} action A plain object representing what changed. It is
* a good idea to keep actions serializable so you can record and replay user
* sessions, or use the time travelling `redux-devtools`.
*
* @returns {Object} For convenience, the same action object you dispatched.
*
* Note that, if you use a custom middleware, it may wrap `dispatch()` to
* return something else (for example, a Promise you can await).
*/
function dispatch(action) {
if (!_utilsIsPlainObject2['default'](action)) {
throw new Error('Actions must be plain objects. Use custom middleware for async actions.');
}
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.');
}
try {
isDispatching = true;
currentState = currentReducer(currentState, action);
} finally {
isDispatching = false;
}
listeners.slice().forEach(function (listener) {
return listener();
});
return action;
}
/**
* Replaces the reducer currently used by the store to calculate the state.
*
* You might need this if your app implements code splitting and you want to
* load some of the reducers dynamically. You might also need this if you
* implement a hot reloading mechanism for Redux.
*
* @param {Function} nextReducer The reducer for the store to use instead.
* @returns {void}
*/
function replaceReducer(nextReducer) {
currentReducer = nextReducer;
dispatch({ type: ActionTypes.INIT });
}
// When a store is created, an "INIT" action is dispatched so that every
// reducer returns their initial state. This effectively populates
// the initial state tree.
dispatch({ type: ActionTypes.INIT });
return {
dispatch: dispatch,
subscribe: subscribe,
getState: getState,
replaceReducer: replaceReducer
};
}
/***/ },
/* 2 */
/***/ function(module, exports) {
/**
* Composes single-argument functions from right to left.
*
* @param {...Function} funcs The functions to compose.
* @returns {Function} A function obtained by composing functions from right to
* left. For example, compose(f, g, h) is identical to x => h(g(f(x))).
*/
"use strict";
exports.__esModule = true;
exports["default"] = compose;
function compose() {
for (var _len = arguments.length, funcs = Array(_len), _key = 0; _key < _len; _key++) {
funcs[_key] = arguments[_key];
}
return function (arg) {
return funcs.reduceRight(function (composed, f) {
return f(composed);
}, arg);
};
}
module.exports = exports["default"];
/***/ },
/* 3 */
/***/ function(module, exports) {
'use strict';
exports.__esModule = true;
exports['default'] = isPlainObject;
var fnToString = function fnToString(fn) {
return Function.prototype.toString.call(fn);
};
/**
* @param {any} obj The object to inspect.
* @returns {boolean} True if the argument appears to be a plain object.
*/
function isPlainObject(obj) {
if (!obj || typeof obj !== 'object') {
return false;
}
var proto = typeof obj.constructor === 'function' ? Object.getPrototypeOf(obj) : Object.prototype;
if (proto === null) {
return true;
}
var constructor = proto.constructor;
return typeof constructor === 'function' && constructor instanceof constructor && fnToString(constructor) === fnToString(Object);
}
module.exports = exports['default'];
/***/ },
/* 4 */
/***/ function(module, exports) {
/**
* Applies a function to every key-value pair inside an object.
*
* @param {Object} obj The source object.
* @param {Function} fn The mapper function that receives the value and the key.
* @returns {Object} A new object that contains the mapped values for the keys.
*/
"use strict";
exports.__esModule = true;
exports["default"] = mapValues;
function mapValues(obj, fn) {
return Object.keys(obj).reduce(function (result, key) {
result[key] = fn(obj[key], key);
return result;
}, {});
}
module.exports = exports["default"];
/***/ },
/* 5 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
exports.__esModule = true;
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
exports['default'] = applyMiddleware;
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
var _compose = __webpack_require__(2);
var _compose2 = _interopRequireDefault(_compose);
/**
* Creates a store enhancer that applies middleware to the dispatch method
* of the Redux store. This is handy for a variety of tasks, such as expressing
* asynchronous actions in a concise manner, or logging every action payload.
*
* See `redux-thunk` package as an example of the Redux middleware.
*
* Because middleware is potentially asynchronous, this should be the first
* store enhancer in the composition chain.
*
* Note that each middleware will be given the `dispatch` and `getState` functions
* as named arguments.
*
* @param {...Function} middlewares The middleware chain to be applied.
* @returns {Function} A store enhancer applying the middleware.
*/
function applyMiddleware() {
for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
middlewares[_key] = arguments[_key];
}
return function (next) {
return function (reducer, initialState) {
var store = next(reducer, initialState);
var _dispatch = store.dispatch;
var chain = [];
var middlewareAPI = {
getState: store.getState,
dispatch: function dispatch(action) {
return _dispatch(action);
}
};
chain = middlewares.map(function (middleware) {
return middleware(middlewareAPI);
});
_dispatch = _compose2['default'].apply(undefined, chain)(store.dispatch);
return _extends({}, store, {
dispatch: _dispatch
});
};
};
}
module.exports = exports['default'];
/***/ },
/* 6 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
exports.__esModule = true;
exports['default'] = bindActionCreators;
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
var _utilsMapValues = __webpack_require__(4);
var _utilsMapValues2 = _interopRequireDefault(_utilsMapValues);
function bindActionCreator(actionCreator, dispatch) {
return function () {
return dispatch(actionCreator.apply(undefined, arguments));
};
}
/**
* Turns an object whose values are action creators, into an object with the
* same keys, but with every function wrapped into a `dispatch` call so they
* may be invoked directly. This is just a convenience method, as you can call
* `store.dispatch(MyActionCreators.doSomething())` yourself just fine.
*
* For convenience, you can also pass a single function as the first argument,
* and get a function in return.
*
* @param {Function|Object} actionCreators An object whose values are action
* creator functions. One handy way to obtain it is to use ES6 `import * as`
* syntax. You may also pass a single function.
*
* @param {Function} dispatch The `dispatch` function available on your Redux
* store.
*
* @returns {Function|Object} The object mimicking the original object, but with
* every action creator wrapped into the `dispatch` call. If you passed a
* function as `actionCreators`, the return value will also be a single
* function.
*/
function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch);
}
if (typeof actionCreators !== 'object' || actionCreators == null) {
// eslint-disable-line no-eq-null
throw new Error('bindActionCreators expected an object or a function, instead received ' + typeof actionCreators + '. ' + 'Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?');
}
return _utilsMapValues2['default'](actionCreators, function (actionCreator) {
return bindActionCreator(actionCreator, dispatch);
});
}
module.exports = exports['default'];
/***/ },
/* 7 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
exports.__esModule = true;
exports['default'] = combineReducers;
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
var _createStore = __webpack_require__(1);
var _utilsIsPlainObject = __webpack_require__(3);
var _utilsIsPlainObject2 = _interopRequireDefault(_utilsIsPlainObject);
var _utilsMapValues = __webpack_require__(4);
var _utilsMapValues2 = _interopRequireDefault(_utilsMapValues);
var _utilsPick = __webpack_require__(8);
var _utilsPick2 = _interopRequireDefault(_utilsPick);
/* eslint-disable no-console */
function getErrorMessage(key, action) {
var actionType = action && action.type;
var actionName = actionType && '"' + actionType.toString() + '"' || 'an action';
return 'Reducer "' + key + '" returned undefined handling ' + actionName + '. ' + 'To ignore an action, you must explicitly return the previous state.';
}
function verifyStateShape(initialState, currentState) {
var reducerKeys = Object.keys(currentState);
if (reducerKeys.length === 0) {
console.error('Store does not have a valid reducer. Make sure the argument passed ' + 'to combineReducers is an object whose values are reducers.');
return;
}
if (!_utilsIsPlainObject2['default'](initialState)) {
console.error('initialState has unexpected type of "' + ({}).toString.call(initialState).match(/\s([a-z|A-Z]+)/)[1] + '". Expected initialState to be an object with the following ' + ('keys: "' + reducerKeys.join('", "') + '"'));
return;
}
var unexpectedKeys = Object.keys(initialState).filter(function (key) {
return reducerKeys.indexOf(key) < 0;
});
if (unexpectedKeys.length > 0) {
console.error('Unexpected ' + (unexpectedKeys.length > 1 ? 'keys' : 'key') + ' ' + ('"' + unexpectedKeys.join('", "') + '" in initialState will be ignored. ') + ('Expected to find one of the known reducer keys instead: "' + reducerKeys.join('", "') + '"'));
}
}
/**
* Turns an object whose values are different reducer functions, into a single
* reducer function. It will call every child reducer, and gather their results
* into a single state object, whose keys correspond to the keys of the passed
* reducer functions.
*
* @param {Object} reducers An object whose values correspond to different
* reducer functions that need to be combined into one. One handy way to obtain
* it is to use ES6 `import * as reducers` syntax. The reducers may never return
* undefined for any action. Instead, they should return their initial state
* if the state passed to them was undefined, and the current state for any
* unrecognized action.
*
* @returns {Function} A reducer function that invokes every reducer inside the
* passed object, and builds a state object with the same shape.
*/
function combineReducers(reducers) {
var finalReducers = _utilsPick2['default'](reducers, function (val) {
return typeof val === 'function';
});
Object.keys(finalReducers).forEach(function (key) {
var reducer = finalReducers[key];
if (typeof reducer(undefined, { type: _createStore.ActionTypes.INIT }) === 'undefined') {
throw new Error('Reducer "' + key + '" returned undefined during initialization. ' + 'If the state passed to the reducer is undefined, you must ' + 'explicitly return the initial state. The initial state may ' + 'not be undefined.');
}
var type = Math.random().toString(36).substring(7).split('').join('.');
if (typeof reducer(undefined, { type: type }) === 'undefined') {
throw new Error('Reducer "' + key + '" returned undefined when probed with a random type. ' + ('Don\'t try to handle ' + _createStore.ActionTypes.INIT + ' or other actions in "redux/*" ') + 'namespace. They are considered private. Instead, you must return the ' + 'current state for any unknown actions, unless it is undefined, ' + 'in which case you must return the initial state, regardless of the ' + 'action type. The initial state may not be undefined.');
}
});
var defaultState = _utilsMapValues2['default'](finalReducers, function () {
return undefined;
});
var stateShapeVerified;
return function combination(state, action) {
if (state === undefined) state = defaultState;
var finalState = _utilsMapValues2['default'](finalReducers, function (reducer, key) {
var newState = reducer(state[key], action);
if (typeof newState === 'undefined') {
throw new Error(getErrorMessage(key, action));
}
return newState;
});
if (true) {
if (!stateShapeVerified) {
verifyStateShape(state, finalState);
stateShapeVerified = true;
}
}
return finalState;
};
}
module.exports = exports['default'];
/***/ },
/* 8 */
/***/ function(module, exports) {
/**
* Picks key-value pairs from an object where values satisfy a predicate.
*
* @param {Object} obj The object to pick from.
* @param {Function} fn The predicate the values must satisfy to be copied.
* @returns {Object} The object with the values that satisfied the predicate.
*/
"use strict";
exports.__esModule = true;
exports["default"] = pick;
function pick(obj, fn) {
return Object.keys(obj).reduce(function (result, key) {
if (fn(obj[key])) {
result[key] = obj[key];
}
return result;
}, {});
}
module.exports = exports["default"];
/***/ }
/******/ ])
});
;

View File

@ -18,7 +18,7 @@
src="chrome://browser/content/devtools/theme-switching.js"/>
<script type="application/javascript" src="chrome://browser/content/devtools/d3.js"/>
<script type="application/javascript" src="dagre-d3.js"/>
<script type="application/javascript" src="chrome://browser/content/devtools/dagre-d3.js"/>
<script type="application/javascript" src="webaudioeditor/includes.js"/>
<script type="application/javascript" src="webaudioeditor/models.js"/>
<script type="application/javascript" src="webaudioeditor/controller.js"/>

View File

@ -471,6 +471,11 @@ These should match what Safari and other Apple applications use on OS X Lion. --
consider translating it as if it said only "Search Settings". -->
<!ENTITY changeSearchSettings.button "Change Search Settings">
<!ENTITY searchInNewTab.label "Search in New Tab">
<!ENTITY searchInNewTab.accesskey "T">
<!ENTITY searchSetAsDefault.label "Set As Default Search Engine">
<!ENTITY searchSetAsDefault.accesskey "D">
<!ENTITY tabView.commandkey "e">
<!ENTITY openLinkCmdInTab.label "Open Link in New Tab">

View File

@ -1,5 +1,6 @@
#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] {
border-bottom: 1px solid hsla(210, 4%, 10%, 0.14);
color: -moz-FieldText;
background-color: hsla(210, 4%, 10%, 0.07);
padding: 6px 0;
-moz-padding-start: 44px;

View File

@ -945,3 +945,15 @@ pref("browser.tabs.showAudioPlayingIcon", true);
pref("dom.serviceWorkers.enabled", true);
pref("dom.serviceWorkers.interception.enabled", true);
#endif
// The remote content URL where FxAccountsWebChannel messages originate. Must use HTTPS.
pref("identity.fxaccounts.remote.webchannel.uri", "https://accounts.firefox.com");
// The remote URL of the Firefox Account profile server.
pref("identity.fxaccounts.remote.profile.uri", "https://profile.accounts.firefox.com/v1");
// The remote URL of the Firefox Account oauth server.
pref("identity.fxaccounts.remote.oauth.uri", "https://oauth.accounts.firefox.com/v1");
// Token server used by Firefox Account-authenticated Sync.
pref("identity.sync.tokenserver.uri", "https://token.services.mozilla.com/1.0/sync/1.5");

View File

@ -12,6 +12,7 @@ import org.mozilla.gecko.util.StringUtils;
public class AboutPages {
// All of our special pages.
public static final String ACCOUNTS = "about:accounts";
public static final String ADDONS = "about:addons";
public static final String CONFIG = "about:config";
public static final String DOWNLOADS = "about:downloads";
@ -72,6 +73,7 @@ public class AboutPages {
}
private static final String[] DEFAULT_ICON_PAGES = new String[] {
ACCOUNTS,
ADDONS,
CONFIG,
DOWNLOADS,

View File

@ -21,6 +21,7 @@ import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.Engaged;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.restrictions.Restriction;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.setup.SyncAccounts;
import org.mozilla.gecko.util.EventCallback;
@ -74,6 +75,16 @@ public class AccountsHelper implements NativeEventListener {
@Override
public void handleMessage(String event, NativeJSObject message, final EventCallback callback) {
if (!RestrictedProfiles.isAllowed(mContext, Restriction.DISALLOW_MODIFY_ACCOUNTS)) {
// We register for messages in all contexts; we drop, with a log and an error to JavaScript,
// when the profile is restricted. It's better to return errors than silently ignore messages.
Log.e(LOGTAG, "Profile is not allowed to modify accounts! Ignoring event: " + event);
if (callback != null) {
callback.sendError("Profile is not allowed to modify accounts!");
}
return;
}
if ("Accounts:CreateFirefoxAccountFromJSON".equals(event)) {
AndroidFxAccount fxAccount = null;
try {

View File

@ -10,7 +10,6 @@ import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.DynamicToolbar.PinReason;
import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
import org.mozilla.gecko.PrintHelper;
import org.mozilla.gecko.Tabs.TabEvents;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.TransitionsTracker;
@ -24,7 +23,6 @@ import org.mozilla.gecko.favicons.LoadFaviconTask;
import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
import org.mozilla.gecko.favicons.decoders.IconDirectoryEntry;
import org.mozilla.gecko.firstrun.FirstrunPane;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
import org.mozilla.gecko.gfx.LayerView;
@ -97,7 +95,6 @@ import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
@ -2581,7 +2578,7 @@ public class BrowserApp extends GeckoApp
if (mFirstrunPane == null) {
final ViewStub firstrunPagerStub = (ViewStub) findViewById(R.id.firstrun_pager_stub);
mFirstrunPane = (FirstrunPane) firstrunPagerStub.inflate();
mFirstrunPane.load(getSupportFragmentManager());
mFirstrunPane.load(getApplicationContext(), getSupportFragmentManager());
mFirstrunPane.registerOnFinishListener(new FirstrunPane.OnFinishListener() {
@Override
public void onFinish() {

View File

@ -164,6 +164,9 @@ public interface TelemetryContract {
// Note: Only used in JavaScript for now, but here for completeness.
PAGEACTION("pageaction"),
// Action triggered from one of a series of views, such as ViewPager.
PANEL("panel"),
// Action triggered from a settings screen.
SETTINGS("settings"),
@ -207,6 +210,9 @@ public interface TelemetryContract {
// Awesomescreen (including frecency search) is active.
AWESOMESCREEN("awesomescreen.1"),
// Used to tag experiments being run.
EXPERIMENT("experiment.1"),
// Started the very first time we believe the application has been launched.
FIRSTRUN("firstrun.1"),

View File

@ -11,19 +11,23 @@ import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.util.Log;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.AnimatorSet;
import com.nineoldandroids.animation.ObjectAnimator;
import com.nineoldandroids.view.ViewHelper;
import org.mozilla.gecko.RestrictedProfiles;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.animation.TransitionsTracker;
import java.util.List;
public class FirstrunPager extends ViewPager {
private Context context;
protected FirstrunPane.OnFinishListener listener;
protected FirstrunPanel.PagerNavigation pagerNavigation;
public FirstrunPager(Context context) {
this(context, null);
@ -34,17 +38,44 @@ public class FirstrunPager extends ViewPager {
this.context = context;
}
public void load(FragmentManager fm, FirstrunPane.OnFinishListener listener) {
final List<FirstrunPagerConfig.FirstrunPanel> panels;
public void load(Context appContext, FragmentManager fm, final FirstrunPane.OnFinishListener onFinishListener) {
final List<FirstrunPagerConfig.FirstrunPanelConfig> panels;
if (RestrictedProfiles.isUserRestricted(context)) {
panels = FirstrunPagerConfig.getRestricted();
} else {
panels = FirstrunPagerConfig.getDefault();
panels = FirstrunPagerConfig.getDefault(appContext);
}
setAdapter(new ViewPagerAdapter(fm, panels));
this.listener = listener;
this.pagerNavigation = new FirstrunPanel.PagerNavigation() {
@Override
public void next() {
final int currentPage = FirstrunPager.this.getCurrentItem();
if (currentPage < FirstrunPager.this.getChildCount() - 1) {
FirstrunPager.this.setCurrentItem(currentPage + 1);
}
}
@Override
public void finish() {
if (onFinishListener != null) {
onFinishListener.onFinish();
}
}
};
addOnPageChangeListener(new OnPageChangeListener() {
@Override
public void onPageScrolled(int i, float v, int i1) {}
@Override
public void onPageSelected(int i) {
Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.PANEL, "onboarding." + i);
}
@Override
public void onPageScrollStateChanged(int i) {}
});
animateLoad();
}
@ -73,17 +104,23 @@ public class FirstrunPager extends ViewPager {
}
private class ViewPagerAdapter extends FragmentPagerAdapter {
private List<FirstrunPagerConfig.FirstrunPanel> panels;
private final List<FirstrunPagerConfig.FirstrunPanelConfig> panels;
private final Fragment[] fragments;
public ViewPagerAdapter(FragmentManager fm, List<FirstrunPagerConfig.FirstrunPanel> panels) {
public ViewPagerAdapter(FragmentManager fm, List<FirstrunPagerConfig.FirstrunPanelConfig> panels) {
super(fm);
this.panels = panels;
this.fragments = new Fragment[panels.size()];
}
@Override
public Fragment getItem(int i) {
final Fragment fragment = Fragment.instantiate(context, panels.get(i).getClassname());
((FirstrunPanel) fragment).setOnFinishListener(listener);
Fragment fragment = this.fragments[i];
if (fragment == null) {
fragment = Fragment.instantiate(context, panels.get(i).getClassname());
((FirstrunPanel) fragment).setPagerNavigation(pagerNavigation);
fragments[i] = fragment;
}
return fragment;
}

View File

@ -5,27 +5,65 @@
package org.mozilla.gecko.firstrun;
import android.content.Context;
import android.util.Log;
import com.keepsafe.switchboard.SwitchBoard;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import java.util.LinkedList;
import java.util.List;
public class FirstrunPagerConfig {
public static List<FirstrunPanel> getDefault() {
final List<FirstrunPanel> panels = new LinkedList<>();
panels.add(new FirstrunPanel(WelcomePanel.class.getName(), WelcomePanel.TITLE_RES));
public static final String LOGTAG = "FirstrunPagerConfig";
public static final String ONBOARDING_A = "onboarding-a";
public static final String ONBOARDING_B = "onboarding-b";
public static List<FirstrunPanelConfig> getDefault(Context context) {
final List<FirstrunPanelConfig> panels = new LinkedList<>();
if (isInExperimentLocal(context, ONBOARDING_A)) {
panels.add(new FirstrunPanelConfig(WelcomePanel.class.getName(), WelcomePanel.TITLE_RES));
Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, ONBOARDING_A);
} else if (isInExperimentLocal(context, ONBOARDING_B)) {
// Strings used for first run, pulled from existing strings.
panels.add(new FirstrunPanelConfig(ImportPanel.class.getName(), ImportPanel.TITLE_RES));
panels.add(new FirstrunPanelConfig(SyncPanel.class.getName(), SyncPanel.TITLE_RES));
Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, ONBOARDING_B);
} else {
Log.d(LOGTAG, "Not in an experiment!");
panels.add(new FirstrunPanelConfig(WelcomePanel.class.getName(), WelcomePanel.TITLE_RES));
}
return panels;
}
public static List<FirstrunPanel> getRestricted() {
final List<FirstrunPanel> panels = new LinkedList<>();
panels.add(new FirstrunPanel(RestrictedWelcomePanel.class.getName(), RestrictedWelcomePanel.TITLE_RES));
/*
* Wrapper method for using local bucketing rather than server-side.
* This needs to match the server-side bucketing used on mozilla-switchboard.herokuapp.com.
*/
private static boolean isInExperimentLocal(Context context, String name) {
if (AppConstants.MOZ_SWITCHBOARD) {
if (SwitchBoard.isInBucket(context, 0, 50)) {
return ONBOARDING_A.equals(name);
} else if (SwitchBoard.isInBucket(context, 50, 100)) {
return ONBOARDING_B.equals(name);
}
}
return false;
}
public static List<FirstrunPanelConfig> getRestricted() {
final List<FirstrunPanelConfig> panels = new LinkedList<>();
panels.add(new FirstrunPanelConfig(RestrictedWelcomePanel.class.getName(), RestrictedWelcomePanel.TITLE_RES));
return panels;
}
public static class FirstrunPanel {
public static class FirstrunPanelConfig {
private String classname;
private int titleRes;
public FirstrunPanel(String resource, int titleRes) {
public FirstrunPanelConfig(String resource, int titleRes) {
this.classname= resource;
this.titleRes = titleRes;
}

View File

@ -15,6 +15,8 @@ import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.AnimatorListenerAdapter;
import com.nineoldandroids.animation.ObjectAnimator;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.animation.TransitionsTracker;
public class FirstrunPane extends LinearLayout {
@ -35,10 +37,10 @@ public class FirstrunPane extends LinearLayout {
super(context, attrs);
}
public void load(FragmentManager fm) {
public void load(Context appContext, FragmentManager fm) {
visible = true;
pager = (FirstrunPager) findViewById(R.id.firstrun_pager);
pager.load(fm, new OnFinishListener() {
pager.load(appContext, fm, new OnFinishListener() {
@Override
public void onFinish() {
hide();
@ -57,6 +59,10 @@ public class FirstrunPane extends LinearLayout {
onFinishListener.onFinish();
}
animateHide();
// Stop all versions of firstrun A/B sessions.
Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FirstrunPagerConfig.ONBOARDING_A);
Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FirstrunPagerConfig.ONBOARDING_B);
}
private void animateHide() {

View File

@ -10,15 +10,25 @@ import android.support.v4.app.Fragment;
public class FirstrunPanel extends Fragment {
public static final int TITLE_RES = -1;
protected FirstrunPane.OnFinishListener onFinishListener;
public interface PagerNavigation {
void next();
void finish();
}
protected PagerNavigation pagerNavigation;
public void setOnFinishListener(FirstrunPane.OnFinishListener listener) {
this.onFinishListener = listener;
public void setPagerNavigation(PagerNavigation listener) {
this.pagerNavigation = listener;
}
protected void next() {
if (pagerNavigation != null) {
pagerNavigation.next();
}
}
protected void close() {
if (onFinishListener != null) {
onFinishListener.onFinish();
if (pagerNavigation != null) {
pagerNavigation.finish();
}
}
}

View File

@ -0,0 +1,159 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* 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/. */
package org.mozilla.gecko.firstrun;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.preferences.AndroidImport;
import org.mozilla.gecko.util.ThreadUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ImportPanel extends FirstrunPanel {
public static final String LOGTAG = "GeckoImportPanel";
public static final int TITLE_RES = R.string.firstrun_import_title;
private static final int AUTOADVANCE_DELAY_MS = 1500;
// These match the item positions in R.array.pref_import_android_entries.
private static int BOOKMARKS_INDEX = 0;
private static int HISTORY_INDEX = 1;
private ImageView confirmImage;
private Button choiceButton;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.firstrun_import_fragment, container, false);
choiceButton = (Button) root.findViewById(R.id.import_action_button);
confirmImage = (ImageView) root.findViewById(R.id.confirm_check);
choiceButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
final List<Integer> checked = new ArrayList<>(Arrays.asList(0, 1));
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-import-action");
builder.setTitle(R.string.firstrun_import_action)
.setMultiChoiceItems(R.array.pref_import_android_entries, makeBooleanArray(R.array.pref_import_android_defaults), new DialogInterface.OnMultiChoiceClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int index, boolean isChecked) {
// Add telemetry for toggling checkboxes.
Telemetry.sendUIEvent(TelemetryContract.Event.EDIT, TelemetryContract.Method.DIALOG, "firstrun-import-dialog-checkbox");
if (isChecked && !checked.contains(index)) {
checked.add(index);
} else if (!isChecked && checked.contains(index)) {
checked.remove(checked.indexOf(index));
}
}
})
.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int i) {
Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.BUTTON, "firstrun-import-dialog");
dialog.dismiss();
}
})
.setPositiveButton(R.string.firstrun_import_dialog_button, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int i) {
final boolean importBookmarks = checked.contains(BOOKMARKS_INDEX);
final boolean importHistory = checked.contains(HISTORY_INDEX);
runImport(importBookmarks, importHistory);
// Telemetry for what options are confirmed.
final int importState = (importBookmarks ? 1 : 0) + (importHistory ? 2 : 0);
Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.BUTTON, "firstrun-import-dialog-" + importState);
dialog.dismiss();
}
});
builder.create().show();
}
});
root.findViewById(R.id.import_link).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-import-next");
pagerNavigation.next();
}
});
return root;
}
private boolean[] makeBooleanArray(int resId) {
final String[] defaults = getResources().getStringArray(resId);
final boolean[] booleanArray = new boolean[defaults.length];
for (int i = 0; i < defaults.length; i++) {
booleanArray[i] = defaults[i].equals("true");
}
return booleanArray;
}
protected void runImport(final boolean doBookmarks, final boolean doHistory) {
Log.d(LOGTAG, "Importing Android history/bookmarks");
if (!doBookmarks && !doHistory) {
return;
}
final Context context = getActivity();
final String dialogTitle = context.getString(R.string.firstrun_import_progress_title);
final ProgressDialog dialog =
ProgressDialog.show(context,
dialogTitle,
context.getString(R.string.bookmarkhistory_import_wait),
true);
final Runnable stopCallback = new Runnable() {
@Override
public void run() {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
confirmImage.setVisibility(View.VISIBLE);
choiceButton.setVisibility(View.GONE);
dialog.dismiss();
ThreadUtils.postDelayedToUiThread(new Runnable() {
@Override
public void run() {
next();
}
}, AUTOADVANCE_DELAY_MS);
}
});
}
};
ThreadUtils.postToBackgroundThread(
// Constructing AndroidImport may need finding the profile,
// which hits disk, so it needs to go into a Runnable too.
new Runnable() {
@Override
public void run() {
new AndroidImport(context, stopCallback, doBookmarks, doHistory).run();
}
}
);
}
}

View File

@ -0,0 +1,49 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* 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/. */
package org.mozilla.gecko.firstrun;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity;
public class SyncPanel extends FirstrunPanel {
// XXX: To simplify localization, this uses the pref_sync string. If this is used in the final product, add a new string to Nightly.
public static final int TITLE_RES = R.string.pref_sync;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.firstrun_sync_fragment, container, false);
// TODO: Update id names.
root.findViewById(R.id.welcome_account).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-sync");
final Intent accountIntent = new Intent(getActivity(), FxAccountGetStartedActivity.class);
startActivity(accountIntent);
close();
}
});
root.findViewById(R.id.welcome_browse).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-browser");
close();
}
});
return root;
}
}

View File

@ -10,10 +10,12 @@ import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountWebFlowActivity;
public class WelcomePanel extends FirstrunPanel {
public static final int TITLE_RES = R.string.firstrun_panel_title_welcome;
@ -27,6 +29,7 @@ public class WelcomePanel extends FirstrunPanel {
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-sync");
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_FIRSTRUN);
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(intent);

View File

@ -100,4 +100,8 @@ public class FxAccountConstants {
public static final String ACTION_FXA_GET_STARTED = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_GET_STARTED";
public static final String ACTION_FXA_STATUS = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_STATUS";
public static final String ACTION_FXA_UPDATE_CREDENTIALS = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_UPDATE_CREDENTIALS";
public static final String ENDPOINT_PREFERENCES = "preferences";
public static final String ENDPOINT_NOTIFICATION = "notification";
public static final String ENDPOINT_FIRSTRUN = "firstrun";
}

View File

@ -6,6 +6,6 @@ package org.mozilla.gecko.fxa.activities;
public class FxAccountConfirmAccountActivityWeb extends FxAccountWebFlowActivity {
public FxAccountConfirmAccountActivityWeb() {
super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST, "settings");
super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST, "manage");
}
}

View File

@ -6,6 +6,6 @@ package org.mozilla.gecko.fxa.activities;
public class FxAccountGetStartedActivityWeb extends FxAccountWebFlowActivity {
public FxAccountGetStartedActivityWeb() {
super(CANNOT_RESUME_WHEN_ACCOUNTS_EXIST, "signin");
super(CANNOT_RESUME_WHEN_ACCOUNTS_EXIST, "signup");
}
}

View File

@ -249,6 +249,7 @@ public class FxAccountStatusFragment
if (preference == needsPasswordPreference) {
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_UPDATE_CREDENTIALS);
intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
final Bundle extras = getExtrasForAccount();
if (extras != null) {
intent.putExtras(extras);
@ -263,6 +264,7 @@ public class FxAccountStatusFragment
if (preference == needsFinishMigratingPreference) {
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_FINISH_MIGRATING);
intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
final Bundle extras = getExtrasForAccount();
if (extras != null) {
intent.putExtras(extras);
@ -284,6 +286,7 @@ public class FxAccountStatusFragment
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
startActivity(intent);
return true;

View File

@ -4,11 +4,11 @@
package org.mozilla.gecko.fxa.activities;
import android.content.Intent;
import android.os.Bundle;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
/**
@ -18,6 +18,13 @@ public class FxAccountWebFlowActivity extends FxAccountAbstractActivity {
protected static final String LOG_TAG = FxAccountWebFlowActivity.class.getSimpleName();
protected static final String ABOUT_ACCOUNTS = "about:accounts";
public static final String EXTRA_ENDPOINT = "entrypoint";
protected static final String[] EXTRAS_TO_PASSTHROUGH = new String[] {
EXTRA_ENDPOINT,
};
private final String action;
private final String extras;
@ -49,8 +56,28 @@ public class FxAccountWebFlowActivity extends FxAccountAbstractActivity {
if (redirected) {
return true;
}
ActivityUtils.openURLInFennec(getApplicationContext(),
ABOUT_ACCOUNTS + "?action=" + action + extras);
final StringBuilder sb = new StringBuilder();
sb.append(ABOUT_ACCOUNTS);
sb.append("?action=");
sb.append(action);
sb.append(extras);
// Pass through a set of known string values from intent extras to about:accounts.
final Intent intent = getIntent();
if (intent != null) {
for (String key : EXTRAS_TO_PASSTHROUGH) {
final String value = intent.getStringExtra(key);
if (value != null) {
sb.append("&");
sb.append(key);
sb.append("=");
sb.append(value);
}
}
}
ActivityUtils.openURLInFennec(getApplicationContext(), sb.toString());
return true;
}

View File

@ -16,6 +16,7 @@ import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.common.telemetry.TelemetryWrapper;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountWebFlowActivity;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.Action;
@ -93,6 +94,9 @@ public class FxAccountNotificationManager {
text = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_text, state.email);
notificationIntent = new Intent(FxAccountConstants.ACTION_FXA_STATUS);
}
notificationIntent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_NOTIFICATION);
Logger.info(LOG_TAG, "State " + state.getStateLabel() + " needs action; offering notification with title: " + title);
FxAccountUtils.pii(LOG_TAG, "And text: " + text);

View File

@ -6,12 +6,22 @@
<!ENTITY no_space_to_start_error "There is not enough space available for &brandShortName; to start.">
<!ENTITY error_loading_file "An error occurred when trying to load files required to run &brandShortName;">
<!ENTITY firstrun_panel_title_welcome "Welcome">
<!ENTITY onboard_start_message3 "Browse with &brandShortName;">
<!ENTITY onboard_start_subtext3 "Make your mobile Web browsing experience truly your own.">
<!ENTITY onboard_start_button_account "Sign in to &brandShortName;">
<!ENTITY onboard_start_button_browser "Start Browsing">
<!ENTITY firstrun_button_next "Next">
<!ENTITY onboard_start_restricted1 "Stay safe and in control with this simplified version of &brandShortName;.">
<!ENTITY firstrun_import_title "Import">
<!ENTITY firstrun_import_message "Welcome to &brandShortName;">
<!ENTITY firstrun_import_subtext "Import your bookmarks and history from another browser.">
<!ENTITY firstrun_import_action "Transfer to &brandShortName;">
<!ENTITY firstrun_import_dialog_button "Transfer">
<!ENTITY firstrun_import_progress_title "Transferring">
<!-- Localization note: These are used as the titles of different pages on the home screen.
They are automatically converted to all caps by the Android platform. -->
<!ENTITY bookmarks_title "Bookmarks">
@ -486,8 +496,6 @@ size. -->
<!ENTITY button_clear "Clear">
<!ENTITY button_copy "Copy">
<!ENTITY firstrun_panel_title_welcome "Welcome">
<!ENTITY home_top_sites_title "Top Sites">
<!-- Localization note (home_top_sites_add): This string is used as placeholder
text underneath empty thumbnails in the Top Sites page on about:home. -->

View File

@ -231,7 +231,9 @@ gbjar.sources += [
'firstrun/FirstrunPagerConfig.java',
'firstrun/FirstrunPane.java',
'firstrun/FirstrunPanel.java',
'firstrun/ImportPanel.java',
'firstrun/RestrictedWelcomePanel.java',
'firstrun/SyncPanel.java',
'firstrun/WelcomePanel.java',
'FormAssistPopup.java',
'GeckoAccessibility.java',
@ -498,6 +500,7 @@ gbjar.sources += [
'toolbar/PageActionLayout.java',
'toolbar/PhoneTabsButton.java',
'toolbar/ShapedButton.java',
'toolbar/ShapedButtonFrameLayout.java',
'toolbar/SiteIdentityPopup.java',
'toolbar/TabCounter.java',
'toolbar/ToolbarDisplayLayout.java',

View File

@ -23,7 +23,7 @@ import android.util.Log;
import java.util.ArrayList;
class AndroidImport implements Runnable {
public class AndroidImport implements Runnable {
/**
* The Android M SDK removed several fields and methods from android.provider.Browser. This class is used as a
* replacement to support building with the new SDK but at the same time still use these fields on lower Android

View File

@ -13,6 +13,7 @@ import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.TelemetryContract.Method;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountWebFlowActivity;
import org.mozilla.gecko.sync.setup.SyncAccounts;
import org.mozilla.gecko.sync.setup.activities.SetupSyncActivity;
@ -40,6 +41,7 @@ class SyncPreference extends Preference {
private void launchFxASetup() {
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
mContext.startActivity(intent);

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@ -3,6 +3,8 @@
- 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/. -->
<!-- If you change this view, update ShapedButton*,
which dynamically resets to this view. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"
@ -11,7 +13,7 @@
<item android:state_focused="true"
android:state_pressed="false"
android:drawable="@color/highlight_shaped_focused"/>
<item android:drawable="@color/text_and_tabs_tray_grey"/>
</selector>

View File

@ -36,7 +36,7 @@
android:src="@drawable/url_bar_translating_edge"
android:scaleType="fitXY"/>
<org.mozilla.gecko.widget.themed.ThemedFrameLayout
<org.mozilla.gecko.toolbar.ShapedButtonFrameLayout
android:id="@+id/menu"
style="@style/UrlBar.ImageButton"
android:layout_alignParentRight="true"
@ -54,7 +54,7 @@
android:src="@drawable/menu"
android:tint="@color/tabs_tray_icon_grey"/>
</org.mozilla.gecko.widget.themed.ThemedFrameLayout>
</org.mozilla.gecko.toolbar.ShapedButtonFrameLayout>
<org.mozilla.gecko.toolbar.PhoneTabsButton android:id="@+id/tabs"
style="@style/UrlBar.ImageButton"

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:orientation="vertical"
android:fillViewport="true">
<LinearLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="@dimen/firstrun_min_height"
android:background="@color/android:white"
android:gravity="center_horizontal"
android:orientation="vertical">
<FrameLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/link_blue">
<ImageView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:src="@drawable/firstrun_background_coffee"/>
<ImageView android:layout_width="@dimen/firstrun_brand_size"
android:layout_height="@dimen/firstrun_brand_size"
android:src="@drawable/large_icon"
android:layout_gravity="center"/>
</FrameLayout>
<TextView android:layout_width="@dimen/firstrun_content_width"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="30dp"
android:textAppearance="@style/TextAppearance.FirstrunLight.Main"
android:text="@string/firstrun_import_message"/>
<TextView android:layout_width="@dimen/firstrun_content_width"
android:layout_height="wrap_content"
android:paddingTop="20dp"
android:paddingBottom="30dp"
android:gravity="center"
android:textAppearance="@style/TextAppearance.FirstrunRegular.Body"
android:text="@string/firstrun_import_subtext"/>
<Button android:id="@+id/import_action_button"
style="@style/Widget.Firstrun.Button"
android:background="@drawable/button_background_action_orange_round"
android:layout_gravity="center"
android:text="@string/firstrun_import_action"/>
<ImageView android:id="@+id/confirm_check"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:visibility="gone"
android:src="@drawable/overlay_check"/>
<TextView android:id="@+id/import_link"
android:layout_width="@dimen/firstrun_content_width"
android:layout_height="wrap_content"
android:padding="20dp"
android:gravity="center"
android:textAppearance="@style/TextAppearance.FirstrunRegular.Link"
android:text="@string/firstrun_button_next"/>
</LinearLayout>
</ScrollView>

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:orientation="vertical"
android:fillViewport="true">
<LinearLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="@dimen/firstrun_min_height"
android:background="@color/android:white"
android:gravity="center_horizontal"
android:orientation="vertical">
<FrameLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/link_blue">
<ImageView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:src="@drawable/firstrun_background_devices"/>
<ImageView android:layout_width="@dimen/firstrun_brand_size"
android:layout_height="@dimen/firstrun_brand_size"
android:src="@drawable/large_icon"
android:layout_gravity="center"/>
</FrameLayout>
<TextView android:layout_width="@dimen/firstrun_content_width"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="30dp"
android:textAppearance="@style/TextAppearance.FirstrunLight.Main"
android:text="@string/firstrun_welcome_message"/>
<TextView android:layout_width="@dimen/firstrun_content_width"
android:layout_height="wrap_content"
android:paddingTop="20dp"
android:paddingBottom="30dp"
android:gravity="center"
android:textAppearance="@style/TextAppearance.FirstrunRegular.Body"
android:text="@string/firstrun_welcome_subtext"/>
<Button android:id="@+id/welcome_account"
style="@style/Widget.Firstrun.Button"
android:background="@drawable/button_background_action_orange_round"
android:layout_gravity="center"
android:text="@string/firstrun_welcome_button_account"/>
<TextView android:id="@+id/welcome_browse"
android:layout_width="@dimen/firstrun_content_width"
android:layout_height="wrap_content"
android:padding="20dp"
android:gravity="center"
android:textAppearance="@style/TextAppearance.FirstrunRegular.Link"
android:text="@string/firstrun_welcome_button_browser"/>
</LinearLayout>
</ScrollView>

View File

@ -38,7 +38,8 @@
<PreferenceCategory android:title="@string/pref_category_logins">
<org.mozilla.gecko.preferences.LinkPreference android:title="@string/pref_manage_logins"
<org.mozilla.gecko.preferences.LinkPreference android:key="android.not_a_preference.signon.manage"
android:title="@string/pref_manage_logins"
url="about:logins"/>
<CheckBoxPreference android:key="signon.rememberSignons"

View File

@ -37,13 +37,24 @@
<string name="no_space_to_start_error">&no_space_to_start_error;</string>
<string name="error_loading_file">&error_loading_file;</string>
<string name="firstrun_panel_title_welcome">&firstrun_panel_title_welcome;</string>
<string name="firstrun_welcome_message">&onboard_start_message3;</string>
<string name="firstrun_welcome_subtext">&onboard_start_subtext3;</string>
<string name="firstrun_welcome_button_account">&onboard_start_button_account;</string>
<string name="firstrun_welcome_button_browser">&onboard_start_button_browser;</string>
<string name="firstrun_button_next">&firstrun_button_next;</string>
<string name="firstrun_empty_contentDescription"></string>
<string name="firstrun_welcome_restricted">&onboard_start_restricted1;</string>
<string name="firstrun_import_title">&firstrun_import_title;</string>
<string name="firstrun_import_message">&firstrun_import_message;</string>
<string name="firstrun_import_subtext">&firstrun_import_subtext;</string>
<string name="firstrun_import_action">&firstrun_import_action;</string>
<string name="firstrun_import_dialog_button">&firstrun_import_dialog_button;</string>
<string name="firstrun_import_progress_title">&firstrun_import_progress_title;</string>
<string name="bookmarks_title">&bookmarks_title;</string>
<string name="history_title">&history_title;</string>
<string name="reading_list_title">&reading_list_title;</string>
@ -409,8 +420,6 @@
<string name="button_no">&button_no;</string>
<string name="button_copy">&button_copy;</string>
<string name="firstrun_panel_title_welcome">&firstrun_panel_title_welcome;</string>
<string name="home_title">&home_title;</string>
<string name="home_top_sites_title">&home_top_sites_title;</string>
<string name="home_top_sites_add">&home_top_sites_add;</string>

View File

@ -23,6 +23,7 @@ import android.text.style.ClickableSpan;
import android.text.style.URLSpan;
import android.view.View;
import android.widget.TextView;
import org.mozilla.gecko.tabqueue.TabQueueDispatcher;
public class ActivityUtils {
private static final String LOG_TAG = "ActivityUtils";
@ -64,6 +65,7 @@ public class ActivityUtils {
}
intent.setClassName(GlobalConstants.BROWSER_INTENT_PACKAGE, GlobalConstants.BROWSER_INTENT_CLASS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(TabQueueDispatcher.SKIP_TAB_QUEUE_FLAG, true);
context.startActivity(intent);
}

View File

@ -18,6 +18,10 @@ import android.graphics.drawable.Drawable;
import android.graphics.drawable.StateListDrawable;
import android.util.AttributeSet;
/**
* A ImageButton with a custom drawn path and lightweight theme support. Note that {@link ShapedButtonFrameLayout}
* copies the lwt support so if you change it here, you should probably change it there.
*/
public class ShapedButton extends ThemedImageButton
implements CanvasDelegate.DrawManager {

View File

@ -0,0 +1,74 @@
/* 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/. */
package org.mozilla.gecko.toolbar;
import org.mozilla.gecko.R;
import org.mozilla.gecko.lwt.LightweightThemeDrawable;
import org.mozilla.gecko.util.ColorUtils;
import org.mozilla.gecko.widget.themed.ThemedFrameLayout;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.StateListDrawable;
import android.util.AttributeSet;
/** A FrameLayout with lightweight theme support. Note that {@link ShapedButton}'s lwt support is basically the same so
* if you change it here, you should probably change it there. Note also that this doesn't have ShapedButton's path code
* so shouldn't have "ShapedButton" in the name, but I wanted to make the connection apparent so I left it.
*/
public class ShapedButtonFrameLayout extends ThemedFrameLayout {
public ShapedButtonFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
// The drawable is constructed as per @drawable/shaped_button.
@Override
public void onLightweightThemeChanged() {
final int background = ColorUtils.getColor(getContext(), R.color.text_and_tabs_tray_grey);
final LightweightThemeDrawable lightWeight = getTheme().getColorDrawable(this, background);
if (lightWeight == null)
return;
lightWeight.setAlpha(34, 34);
final StateListDrawable stateList = new StateListDrawable();
stateList.addState(PRESSED_ENABLED_STATE_SET, getColorDrawable(R.color.highlight_shaped));
stateList.addState(FOCUSED_STATE_SET, getColorDrawable(R.color.highlight_shaped_focused));
stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.text_and_tabs_tray_grey));
stateList.addState(EMPTY_STATE_SET, lightWeight);
setBackgroundDrawable(stateList);
}
@Override
public void onLightweightThemeReset() {
setBackgroundResource(R.drawable.shaped_button);
}
@Override
public void setBackgroundDrawable(Drawable drawable) {
if (getBackground() == null || drawable == null) {
super.setBackgroundDrawable(drawable);
return;
}
int[] padding = new int[] { getPaddingLeft(),
getPaddingTop(),
getPaddingRight(),
getPaddingBottom()
};
drawable.setLevel(getBackground().getLevel());
super.setBackgroundDrawable(drawable);
setPadding(padding[0], padding[1], padding[2], padding[3]);
}
@Override
public void setBackgroundResource(int resId) {
setBackgroundDrawable(getResources().getDrawable(resId));
}
}

View File

@ -0,0 +1,327 @@
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
/* 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/. */
/**
* Wrap a remote fxa-content-server.
*
* An about:accounts tab loads and displays an fxa-content-server page,
* depending on the current Android Account status and an optional 'action'
* parameter.
*
* We show a spinner while the remote iframe is loading. We expect the
* WebChannel message listening to the fxa-content-server to send this tab's
* <browser>'s messageManager a LOADED message when the remote iframe provides
* the WebChannel LOADED message. See the messageManager registration and the
* |loadedDeferred| promise. This loosely couples the WebChannel implementation
* and about:accounts! (We need this coupling in order to distinguish
* WebChannel LOADED messages produced by multiple about:accounts tabs.)
*
* We capture error conditions by accessing the inner nsIWebNavigation of the
* iframe directly.
*/
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu} = Components; /*global Components */
Cu.import("resource://gre/modules/Accounts.jsm"); /*global Accounts */
Cu.import("resource://gre/modules/PromiseUtils.jsm"); /*global PromiseUtils */
Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global XPCOMUtils */
const ACTION_URL_PARAM = "action";
const COMMAND_LOADED = "fxaccounts:loaded";
const log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccounts");
XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls",
"@mozilla.org/parental-controls-service;1", "nsIParentalControlsService");
// Shows the toplevel element with |id| to be shown - all other top-level
// elements are hidden.
// If |id| is 'spinner', then 'remote' is also shown, with opacity 0.
function show(id) {
let allTop = document.querySelectorAll(".toplevel");
for (let elt of allTop) {
if (elt.getAttribute("id") == id) {
elt.style.display = 'block';
} else {
elt.style.display = 'none';
}
}
if (id == 'spinner') {
document.getElementById('remote').style.display = 'block';
document.getElementById('remote').style.opacity = 0;
}
}
var loadedDeferred = null;
// We have a new load starting. Replace the existing promise with a new one,
// and queue up the transition to remote content.
function deferTransitionToRemoteAfterLoaded() {
log.d('Waiting for LOADED message.');
loadedDeferred = PromiseUtils.defer();
loadedDeferred.promise.then(() => {
document.getElementById("remote").style.opacity = 0;
show("remote");
document.getElementById("remote").style.opacity = 1;
});
}
function handleLoadedMessage(message) {
log.d('Got LOADED message!');
loadedDeferred.resolve();
};
let wrapper = {
iframe: null,
url: null,
init: function (url) {
deferTransitionToRemoteAfterLoaded();
let iframe = document.getElementById("remote");
this.iframe = iframe;
this.iframe.QueryInterface(Ci.nsIFrameLoaderOwner);
let docShell = this.iframe.frameLoader.docShell;
docShell.QueryInterface(Ci.nsIWebProgress);
docShell.addProgressListener(this.iframeListener, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
this.url = url;
// Set the iframe's location with loadURI/LOAD_FLAGS_BYPASS_HISTORY to
// avoid having a new history entry being added.
let webNav = iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation);
webNav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null);
},
retry: function () {
deferTransitionToRemoteAfterLoaded();
let webNav = this.iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation);
webNav.loadURI(this.url, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null);
},
iframeListener: {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference,
Ci.nsISupports]),
onStateChange: function(aWebProgress, aRequest, aState, aStatus) {
let failure = false;
// Captive portals sometimes redirect users
if ((aState & Ci.nsIWebProgressListener.STATE_REDIRECTING)) {
failure = true;
} else if ((aState & Ci.nsIWebProgressListener.STATE_STOP)) {
if (aRequest instanceof Ci.nsIHttpChannel) {
try {
failure = aRequest.responseStatus != 200;
} catch (e) {
failure = aStatus != Components.results.NS_OK;
}
}
}
// Calling cancel() will raise some OnStateChange notifications by itself,
// so avoid doing that more than once
if (failure && aStatus != Components.results.NS_BINDING_ABORTED) {
aRequest.cancel(Components.results.NS_BINDING_ABORTED);
show("networkError");
}
},
onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
if (aRequest && aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
aRequest.cancel(Components.results.NS_BINDING_ABORTED);
show("networkError");
}
},
onProgressChange: function() {},
onStatusChange: function() {},
onSecurityChange: function() {},
},
};
function retry() {
log.i("Retrying.");
show("spinner");
wrapper.retry();
}
function openPrefs() {
log.i("Opening Sync preferences.");
// If an Android Account exists, this will open the Status Activity.
// Otherwise, it will begin the Get Started flow. This should only be shown
// when an Account actually exists.
Accounts.launchSetup();
}
function getURLForAction(action, urlParams) {
let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri");
url = url + (url.endsWith("/") ? "" : "/") + action;
const CONTEXT = "fx_fennec_v1";
// The only service managed by Fennec, to date, is Firefox Sync.
const SERVICE = "sync";
urlParams = urlParams || new URLSearchParams("");
urlParams.set('service', SERVICE);
urlParams.set('context', CONTEXT);
// Ideally we'd just merge urlParams with new URL(url).searchParams, but our
// URLSearchParams implementation doesn't support iteration (bug 1085284).
let urlParamStr = urlParams.toString();
if (urlParamStr) {
url += (url.includes("?") ? "&" : "?") + urlParamStr;
}
return url;
}
function updateDisplayedEmail(user) {
let emailDiv = document.getElementById("email");
if (emailDiv && user) {
emailDiv.textContent = user.email;
}
}
function init() {
// Test for restrictions before getFirefoxAccount(), since that will fail if
// we are restricted.
if (!ParentalControls.isAllowed(ParentalControls.MODIFY_ACCOUNTS)) {
// It's better to log and show an error message than to invite user
// confusion by removing about:accounts entirely. That is, if the user is
// restricted, this way they'll discover as much and may be able to get
// out of their restricted profile. If we remove about:accounts entirely,
// it will look like Fennec is buggy, and the user will be very confused.
log.e("This profile cannot connect to Firefox Accounts: showing restricted error.");
show("restrictedError");
return;
}
Accounts.getFirefoxAccount().then(user => {
// It's possible for the window to start closing before getting the user
// completes. Tests in particular can cause this.
if (window.closed) {
return;
}
updateDisplayedEmail(user);
// Ideally we'd use new URL(document.URL).searchParams, but for about: URIs,
// searchParams is empty.
let urlParams = new URLSearchParams(document.URL.split("?")[1] || "");
let action = urlParams.get(ACTION_URL_PARAM);
urlParams.delete(ACTION_URL_PARAM);
switch (action) {
case "signup":
if (user) {
// Asking to sign-up when already signed in just shows prefs.
show("prefs");
} else {
show("spinner");
wrapper.init(getURLForAction("signup", urlParams));
}
break;
case "signin":
if (user) {
// Asking to sign-in when already signed in just shows prefs.
show("prefs");
} else {
show("spinner");
wrapper.init(getURLForAction("signin", urlParams));
}
break;
case "force_auth":
if (user) {
show("spinner");
urlParams.set("email", user.email); // In future, pin using the UID.
wrapper.init(getURLForAction("force_auth", urlParams));
} else {
show("spinner");
wrapper.init(getURLForAction("signup", urlParams));
}
break;
case "manage":
if (user) {
show("spinner");
urlParams.set("email", user.email); // In future, pin using the UID.
wrapper.init(getURLForAction("settings", urlParams));
} else {
show("spinner");
wrapper.init(getURLForAction("signup", urlParams));
}
break;
default:
// Unrecognized or no action specified.
if (action) {
log.w("Ignoring unrecognized action: " + action);
}
if (user) {
show("prefs");
} else {
show("spinner");
wrapper.init(getURLForAction("signup", urlParams));
}
break;
}
}).catch(e => {
log.e("Failed to get the signed in user: " + e.toString());
});
}
document.addEventListener("DOMContentLoaded", function onload() {
document.removeEventListener("DOMContentLoaded", onload, true);
init();
var buttonRetry = document.getElementById('buttonRetry');
buttonRetry.addEventListener('click', retry);
var buttonOpenPrefs = document.getElementById('buttonOpenPrefs');
buttonOpenPrefs.addEventListener('click', openPrefs);
}, true);
// This window is contained in a XUL <browser> element. Return the
// messageManager of that <browser> element, or null.
function getBrowserMessageManager() {
let browser = window
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow)
.QueryInterface(Ci.nsIDOMChromeWindow)
.BrowserApp
.getBrowserForDocument(document);
if (browser) {
return browser.messageManager;
}
return null;
}
// Add a single listener for 'loaded' messages from the iframe in this
// <browser>. These 'loaded' messages are ferried from the WebChannel to just
// this <browser>.
let mm = getBrowserMessageManager();
if (mm) {
mm.addMessageListener(COMMAND_LOADED, handleLoadedMessage);
} else {
log.e('No messageManager, not listening for LOADED message!');
}
window.addEventListener("unload", function(event) {
try {
let mm = getBrowserMessageManager();
if (mm) {
mm.removeMessageListener(COMMAND_LOADED, handleLoadedMessage);
}
} catch (e) {
// This could fail if the page is being torn down, the tab is being
// destroyed, etc.
log.w('Not removing listener for LOADED message: ' + e.toString());
}
});

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
%brandDTD;
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
%globalDTD;
<!ENTITY % aboutDTD SYSTEM "chrome://browser/locale/aboutAccounts.dtd">
%aboutDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml" dir="&locale.dir;">
<head>
<title>Firefox Sync</title>
<meta name="viewport" content="width=device-width; user-scalable=0" />
<link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
<link rel="stylesheet" href="chrome://browser/skin/spinner.css" type="text/css"/>
<link rel="stylesheet" href="chrome://browser/skin/aboutBase.css" type="text/css"/>
<link rel="stylesheet" href="chrome://browser/skin/aboutAccounts.css" type="text/css"/>
</head>
<body>
<div id="spinner" class="toplevel">
<div class="container flex-column">
<!-- Empty text-container for spacing. -->
<div class="text-container flex-column" />
<div class="mui-refresh-main">
<div class="mui-refresh-wrapper">
<div class="mui-spinner-wrapper">
<div class="mui-spinner-main">
<div class="mui-spinner-left">
<div class="mui-half-circle-left" />
</div>
<div class="mui-spinner-right">
<div class="mui-half-circle-right" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<iframe mozframetype="content" id="remote" class="toplevel" />
<div id="prefs" class="toplevel">
<div class="container flex-column">
<div class="text-container flex-column">
<div class="text">&aboutAccounts.connected.title;</div>
<div class="hint">&aboutAccounts.connected.description;</div>
<div id="email" class="hint"></div>
</div>
<a id="buttonOpenPrefs" tabindex="0" href="#">&aboutAccounts.syncPreferences.label;</a>
</div>
</div>
<div id="networkError" class="toplevel">
<div class="container flex-column">
<div class="text-container flex-column">
<div class="text">&aboutAccounts.noConnection.title;</div>
</div>
<div class="button-row">
<button id="buttonRetry" class="button" tabindex="1">&aboutAccounts.retry.label;</button>
</div>
</div>
</div>
<div id="restrictedError" class="toplevel">
<div class="container flex-column">
<div class="text-container flex-column">
<div class="text">&aboutAccounts.restrictedError.title;</div>
<div class="hint">&aboutAccounts.restrictedError.description;</div>
</div>
</div>
</div>
<script type="application/javascript;version=1.8" src="chrome://browser/content/aboutAccounts.js"></script>
</body>
</html>

View File

@ -16,6 +16,7 @@
<title>&aboutLogins.title;</title>
<meta name="viewport" content="width=device-width; user-scalable=0" />
<link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
<link rel="stylesheet" href="chrome://browser/skin/spinner.css" type="text/css"/>
<link rel="stylesheet" href="chrome://browser/skin/aboutBase.css" type="text/css"/>
<link rel="stylesheet" href="chrome://browser/skin/aboutLogins.css" type="text/css"/>
<script type="application/javascript;version=1.8" src="chrome://browser/content/aboutLogins.js"></script>
@ -38,7 +39,22 @@
</div>
<div id="logins-list-loading-body" class="hidden">
<div id="loading-img-container">
<object type="image/svg+xml" id="spinner" data="chrome://browser/skin/images/spinning_throbber.svg"/>
<div id="spinner" class="mui-refresh-main">
<div class="mui-refresh-wrapper">
<div class="mui-spinner-wrapper">
<div class="mui-spinner-main">
<div class="mui-spinner-left">
<div class="mui-half-circle-left" />
</div>
<div class="mui-spinner-right">
<div class="mui-half-circle-right" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="empty-body" class="hidden">

View File

@ -555,9 +555,15 @@ var BrowserApp = {
// about:accounts, which can happen when starting the Firefox Account flow
// from the first run experience, or via the Firefox Account Status
// Activity, we can and do miss messages from the fxa-content-server.
console.log("browser.js: loading Firefox Accounts WebChannel");
Cu.import("resource://gre/modules/FxAccountsWebChannel.jsm");
EnsureFxAccountsWebChannel();
// However, we never allow suitably restricted profiles from listening to
// fxa-content-server messages.
if (ParentalControls.isAllowed(ParentalControls.MODIFY_ACCOUNTS)) {
console.log("browser.js: loading Firefox Accounts WebChannel");
Cu.import("resource://gre/modules/FxAccountsWebChannel.jsm");
EnsureFxAccountsWebChannel();
} else {
console.log("browser.js: not loading Firefox Accounts WebChannel; this profile cannot connect to Firefox Accounts.");
}
}
// Notify Java that Gecko has loaded.

View File

@ -59,6 +59,10 @@ chrome.jar:
#ifdef MOZ_DEVICES
content/aboutDevices.xhtml (content/aboutDevices.xhtml)
content/aboutDevices.js (content/aboutDevices.js)
#endif
#ifndef MOZ_ANDROID_NATIVE_ACCOUNT_UI
content/aboutAccounts.xhtml (content/aboutAccounts.xhtml)
content/aboutAccounts.js (content/aboutAccounts.js)
#endif
content/aboutLogins.xhtml (content/aboutLogins.xhtml)
content/aboutLogins.js (content/aboutLogins.js)

View File

@ -89,6 +89,12 @@ if (AppConstants.MOZ_DEVICES) {
privileged: true
};
}
if (!AppConstants.MOZ_ANDROID_NATIVE_ACCOUNT_UI) {
modules['accounts'] = {
uri: "chrome://browser/content/aboutAccounts.xhtml",
privileged: true
};
}
function AboutRedirector() {}
AboutRedirector.prototype = {

View File

@ -20,7 +20,9 @@ contract @mozilla.org/network/protocol/about;1?what=blocked {322ba47e-7047-4f71-
#ifdef MOZ_DEVICES
contract @mozilla.org/network/protocol/about;1?what=devices {322ba47e-7047-4f71-aebf-cb7d69325cd9}
#endif
#ifndef MOZ_ANDROID_NATIVE_ACCOUNT_UI
contract @mozilla.org/network/protocol/about;1?what=accounts {322ba47e-7047-4f71-aebf-cb7d69325cd9}
#endif
contract @mozilla.org/network/protocol/about;1?what=logins {322ba47e-7047-4f71-aebf-cb7d69325cd9}
# DirectoryProvider.js

View File

@ -0,0 +1,13 @@
<!-- 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/. -->
<!ENTITY aboutAccounts.connected.title "Firefox Accounts">
<!ENTITY aboutAccounts.connected.description "You are connected as">
<!ENTITY aboutAccounts.syncPreferences.label "Tap here to check Sync settings">
<!ENTITY aboutAccounts.noConnection.title "No Internet connection">
<!ENTITY aboutAccounts.retry.label "Try again">
<!ENTITY aboutAccounts.restrictedError.title "Restricted">
<!ENTITY aboutAccounts.restrictedError.description "You cannot manage Firefox Accounts from this profile.">

View File

@ -8,6 +8,7 @@
% locale browser @AB_CD@ %locale/@AB_CD@/browser/
locale/@AB_CD@/browser/about.dtd (%chrome/about.dtd)
#ifndef MOZ_ANDROID_NATIVE_ACCOUNT_UI
locale/@AB_CD@/browser/aboutAccounts.dtd (%chrome/aboutAccounts.dtd)
locale/@AB_CD@/browser/aboutAccounts.properties (%chrome/aboutAccounts.properties)
#endif
locale/@AB_CD@/browser/aboutAddons.dtd (%chrome/aboutAddons.dtd)

View File

@ -0,0 +1,91 @@
/* 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/. */
html, body {
height: 100%;
width: 100%;
}
div {
transition: opacity 0.4s ease-in;
}
#spinner {
transition: opacity 0.2s ease-in;
}
#remote {
border: 0;
opacity: 0;
transition: opacity 0.4s ease-in;
}
.text {
color: #363B40;
font-size: 25px;
font-weight: lighter;
margin-bottom: 20px;
}
.hint {
color: #777777;
font-size: 20px;
margin-bottom: 20px;
}
a {
color: #0096DD; /* link_blue */
text-decoration: none;
font-size: 20px;
margin-bottom: 20px;
}
a:active {
color: #0082C6; /* link_blue_pressed */
}
.toplevel {
width: 100%;
height: 100%;
position: absolute;
}
.container {
height: 100%;
padding-left: 30px;
padding-right: 30px;
}
.text-container {
padding-top: 60px;
padding-left: 30px;
padding-right: 30px;
}
.flex-column {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.button-row {
flex: 0;
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
text-align: center;
justify-content: center;
}
.button {
flex: 1;
height: 60px;
background-color: #E66000; /*matched to action_orange in java codebase*/
color: #FFFFFF;
font-size: 20px;
border-radius: 4px;
border-width: 0px;
}

View File

@ -200,8 +200,6 @@ body {
#spinner {
margin-top: 60px;
height: 60px;
width: 60px;
}
#empty-body {

View File

@ -1,61 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- 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/. -->
<svg class="spinner" width="65px" height="65px" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg">
<style type="text/css">
.spinner {
animation: rotator 1.4s linear infinite;
}
@keyframes rotator {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(270deg);
}
}
.path {
stroke-dasharray: 187;
stroke-dashoffset: 0;
transform-origin: center;
animation: dash 1.4s ease-in-out infinite, colors 5.6s ease-in-out infinite;
}
@keyframes colors {
0% {
stroke: #FF9500;
}
25% {
stroke: #FF9500;
}
50% {
stroke: #FF9500;
}
75% {
stroke: #FF9500;
}
100% {
stroke: #FF9500;
}
}
@keyframes dash {
0% {
stroke-dashoffset: 187;
}
50% {
stroke-dashoffset: 46.75;
transform: rotate(135deg);
}
100% {
stroke-dashoffset: 187;
transform: rotate(450deg);
}
}
</style>
<circle class="path" fill="none" stroke-width="6" stroke-linecap="round" cx="33" cy="33" r="30"></circle>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -8,6 +8,9 @@ chrome.jar:
% skin browser classic/1.0 %skin/
skin/aboutPage.css (aboutPage.css)
skin/about.css (about.css)
#ifndef MOZ_ANDROID_NATIVE_ACCOUNT_UI
skin/aboutAccounts.css (aboutAccounts.css)
#endif
* skin/aboutAddons.css (aboutAddons.css)
* skin/aboutBase.css (aboutBase.css)
#ifdef MOZ_DEVICES
@ -29,6 +32,7 @@ chrome.jar:
skin/config.css (config.css)
skin/touchcontrols.css (touchcontrols.css)
skin/netError.css (netError.css)
skin/spinner.css (spinner.css)
% override chrome://global/skin/about.css chrome://browser/skin/about.css
% override chrome://global/skin/aboutMemory.css chrome://browser/skin/aboutMemory.css
% override chrome://global/skin/aboutReader.css chrome://browser/skin/aboutReader.css
@ -44,7 +48,6 @@ chrome.jar:
#
* skin/aboutLogins.css (aboutLogins.css)
skin/images/spinning_throbber.svg (images/spinning_throbber.svg)
skin/images/search.png (images/search.png)
skin/images/lock.png (images/lock.png)

View File

@ -0,0 +1,124 @@
/* 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/. */
.mui-refresh-main {
padding: 0;
overflow: hidden;
border-radius: 999px;
position: relative;
}
.mui-refresh-wrapper {
width: 60px;
height: 60px;
}
.mui-spinner-main {
width: 60px;
height: 60px;
position: relative;
animation: sporadic-rotate 5.25s cubic-bezier(.35, 0, .25, 1) infinite;
}
.mui-spinner-wrapper {
animation: outer-rotate 2.91667s linear infinite;
}
.mui-spinner-left, .mui-spinner-right {
position: absolute;
top: 0;
height: 60px;
width: 30px;
overflow: hidden;
}
.mui-spinner-left {
left: 0;
}
.mui-spinner-right {
right: 0;
}
.mui-half-circle-left, .mui-half-circle-right {
position: absolute;
top: 0;
width: 60px;
height: 60px;
box-sizing: border-box;
border-width: 5px;
border-style: solid;
border-color: #000 #000 transparent;
border-radius: 999px;
animation-iteration-count: infinite;
animation-duration: 1.3125s;
animation-timing-function: cubic-bezier(.35, 0, .25, 1);
}
.mui-half-circle-left {
left: 0;
border-right-color: transparent;
border-top-color: #FF9500; /*matched to fennec_ui_orange in java codebase*/
border-left-color: #FF9500; /*matched to fennec_ui_orange in java codebase*/
animation-name: left-wobble;
}
.mui-half-circle-right {
right: 0;
border-left-color: transparent;
border-top-color: #FF9500; /*matched to fennec_ui_orange in java codebase*/
border-right-color: #FF9500; /*matched to fennec_ui_orange in java codebase*/
animation-name: right-wobble;
}
@keyframes outer-rotate {
100% {
transform: rotate(360deg);
}
}
@keyframes left-wobble {
0%, 100% {
transform: rotate(130deg);
}
50% {
transform: rotate(-5deg);
}
}
@keyframes right-wobble {
0%, 100% {
transform: rotate(-130deg);
}
50% {
transform: rotate(5deg);
}
}
@keyframes sporadic-rotate {
12.5% {
transform: rotate(135deg);
}
25% {
transform: rotate(270deg);
}
37.5% {
transform: rotate(405deg);
}
50% {
transform: rotate(540deg);
}
62.5% {
transform: rotate(675deg);
}
75% {
transform: rotate(810deg);
}
87.5% {
transform: rotate(945deg);
}
100% {
transform: rotate(1080deg);
}
}

View File

@ -8176,7 +8176,7 @@
},
"LOOP_SHARING_ROOM_URL": {
"alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"],
"expires_in_version": "44",
"expires_in_version": "45",
"kind": "enumerated",
"n_values": 8,
"releaseChannelCollection": "opt-out",
@ -8184,7 +8184,7 @@
},
"LOOP_ROOM_CREATE": {
"alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"],
"expires_in_version": "44",
"expires_in_version": "45",
"kind": "enumerated",
"n_values": 4,
"releaseChannelCollection": "opt-out",
@ -8192,7 +8192,7 @@
},
"LOOP_ROOM_DELETE": {
"alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"],
"expires_in_version": "44",
"expires_in_version": "45",
"kind": "enumerated",
"n_values": 4,
"releaseChannelCollection": "opt-out",
@ -8200,7 +8200,7 @@
},
"LOOP_ROOM_CONTEXT_ADD": {
"alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"],
"expires_in_version": "44",
"expires_in_version": "45",
"kind": "enumerated",
"n_values": 8,
"releaseChannelCollection": "opt-out",
@ -8208,7 +8208,7 @@
},
"LOOP_ROOM_CONTEXT_CLICK": {
"alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"],
"expires_in_version": "44",
"expires_in_version": "45",
"kind": "count",
"releaseChannelCollection": "opt-out",
"description": "Number times room context is clicked to visit the attached URL"

Some files were not shown because too many files have changed in this diff Show More