Bug 1350411 - Add Message Channel for Activity Stream system add-on
MozReview-Commit-ID: DCcGDjKdIHh
--- a/browser/extensions/activity-stream/common/Actions.jsm
+++ b/browser/extensions/activity-stream/common/Actions.jsm
@@ -1,19 +1,130 @@
/* 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";
+this.MAIN_MESSAGE_TYPE = "ActivityStream:Main";
+this.CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
+
this.actionTypes = [
"INIT",
"UNINIT",
+ "NEW_TAB_INITIAL_STATE",
+ "NEW_TAB_LOAD",
+ "NEW_TAB_UNLOAD"
// The line below creates an object like this:
// {
// INIT: "INIT",
// UNINIT: "UNINIT"
// }
// It prevents accidentally adding a different key/value name.
].reduce((obj, type) => { obj[type] = type; return obj; }, {});
+// Helper function for creating routed actions between content and main
+// Not intended to be used by consumers
+function _RouteMessage(action, options) {
+ const meta = action.meta ? Object.assign({}, action.meta) : {};
+ if (!options || !options.from || !options.to) {
+ throw new Error("Routed Messages must have options as the second parameter, and must at least include a .from and .to property.");
+ }
+ // For each of these fields, if they are passed as an option,
+ // add them to the action. If they are not defined, remove them.
+ ["from", "to", "toTarget", "fromTarget", "skipOrigin"].forEach(o => {
+ if (typeof options[o] !== "undefined") {
+ meta[o] = options[o];
+ } else if (meta[o]) {
+ delete meta[o];
+ }
+ });
+ return Object.assign({}, action, {meta});
+}
+
+/**
+ * SendToMain - Creates a message that will be sent to the Main process.
+ *
+ * @param {object} action Any redux action (required)
+ * @param {object} options
+ * @param {string} options.fromTarget The id of the content port from which the action originated. (optional)
+ * @return {object} An action with added .meta properties
+ */
+function SendToMain(action, options = {}) {
+ return _RouteMessage(action, {
+ from: CONTENT_MESSAGE_TYPE,
+ to: MAIN_MESSAGE_TYPE,
+ fromTarget: options.fromTarget
+ });
+}
+
+/**
+ * BroadcastToContent - Creates a message that will be sent to ALL content processes.
+ *
+ * @param {object} action Any redux action (required)
+ * @return {object} An action with added .meta properties
+ */
+function BroadcastToContent(action) {
+ return _RouteMessage(action, {
+ from: MAIN_MESSAGE_TYPE,
+ to: CONTENT_MESSAGE_TYPE
+ });
+}
+
+/**
+ * SendToContent - Creates a message that will be sent to a particular Content process.
+ *
+ * @param {object} action Any redux action (required)
+ * @param {string} target The id of a content port
+ * @return {object} An action with added .meta properties
+ */
+function SendToContent(action, target) {
+ if (!target) {
+ throw new Error("You must provide a target ID as the second parameter of SendToContent. If you want to send to all content processes, use BroadcastToContent");
+ }
+ return _RouteMessage(action, {
+ from: MAIN_MESSAGE_TYPE,
+ to: CONTENT_MESSAGE_TYPE,
+ toTarget: target
+ });
+}
+
+this.actionCreators = {
+ SendToMain,
+ SendToContent,
+ BroadcastToContent
+};
+
+// These are helpers to test for certain kinds of actions
+this.actionUtils = {
+ isSendToMain(action) {
+ if (!action.meta) {
+ return false;
+ }
+ return action.meta.to === MAIN_MESSAGE_TYPE && action.meta.from === CONTENT_MESSAGE_TYPE;
+ },
+ isBroadcastToContent(action) {
+ if (!action.meta) {
+ return false;
+ }
+ if (action.meta.to === CONTENT_MESSAGE_TYPE && !action.meta.toTarget) {
+ return true;
+ }
+ return false;
+ },
+ isSendToContent(action) {
+ if (!action.meta) {
+ return false;
+ }
+ if (action.meta.to === CONTENT_MESSAGE_TYPE && action.meta.toTarget) {
+ return true;
+ }
+ return false;
+ },
+ _RouteMessage
+};
+
this.EXPORTED_SYMBOLS = [
- "actionTypes"
+ "actionTypes",
+ "actionCreators",
+ "actionUtils",
+ "MAIN_MESSAGE_TYPE",
+ "CONTENT_MESSAGE_TYPE"
];
--- a/browser/extensions/activity-stream/data/content/activity-stream.html
+++ b/browser/extensions/activity-stream/data/content/activity-stream.html
@@ -2,11 +2,30 @@
<html lang="en-us" dir="ltr">
<head>
<meta charset="utf-8">
<title>New Tab</title>
</head>
<body>
<div id="root">
<h1>New Tab</h1>
+ <ul id="top-sites"></ul>
</div>
+ <script>
+ const topSitesEl = document.getElementById("top-sites");
+ window.addMessageListener("ActivityStream:MainToContent", msg => {
+ if (msg.data.type === "NEW_TAB_INITIAL_STATE") {
+ const fragment = document.createDocumentFragment()
+ for (const row of msg.data.data.TopSites.rows) {
+ const li = document.createElement("li");
+ const a = document.createElement("a");
+ a.href = row.url;
+ a.textContent = row.title;
+ li.appendChild(a);
+ fragment.appendChild(li);
+ }
+ topSitesEl.appendChild(fragment);
+ }
+ });
+
+ </script>
</body>
</html>
--- a/browser/extensions/activity-stream/lib/ActivityStream.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStream.jsm
@@ -1,16 +1,19 @@
/* 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 {utils: Cu} = Components;
const {Store} = Cu.import("resource://activity-stream/lib/Store.jsm", {});
+// Feeds
+const {NewTabInit} = Cu.import("resource://activity-stream/lib/NewTabInit.jsm", {});
+
this.ActivityStream = class ActivityStream {
/**
* constructor - Initializes an instance of ActivityStream
*
* @param {object} options Options for the ActivityStream instance
* @param {string} options.id Add-on ID. e.g. "activity-stream@mozilla.org".
* @param {string} options.version Version of the add-on. e.g. "0.1.0"
@@ -18,17 +21,19 @@ this.ActivityStream = class ActivityStre
*/
constructor(options) {
this.initialized = false;
this.options = options;
this.store = new Store();
}
init() {
this.initialized = true;
- this.store.init();
+ this.store.init([
+ new NewTabInit()
+ ]);
}
uninit() {
this.store.uninit();
this.initialized = false;
}
};
this.EXPORTED_SYMBOLS = ["ActivityStream"];
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
@@ -0,0 +1,197 @@
+/* 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/. */
+/* globals AboutNewTab, RemotePages, XPCOMUtils */
+
+"use strict";
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const {
+ actionUtils: au,
+ actionCreators: ac,
+ actionTypes: at
+} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
+
+XPCOMUtils.defineLazyModuleGetter(this, "AboutNewTab",
+ "resource:///modules/AboutNewTab.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "RemotePages",
+ "resource://gre/modules/RemotePageManager.jsm");
+
+const ABOUT_NEW_TAB_URL = "about:newtab";
+
+const DEFAULT_OPTIONS = {
+ dispatch(action) {
+ throw new Error(`\nMessageChannel: Received action ${action.type}, but no dispatcher was defined.\n`);
+ },
+ pageURL: ABOUT_NEW_TAB_URL,
+ outgoingMessageName: "ActivityStream:MainToContent",
+ incomingMessageName: "ActivityStream:ContentToMain"
+};
+
+this.ActivityStreamMessageChannel = class ActivityStreamMessageChannel {
+ /**
+ * ActivityStreamMessageChannel - This module connects a Redux store to a RemotePageManager in Firefox.
+ * Call .createChannel to start the connection, and .destroyChannel to destroy it.
+ * You should use the BroadcastToContent, SendToContent, and SendToMain action creators
+ * in common/Actions.jsm to help you create actions that will be automatically routed
+ * to the correct location.
+ *
+ * @param {object} options
+ * @param {function} options.dispatch The dispatch method from a Redux store
+ * @param {string} options.pageURL The URL to which a RemotePageManager should be attached.
+ * Note that if it is about:newtab, the existing RemotePageManager
+ * for about:newtab will also be disabled
+ * @param {string} options.outgoingMessageName The name of the message sent to child processes
+ * @param {string} options.incomingMessageName The name of the message received from child processes
+ * @return {ActivityStreamMessageChannel}
+ */
+ constructor(options = {}) {
+ Object.assign(this, DEFAULT_OPTIONS, options);
+ this.channel = null;
+
+ this.middleware = this.middleware.bind(this);
+ this.onMessage = this.onMessage.bind(this);
+ this.onNewTabLoad = this.onNewTabLoad.bind(this);
+ this.onNewTabUnload = this.onNewTabUnload.bind(this);
+ }
+
+ /**
+ * middleware - Redux middleware that looks for SendToContent and BroadcastToContent type
+ * actions, and sends them out.
+ *
+ * @param {object} store A redux store
+ * @return {function} Redux middleware
+ */
+ middleware(store) {
+ return next => action => {
+ if (!this.channel) {
+ next(action);
+ return;
+ }
+ if (au.isSendToContent(action)) {
+ this.send(action);
+ } else if (au.isBroadcastToContent(action)) {
+ this.broadcast(action);
+ }
+ next(action);
+ };
+ }
+
+ /**
+ * onActionFromContent - Handler for actions from a content processes
+ *
+ * @param {object} action A Redux action
+ * @param {string} targetId The portID of the port that sent the message
+ */
+ onActionFromContent(action, targetId) {
+ this.dispatch(ac.SendToMain(action, {fromTarget: targetId}));
+ }
+
+ /**
+ * broadcast - Sends an action to all ports
+ *
+ * @param {object} action A Redux action
+ */
+ broadcast(action) {
+ this.channel.sendAsyncMessage(this.outgoingMessageName, action);
+ }
+
+ /**
+ * send - Sends an action to a specific port
+ *
+ * @param {obj} action A redux action; it should contain a portID in the meta.toTarget property
+ */
+ send(action) {
+ const targetId = action.meta && action.meta.toTarget;
+ const target = this.getTargetById(targetId);
+ if (!target) {
+ // The target is no longer around - maybe the user closed the page
+ return;
+ }
+ target.sendAsyncMessage(this.outgoingMessageName, action);
+ }
+
+ /**
+ * getIdByTarget - Retrieve the id of a message target, if it exists in this.targets
+ *
+ * @param {obj} targetObj A message target
+ * @return {string|null} The unique id of the target, if it exists.
+ */
+ getTargetById(id) {
+ for (let port of this.channel.messagePorts) {
+ if (port.portID === id) {
+ return port;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * createChannel - Create RemotePages channel to establishing message passing
+ * between the main process and child pages
+ */
+ createChannel() {
+ // RemotePageManager must be disabled for about:newtab, since only one can exist at once
+ if (this.pageURL === ABOUT_NEW_TAB_URL) {
+ AboutNewTab.override();
+ }
+ this.channel = new RemotePages(this.pageURL);
+ this.channel.addMessageListener("RemotePage:Load", this.onNewTabLoad);
+ this.channel.addMessageListener("RemotePage:Unload", this.onNewTabUnload);
+ this.channel.addMessageListener(this.incomingMessageName, this.onMessage);
+ }
+
+ /**
+ * destroyChannel - Destroys the RemotePages channel
+ */
+ destroyChannel() {
+ this.channel.destroy();
+ this.channel = null;
+ if (this.pageURL === ABOUT_NEW_TAB_URL) {
+ AboutNewTab.reset();
+ }
+ }
+
+ /**
+ * onNewTabLoad - Handler for special RemotePage:Load message fired by RemotePages
+ *
+ * @param {obj} msg The messsage from a page that was just loaded
+ */
+ onNewTabLoad(msg) {
+ this.onActionFromContent({type: at.NEW_TAB_LOAD}, msg.target.portID);
+ }
+
+ /**
+ * onNewTabUnloadLoad - Handler for special RemotePage:Unload message fired by RemotePages
+ *
+ * @param {obj} msg The messsage from a page that was just unloaded
+ */
+ onNewTabUnload(msg) {
+ this.onActionFromContent({type: at.NEW_TAB_UNLOAD}, msg.target.portID);
+ }
+
+ /**
+ * onMessage - Handles custom messages from content. It expects all messages to
+ * be formatted as Redux actions, and dispatches them to this.store
+ *
+ * @param {obj} msg A custom message from content
+ * @param {obj} msg.action A Redux action (e.g. {type: "HELLO_WORLD"})
+ * @param {obj} msg.target A message target
+ */
+ onMessage(msg) {
+ const action = msg.data;
+ const {portID} = msg.target;
+ if (!action || !action.type) {
+ Cu.reportError(new Error(`Received an improperly formatted message from ${portID}`));
+ return;
+ }
+ this.onActionFromContent(action, msg.target.portID);
+ }
+}
+
+this.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
+this.EXPORTED_SYMBOLS = ["ActivityStreamMessageChannel", "DEFAULT_OPTIONS"];
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/lib/NewTabInit.jsm
@@ -0,0 +1,25 @@
+/* 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 {utils: Cu} = Components;
+const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
+
+/**
+ * NewTabInit - A placeholder for now. This will send a copy of the state to all
+ * newly opened tabs.
+ */
+this.NewTabInit = class NewTabInit {
+ onAction(action) {
+ let newAction;
+ switch (action.type) {
+ case at.NEW_TAB_LOAD:
+ newAction = {type: at.NEW_TAB_INITIAL_STATE, data: this.store.getState()};
+ this.store.dispatch(ac.SendToContent(newAction, action.meta.fromTarget));
+ break;
+ }
+ }
+};
+
+this.EXPORTED_SYMBOLS = ["NewTabInit"];
--- a/browser/extensions/activity-stream/lib/Store.jsm
+++ b/browser/extensions/activity-stream/lib/Store.jsm
@@ -3,76 +3,83 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {utils: Cu} = Components;
const {redux} = Cu.import("resource://activity-stream/vendor/Redux.jsm", {});
const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
const {reducers} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
+const {ActivityStreamMessageChannel} = Cu.import("resource://activity-stream/lib/ActivityStreamMessageChannel.jsm", {});
/**
* Store - This has a similar structure to a redux store, but includes some extra
- * functionality. It accepts an array of "Feeds" on inititalization, which
+ * functionality to allow for routing of actions between the Main processes
+ * and child processes via a ActivityStreamMessageChannel.
+ * It also accepts an array of "Feeds" on inititalization, which
* can listen for any action that is dispatched through the store.
*/
this.Store = class Store {
/**
- * constructor - The redux store is created here,
+ * constructor - The redux store and message manager are created here,
* but no listeners are added until "init" is called.
*/
constructor() {
this._middleware = this._middleware.bind(this);
// Bind each redux method so we can call it directly from the Store. E.g.,
// store.dispatch() will call store._store.dispatch();
["dispatch", "getState", "subscribe"].forEach(method => {
this[method] = function(...args) {
return this._store[method](...args);
}.bind(this);
});
this.feeds = new Set();
+ this._messageChannel = new ActivityStreamMessageChannel({dispatch: this.dispatch});
this._store = redux.createStore(
redux.combineReducers(reducers),
- redux.applyMiddleware(this._middleware)
+ redux.applyMiddleware(this._middleware, this._messageChannel.middleware)
);
}
/**
* _middleware - This is redux middleware consumed by redux.createStore.
* it calls each feed's .onAction method, if one
* is defined.
*/
_middleware(store) {
return next => action => {
next(action);
this.feeds.forEach(s => s.onAction && s.onAction(action));
};
}
/**
- * init - Initializes the MessageManager channel, and adds feeds.
+ * init - Initializes the ActivityStreamMessageChannel channel, and adds feeds.
* After initialization has finished, an INIT action is dispatched.
*
* @param {array} feeds An array of objects with an optional .onAction method
*/
init(feeds) {
if (feeds) {
feeds.forEach(subscriber => {
subscriber.store = this;
this.feeds.add(subscriber);
});
}
+ this._messageChannel.createChannel();
this.dispatch({type: at.INIT});
}
/**
- * uninit - Clears all feeds, dispatches an UNINIT action
+ * uninit - Clears all feeds, dispatches an UNINIT action, and
+ * destroys the message manager channel.
*
* @return {type} description
*/
uninit() {
this.feeds.clear();
this.dispatch({type: at.UNINIT});
+ this._messageChannel.destroyChannel();
}
};
this.EXPORTED_SYMBOLS = ["Store"];