Bug 1359481 - Add top sites and search to activity-stream system add-on. r=Mardak
authorEd Lee <edilee@mozilla.com>
Tue, 25 Apr 2017 13:31:29 -0700
changeset 354947 a3b4b98b525b
parent 354946 24f0f4caa031
child 354948 488d0f41b405
push id31717
push usercbook@mozilla.com
push dateWed, 26 Apr 2017 06:41:51 +0000
treeherdermozilla-central@0f5ba06c4c59 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMardak
bugs1359481
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1359481 - Add top sites and search to activity-stream system add-on. r=Mardak
.eslintignore
browser/extensions/activity-stream/bootstrap.js
browser/extensions/activity-stream/common/Actions.jsm
browser/extensions/activity-stream/common/Reducers.jsm
browser/extensions/activity-stream/data/content/activity-stream.bundle.js
browser/extensions/activity-stream/data/content/activity-stream.css
browser/extensions/activity-stream/data/content/activity-stream.html
browser/extensions/activity-stream/data/content/assets/glyph-forward-16-white.svg
browser/extensions/activity-stream/data/content/assets/glyph-forward-16.svg
browser/extensions/activity-stream/data/content/assets/glyph-search-16.svg
browser/extensions/activity-stream/data/content/assets/glyph-search-history.svg
browser/extensions/activity-stream/jar.mn
browser/extensions/activity-stream/lib/ActivityStream.jsm
browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
browser/extensions/activity-stream/lib/SearchFeed.jsm
browser/extensions/activity-stream/lib/Store.jsm
browser/extensions/activity-stream/lib/TopSitesFeed.jsm
browser/extensions/activity-stream/test/.eslintrc.js
browser/extensions/activity-stream/test/functional/mochitest/.eslintrc.js
browser/extensions/activity-stream/test/functional/mochitest/browser.ini
browser/extensions/activity-stream/test/functional/mochitest/browser_as_load_location.js
browser/extensions/activity-stream/test/functional/mochitest/browser_dummy_test.js
browser/extensions/activity-stream/test/mozinfo.json
browser/extensions/activity-stream/test/unit/common/Actions.test.js
browser/extensions/activity-stream/test/unit/common/Reducers.test.js
browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
browser/extensions/activity-stream/test/unit/lib/ActivityStreamMessageChannel.test.js
browser/extensions/activity-stream/test/unit/lib/SearchFeed.test.js
browser/extensions/activity-stream/test/unit/lib/Store.test.js
browser/extensions/activity-stream/test/unit/lib/TelemetrySender.test.js
browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
browser/extensions/activity-stream/test/unit/lib/init-store.test.js
browser/extensions/activity-stream/test/unit/unit-entry.js
browser/extensions/activity-stream/test/unit/utils.js
browser/extensions/activity-stream/vendor/redux.js
--- a/.eslintignore
+++ b/.eslintignore
@@ -67,17 +67,18 @@ browser/components/translation/cld2/**
 # Screenshots is imported as a system add-on and has its own lint rules currently.
 browser/extensions/screenshots/**
 browser/extensions/pdfjs/content/build**
 browser/extensions/pdfjs/content/web**
 # generated or library files in pocket
 browser/extensions/pocket/content/panels/js/tmpl.js
 browser/extensions/pocket/content/panels/js/vendor/**
 browser/locales/**
-# vendor library files in activity-stream
+# generated or library files in activity-stream
+browser/extensions/activity-stream/data/content/activity-stream.bundle.js
 browser/extensions/activity-stream/vendor/**
 # imported from chromium
 browser/extensions/mortar/**
 
 # devtools/ exclusions
 devtools/client/canvasdebugger/**
 devtools/client/commandline/**
 devtools/client/debugger/**
--- a/browser/extensions/activity-stream/bootstrap.js
+++ b/browser/extensions/activity-stream/bootstrap.js
@@ -1,29 +1,32 @@
 /* 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 Components, XPCOMUtils, Preferences, ActivityStream */
+/* globals Components, XPCOMUtils, Preferences, Services, ActivityStream */
 "use strict";
 
 const {utils: Cu} = Components;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ActivityStream",
   "resource://activity-stream/lib/ActivityStream.jsm");
 
+const BROWSER_READY_NOTIFICATION = "browser-ui-startup-complete";
 const ACTIVITY_STREAM_ENABLED_PREF = "browser.newtabpage.activity-stream.enabled";
 const REASON_STARTUP_ON_PREF_CHANGE = "PREF_ON";
 const REASON_SHUTDOWN_ON_PREF_CHANGE = "PREF_OFF";
 
 const ACTIVITY_STREAM_OPTIONS = {newTabURL: "about:newtab"};
 
 let activityStream;
 let startupData;
+let startupReason;
 
 /**
  * init - Initializes an instance of ActivityStream. This could be called by
  *        the startup() function exposed by bootstrap.js, or it could be called
  *        when ACTIVITY_STREAM_ENABLED_PREF is changed from false to true.
  *
  * @param  {string} reason - Reason for initialization. Could be install, upgrade, or PREF_ON
  */
@@ -59,36 +62,47 @@ function uninit(reason) {
 function onPrefChanged(isEnabled) {
   if (isEnabled) {
     init(REASON_STARTUP_ON_PREF_CHANGE);
   } else {
     uninit(REASON_SHUTDOWN_ON_PREF_CHANGE);
   }
 }
 
+function observe(subject, topic, data) {
+  switch (topic) {
+    case BROWSER_READY_NOTIFICATION:
+      // Listen for changes to the pref that enables Activity Stream
+      Preferences.observe(ACTIVITY_STREAM_ENABLED_PREF, onPrefChanged);
+      // Only initialize if the pref is true
+      if (Preferences.get(ACTIVITY_STREAM_ENABLED_PREF)) {
+        init(startupReason);
+        Services.obs.removeObserver(this, BROWSER_READY_NOTIFICATION);
+      }
+      break;
+  }
+}
+
 // The functions below are required by bootstrap.js
 
 this.install = function install(data, reason) {};
 
 this.startup = function startup(data, reason) {
+  // Only start Activity Stream up when the browser UI is ready
+  Services.obs.addObserver(observe, BROWSER_READY_NOTIFICATION);
+
   // Cache startup data which contains stuff like the version number, etc.
   // so we can use it when we init
   startupData = data;
-
-  // Listen for changes to the pref that enables Activity Stream
-  Preferences.observe(ACTIVITY_STREAM_ENABLED_PREF, onPrefChanged);
-
-  // Only initialize if the pref is true
-  if (Preferences.get(ACTIVITY_STREAM_ENABLED_PREF)) {
-    init(reason);
-  }
+  startupReason = reason;
 };
 
 this.shutdown = function shutdown(data, reason) {
   // Uninitialize Activity Stream
   startupData = null;
+  startupReason = null;
   uninit(reason);
 
   // Stop listening to the pref that enables Activity Stream
   Preferences.ignore(ACTIVITY_STREAM_ENABLED_PREF, onPrefChanged);
 };
 
 this.uninstall = function uninstall(data, reason) {};
--- a/browser/extensions/activity-stream/common/Actions.jsm
+++ b/browser/extensions/activity-stream/common/Actions.jsm
@@ -1,22 +1,26 @@
 /* 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";
+const MAIN_MESSAGE_TYPE = "ActivityStream:Main";
+const CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
 
-this.actionTypes = [
+const actionTypes = [
   "INIT",
   "UNINIT",
   "NEW_TAB_INITIAL_STATE",
   "NEW_TAB_LOAD",
-  "NEW_TAB_UNLOAD"
+  "NEW_TAB_UNLOAD",
+  "PERFORM_SEARCH",
+  "SCREENSHOT_UPDATED",
+  "SEARCH_STATE_UPDATED",
+  "TOP_SITES_UPDATED"
 // 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; }, {});
 
@@ -81,16 +85,18 @@ function SendToContent(action, target) {
   }
   return _RouteMessage(action, {
     from: MAIN_MESSAGE_TYPE,
     to: CONTENT_MESSAGE_TYPE,
     toTarget: target
   });
 }
 
+this.actionTypes = actionTypes;
+
 this.actionCreators = {
   SendToMain,
   SendToContent,
   BroadcastToContent
 };
 
 // These are helpers to test for certain kinds of actions
 this.actionUtils = {
--- a/browser/extensions/activity-stream/common/Reducers.jsm
+++ b/browser/extensions/activity-stream/common/Reducers.jsm
@@ -1,44 +1,65 @@
 /* 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.INITIAL_STATE = {
+const {actionTypes: at} = Components.utils.import("resource://activity-stream/common/Actions.jsm", {});
+
+const INITIAL_STATE = {
   TopSites: {
-    rows: [
-      {
-        "title": "Facebook",
-        "url": "https://www.facebook.com/"
-      },
-      {
-        "title": "YouTube",
-        "url": "https://www.youtube.com/"
-      },
-      {
-        "title": "Amazon",
-        "url": "http://www.amazon.com/"
-      },
-      {
-        "title": "Yahoo",
-        "url": "https://www.yahoo.com/"
-      },
-      {
-        "title": "eBay",
-        "url": "http://www.ebay.com"
-      },
-      {
-        "title": "Twitter",
-        "url": "https://twitter.com/"
-      }
-    ]
+    init: false,
+    rows: []
+  },
+  Search: {
+    currentEngine: {
+      name: "",
+      icon: ""
+    },
+    engines: []
   }
 };
 
 // TODO: Handle some real actions here, once we have a TopSites feed working
 function TopSites(prevState = INITIAL_STATE.TopSites, action) {
-  return prevState;
+  let hasMatch;
+  let newRows;
+  switch (action.type) {
+    case at.TOP_SITES_UPDATED:
+      if (!action.data) {
+        return prevState;
+      }
+      return Object.assign({}, prevState, {init: true, rows: action.data});
+    case at.SCREENSHOT_UPDATED:
+      newRows = prevState.rows.map(row => {
+        if (row.url === action.data.url) {
+          hasMatch = true;
+          return Object.assign({}, row, {screenshot: action.data.screenshot});
+        }
+        return row;
+      });
+      return hasMatch ? Object.assign({}, prevState, {rows: newRows}) : prevState;
+    default:
+      return prevState;
+  }
 }
 
-this.reducers = {TopSites};
+function Search(prevState = INITIAL_STATE.Search, action) {
+  switch (action.type) {
+    case at.SEARCH_STATE_UPDATED: {
+      if (!action.data) {
+        return prevState;
+      }
+      let {currentEngine, engines} = action.data;
+      return Object.assign({}, prevState, {
+        currentEngine,
+        engines
+      });
+    }
+    default:
+      return prevState;
+  }
+}
+this.INITIAL_STATE = INITIAL_STATE;
+this.reducers = {TopSites, Search};
 
 this.EXPORTED_SYMBOLS = ["reducers", "INITIAL_STATE"];
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
@@ -0,0 +1,570 @@
+/******/ (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] = {
+/******/ 			i: moduleId,
+/******/ 			l: false,
+/******/ 			exports: {}
+/******/ 		};
+/******/
+/******/ 		// Execute the module function
+/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ 		// Flag the module as loaded
+/******/ 		module.l = 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;
+/******/
+/******/ 	// identity function for calling harmony imports with the correct context
+/******/ 	__webpack_require__.i = function(value) { return value; };
+/******/
+/******/ 	// define getter function for harmony exports
+/******/ 	__webpack_require__.d = function(exports, name, getter) {
+/******/ 		if(!__webpack_require__.o(exports, name)) {
+/******/ 			Object.defineProperty(exports, name, {
+/******/ 				configurable: false,
+/******/ 				enumerable: true,
+/******/ 				get: getter
+/******/ 			});
+/******/ 		}
+/******/ 	};
+/******/
+/******/ 	// getDefaultExport function for compatibility with non-harmony modules
+/******/ 	__webpack_require__.n = function(module) {
+/******/ 		var getter = module && module.__esModule ?
+/******/ 			function getDefault() { return module['default']; } :
+/******/ 			function getModuleExports() { return module; };
+/******/ 		__webpack_require__.d(getter, 'a', getter);
+/******/ 		return getter;
+/******/ 	};
+/******/
+/******/ 	// Object.prototype.hasOwnProperty.call
+/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ 	// __webpack_public_path__
+/******/ 	__webpack_require__.p = "";
+/******/
+/******/ 	// Load entry module and return exports
+/******/ 	return __webpack_require__(__webpack_require__.s = 10);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports) {
+
+module.exports = React;
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports) {
+
+module.exports = ReactRedux;
+
+/***/ }),
+/* 2 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/* 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/. */
+
+
+const MAIN_MESSAGE_TYPE = "ActivityStream:Main";
+const CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
+
+const actionTypes = ["INIT", "UNINIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_UNLOAD", "PERFORM_SEARCH", "SCREENSHOT_UPDATED", "SEARCH_STATE_UPDATED", "TOP_SITES_UPDATED"
+// 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) {
+  let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+
+  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
+  });
+}
+
+var actionCreators = {
+  SendToMain,
+  SendToContent,
+  BroadcastToContent
+};
+
+// These are helpers to test for certain kinds of actions
+
+var 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
+};
+module.exports = {
+  actionTypes,
+  actionCreators,
+  actionUtils,
+  MAIN_MESSAGE_TYPE,
+  CONTENT_MESSAGE_TYPE
+};
+
+/***/ }),
+/* 3 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+const React = __webpack_require__(0);
+const TopSites = __webpack_require__(8);
+const Search = __webpack_require__(7);
+
+const Base = () => React.createElement(
+  "div",
+  { className: "outer-wrapper" },
+  React.createElement(
+    "main",
+    null,
+    React.createElement(Search, null),
+    React.createElement(TopSites, null)
+  )
+);
+
+module.exports = Base;
+
+/***/ }),
+/* 4 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+/* globals sendAsyncMessage, addMessageListener */
+
+var _require = __webpack_require__(9);
+
+const createStore = _require.createStore,
+      combineReducers = _require.combineReducers,
+      applyMiddleware = _require.applyMiddleware;
+
+var _require2 = __webpack_require__(2);
+
+const au = _require2.actionUtils;
+
+
+const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE";
+const OUTGOING_MESSAGE_NAME = "ActivityStream:ContentToMain";
+const INCOMING_MESSAGE_NAME = "ActivityStream:MainToContent";
+
+/**
+ * A higher-order function which returns a reducer that, on MERGE_STORE action,
+ * will return the action.data object merged into the previous state.
+ *
+ * For all other actions, it merely calls mainReducer.
+ *
+ * Because we want this to merge the entire state object, it's written as a
+ * higher order function which takes the main reducer (itself often a call to
+ * combineReducers) as a parameter.
+ *
+ * @param  {function} mainReducer reducer to call if action != MERGE_STORE_ACTION
+ * @return {function}             a reducer that, on MERGE_STORE_ACTION action,
+ *                                will return the action.data object merged
+ *                                into the previous state, and the result
+ *                                of calling mainReducer otherwise.
+ */
+function mergeStateReducer(mainReducer) {
+  return (prevState, action) => {
+    if (action.type === MERGE_STORE_ACTION) {
+      return Object.assign({}, prevState, action.data);
+    }
+
+    return mainReducer(prevState, action);
+  };
+}
+
+/**
+ * messageMiddleware - Middleware that looks for SentToMain type actions, and sends them if necessary
+ */
+const messageMiddleware = store => next => action => {
+  if (au.isSendToMain(action)) {
+    sendAsyncMessage(OUTGOING_MESSAGE_NAME, action);
+  }
+  next(action);
+};
+
+/**
+ * initStore - Create a store and listen for incoming actions
+ *
+ * @param  {object} reducers An object containing Redux reducers
+ * @return {object}          A redux store
+ */
+module.exports = function initStore(reducers) {
+  const store = createStore(mergeStateReducer(combineReducers(reducers)), applyMiddleware(messageMiddleware));
+
+  addMessageListener(INCOMING_MESSAGE_NAME, msg => {
+    store.dispatch(msg.data);
+  });
+
+  return store;
+};
+
+module.exports.MERGE_STORE_ACTION = MERGE_STORE_ACTION;
+module.exports.OUTGOING_MESSAGE_NAME = OUTGOING_MESSAGE_NAME;
+module.exports.INCOMING_MESSAGE_NAME = INCOMING_MESSAGE_NAME;
+
+/***/ }),
+/* 5 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/* 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/. */
+
+
+var _require = __webpack_require__(2);
+
+const at = _require.actionTypes;
+
+
+const INITIAL_STATE = {
+  TopSites: {
+    init: false,
+    rows: []
+  },
+  Search: {
+    currentEngine: {
+      name: "",
+      icon: ""
+    },
+    engines: []
+  }
+};
+
+// TODO: Handle some real actions here, once we have a TopSites feed working
+function TopSites() {
+  let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.TopSites;
+  let action = arguments[1];
+
+  let hasMatch;
+  let newRows;
+  switch (action.type) {
+    case at.TOP_SITES_UPDATED:
+      if (!action.data) {
+        return prevState;
+      }
+      return Object.assign({}, prevState, { init: true, rows: action.data });
+    case at.SCREENSHOT_UPDATED:
+      newRows = prevState.rows.map(row => {
+        if (row.url === action.data.url) {
+          hasMatch = true;
+          return Object.assign({}, row, { screenshot: action.data.screenshot });
+        }
+        return row;
+      });
+      return hasMatch ? Object.assign({}, prevState, { rows: newRows }) : prevState;
+    default:
+      return prevState;
+  }
+}
+
+function Search() {
+  let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Search;
+  let action = arguments[1];
+
+  switch (action.type) {
+    case at.SEARCH_STATE_UPDATED:
+      {
+        if (!action.data) {
+          return prevState;
+        }
+        var _action$data = action.data;
+        let currentEngine = _action$data.currentEngine,
+            engines = _action$data.engines;
+
+        return Object.assign({}, prevState, {
+          currentEngine,
+          engines
+        });
+      }
+    default:
+      return prevState;
+  }
+}
+var reducers = { TopSites, Search };
+module.exports = {
+  reducers,
+  INITIAL_STATE
+};
+
+/***/ }),
+/* 6 */
+/***/ (function(module, exports) {
+
+module.exports = ReactDOM;
+
+/***/ }),
+/* 7 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+const React = __webpack_require__(0);
+
+var _require = __webpack_require__(1);
+
+const connect = _require.connect;
+
+var _require2 = __webpack_require__(2);
+
+const actionTypes = _require2.actionTypes,
+      actionCreators = _require2.actionCreators;
+
+
+const Search = React.createClass({
+  displayName: "Search",
+
+  getInitialState() {
+    return { searchString: "" };
+  },
+  performSearch(options) {
+    let searchData = {
+      engineName: options.engineName,
+      searchString: options.searchString,
+      searchPurpose: "newtab",
+      healthReportKey: "newtab"
+    };
+    this.props.dispatch(actionCreators.SendToMain({ type: actionTypes.PERFORM_SEARCH, data: searchData }));
+  },
+  onClick(event) {
+    const currentEngine = this.props.Search.currentEngine;
+
+    event.preventDefault();
+    this.performSearch({ engineName: currentEngine.name, searchString: this.state.searchString });
+  },
+  onChange(event) {
+    this.setState({ searchString: event.target.value });
+  },
+  render() {
+    return React.createElement(
+      "form",
+      { className: "search-wrapper" },
+      React.createElement("span", { className: "search-label" }),
+      React.createElement("input", { value: this.state.searchString, type: "search",
+        onChange: this.onChange,
+        maxLength: "256", title: "Submit search",
+        placeholder: "Search the Web" }),
+      React.createElement("button", { onClick: this.onClick })
+    );
+  }
+});
+
+module.exports = connect(state => ({ Search: state.Search }))(Search);
+
+/***/ }),
+/* 8 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+const React = __webpack_require__(0);
+
+var _require = __webpack_require__(1);
+
+const connect = _require.connect;
+
+
+function displayURL(url) {
+  return new URL(url).hostname.replace(/^www./, "");
+}
+
+const TopSites = props => React.createElement(
+  "section",
+  null,
+  React.createElement(
+    "h3",
+    { className: "section-title" },
+    "Top Sites"
+  ),
+  React.createElement(
+    "ul",
+    { className: "top-sites-list" },
+    props.TopSites.rows.map(link => {
+      const title = displayURL(link.url);
+      const className = `screenshot${link.screenshot ? " active" : ""}`;
+      const style = { backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none" };
+      return React.createElement(
+        "li",
+        { key: link.url },
+        React.createElement(
+          "a",
+          { href: link.url },
+          React.createElement(
+            "div",
+            { className: "tile" },
+            React.createElement(
+              "span",
+              { className: "letter-fallback", ariaHidden: true },
+              title[0]
+            ),
+            React.createElement("div", { className: className, style: style })
+          ),
+          React.createElement(
+            "div",
+            { className: "title" },
+            title
+          )
+        )
+      );
+    })
+  )
+);
+
+module.exports = connect(state => ({ TopSites: state.TopSites }))(TopSites);
+
+/***/ }),
+/* 9 */
+/***/ (function(module, exports) {
+
+module.exports = Redux;
+
+/***/ }),
+/* 10 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+/* globals addMessageListener, removeMessageListener */
+const React = __webpack_require__(0);
+const ReactDOM = __webpack_require__(6);
+const Base = __webpack_require__(3);
+
+var _require = __webpack_require__(1);
+
+const Provider = _require.Provider;
+
+const initStore = __webpack_require__(4);
+
+var _require2 = __webpack_require__(5);
+
+const reducers = _require2.reducers;
+
+
+const store = initStore(reducers);
+
+ReactDOM.render(React.createElement(
+  Provider,
+  { store: store },
+  React.createElement(Base, null)
+), document.getElementById("root"));
+
+/***/ })
+/******/ ]);
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/activity-stream.css
@@ -0,0 +1,334 @@
+html {
+  box-sizing: border-box; }
+
+*,
+*::before,
+*::after {
+  box-sizing: inherit; }
+
+body {
+  margin: 0; }
+
+button,
+input {
+  font-family: inherit;
+  font-size: inherit; }
+
+[hidden] {
+  display: none !important; }
+
+html,
+body,
+#root {
+  height: 100%; }
+
+body {
+  background: #F6F6F8;
+  color: #383E49;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Ubuntu', 'Helvetica Neue', sans-serif;
+  font-size: 16px; }
+
+h1,
+h2 {
+  font-weight: normal; }
+
+a {
+  color: #00AFF7;
+  text-decoration: none; }
+  a:hover {
+    color: #2bc1ff; }
+
+.sr-only {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip: rect(0, 0, 0, 0);
+  border: 0; }
+
+.inner-border {
+  border: 1px solid rgba(0, 0, 0, 0.1);
+  border-radius: 3px;
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  pointer-events: none;
+  z-index: 100; }
+
+@keyframes fadeIn {
+  from {
+    opacity: 0; }
+  to {
+    opacity: 1; } }
+
+.show-on-init {
+  opacity: 0;
+  transition: opacity 0.2s ease-in; }
+  .show-on-init.on {
+    opacity: 1;
+    animation: fadeIn 0.2s; }
+
+.actions {
+  border-top: solid 1px rgba(0, 0, 0, 0.1);
+  display: flex;
+  flex-direction: row;
+  margin: 0;
+  padding: 15px 25px;
+  justify-content: flex-start; }
+  .actions button {
+    background: #FBFBFB;
+    border: solid 1px #BFBFBF;
+    border-radius: 5px;
+    color: #858585;
+    cursor: pointer;
+    padding: 10px 30px; }
+    .actions button:hover {
+      box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.1);
+      transition: box-shadow 150ms; }
+    .actions button.done {
+      background: #0695F9;
+      border: solid 1px #1677CF;
+      color: #FFF;
+      margin-inline-start: auto; }
+
+.outer-wrapper {
+  display: flex;
+  flex-grow: 1;
+  padding: 62px 32px 32px;
+  height: 100%; }
+
+main {
+  margin: auto; }
+  @media (min-width: 672px) {
+    main {
+      width: 608px; } }
+  @media (min-width: 800px) {
+    main {
+      width: 736px; } }
+  main section {
+    margin-bottom: 41px; }
+
+.section-title {
+  color: #6E707E;
+  font-size: 13px;
+  font-weight: bold;
+  text-transform: uppercase;
+  margin: 0 0 18px; }
+
+.top-sites-list {
+  list-style: none;
+  margin: 0;
+  padding: 0;
+  margin-inline-end: -32px; }
+  @media (min-width: 672px) {
+    .top-sites-list {
+      width: 640px; } }
+  @media (min-width: 800px) {
+    .top-sites-list {
+      width: 768px; } }
+  .top-sites-list li {
+    display: inline-block;
+    margin: 0 0 18px;
+    margin-inline-end: 32px; }
+  .top-sites-list a {
+    display: block;
+    color: inherit; }
+  .top-sites-list .tile {
+    position: relative;
+    height: 96px;
+    width: 96px;
+    border-radius: 6px;
+    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
+    color: #A0A0A0;
+    font-weight: 200;
+    font-size: 32px;
+    text-transform: uppercase;
+    display: flex;
+    align-items: center;
+    justify-content: center; }
+    .top-sites-list .tile:hover {
+      box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1), 0 0 0 5px rgba(0, 0, 0, 0.1);
+      transition: box-shadow 150ms; }
+  .top-sites-list .screenshot {
+    position: absolute;
+    top: 0;
+    left: 0;
+    height: 100%;
+    width: 100%;
+    background-color: #FFF;
+    border-radius: 6px;
+    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
+    background-size: 250%;
+    background-position: top left;
+    transition: opacity 1s;
+    opacity: 0; }
+    .top-sites-list .screenshot.active {
+      opacity: 1; }
+  .top-sites-list .title {
+    height: 30px;
+    line-height: 30px;
+    text-align: center;
+    white-space: nowrap;
+    font-size: 11px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    width: 96px; }
+
+.search-wrapper {
+  cursor: default;
+  display: flex;
+  position: relative;
+  margin: 0 0 48px;
+  width: 100%;
+  height: 36px; }
+  .search-wrapper .search-container {
+    z-index: 1001;
+    background: #FFF;
+    position: absolute;
+    left: 0;
+    right: 0;
+    top: 100%;
+    margin-top: -2px;
+    border: 1px solid #BFBFBF;
+    font-size: 12px;
+    box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
+    overflow: hidden; }
+    .search-wrapper .search-container .search-title {
+      color: #666;
+      padding: 5px 10px;
+      background-color: #F7F7F7;
+      display: flex;
+      align-items: center;
+      word-break: break-all; }
+      .search-wrapper .search-container .search-title p {
+        margin: 0; }
+      .search-wrapper .search-container .search-title #current-engine-icon {
+        margin-inline-end: 8px; }
+    .search-wrapper .search-container section {
+      border-bottom: 1px solid #EAEAEA;
+      margin-bottom: 0; }
+    .search-wrapper .search-container .search-suggestions ul {
+      padding: 0;
+      margin: 0;
+      list-style: none; }
+      .search-wrapper .search-container .search-suggestions ul li a {
+        cursor: default;
+        color: #000;
+        display: block;
+        padding: 4px 36px; }
+        .search-wrapper .search-container .search-suggestions ul li a:hover, .search-wrapper .search-container .search-suggestions ul li a.active {
+          background: #0996F8;
+          color: #FFF; }
+    .search-wrapper .search-container .history-search-suggestions {
+      border-bottom: 0; }
+      .search-wrapper .search-container .history-search-suggestions ul {
+        padding: 0;
+        margin: 0;
+        list-style: none; }
+        .search-wrapper .search-container .history-search-suggestions ul li a {
+          cursor: default;
+          color: #000;
+          display: block;
+          padding: 4px 10px; }
+          .search-wrapper .search-container .history-search-suggestions ul li a:hover, .search-wrapper .search-container .history-search-suggestions ul li a.active {
+            background: #0996F8;
+            color: #FFF; }
+          .search-wrapper .search-container .history-search-suggestions ul li a:hover > #historyIcon,
+          .search-wrapper .search-container .history-search-suggestions ul li a.active > #historyIcon {
+            background-image: url("assets/glyph-search-history.svg#search-history-active"); }
+    .search-wrapper .search-container .history-search-suggestions #historyIcon {
+      width: 16px;
+      height: 16px;
+      display: inline-block;
+      margin-inline-end: 10px;
+      margin-bottom: -3px;
+      background-image: url("assets/glyph-search-history.svg#search-history"); }
+    .search-wrapper .search-container .search-partners ul {
+      padding: 0;
+      margin: 0;
+      list-style: none; }
+      .search-wrapper .search-container .search-partners ul li {
+        display: inline-block;
+        padding: 5px 0; }
+        .search-wrapper .search-container .search-partners ul li a {
+          display: block;
+          padding: 3px 16px;
+          border-right: 1px solid #BFBFBF; }
+        .search-wrapper .search-container .search-partners ul li:hover, .search-wrapper .search-container .search-partners ul li.active {
+          background: #0996F8;
+          color: #FFF; }
+          .search-wrapper .search-container .search-partners ul li:hover a, .search-wrapper .search-container .search-partners ul li.active a {
+            border-color: transparent; }
+    .search-wrapper .search-container .search-settings button {
+      color: #666;
+      margin: 0;
+      padding: 0;
+      height: 32px;
+      text-align: center;
+      width: 100%;
+      border-style: solid none none;
+      border-radius: 0;
+      background: #F7F7F7;
+      border-top: 0; }
+      .search-wrapper .search-container .search-settings button:hover, .search-wrapper .search-container .search-settings button.active {
+        background: #EBEBEB;
+        box-shadow: none; }
+  .search-wrapper input {
+    border: 0;
+    box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.1);
+    flex-grow: 1;
+    margin: 0;
+    outline: none;
+    padding: 0 12px 0 35px;
+    height: 100%;
+    border-top-left-radius: 4px;
+    border-bottom-left-radius: 4px;
+    padding-inline-start: 35px; }
+    .search-wrapper input:focus {
+      border-color: #0996F8;
+      box-shadow: 0 0 0 2px #0996F8;
+      transition: box-shadow 150ms;
+      z-index: 1; }
+    .search-wrapper input:focus + button {
+      z-index: 1;
+      transition: box-shadow 150ms;
+      box-shadow: 0 0 0 2px #0996F8;
+      background-color: #0996F8;
+      background-image: url("assets/glyph-forward-16-white.svg");
+      color: #FFF; }
+  .search-wrapper input:dir(rtl) {
+    border-radius: 0 4px 4px 0; }
+  .search-wrapper .search-label {
+    background: url("assets/glyph-search-16.svg") no-repeat center center/20px;
+    position: absolute;
+    top: 0;
+    offset-inline-start: 0;
+    height: 100%;
+    width: 35px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 2; }
+  .search-wrapper button {
+    border-radius: 0 3px 3px 0;
+    margin-inline-start: -1px;
+    border: 0;
+    width: 36px;
+    padding: 0;
+    transition: box-shadow 150ms;
+    box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.1);
+    background: #FFF url("assets/glyph-forward-16.svg") no-repeat center center;
+    background-size: 16px 16px; }
+    .search-wrapper button:hover {
+      z-index: 1;
+      transition: box-shadow 150ms;
+      box-shadow: 0 1px 0 0 rgba(0, 0, 1, 0.5);
+      background-color: #0996F8;
+      background-image: url("assets/glyph-forward-16-white.svg");
+      color: #FFF; }
+  .search-wrapper button:dir(rtl) {
+    transform: scaleX(-1); }
--- a/browser/extensions/activity-stream/data/content/activity-stream.html
+++ b/browser/extensions/activity-stream/data/content/activity-stream.html
@@ -1,31 +1,16 @@
 <!doctype html>
 <html lang="en-us" dir="ltr">
   <head>
     <meta charset="utf-8">
     <title>New Tab</title>
+    <link rel="stylesheet" href="resource://activity-stream/data/content/activity-stream.css" />
   </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 class="activity-stream">
+    <div id="root"></div>
+    <script src="resource://activity-stream/vendor/react.js"></script>
+    <script src="resource://activity-stream/vendor/react-dom.js"></script>
+    <script src="resource://activity-stream/vendor/redux.js"></script>
+    <script src="resource://activity-stream/vendor/react-redux.js"></script>
+    <script src="resource://activity-stream/data/content/activity-stream.bundle.js"></script>
   </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/assets/glyph-forward-16-white.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <title>Forward - 16</title>
+  <g>
+    <polyline points="9 2 15 8 9 14" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+    <line x1="14" y1="8" x2="1" y2="8" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+  </g>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/assets/glyph-forward-16.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <title>Forward - 16</title>
+  <g>
+    <polyline points="9 2 15 8 9 14" fill="none" stroke="#a0a0a0" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+    <line x1="14" y1="8" x2="1" y2="8" fill="none" stroke="#a0a0a0" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+  </g>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/assets/glyph-search-16.svg
@@ -0,0 +1,13 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
+  <defs>
+    <style>
+      .cls-1 {
+        fill: #a0a0a0;
+        fill-rule: evenodd;
+      }
+    </style>
+  </defs>
+  <g id="glyph-search-16">
+    <path id="Icon_-_Search_-_16" data-name="Icon - Search - 16" class="cls-1" d="M226.989,348.571l-2.2,2.2-9.533-9.534a11.436,11.436,0,1,1,2.2-2.2ZM208.37,323.745a8.407,8.407,0,1,0,8.406,8.406A8.406,8.406,0,0,0,208.37,323.745Z" transform="translate(-196 -320)"/>
+  </g>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/assets/glyph-search-history.svg
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
+  <style>
+    use:not(:target) {
+      display: none;
+    }
+    use {
+      fill: graytext;
+    }
+    use[id$="-active"] {
+      fill: HighlightText;
+    }
+  </style>
+  <defs>
+    <path id="search-history-glyph" d="M8,1C4.1,1,1,4.1,1,8c0,3.9,3.1,7,7,7c3.9,0,7-3.1,7-7 C15,4.1,11.9,1,8,1z M8,13.3c-2.9,0-5.3-2.4-5.3-5.3S5.1,2.7,8,2.7c2.9,0,5.3,2.4,5.3,5.3S10.9,13.3,8,13.3z M10.5,7H9V5 c0-0.6-0.4-1-1-1S7,4.4,7,5v3c0,0.6,0.4,1,1,1h2.5c0.6,0,1-0.4,1-1C11.5,7.4,11.1,7,10.5,7z"/>
+  </defs>
+  <use id="search-history" xlink:href="#search-history-glyph"/>
+  <use id="search-history-active" xlink:href="#search-history-glyph"/>
+</svg>
--- a/browser/extensions/activity-stream/jar.mn
+++ b/browser/extensions/activity-stream/jar.mn
@@ -2,9 +2,13 @@
 # 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/.
 
 [features/activity-stream@mozilla.org] chrome.jar:
 % resource activity-stream %content/
   content/lib/ (./lib/*)
   content/common/ (./common/*)
   content/vendor/Redux.jsm (./vendor/Redux.jsm)
+  content/vendor/react.js (./vendor/react.js)
+  content/vendor/react-dom.js (./vendor/react-dom.js)
+  content/vendor/redux.js (./vendor/redux.js)
+  content/vendor/react-redux.js (./vendor/react-redux.js)
   content/data/ (./data/*)
--- a/browser/extensions/activity-stream/lib/ActivityStream.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStream.jsm
@@ -1,39 +1,62 @@
 /* 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 XPCOMUtils, NewTabInit, TopSitesFeed, SearchFeed */
+
 "use strict";
 
 const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 const {Store} = Cu.import("resource://activity-stream/lib/Store.jsm", {});
+const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
 // Feeds
-const {NewTabInit} = Cu.import("resource://activity-stream/lib/NewTabInit.jsm", {});
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabInit",
+  "resource://activity-stream/lib/NewTabInit.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TopSitesFeed",
+  "resource://activity-stream/lib/TopSitesFeed.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SearchFeed",
+  "resource://activity-stream/lib/SearchFeed.jsm");
+
+const feeds = {
+  // When you add a feed here:
+  // 1. The key in this object should directly refer to a pref, not including the
+  //    prefix (so "feeds.newtabinit" refers to the
+  //    "browser.newtabpage.activity-stream.feeds.newtabinit" pref)
+  // 2. The value should be a function that returns a feed.
+  // 3. You should use XPCOMUtils.defineLazyModuleGetter to import the Feed,
+  //    so it isn't loaded until the feed is enabled.
+  "feeds.newtabinit": () => new NewTabInit(),
+  "feeds.topsites": () => new TopSitesFeed(),
+  "feeds.search": () => new SearchFeed()
+};
 
 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"
    * @param  {string} options.newTabURL URL of New Tab page on which A.S. is displayed. e.g. "about:newtab"
    */
   constructor(options) {
     this.initialized = false;
     this.options = options;
     this.store = new Store();
+    this.feeds = feeds;
   }
   init() {
     this.initialized = true;
-    this.store.init([
-      new NewTabInit()
-    ]);
+    this.store.init(this.feeds);
+    this.store.dispatch({type: at.INIT});
   }
   uninit() {
+    this.store.dispatch({type: at.UNINIT});
     this.store.uninit();
     this.initialized = false;
   }
 };
 
 this.EXPORTED_SYMBOLS = ["ActivityStream"];
--- a/browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
@@ -28,16 +28,17 @@ const DEFAULT_OPTIONS = {
     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
@@ -178,20 +179,24 @@ this.ActivityStreamMessageChannel = clas
    * 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) {
+    if (!msg.data || !msg.data.type) {
       Cu.reportError(new Error(`Received an improperly formatted message from ${portID}`));
       return;
     }
-    this.onActionFromContent(action, msg.target.portID);
+    let action = {};
+    Object.assign(action, msg.data);
+    // target is used to access a browser reference that came from the content
+    // and should only be used in feeds (not reducers)
+    action._target = msg.target;
+    this.onActionFromContent(action, portID);
   }
 }
 
 this.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
 this.EXPORTED_SYMBOLS = ["ActivityStreamMessageChannel", "DEFAULT_OPTIONS"];
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/lib/SearchFeed.jsm
@@ -0,0 +1,65 @@
+/* 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 ContentSearch, XPCOMUtils, Services */
+"use strict";
+
+const {utils: Cu} = Components;
+const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
+const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified";
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch",
+  "resource:///modules/ContentSearch.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+  "resource://gre/modules/Services.jsm");
+
+this.SearchFeed = class SearchFeed {
+  addObservers() {
+    Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC);
+  }
+  removeObservers() {
+    Services.obs.removeObserver(this, SEARCH_ENGINE_TOPIC);
+  }
+  observe(subject, topic, data) {
+    switch (topic) {
+      case SEARCH_ENGINE_TOPIC:
+        if (data !== "engine-default") {
+          this.getState();
+        }
+        break;
+    }
+  }
+  async getState() {
+    const state = await ContentSearch.currentStateObj(true);
+    const engines = state.engines.map(engine => ({
+      name: engine.name,
+      icon: engine.iconBuffer
+    }));
+    const currentEngine = {
+      name: state.currentEngine.name,
+      icon: state.currentEngine.iconBuffer
+    };
+    const action = {type: at.SEARCH_STATE_UPDATED, data: {engines, currentEngine}};
+    this.store.dispatch(ac.BroadcastToContent(action));
+  }
+  performSearch(browser, data) {
+    ContentSearch.performSearch({target: browser}, data);
+  }
+  onAction(action) {
+    switch (action.type) {
+      case at.INIT:
+        this.addObservers();
+        this.getState();
+        break;
+      case at.PERFORM_SEARCH:
+        this.performSearch(action._target.browser, action.data);
+        break;
+      case at.UNINIT:
+        this.removeObservers();
+        break;
+    }
+  }
+};
+this.EXPORTED_SYMBOLS = ["SearchFeed"];
--- a/browser/extensions/activity-stream/lib/Store.jsm
+++ b/browser/extensions/activity-stream/lib/Store.jsm
@@ -1,20 +1,23 @@
 /* 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/. */
+/* global Preferences */
 "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", {});
 
+const PREF_PREFIX = "browser.newtabpage.activity-stream.";
+Cu.import("resource://gre/modules/Preferences.jsm");
+
 /**
  * Store - This has a similar structure to a redux store, but includes some extra
  *         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 {
@@ -27,17 +30,19 @@ this.Store = class Store {
     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.feeds = new Map();
+    this._feedFactories = null;
+    this._prefHandlers = new Map();
     this._messageChannel = new ActivityStreamMessageChannel({dispatch: this.dispatch});
     this._store = redux.createStore(
       redux.combineReducers(reducers),
       redux.applyMiddleware(this._middleware, this._messageChannel.middleware)
     );
   }
 
   /**
@@ -48,38 +53,98 @@ this.Store = class Store {
   _middleware(store) {
     return next => action => {
       next(action);
       this.feeds.forEach(s => s.onAction && s.onAction(action));
     };
   }
 
   /**
+   * initFeed - Initializes a feed by calling its constructor function
+   *
+   * @param  {string} feedName The name of a feed, as defined in the object
+   *                           passed to Store.init
+   */
+  initFeed(feedName) {
+    const feed = this._feedFactories[feedName]();
+    feed.store = this;
+    this.feeds.set(feedName, feed);
+  }
+
+  /**
+   * uninitFeed - Removes a feed and calls its uninit function if defined
+   *
+   * @param  {string} feedName The name of a feed, as defined in the object
+   *                           passed to Store.init
+   */
+  uninitFeed(feedName) {
+    const feed = this.feeds.get(feedName);
+    if (!feed) {
+      return;
+    }
+    if (feed.uninit) {
+      feed.uninit();
+    }
+    this.feeds.delete(feedName);
+  }
+
+  /**
+   * maybeStartFeedAndListenForPrefChanges - Listen for pref changes that turn a
+   *     feed off/on, and as long as that pref was not explicitly set to
+   *     false, initialize the feed immediately.
+   *
+   * @param  {string} name The name of a feed, as defined in the object passed
+   *                       to Store.init
+   */
+  maybeStartFeedAndListenForPrefChanges(name) {
+    const prefName = PREF_PREFIX + name;
+
+    // If the pref was never set, set it to true by default.
+    if (!Preferences.has(prefName)) {
+      Preferences.set(prefName, true);
+    }
+
+    // Create a listener that turns the feed off/on based on changes
+    // to the pref, and cache it so we can unlisten on shut-down.
+    const onPrefChanged = isEnabled => (isEnabled ? this.initFeed(name) : this.uninitFeed(name));
+    this._prefHandlers.set(prefName, onPrefChanged);
+    Preferences.observe(prefName, onPrefChanged);
+
+    // TODO: This should propbably be done in a generic pref manager for Activity Stream.
+    // If the pref is true, start the feed immediately.
+    if (Preferences.get(prefName)) {
+      this.initFeed(name);
+    }
+  }
+
+  /**
    * 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);
-      });
+  init(feedConstructors) {
+    if (feedConstructors) {
+      this._feedFactories = feedConstructors;
+      for (const name of Object.keys(feedConstructors)) {
+        this.maybeStartFeedAndListenForPrefChanges(name);
+      }
     }
     this._messageChannel.createChannel();
-    this.dispatch({type: at.INIT});
   }
 
   /**
-   * uninit - Clears all feeds, dispatches an UNINIT action, and
-   *          destroys the message manager channel.
+   * uninit -  Uninitalizes each feed, clears them, and destroys the message
+   *           manager channel.
    *
    * @return {type}  description
    */
   uninit() {
+    this.feeds.forEach(feed => this.uninitFeed(feed));
+    this._prefHandlers.forEach((handler, pref) => Preferences.ignore(pref, handler));
+    this._prefHandlers.clear();
+    this._feedFactories = null;
     this.feeds.clear();
-    this.dispatch({type: at.UNINIT});
     this._messageChannel.destroyChannel();
   }
 };
 
-this.EXPORTED_SYMBOLS = ["Store"];
+this.PREF_PREFIX = PREF_PREFIX;
+this.EXPORTED_SYMBOLS = ["Store", "PREF_PREFIX"];
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
@@ -0,0 +1,83 @@
+/* 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 PlacesProvider, PreviewProvider */
+"use strict";
+
+const {utils: Cu} = Components;
+const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
+
+Cu.import("resource:///modules/PlacesProvider.jsm");
+Cu.import("resource:///modules/PreviewProvider.jsm");
+
+const TOP_SITES_SHOWMORE_LENGTH = 12;
+const UPDATE_TIME = 15 * 60 * 1000; // 15 minutes
+const DEFAULT_TOP_SITES = [
+  {"url": "https://www.facebook.com/"},
+  {"url": "https://www.youtube.com/"},
+  {"url": "http://www.amazon.com/"},
+  {"url": "https://www.yahoo.com/"},
+  {"url": "http://www.ebay.com"},
+  {"url": "https://twitter.com/"}
+].map(row => Object.assign(row, {isDefault: true}));
+
+this.TopSitesFeed = class TopSitesFeed {
+  constructor() {
+    this.lastUpdated = 0;
+  }
+  async getScreenshot(url) {
+    let screenshot = await PreviewProvider.getThumbnail(url);
+    const action = {type: at.SCREENSHOT_UPDATED, data: {url, screenshot}};
+    this.store.dispatch(ac.BroadcastToContent(action));
+  }
+  async getLinksWithDefaults(action) {
+    let links = await PlacesProvider.links.getLinks();
+
+    if (!links) {
+      links = [];
+    } else {
+      links = links.filter(link => link && link.type !== "affiliate").slice(0, 12);
+    }
+
+    if (links.length < TOP_SITES_SHOWMORE_LENGTH) {
+      links = [...links, ...DEFAULT_TOP_SITES].slice(0, TOP_SITES_SHOWMORE_LENGTH);
+    }
+
+    return links;
+  }
+  async refresh(action) {
+    const links = await this.getLinksWithDefaults();
+    const newAction = {type: at.TOP_SITES_UPDATED, data: links};
+
+    // Send an update to content so the preloaded tab can get the updated content
+    this.store.dispatch(ac.SendToContent(newAction, action.meta.fromTarget));
+    this.lastUpdated = Date.now();
+
+    // Now, get a screenshot for every item
+    for (let link of links) {
+      this.getScreenshot(link.url);
+    }
+  }
+  onAction(action) {
+    let realRows;
+    switch (action.type) {
+      case at.NEW_TAB_LOAD:
+        // Only check against real rows returned from history, not default ones.
+        realRows = this.store.getState().TopSites.rows.filter(row => !row.isDefault);
+        // When a new tab is opened, if we don't have enough top sites yet, refresh the data.
+        if (realRows.length < TOP_SITES_SHOWMORE_LENGTH) {
+          this.refresh(action);
+        } else if (Date.now() - this.lastUpdated >= UPDATE_TIME) {
+          // When a new tab is opened, if the last time we refreshed the data
+          // is greater than 15 minutes, refresh the data.
+          this.refresh(action);
+        }
+        break;
+    }
+  }
+};
+
+this.UPDATE_TIME = UPDATE_TIME;
+this.TOP_SITES_SHOWMORE_LENGTH = TOP_SITES_SHOWMORE_LENGTH;
+this.DEFAULT_TOP_SITES = DEFAULT_TOP_SITES;
+this.EXPORTED_SYMBOLS = ["TopSitesFeed", "UPDATE_TIME", "DEFAULT_TOP_SITES", "TOP_SITES_SHOWMORE_LENGTH"];
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/.eslintrc.js
@@ -0,0 +1,11 @@
+module.exports = {
+  "env": {
+    "node": true,
+    "es6": true,
+    "mocha": true
+  },
+  "globals": {
+    "assert": true,
+    "sinon": true
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/functional/mochitest/.eslintrc.js
@@ -0,0 +1,42 @@
+module.exports = {
+  "globals": {
+    "add_task": false,
+    "Assert": false,
+    "BrowserOpenTab": false,
+    "BrowserTestUtils": false,
+    "content": false,
+    "ContentTask": false,
+    "ContentTaskUtils": false,
+    "Components": false,
+    "EventUtils": false,
+    "executeSoon": false,
+    "expectUncaughtException": false,
+    "export_assertions": false,
+    "extractJarToTmp": false,
+    "finish": false,
+    "getJar": false,
+    "getRootDirectory": false,
+    "getTestFilePath": false,
+    "gBrowser": false,
+    "gTestPath": false,
+    "info": false,
+    "is": false,
+    "isnot": false,
+    "ok": false,
+    "OpenBrowserWindow": false,
+    "Preferences": false,
+    "registerCleanupFunction": false,
+    "requestLongerTimeout": false,
+    "Services": false,
+    "SimpleTest": false,
+    "SpecialPowers": false,
+    "TestUtils": false,
+    "thisTestLeaksUncaughtRejectionsAndShouldBeFixed": false,
+    "todo": false,
+    "todo_is": false,
+    "todo_isnot": false,
+    "waitForClipboard": false,
+    "waitForExplicitFinish": false,
+    "waitForFocus": false
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/functional/mochitest/browser.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+# XXX This defaults to forcing activity-stream tests to be skipped in m-c,
+# since, as of this writing, mozilla-central itself is still turned off.
+# The tests can be run locally using 'npm run mochitest' which does various
+# overrides.
+skip-if=!activity_stream
+
+[browser_dummy_test.js]
+skip-if=true
+# XXX The above test is required because having only one test causes
+# The default skip-if to silently fail.  As soon as we add another test here, 
+# we should get rid of it, and the following line.
+[browser_as_load_location.js]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/functional/mochitest/browser_as_load_location.js
@@ -0,0 +1,34 @@
+"use strict";
+
+let Cu = Components.utils;
+Cu.import("resource://gre/modules/Services.jsm");
+
+/**
+ * Tests that opening a new tab opens a page with the expected activity stream
+ * content.
+ *
+ * XXX /browser/components/newtab/tests/browser/browser_newtab_overrides in
+ * mozilla-central is where this test was adapted from.  Once we get decide on
+ * and implement how we're going to set the URL in mozilla-central, we may well
+ * want to (separately from this test), clone/adapt that entire file for our
+ * new setup.
+ */
+add_task(async function checkActivityStreamLoads() {
+  const asURL = "resource://activity-stream/data/content/activity-stream.html";
+
+  // simulate a newtab open as a user would
+  BrowserOpenTab();
+
+  // wait until the browser loads
+  let browser = gBrowser.selectedBrowser;
+  await BrowserTestUtils.browserLoaded(browser);
+
+  // check what the content task thinks has been loaded.
+  await ContentTask.spawn(browser, {url: asURL}, args => {
+    Assert.ok(content.document.querySelector("body.activity-stream"),
+      'Got <body class="activity-stream" Element');
+  });
+
+  // avoid leakage
+  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/functional/mochitest/browser_dummy_test.js
@@ -0,0 +1,34 @@
+"use strict";
+
+let Cu = Components.utils;
+Cu.import("resource://gre/modules/Services.jsm");
+
+/**
+ * Tests that opening a new tab opens a page with the expected activity stream
+ * content.
+ *
+ * XXX /browser/components/newtab/tests/browser/browser_newtab_overrides in
+ * mozilla-central is where this test was adapted from.  Once we get decide on
+ * and implement how we're going to set the URL in mozilla-central, we may well
+ * want to (separately from this test), clone/adapt that entire file for our
+ * new setup.
+ */
+add_task(async function checkActivityStreamLoads() {
+  const asURL = "resource://activity-stream/data/content/activity-stream.html";
+
+  // simulate a newtab open as a user would
+  BrowserOpenTab();
+
+  // wait until the browser loads
+  let browser = gBrowser.selectedBrowser;
+  await BrowserTestUtils.browserLoaded(browser);
+
+  // check what the content task thinks has been loaded.
+  await ContentTask.spawn(browser, {url: asURL}, args => {
+    Assert.ok(content.document.querySelector("body.activity-stream"),
+      'Got <body class="activity-stream" Element');
+  });
+
+  // avoid leakage
+  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/mozinfo.json
@@ -0,0 +1,3 @@
+{
+  "activity_stream": true
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/common/Actions.test.js
@@ -0,0 +1,93 @@
+const {
+  actionCreators: ac,
+  actionUtils: au,
+  MAIN_MESSAGE_TYPE,
+  CONTENT_MESSAGE_TYPE
+} = require("common/Actions.jsm");
+
+describe("ActionCreators", () => {
+  describe("_RouteMessage", () => {
+    it("should throw if options are not passed as the second param", () => {
+      assert.throws(() => {
+        au._RouteMessage({type: "FOO"});
+      });
+    });
+    it("should set all defined options on the .meta property of the new action", () => {
+      assert.deepEqual(
+        au._RouteMessage({type: "FOO", meta: {hello: "world"}}, {from: "foo", to: "bar"}),
+        {type: "FOO", meta: {hello: "world", from: "foo", to: "bar"}}
+      );
+    });
+    it("should remove any undefined options related to message routing", () => {
+      const action = au._RouteMessage({type: "FOO", meta: {fromTarget: "bar"}}, {from: "foo", to: "bar"});
+      assert.isUndefined(action.meta.fromTarget);
+    });
+  });
+  describe("SendToMain", () => {
+    it("should create the right action", () => {
+      const action = {type: "FOO", data: "BAR"};
+      const newAction = ac.SendToMain(action);
+      assert.deepEqual(newAction, {
+        type: "FOO",
+        data: "BAR",
+        meta: {from: CONTENT_MESSAGE_TYPE, to: MAIN_MESSAGE_TYPE}
+      });
+    });
+    describe("isSendToMain", () => {
+      it("should return true if action is SendToMain", () => {
+        const newAction = ac.SendToMain({type: "FOO"});
+        assert.isTrue(au.isSendToMain(newAction));
+      });
+      it("should return false if action is not SendToMain", () => {
+        assert.isFalse(au.isSendToMain({type: "FOO"}));
+      });
+    });
+  });
+  describe("SendToContent", () => {
+    it("should create the right action", () => {
+      const action = {type: "FOO", data: "BAR"};
+      const targetId = "abc123";
+      const newAction = ac.SendToContent(action, targetId);
+      assert.deepEqual(newAction, {
+        type: "FOO",
+        data: "BAR",
+        meta: {from: MAIN_MESSAGE_TYPE, to: CONTENT_MESSAGE_TYPE, toTarget: targetId}
+      });
+    });
+    it("should throw if no targetId is provided", () => {
+      assert.throws(() => {
+        ac.SendToContent({type: "FOO"});
+      });
+    });
+    describe("isSendToContent", () => {
+      it("should return true if action is SendToContent", () => {
+        const newAction = ac.SendToContent({type: "FOO"}, "foo123");
+        assert.isTrue(au.isSendToContent(newAction));
+      });
+      it("should return false if action is not SendToMain", () => {
+        assert.isFalse(au.isSendToContent({type: "FOO"}));
+        assert.isFalse(au.isSendToContent(ac.BroadcastToContent({type: "FOO"})));
+      });
+    });
+  });
+  describe("BroadcastToContent", () => {
+    it("should create the right action", () => {
+      const action = {type: "FOO", data: "BAR"};
+      const newAction = ac.BroadcastToContent(action);
+      assert.deepEqual(newAction, {
+        type: "FOO",
+        data: "BAR",
+        meta: {from: MAIN_MESSAGE_TYPE, to: CONTENT_MESSAGE_TYPE}
+      });
+    });
+    describe("isBroadcastToContent", () => {
+      it("should return true if action is BroadcastToContent", () => {
+        assert.isTrue(au.isBroadcastToContent(ac.BroadcastToContent({type: "FOO"})));
+      });
+      it("should return false if action is not BroadcastToContent", () => {
+        assert.isFalse(au.isBroadcastToContent({type: "FOO"}));
+        assert.isFalse(au.isBroadcastToContent(ac.SendToContent({type: "FOO"}, "foo123")));
+      });
+    });
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
@@ -0,0 +1,51 @@
+const {reducers, INITIAL_STATE} = require("common/Reducers.jsm");
+const {TopSites, Search} = reducers;
+const {actionTypes: at} = require("common/Actions.jsm");
+
+describe("Reducers", () => {
+  describe("TopSites", () => {
+    it("should return the initial state", () => {
+      const nextState = TopSites(undefined, {type: "FOO"});
+      assert.equal(nextState, INITIAL_STATE.TopSites);
+    });
+    it("should add top sites on TOP_SITES_UPDATED", () => {
+      const newRows = [{url: "foo.com"}, {url: "bar.com"}];
+      const nextState = TopSites(undefined, {type: at.TOP_SITES_UPDATED, data: newRows});
+      assert.equal(nextState.rows, newRows);
+    });
+    it("should not update state for empty action.data on TOP_SITES_UPDATED", () => {
+      const nextState = TopSites(undefined, {type: at.TOP_SITES_UPDATED});
+      assert.equal(nextState, INITIAL_STATE.TopSites);
+    });
+    it("should add screenshots for SCREENSHOT_UPDATED", () => {
+      const oldState = {rows: [{url: "foo.com"}, {url: "bar.com"}]};
+      const action = {type: at.SCREENSHOT_UPDATED, data: {url: "bar.com", screenshot: "data:123"}};
+      const nextState = TopSites(oldState, action);
+      assert.deepEqual(nextState.rows, [{url: "foo.com"}, {url: "bar.com", screenshot: "data:123"}]);
+    });
+    it("should not modify rows if nothing matches the url for SCREENSHOT_UPDATED", () => {
+      const oldState = {rows: [{url: "foo.com"}, {url: "bar.com"}]};
+      const action = {type: at.SCREENSHOT_UPDATED, data: {url: "baz.com", screenshot: "data:123"}};
+      const nextState = TopSites(oldState, action);
+      assert.deepEqual(nextState, oldState);
+    });
+  });
+  describe("Search", () => {
+    it("should return the initial state", () => {
+      const nextState = Search(undefined, {type: "FOO"});
+      assert.equal(nextState, INITIAL_STATE.Search);
+    });
+    it("should not update state for empty action.data on Search", () => {
+      const nextState = Search(undefined, {type: at.SEARCH_STATE_UPDATED});
+      assert.equal(nextState, INITIAL_STATE.Search);
+    });
+    it("should update the current engine and the engines on SEARCH_STATE_UPDATED", () => {
+      const newEngine = {name: "Google", iconBuffer: "icon.ico"};
+      const nextState = Search(undefined, {type: at.SEARCH_STATE_UPDATED, data: {currentEngine: newEngine, engines: [newEngine]}});
+      assert.equal(nextState.currentEngine.name, newEngine.name);
+      assert.equal(nextState.currentEngine.icon, newEngine.icon);
+      assert.equal(nextState.engines[0].name, newEngine.name);
+      assert.equal(nextState.engines[0].icon, newEngine.icon);
+    });
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
@@ -0,0 +1,70 @@
+const injector = require("inject!lib/ActivityStream.jsm");
+
+describe("ActivityStream", () => {
+  let sandbox;
+  let as;
+  let ActivityStream;
+  function NewTabInit() {}
+  function TopSitesFeed() {}
+  function SearchFeed() {}
+  before(() => {
+    sandbox = sinon.sandbox.create();
+    ({ActivityStream} = injector({
+      "lib/NewTabInit.jsm": {NewTabInit},
+      "lib/TopSitesFeed.jsm": {TopSitesFeed},
+      "lib/SearchFeed.jsm": {SearchFeed}
+    }));
+  });
+
+  afterEach(() => sandbox.restore());
+
+  beforeEach(() => {
+    as = new ActivityStream();
+    sandbox.stub(as.store, "init");
+    sandbox.stub(as.store, "uninit");
+  });
+
+  it("should exist", () => {
+    assert.ok(ActivityStream);
+  });
+  it("should initialize with .initialized=false", () => {
+    assert.isFalse(as.initialized, ".initialized");
+  });
+  describe("#init", () => {
+    beforeEach(() => {
+      as.init();
+    });
+    it("should set .initialized to true", () => {
+      assert.isTrue(as.initialized, ".initialized");
+    });
+    it("should call .store.init", () => {
+      assert.calledOnce(as.store.init);
+    });
+  });
+  describe("#uninit", () => {
+    beforeEach(() => {
+      as.init();
+      as.uninit();
+    });
+    it("should set .initialized to false", () => {
+      assert.isFalse(as.initialized, ".initialized");
+    });
+    it("should call .store.uninit", () => {
+      assert.calledOnce(as.store.uninit);
+    });
+  });
+  describe("feeds", () => {
+    it("should create a NewTabInit feed", () => {
+      const feed = as.feeds["feeds.newtabinit"]();
+      assert.instanceOf(feed, NewTabInit);
+    });
+    it("should create a TopSites feed", () => {
+      const feed = as.feeds["feeds.topsites"]();
+      assert.instanceOf(feed, TopSitesFeed);
+    });
+    it("should create a Search feed", () => {
+      const feed = as.feeds["feeds.search"]();
+      assert.instanceOf(feed, SearchFeed);
+    });
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStreamMessageChannel.test.js
@@ -0,0 +1,235 @@
+const {ActivityStreamMessageChannel, DEFAULT_OPTIONS} = require("lib/ActivityStreamMessageChannel.jsm");
+const {addNumberReducer, GlobalOverrider} = require("test/unit/utils");
+const {createStore, applyMiddleware} = require("redux");
+const {actionTypes: at, actionCreators: ac} = require("common/Actions.jsm");
+
+const OPTIONS = ["pageURL, outgoingMessageName", "incomingMessageName", "dispatch"];
+
+describe("ActivityStreamMessageChannel", () => {
+  let globals;
+  let dispatch;
+  let mm;
+  before(() => {
+    function RP(url) {
+      this.url = url;
+      this.messagePorts = [];
+      this.addMessageListener = globals.sandbox.spy();
+      this.sendAsyncMessage = globals.sandbox.spy();
+      this.destroy = globals.sandbox.spy();
+    }
+    globals = new GlobalOverrider();
+    globals.set("AboutNewTab", {
+      override: globals.sandbox.spy(),
+      reset: globals.sandbox.spy()
+    });
+    globals.set("RemotePages", RP);
+    dispatch = globals.sandbox.spy();
+  });
+  beforeEach(() => {
+    mm = new ActivityStreamMessageChannel({dispatch});
+  });
+
+  afterEach(() => globals.reset());
+  after(() => globals.restore());
+
+  it("should exist", () => {
+    assert.ok(ActivityStreamMessageChannel);
+  });
+  it("should apply default options", () => {
+    mm = new ActivityStreamMessageChannel();
+    OPTIONS.forEach(o => assert.equal(mm[o], DEFAULT_OPTIONS[o], o));
+  });
+  it("should add options", () => {
+    const options = {dispatch: () => {}, pageURL: "FOO.html", outgoingMessageName: "OUT", incomingMessageName: "IN"};
+    mm = new ActivityStreamMessageChannel(options);
+    OPTIONS.forEach(o => assert.equal(mm[o], options[o], o));
+  });
+  it("should throw an error if no dispatcher was provided", () => {
+    mm = new ActivityStreamMessageChannel();
+    assert.throws(() => mm.dispatch({type: "FOO"}));
+  });
+  describe("Creating/destroying the channel", () => {
+    describe("#createChannel", () => {
+      it("should create .channel with the correct URL", () => {
+        mm.createChannel();
+        assert.ok(mm.channel);
+        assert.equal(mm.channel.url, mm.pageURL);
+      });
+      it("should add 3 message listeners", () => {
+        mm.createChannel();
+        assert.callCount(mm.channel.addMessageListener, 3);
+      });
+      it("should add the custom message listener to the channel", () => {
+        mm.createChannel();
+        assert.calledWith(mm.channel.addMessageListener, mm.incomingMessageName, mm.onMessage);
+      });
+      it("should override AboutNewTab", () => {
+        mm.createChannel();
+        assert.calledOnce(global.AboutNewTab.override);
+      });
+      it("should not override AboutNewTab if the pageURL is not about:newtab", () => {
+        mm = new ActivityStreamMessageChannel({pageURL: "foo.html"});
+        mm.createChannel();
+        assert.notCalled(global.AboutNewTab.override);
+      });
+    });
+    describe("#destroyChannel", () => {
+      let channel;
+      beforeEach(() => {
+        mm.createChannel();
+        channel = mm.channel;
+      });
+      it("should call channel.destroy()", () => {
+        mm.destroyChannel();
+        assert.calledOnce(channel.destroy);
+      });
+      it("should set .channel to null", () => {
+        mm.destroyChannel();
+        assert.isNull(mm.channel);
+      });
+      it("should reset AboutNewTab", () => {
+        mm.destroyChannel();
+        assert.calledOnce(global.AboutNewTab.reset);
+      });
+      it("should not reset AboutNewTab if the pageURL is not about:newtab", () => {
+        mm = new ActivityStreamMessageChannel({pageURL: "foo.html"});
+        mm.createChannel();
+        mm.destroyChannel();
+        assert.notCalled(global.AboutNewTab.reset);
+      });
+    });
+  });
+  describe("Message handling", () => {
+    describe("#getTargetById", () => {
+      it("should get an id if it exists", () => {
+        const t = {portID: "foo"};
+        mm.createChannel();
+        mm.channel.messagePorts.push(t);
+        assert.equal(mm.getTargetById("foo"), t);
+      });
+      it("should return null if the target doesn't exist", () => {
+        const t = {portID: "foo"};
+        mm.createChannel();
+        mm.channel.messagePorts.push(t);
+        assert.equal(mm.getTargetById("bar"), null);
+      });
+    });
+    describe("#onNewTabLoad", () => {
+      it("should dispatch a NEW_TAB_LOAD action", () => {
+        const t = {portID: "foo"};
+        sinon.stub(mm, "onActionFromContent");
+        mm.onNewTabLoad({target: t});
+        assert.calledWith(mm.onActionFromContent, {type: at.NEW_TAB_LOAD}, "foo");
+      });
+    });
+    describe("#onNewTabUnload", () => {
+      it("should dispatch a NEW_TAB_UNLOAD action", () => {
+        const t = {portID: "foo"};
+        sinon.stub(mm, "onActionFromContent");
+        mm.onNewTabUnload({target: t});
+        assert.calledWith(mm.onActionFromContent, {type: at.NEW_TAB_UNLOAD}, "foo");
+      });
+    });
+    describe("#onMessage", () => {
+      it("should report an error if the msg.data is missing", () => {
+        mm.onMessage({target: {portID: "foo"}});
+        assert.calledOnce(global.Components.utils.reportError);
+      });
+      it("should report an error if the msg.data.type is missing", () => {
+        mm.onMessage({target: {portID: "foo"}, data: "foo"});
+        assert.calledOnce(global.Components.utils.reportError);
+      });
+      it("should call onActionFromContent", () => {
+        sinon.stub(mm, "onActionFromContent");
+        const action = {data: {data: {}, type: "FOO"}, target: {portID: "foo"}};
+        const expectedAction = {
+          type: action.data.type,
+          data: action.data.data,
+          _target: {portID: "foo"}
+        };
+        mm.onMessage(action);
+        assert.calledWith(mm.onActionFromContent, expectedAction, "foo");
+      });
+    });
+  });
+  describe("Sending and broadcasting", () => {
+    describe("#send", () => {
+      it("should send a message on the right port", () => {
+        const t = {portID: "foo", sendAsyncMessage: sinon.spy()};
+        mm.createChannel();
+        mm.channel.messagePorts = [t];
+        const action = ac.SendToContent({type: "HELLO"}, "foo");
+        mm.send(action, "foo");
+        assert.calledWith(t.sendAsyncMessage, DEFAULT_OPTIONS.outgoingMessageName, action);
+      });
+      it("should not throw if the target isn't around", () => {
+        mm.createChannel();
+        // port is not added to the channel
+        const action = ac.SendToContent({type: "HELLO"}, "foo");
+
+        assert.doesNotThrow(() => mm.send(action, "foo"));
+      });
+    });
+    describe("#broadcast", () => {
+      it("should send a message on the channel", () => {
+        mm.createChannel();
+        const action = ac.BroadcastToContent({type: "HELLO"});
+        mm.broadcast(action);
+        assert.calledWith(mm.channel.sendAsyncMessage, DEFAULT_OPTIONS.outgoingMessageName, action);
+      });
+    });
+  });
+  describe("Handling actions", () => {
+    describe("#onActionFromContent", () => {
+      beforeEach(() => mm.onActionFromContent({type: "FOO"}, "foo"));
+      it("should dispatch a SendToMain action", () => {
+        assert.calledOnce(dispatch);
+        const action = dispatch.firstCall.args[0];
+        assert.equal(action.type, "FOO", "action.type");
+      });
+      it("should have the right fromTarget", () => {
+        const action = dispatch.firstCall.args[0];
+        assert.equal(action.meta.fromTarget, "foo", "meta.fromTarget");
+      });
+    });
+    describe("#middleware", () => {
+      let store;
+      beforeEach(() => {
+        store = createStore(addNumberReducer, applyMiddleware(mm.middleware));
+      });
+      it("should just call next if no channel is found", () => {
+        store.dispatch({type: "ADD", data: 10});
+        assert.equal(store.getState(), 10);
+      });
+      it("should call .send if the action is SendToContent", () => {
+        sinon.stub(mm, "send");
+        const action = ac.SendToContent({type: "FOO"}, "foo");
+
+        mm.createChannel();
+        store.dispatch(action);
+
+        assert.calledWith(mm.send, action);
+      });
+      it("should call .broadcast if the action is BroadcastToContent", () => {
+        sinon.stub(mm, "broadcast");
+        const action = ac.BroadcastToContent({type: "FOO"});
+
+        mm.createChannel();
+        store.dispatch(action);
+
+        assert.calledWith(mm.broadcast, action);
+      });
+      it("should dispatch other actions normally", () => {
+        sinon.stub(mm, "send");
+        sinon.stub(mm, "broadcast");
+
+        mm.createChannel();
+        store.dispatch({type: "ADD", data: 1});
+
+        assert.equal(store.getState(), 1);
+        assert.notCalled(mm.send);
+        assert.notCalled(mm.broadcast);
+      });
+    });
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/SearchFeed.test.js
@@ -0,0 +1,77 @@
+"use strict";
+const {SearchFeed} = require("lib/SearchFeed.jsm");
+const {GlobalOverrider} = require("test/unit/utils");
+const {actionTypes: at} = require("common/Actions.jsm");
+const fakeEngines = [{name: "Google", iconBuffer: "icon.ico"}];
+describe("Search Feed", () => {
+  let feed;
+  let globals;
+  before(() => {
+    globals = new GlobalOverrider();
+    globals.set("ContentSearch", {
+      currentStateObj: globals.sandbox.spy(() => Promise.resolve({engines: fakeEngines, currentEngine: {}})),
+      performSearch: globals.sandbox.spy((browser, searchData) => Promise.resolve({browser, searchData}))
+    });
+  });
+  beforeEach(() => {
+    feed = new SearchFeed();
+    feed.store = {dispatch: sinon.spy()};
+  });
+  afterEach(() => globals.reset());
+  after(() => globals.restore());
+
+  it("should call get state (with true) from the content search provider on INIT", () => {
+    feed.onAction({type: at.INIT});
+    // calling currentStateObj with 'true' allows us to return a data uri for the
+    // icon, instead of an array buffer
+    assert.calledWith(global.ContentSearch.currentStateObj, true);
+  });
+  it("should get the the state on INIT", () => {
+    sinon.stub(feed, "getState");
+    feed.onAction({type: at.INIT});
+    assert.calledOnce(feed.getState);
+  });
+  it("should add observers on INIT", () => {
+    sinon.stub(feed, "addObservers");
+    feed.onAction({type: at.INIT});
+    assert.calledOnce(feed.addObservers);
+  });
+  it("should remove observers on UNINIT", () => {
+    sinon.stub(feed, "removeObservers");
+    feed.onAction({type: at.UNINIT});
+    assert.calledOnce(feed.removeObservers);
+  });
+  it("should call services.obs.addObserver on INIT", () => {
+    feed.onAction({type: at.INIT});
+    assert.calledOnce(global.Services.obs.addObserver);
+  });
+  it("should call services.obs.removeObserver on UNINIT", () => {
+    feed.onAction({type: at.UNINIT});
+    assert.calledOnce(global.Services.obs.removeObserver);
+  });
+  it("should dispatch one event with the state", () => (
+    feed.getState().then(() => {
+      assert.calledOnce(feed.store.dispatch);
+    })
+  ));
+  it("should perform a search on PERFORM_SEARCH", () => {
+    sinon.stub(feed, "performSearch");
+    feed.onAction({_target: {browser: {}}, type: at.PERFORM_SEARCH});
+    assert.calledOnce(feed.performSearch);
+  });
+  it("should call performSearch with an action", () => {
+    const action = {_target: {browser: "browser"}, data: {searchString: "hello"}};
+    feed.performSearch(action._target.browser, action.data);
+    assert.calledWith(global.ContentSearch.performSearch, {target: action._target.browser}, action.data);
+  });
+  it("should get the state if we change the search engines", () => {
+    sinon.stub(feed, "getState");
+    feed.observe(null, "browser-search-engine-modified", "engine-current");
+    assert.calledOnce(feed.getState);
+  });
+  it("shouldn't get the state if it's not the right notification", () => {
+    sinon.stub(feed, "getState");
+    feed.observe(null, "some-other-notification", "engine-current");
+    assert.notCalled(feed.getState);
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/Store.test.js
@@ -0,0 +1,210 @@
+const injector = require("inject!lib/Store.jsm");
+const {createStore} = require("redux");
+const {addNumberReducer} = require("test/unit/utils");
+const {GlobalOverrider} = require("test/unit/utils");
+describe("Store", () => {
+  let Store;
+  let Preferences;
+  let sandbox;
+  let store;
+  let globals;
+  let PREF_PREFIX;
+  beforeEach(() => {
+    globals = new GlobalOverrider();
+    sandbox = globals.sandbox;
+    Preferences = new Map();
+    Preferences.observe = sandbox.spy();
+    Preferences.ignore = sandbox.spy();
+    globals.set("Preferences", Preferences);
+    function ActivityStreamMessageChannel(options) {
+      this.dispatch = options.dispatch;
+      this.createChannel = sandbox.spy();
+      this.destroyChannel = sandbox.spy();
+      this.middleware = sandbox.spy(s => next => action => next(action));
+    }
+    ({Store, PREF_PREFIX} = injector({"lib/ActivityStreamMessageChannel.jsm": {ActivityStreamMessageChannel}}));
+    store = new Store();
+  });
+  afterEach(() => {
+    Preferences.clear();
+    globals.restore();
+  });
+  it("should have an .feeds property that is a Map", () => {
+    assert.instanceOf(store.feeds, Map);
+    assert.equal(store.feeds.size, 0, ".feeds.size");
+  });
+  it("should have a redux store at ._store", () => {
+    assert.ok(store._store);
+    assert.property(store, "dispatch");
+    assert.property(store, "getState");
+  });
+  it("should create a ActivityStreamMessageChannel with the right dispatcher", () => {
+    assert.ok(store._messageChannel);
+    assert.equal(store._messageChannel.dispatch, store.dispatch);
+  });
+  it("should connect the ActivityStreamMessageChannel's middleware", () => {
+    store.dispatch({type: "FOO"});
+    assert.calledOnce(store._messageChannel.middleware);
+  });
+  describe("#initFeed", () => {
+    it("should add an instance of the feed to .feeds", () => {
+      class Foo {}
+      Preferences.set(`${PREF_PREFIX}foo`, false);
+      store.init({foo: () => new Foo()});
+      store.initFeed("foo");
+
+      assert.isTrue(store.feeds.has("foo"), "foo is set");
+      assert.instanceOf(store.feeds.get("foo"), Foo);
+    });
+    it("should add a .store property to the feed", () => {
+      class Foo {}
+      store._feedFactories = {foo: () => new Foo()};
+      store.initFeed("foo");
+
+      assert.propertyVal(store.feeds.get("foo"), "store", store);
+    });
+  });
+  describe("#uninitFeed", () => {
+    it("should not throw if no feed with that name exists", () => {
+      assert.doesNotThrow(() => {
+        store.uninitFeed("bar");
+      });
+    });
+    it("should call the feed's uninit function if it is defined", () => {
+      let feed;
+      function createFeed() {
+        feed = {uninit: sinon.spy()};
+        return feed;
+      }
+      store._feedFactories = {foo: createFeed};
+
+      store.initFeed("foo");
+      store.uninitFeed("foo");
+
+      assert.calledOnce(feed.uninit);
+    });
+    it("should remove the feed from .feeds", () => {
+      class Foo {}
+      store._feedFactories = {foo: () => new Foo()};
+
+      store.initFeed("foo");
+      store.uninitFeed("foo");
+
+      assert.isFalse(store.feeds.has("foo"), "foo is not in .feeds");
+    });
+  });
+  describe("maybeStartFeedAndListenForPrefChanges", () => {
+    beforeEach(() => {
+      sinon.stub(store, "initFeed");
+      sinon.stub(store, "uninitFeed");
+    });
+    it("should set the new pref in Preferences to true, if it was never defined", () => {
+      store.maybeStartFeedAndListenForPrefChanges("foo");
+      assert.isTrue(Preferences.get(`${PREF_PREFIX}foo`));
+    });
+    it("should not override the pref if it was already set", () => {
+      Preferences.set(`${PREF_PREFIX}foo`, false);
+      store.maybeStartFeedAndListenForPrefChanges("foo");
+      assert.isFalse(Preferences.get(`${PREF_PREFIX}foo`));
+    });
+    it("should initialize the feed if the Pref is set to true", () => {
+      Preferences.set(`${PREF_PREFIX}foo`, true);
+      store.maybeStartFeedAndListenForPrefChanges("foo");
+      assert.calledWith(store.initFeed, "foo");
+    });
+    it("should not initialize the feed if the Pref is set to false", () => {
+      Preferences.set(`${PREF_PREFIX}foo`, false);
+      store.maybeStartFeedAndListenForPrefChanges("foo");
+      assert.notCalled(store.initFeed);
+    });
+    it("should observe the pref", () => {
+      store.maybeStartFeedAndListenForPrefChanges("foo");
+      assert.calledWith(Preferences.observe, `${PREF_PREFIX}foo`, store._prefHandlers.get(`${PREF_PREFIX}foo`));
+    });
+    describe("handler", () => {
+      let handler;
+      beforeEach(() => {
+        store.maybeStartFeedAndListenForPrefChanges("foo");
+        handler = store._prefHandlers.get(`${PREF_PREFIX}foo`);
+      });
+      it("should initialize the feed if called with true", () => {
+        handler(true);
+        assert.calledWith(store.initFeed, "foo");
+      });
+      it("should uninitialize the feed if called with false", () => {
+        handler(false);
+        assert.calledWith(store.uninitFeed, "foo");
+      });
+    });
+  });
+  describe("#init", () => {
+    it("should call .maybeStartFeedAndListenForPrefChanges with each key", () => {
+      sinon.stub(store, "maybeStartFeedAndListenForPrefChanges");
+      store.init({foo: () => {}, bar: () => {}});
+      assert.calledWith(store.maybeStartFeedAndListenForPrefChanges, "foo");
+      assert.calledWith(store.maybeStartFeedAndListenForPrefChanges, "bar");
+    });
+    it("should initialize the ActivityStreamMessageChannel channel", () => {
+      store.init();
+      assert.calledOnce(store._messageChannel.createChannel);
+    });
+  });
+  describe("#uninit", () => {
+    it("should clear .feeds, ._prefHandlers, and ._feedFactories", () => {
+      store.init({
+        a: () => ({}),
+        b: () => ({}),
+        c: () => ({})
+      });
+
+      store.uninit();
+
+      assert.equal(store.feeds.size, 0);
+      assert.equal(store._prefHandlers.size, 0);
+      assert.isNull(store._feedFactories);
+    });
+    it("should destroy the ActivityStreamMessageChannel channel", () => {
+      store.uninit();
+      assert.calledOnce(store._messageChannel.destroyChannel);
+    });
+  });
+  describe("#getState", () => {
+    it("should return the redux state", () => {
+      store._store = createStore((prevState = 123) => prevState);
+      const {getState} = store;
+      assert.equal(getState(), 123);
+    });
+  });
+  describe("#dispatch", () => {
+    it("should call .onAction of each feed", () => {
+      const {dispatch} = store;
+      const sub = {onAction: sinon.spy()};
+      const action = {type: "FOO"};
+
+      store.init({sub: () => sub});
+
+      dispatch(action);
+
+      assert.calledWith(sub.onAction, action);
+    });
+    it("should call the reducers", () => {
+      const {dispatch} = store;
+      store._store = createStore(addNumberReducer);
+
+      dispatch({type: "ADD", data: 14});
+
+      assert.equal(store.getState(), 14);
+    });
+  });
+  describe("#subscribe", () => {
+    it("should subscribe to changes to the store", () => {
+      const sub = sinon.spy();
+      const action = {type: "FOO"};
+
+      store.subscribe(sub);
+      store.dispatch(action);
+
+      assert.calledOnce(sub);
+    });
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/TelemetrySender.test.js
@@ -0,0 +1,271 @@
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/publicdomain/zero/1.0/
+
+const {GlobalOverrider, FakePrefs} = require("test/unit/utils");
+const {TelemetrySender} = require("lib/TelemetrySender.jsm");
+
+/**
+ * A reference to the fake preferences object created by the TelemetrySender
+ * constructor so that we can use the API.
+ */
+let fakePrefs;
+const prefInitHook = function() {
+  fakePrefs = this; // eslint-disable-line consistent-this
+};
+const tsArgs = {prefInitHook};
+
+describe("TelemetrySender", () => {
+  let globals;
+  let tSender;
+  let fetchStub;
+  const observerTopics = ["user-action-event", "performance-event",
+    "tab-session-complete", "undesired-event"];
+  const fakeEndpointUrl = "http://127.0.0.1/stuff";
+  const fakePingJSON = JSON.stringify({action: "fake_action", monkey: 1});
+  const fakeFetchHttpErrorResponse = {ok: false, status: 400};
+  const fakeFetchSuccessResponse = {ok: true, status: 200};
+
+  function assertNotificationObserversAdded() {
+    observerTopics.forEach(topic => {
+      assert.calledWithExactly(
+        global.Services.obs.addObserver, tSender, topic, true);
+    });
+  }
+
+  function assertNotificationObserversRemoved() {
+    observerTopics.forEach(topic => {
+      assert.calledWithExactly(
+        global.Services.obs.removeObserver, tSender, topic);
+    });
+  }
+
+  before(() => {
+    globals = new GlobalOverrider();
+
+    fetchStub = globals.sandbox.stub();
+
+    globals.set("Preferences", FakePrefs);
+    globals.set("fetch", fetchStub);
+  });
+
+  beforeEach(() => {
+  });
+
+  afterEach(() => {
+    globals.reset();
+    FakePrefs.prototype.prefs = {};
+  });
+
+  after(() => globals.restore());
+
+  it("should construct the Prefs object with the right branch", () => {
+    globals.sandbox.spy(global, "Preferences");
+
+    tSender = new TelemetrySender(tsArgs);
+
+    assert.calledOnce(global.Preferences);
+    assert.calledWith(global.Preferences,
+      sinon.match.has("branch", "browser.newtabpage.activity-stream"));
+  });
+
+  it("should set the enabled prop to false if the pref is false", () => {
+    FakePrefs.prototype.prefs = {telemetry: false};
+
+    tSender = new TelemetrySender(tsArgs);
+
+    assert.isFalse(tSender.enabled);
+  });
+
+  it("should not add notification observers if the enabled pref is false", () => {
+    FakePrefs.prototype.prefs = {telemetry: false};
+
+    tSender = new TelemetrySender(tsArgs);
+
+    assert.notCalled(global.Services.obs.addObserver);
+  });
+
+  it("should set the enabled prop to true if the pref is true", () => {
+    FakePrefs.prototype.prefs = {telemetry: true};
+
+    tSender = new TelemetrySender(tsArgs);
+
+    assert.isTrue(tSender.enabled);
+  });
+
+  it("should add all notification observers if the enabled pref is true", () => {
+    FakePrefs.prototype.prefs = {telemetry: true};
+
+    tSender = new TelemetrySender(tsArgs);
+
+    assertNotificationObserversAdded();
+  });
+
+  describe("#_sendPing()", () => {
+    beforeEach(() => {
+      FakePrefs.prototype.prefs = {
+        "telemetry": true,
+        "telemetry.ping.endpoint": fakeEndpointUrl
+      };
+      tSender = new TelemetrySender(tsArgs);
+    });
+
+    it("should POST given ping data to telemetry.ping.endpoint pref w/fetch",
+    async () => {
+      fetchStub.resolves(fakeFetchSuccessResponse);
+      await tSender._sendPing(fakePingJSON);
+
+      assert.calledOnce(fetchStub);
+      assert.calledWithExactly(fetchStub, fakeEndpointUrl,
+        {method: "POST", body: fakePingJSON});
+    });
+
+    it("should log HTTP failures using Cu.reportError", async () => {
+      fetchStub.resolves(fakeFetchHttpErrorResponse);
+
+      await tSender._sendPing(fakePingJSON);
+
+      assert.called(Components.utils.reportError);
+    });
+
+    it("should log an error using Cu.reportError if fetch rejects", async () => {
+      fetchStub.rejects("Oh noes!");
+
+      await tSender._sendPing(fakePingJSON);
+
+      assert.called(Components.utils.reportError);
+    });
+
+    it("should log if logging is on && if action is not activity_stream_performance", async () => {
+      FakePrefs.prototype.prefs = {
+        "telemetry": true,
+        "performance.log": true
+      };
+      fetchStub.resolves(fakeFetchSuccessResponse);
+      tSender = new TelemetrySender(tsArgs);
+
+      await tSender._sendPing(fakePingJSON);
+
+      assert.called(console.log); // eslint-disable-line no-console
+    });
+  });
+
+  describe("#observe()", () => {
+    before(() => {
+      globals.sandbox.stub(TelemetrySender.prototype, "_sendPing");
+    });
+
+    observerTopics.forEach(topic => {
+      it(`should call this._sendPing with data for ${topic}`, () => {
+        const fakeSubject = "fakeSubject";
+        tSender = new TelemetrySender(tsArgs);
+
+        tSender.observe(fakeSubject, topic, fakePingJSON);
+
+        assert.calledOnce(TelemetrySender.prototype._sendPing);
+        assert.calledWithExactly(TelemetrySender.prototype._sendPing,
+          fakePingJSON);
+      });
+    });
+
+    it("should not call this._sendPing for 'nonexistent-topic'", () => {
+      const fakeSubject = "fakeSubject";
+      tSender = new TelemetrySender(tsArgs);
+
+      tSender.observe(fakeSubject, "nonexistent-topic", fakePingJSON);
+
+      assert.notCalled(TelemetrySender.prototype._sendPing);
+    });
+  });
+
+  describe("#uninit()", () => {
+    it("should remove the telemetry pref listener", () => {
+      tSender = new TelemetrySender(tsArgs);
+      assert.property(fakePrefs.observers, "telemetry");
+
+      tSender.uninit();
+
+      assert.notProperty(fakePrefs.observers, "telemetry");
+    });
+
+    it("should remove all notification observers if telemetry pref is true", () => {
+      FakePrefs.prototype.prefs = {telemetry: true};
+      tSender = new TelemetrySender(tsArgs);
+
+      tSender.uninit();
+
+      assertNotificationObserversRemoved();
+    });
+
+    it("should not remove notification observers if telemetry pref is false", () => {
+      FakePrefs.prototype.prefs = {telemetry: false};
+      tSender = new TelemetrySender(tsArgs);
+
+      tSender.uninit();
+
+      assert.notCalled(global.Services.obs.removeObserver);
+    });
+
+    it("should call Cu.reportError if this._prefs.ignore throws", () => {
+      globals.sandbox.stub(FakePrefs.prototype, "ignore").throws("Some Error");
+      tSender = new TelemetrySender(tsArgs);
+
+      tSender.uninit();
+
+      assert.called(global.Components.utils.reportError);
+    });
+  });
+
+  describe("Misc pref changes", () => {
+    describe("telemetry changes from true to false", () => {
+      beforeEach(() => {
+        FakePrefs.prototype.prefs = {"telemetry": true};
+        tSender = new TelemetrySender(tsArgs);
+        assert.propertyVal(tSender, "enabled", true);
+      });
+
+      it("should set the enabled property to false", () => {
+        fakePrefs.set("telemetry", false);
+
+        assert.propertyVal(tSender, "enabled", false);
+      });
+
+      it("should remove all notification observers", () => {
+        fakePrefs.set("telemetry", false);
+
+        assertNotificationObserversRemoved();
+      });
+    });
+
+    describe("telemetry changes from false to true", () => {
+      beforeEach(() => {
+        FakePrefs.prototype.prefs = {"telemetry": false};
+        tSender = new TelemetrySender(tsArgs);
+        assert.propertyVal(tSender, "enabled", false);
+      });
+
+      it("should set the enabled property to true", () => {
+        fakePrefs.set("telemetry", true);
+
+        assert.propertyVal(tSender, "enabled", true);
+      });
+
+      it("should add all topic observers", () => {
+        fakePrefs.set("telemetry", true);
+
+        assertNotificationObserversAdded();
+      });
+    });
+
+    describe("performance.log changes from false to true", () => {
+      it("should change this.logging from false to true", () => {
+        FakePrefs.prototype.prefs = {"performance.log": false};
+        tSender = new TelemetrySender(tsArgs);
+        assert.propertyVal(tSender, "logging", false);
+
+        fakePrefs.set("performance.log", true);
+
+        assert.propertyVal(tSender, "logging", true);
+      });
+    });
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
@@ -0,0 +1,116 @@
+"use strict";
+const {TopSitesFeed, UPDATE_TIME, TOP_SITES_SHOWMORE_LENGTH, DEFAULT_TOP_SITES} = require("lib/TopSitesFeed.jsm");
+const {GlobalOverrider} = require("test/unit/utils");
+const action = {meta: {fromTarget: {}}};
+const {actionTypes: at} = require("common/Actions.jsm");
+const FAKE_LINKS = new Array(TOP_SITES_SHOWMORE_LENGTH).fill(null).map((v, i) => ({url: `site${i}.com`}));
+const FAKE_SCREENSHOT = "data123";
+
+describe("Top Sites Feed", () => {
+  let feed;
+  let globals;
+  let sandbox;
+  let links;
+  let clock;
+  before(() => {
+    globals = new GlobalOverrider();
+    sandbox = globals.sandbox;
+  });
+  beforeEach(() => {
+    globals.set("PlacesProvider", {links: {getLinks: sandbox.spy(() => Promise.resolve(links))}});
+    globals.set("PreviewProvider", {getThumbnail: sandbox.spy(() => Promise.resolve(FAKE_SCREENSHOT))});
+    feed = new TopSitesFeed();
+    feed.store = {dispatch: sinon.spy(), getState() { return {TopSites: {rows: Array(12).fill("site")}}; }};
+    links = FAKE_LINKS;
+    clock = sinon.useFakeTimers();
+  });
+  afterEach(() => {
+    globals.restore();
+    clock.restore();
+  });
+
+  it("should have default sites with .isDefault = true", () => {
+    DEFAULT_TOP_SITES.forEach(link => assert.propertyVal(link, "isDefault", true));
+  });
+
+  describe("#getLinksWithDefaults", () => {
+    it("should get the links from Places Provider", async () => {
+      const result = await feed.getLinksWithDefaults();
+      assert.deepEqual(result, links);
+      assert.calledOnce(global.PlacesProvider.links.getLinks);
+    });
+    it("should add defaults if there are are not enough links", async () => {
+      links = [{url: "foo.com"}];
+      const result = await feed.getLinksWithDefaults();
+      assert.deepEqual(result, [{url: "foo.com"}, ...DEFAULT_TOP_SITES]);
+    });
+    it("should only add defaults up to TOP_SITES_SHOWMORE_LENGTH", async () => {
+      links = new Array(TOP_SITES_SHOWMORE_LENGTH - 1).fill({url: "foo.com"});
+      const result = await feed.getLinksWithDefaults();
+      assert.lengthOf(result, TOP_SITES_SHOWMORE_LENGTH);
+      assert.deepEqual(result, [...links, DEFAULT_TOP_SITES[0]]);
+    });
+    it("should not throw if PlacesProvider returns null", () => {
+      links = null;
+      assert.doesNotThrow(() => {
+        feed.getLinksWithDefaults(action);
+      });
+    });
+  });
+  describe("#refresh", () => {
+    it("should dispatch an action with the links returned", async () => {
+      sandbox.stub(feed, "getScreenshot");
+      await feed.refresh(action);
+      assert.calledOnce(feed.store.dispatch);
+      assert.propertyVal(feed.store.dispatch.firstCall.args[0], "type", at.TOP_SITES_UPDATED);
+      assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, links);
+    });
+    it("should call .getScreenshot for each link", async () => {
+      sandbox.stub(feed, "getScreenshot");
+      await feed.refresh(action);
+
+      links.forEach(link => assert.calledWith(feed.getScreenshot, link.url));
+    });
+  });
+  describe("getScreenshot", () => {
+    it("should call PreviewProvider.getThumbnail with the right url", async () => {
+      const url = "foo.com";
+      await feed.getScreenshot(url);
+      assert.calledWith(global.PreviewProvider.getThumbnail, url);
+    });
+  });
+  describe("#onAction", () => {
+    it("should call refresh if there are not enough sites on NEW_TAB_LOAD", () => {
+      feed.store.getState = function() { return {TopSites: {rows: []}}; };
+      sinon.stub(feed, "refresh");
+      feed.onAction({type: at.NEW_TAB_LOAD});
+      assert.calledOnce(feed.refresh);
+    });
+    it("should call refresh if there are not sites on NEW_TAB_LOAD, not counting defaults", () => {
+      feed.store.getState = function() { return {TopSites: {rows: [{url: "foo.com"}, ...DEFAULT_TOP_SITES]}}; };
+      sinon.stub(feed, "refresh");
+      feed.onAction({type: at.NEW_TAB_LOAD});
+      assert.calledOnce(feed.refresh);
+    });
+    it("should not call refresh if there are enough sites on NEW_TAB_LOAD", () => {
+      feed.lastUpdated = Date.now();
+      sinon.stub(feed, "refresh");
+      feed.onAction({type: at.NEW_TAB_LOAD});
+      assert.notCalled(feed.refresh);
+    });
+    it("should call refresh if .lastUpdated is too old on NEW_TAB_LOAD", () => {
+      feed.lastUpdated = 0;
+      clock.tick(UPDATE_TIME);
+      sinon.stub(feed, "refresh");
+      feed.onAction({type: at.NEW_TAB_LOAD});
+      assert.calledOnce(feed.refresh);
+    });
+    it("should not call refresh if .lastUpdated is less than update time on NEW_TAB_LOAD", () => {
+      feed.lastUpdated = 0;
+      clock.tick(UPDATE_TIME - 1);
+      sinon.stub(feed, "refresh");
+      feed.onAction({type: at.NEW_TAB_LOAD});
+      assert.notCalled(feed.refresh);
+    });
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/init-store.test.js
@@ -0,0 +1,43 @@
+const initStore = require("content-src/lib/init-store");
+const {GlobalOverrider, addNumberReducer} = require("test/unit/utils");
+const {actionCreators: ac} = require("common/Actions.jsm");
+
+describe("initStore", () => {
+  let globals;
+  let store;
+  before(() => {
+    globals = new GlobalOverrider();
+    globals.set("sendAsyncMessage", globals.sandbox.spy());
+    globals.set("addMessageListener", globals.sandbox.spy());
+  });
+  beforeEach(() => {
+    store = initStore({number: addNumberReducer});
+  });
+  afterEach(() => globals.reset());
+  after(() => globals.restore());
+  it("should create a store with the provided reducers", () => {
+    assert.ok(store);
+    assert.property(store.getState(), "number");
+  });
+  it("should add a listener for incoming actions", () => {
+    assert.calledWith(global.addMessageListener, initStore.INCOMING_MESSAGE_NAME);
+    const callback = global.addMessageListener.firstCall.args[1];
+    globals.sandbox.spy(store, "dispatch");
+    const message = {name: initStore.INCOMING_MESSAGE_NAME, data: {type: "FOO"}};
+    callback(message);
+    assert.calledWith(store.dispatch, message.data);
+  });
+  it("should replace the state if a MERGE_STORE_ACTION is dispatched", () => {
+    store.dispatch({type: initStore.MERGE_STORE_ACTION, data: {number: 42}});
+    assert.deepEqual(store.getState(), {number: 42});
+  });
+  it("should send out SendToMain ations", () => {
+    const action = ac.SendToMain({type: "FOO"});
+    store.dispatch(action);
+    assert.calledWith(global.sendAsyncMessage, initStore.OUTGOING_MESSAGE_NAME, action);
+  });
+  it("should not send out other types of ations", () => {
+    store.dispatch({type: "FOO"});
+    assert.notCalled(global.sendAsyncMessage);
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/unit-entry.js
@@ -0,0 +1,38 @@
+const {GlobalOverrider} = require("test/unit/utils");
+
+const req = require.context(".", true, /\.test\.js$/);
+const files = req.keys();
+
+// This exposes sinon assertions to chai.assert
+sinon.assert.expose(assert, {prefix: ""});
+
+let overrider = new GlobalOverrider();
+overrider.set({
+  Components: {
+    interfaces: {},
+    utils: {
+      import: overrider.sandbox.spy(),
+      importGlobalProperties: overrider.sandbox.spy(),
+      reportError: overrider.sandbox.spy()
+    }
+  },
+  XPCOMUtils: {
+    defineLazyModuleGetter: overrider.sandbox.spy(),
+    defineLazyServiceGetter: overrider.sandbox.spy(),
+    generateQI: overrider.sandbox.stub().returns(() => {})
+  },
+  console: {log: overrider.sandbox.spy()},
+  dump: overrider.sandbox.spy(),
+  Services: {
+    obs: {
+      addObserver: overrider.sandbox.spy(),
+      removeObserver: overrider.sandbox.spy()
+    }
+  }
+});
+
+describe("activity-stream", () => {
+  afterEach(() => overrider.reset());
+  after(() => overrider.restore());
+  files.forEach(file => req(file));
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/utils.js
@@ -0,0 +1,122 @@
+/**
+ * GlobalOverrider - Utility that allows you to override properties on the global object.
+ *                   See unit-entry.js for example usage.
+ */
+class GlobalOverrider {
+  constructor() {
+    this.originalGlobals = new Map();
+    this.sandbox = sinon.sandbox.create();
+  }
+
+  /**
+   * _override - Internal method to override properties on the global object.
+   *             The first time a given key is overridden, we cache the original
+   *             value in this.originalGlobals so that later it can be restored.
+   *
+   * @param  {string} key The identifier of the property
+   * @param  {any} value The value to which the property should be reassigned
+   */
+  _override(key, value) {
+    if (key === "Components") {
+      // Components can be reassigned, but it will subsequently throw a deprecation
+      // error in Firefox which will stop execution. Adding the assignment statement
+      // to a try/catch block will prevent this from happening.
+      try {
+        global[key] = value;
+      } catch (e) {} // eslint-disable-line no-empty
+      return;
+    }
+    if (!this.originalGlobals.has(key)) {
+      this.originalGlobals.set(key, global[key]);
+    }
+    global[key] = value;
+  }
+
+  /**
+   * set - Override a given property, or all properties on an object
+   *
+   * @param  {string|object} key If a string, the identifier of the property
+   *                             If an object, a number of properties and values to which they should be reassigned.
+   * @param  {any} value The value to which the property should be reassigned
+   * @return {type}       description
+   */
+  set(key, value) {
+    if (!value && typeof key === "object") {
+      const overrides = key;
+      Object.keys(overrides).forEach(k => this._override(k, overrides[k]));
+    } else {
+      this._override(key, value);
+    }
+  }
+
+  /**
+   * reset - Reset the global sandbox, so all state on spies, stubs etc. is cleared.
+   *         You probably want to call this after each test.
+   */
+  reset() {
+    this.sandbox.reset();
+  }
+
+  /**
+   * restore - Restore the global sandbox and reset all overriden properties to
+   *           their original values. You should call this after all tests have completed.
+   */
+  restore() {
+    this.sandbox.restore();
+    this.originalGlobals.forEach((value, key) => {
+      global[key] = value;
+    });
+  }
+}
+
+/**
+ * Very simple fake for the most basic semantics of Preferences.jsm. Lots of
+ * things aren't yet supported.  Feel free to add them in.
+ *
+ * @param {Object} args - optional arguments
+ * @param {Function} args.initHook - if present, will be called back
+ *                   inside the constructor. Typically used from tests
+ *                   to save off a pointer to the created instance so that
+ *                   stubs and spies can be inspected by the test code.
+ */
+function FakePrefs(args) {
+  if (args) {
+    if ("initHook" in args) {
+      args.initHook.call(this);
+    }
+  }
+}
+FakePrefs.prototype = {
+  observers: {},
+  observe(prefName, callback) {
+    this.observers[prefName] = callback;
+  },
+  ignore(prefName, callback) {
+    if (prefName in this.observers) {
+      delete this.observers[prefName];
+    }
+  },
+
+  prefs: {},
+  get(prefName) { return this.prefs[prefName]; },
+  set(prefName, value) {
+    this.prefs[prefName] = value;
+
+    if (prefName in this.observers) {
+      this.observers[prefName](value);
+    }
+  }
+};
+
+/**
+ * addNumberReducer - a simple dummy reducer for testing that adds a number
+ */
+function addNumberReducer(prevState = 0, action) {
+  return action.type === "ADD" ? prevState + action.data : prevState;
+}
+
+module.exports = {
+  FakePrefs,
+  GlobalOverrider,
+  addNumberReducer
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/vendor/redux.js
@@ -0,0 +1,948 @@
+/**
+ * Redux v.3.6.0
+ */
+(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;
+	exports.compose = exports.applyMiddleware = exports.bindActionCreators = exports.combineReducers = exports.createStore = undefined;
+
+	var _createStore = __webpack_require__(2);
+
+	var _createStore2 = _interopRequireDefault(_createStore);
+
+	var _combineReducers = __webpack_require__(7);
+
+	var _combineReducers2 = _interopRequireDefault(_combineReducers);
+
+	var _bindActionCreators = __webpack_require__(6);
+
+	var _bindActionCreators2 = _interopRequireDefault(_bindActionCreators);
+
+	var _applyMiddleware = __webpack_require__(5);
+
+	var _applyMiddleware2 = _interopRequireDefault(_applyMiddleware);
+
+	var _compose = __webpack_require__(1);
+
+	var _compose2 = _interopRequireDefault(_compose);
+
+	var _warning = __webpack_require__(3);
+
+	var _warning2 = _interopRequireDefault(_warning);
+
+	function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+	/*
+	* This is a dummy function to check if the function name has been altered by minification.
+	* If the function has been minified and NODE_ENV !== 'production', warn the user.
+	*/
+	function isCrushed() {}
+
+	if (("development") !== 'production' && typeof isCrushed.name === 'string' && isCrushed.name !== 'isCrushed') {
+	  (0, _warning2['default'])('You are currently using minified code outside of NODE_ENV === \'production\'. ' + 'This means that you are running a slower development build of Redux. ' + 'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' + 'or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ' + 'to ensure you have the correct code for your production build.');
+	}
+
+	exports.createStore = _createStore2['default'];
+	exports.combineReducers = _combineReducers2['default'];
+	exports.bindActionCreators = _bindActionCreators2['default'];
+	exports.applyMiddleware = _applyMiddleware2['default'];
+	exports.compose = _compose2['default'];
+
+/***/ },
+/* 1 */
+/***/ function(module, exports) {
+
+	"use strict";
+
+	exports.__esModule = true;
+	exports["default"] = compose;
+	/**
+	 * Composes single-argument functions from right to left. The rightmost
+	 * function can take multiple arguments as it provides the signature for
+	 * the resulting composite function.
+	 *
+	 * @param {...Function} funcs The functions to compose.
+	 * @returns {Function} A function obtained by composing the argument functions
+	 * from right to left. For example, compose(f, g, h) is identical to doing
+	 * (...args) => f(g(h(...args))).
+	 */
+
+	function compose() {
+	  for (var _len = arguments.length, funcs = Array(_len), _key = 0; _key < _len; _key++) {
+	    funcs[_key] = arguments[_key];
+	  }
+
+	  if (funcs.length === 0) {
+	    return function (arg) {
+	      return arg;
+	    };
+	  }
+
+	  if (funcs.length === 1) {
+	    return funcs[0];
+	  }
+
+	  var last = funcs[funcs.length - 1];
+	  var rest = funcs.slice(0, -1);
+	  return function () {
+	    return rest.reduceRight(function (composed, f) {
+	      return f(composed);
+	    }, last.apply(undefined, arguments));
+	  };
+	}
+
+/***/ },
+/* 2 */
+/***/ function(module, exports, __webpack_require__) {
+
+	'use strict';
+
+	exports.__esModule = true;
+	exports.ActionTypes = undefined;
+	exports['default'] = createStore;
+
+	var _isPlainObject = __webpack_require__(4);
+
+	var _isPlainObject2 = _interopRequireDefault(_isPlainObject);
+
+	var _symbolObservable = __webpack_require__(12);
+
+	var _symbolObservable2 = _interopRequireDefault(_symbolObservable);
+
+	function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+	/**
+	 * 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 = exports.ActionTypes = {
+	  INIT: '@@redux/INIT'
+	};
+
+	/**
+	 * 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} [preloadedState] 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.
+	 *
+	 * @param {Function} enhancer The store enhancer. You may optionally specify it
+	 * to enhance the store with third-party capabilities such as middleware,
+	 * time travel, persistence, etc. The only store enhancer that ships with Redux
+	 * is `applyMiddleware()`.
+	 *
+	 * @returns {Store} A Redux store that lets you read the state, dispatch actions
+	 * and subscribe to changes.
+	 */
+	function createStore(reducer, preloadedState, enhancer) {
+	  var _ref2;
+
+	  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
+	    enhancer = preloadedState;
+	    preloadedState = undefined;
+	  }
+
+	  if (typeof enhancer !== 'undefined') {
+	    if (typeof enhancer !== 'function') {
+	      throw new Error('Expected the enhancer to be a function.');
+	    }
+
+	    return enhancer(createStore)(reducer, preloadedState);
+	  }
+
+	  if (typeof reducer !== 'function') {
+	    throw new Error('Expected the reducer to be a function.');
+	  }
+
+	  var currentReducer = reducer;
+	  var currentState = preloadedState;
+	  var currentListeners = [];
+	  var nextListeners = currentListeners;
+	  var isDispatching = false;
+
+	  function ensureCanMutateNextListeners() {
+	    if (nextListeners === currentListeners) {
+	      nextListeners = currentListeners.slice();
+	    }
+	  }
+
+	  /**
+	   * 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.
+	   *
+	   * You may call `dispatch()` from a change listener, with the following
+	   * caveats:
+	   *
+	   * 1. The subscriptions are snapshotted just before every `dispatch()` call.
+	   * If you subscribe or unsubscribe while the listeners are being invoked, this
+	   * will not have any effect on the `dispatch()` that is currently in progress.
+	   * However, the next `dispatch()` call, whether nested or not, will use a more
+	   * recent snapshot of the subscription list.
+	   *
+	   * 2. The listener should not expect to see all state changes, as the state
+	   * might have been updated multiple times during a nested `dispatch()` before
+	   * the listener is called. It is, however, guaranteed that all subscribers
+	   * registered before the `dispatch()` started will be called with the latest
+	   * state by the time it exits.
+	   *
+	   * @param {Function} listener A callback to be invoked on every dispatch.
+	   * @returns {Function} A function to remove this change listener.
+	   */
+	  function subscribe(listener) {
+	    if (typeof listener !== 'function') {
+	      throw new Error('Expected listener to be a function.');
+	    }
+
+	    var isSubscribed = true;
+
+	    ensureCanMutateNextListeners();
+	    nextListeners.push(listener);
+
+	    return function unsubscribe() {
+	      if (!isSubscribed) {
+	        return;
+	      }
+
+	      isSubscribed = false;
+
+	      ensureCanMutateNextListeners();
+	      var index = nextListeners.indexOf(listener);
+	      nextListeners.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`. An action must have
+	   * a `type` property which may not be `undefined`. It is a good idea to use
+	   * string constants for action types.
+	   *
+	   * @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 (!(0, _isPlainObject2['default'])(action)) {
+	      throw new Error('Actions must be plain objects. ' + 'Use custom middleware for async actions.');
+	    }
+
+	    if (typeof action.type === 'undefined') {
+	      throw new Error('Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?');
+	    }
+
+	    if (isDispatching) {
+	      throw new Error('Reducers may not dispatch actions.');
+	    }
+
+	    try {
+	      isDispatching = true;
+	      currentState = currentReducer(currentState, action);
+	    } finally {
+	      isDispatching = false;
+	    }
+
+	    var listeners = currentListeners = nextListeners;
+	    for (var i = 0; i < listeners.length; i++) {
+	      listeners[i]();
+	    }
+
+	    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) {
+	    if (typeof nextReducer !== 'function') {
+	      throw new Error('Expected the nextReducer to be a function.');
+	    }
+
+	    currentReducer = nextReducer;
+	    dispatch({ type: ActionTypes.INIT });
+	  }
+
+	  /**
+	   * Interoperability point for observable/reactive libraries.
+	   * @returns {observable} A minimal observable of state changes.
+	   * For more information, see the observable proposal:
+	   * https://github.com/zenparsing/es-observable
+	   */
+	  function observable() {
+	    var _ref;
+
+	    var outerSubscribe = subscribe;
+	    return _ref = {
+	      /**
+	       * The minimal observable subscription method.
+	       * @param {Object} observer Any object that can be used as an observer.
+	       * The observer object should have a `next` method.
+	       * @returns {subscription} An object with an `unsubscribe` method that can
+	       * be used to unsubscribe the observable from the store, and prevent further
+	       * emission of values from the observable.
+	       */
+	      subscribe: function subscribe(observer) {
+	        if (typeof observer !== 'object') {
+	          throw new TypeError('Expected the observer to be an object.');
+	        }
+
+	        function observeState() {
+	          if (observer.next) {
+	            observer.next(getState());
+	          }
+	        }
+
+	        observeState();
+	        var unsubscribe = outerSubscribe(observeState);
+	        return { unsubscribe: unsubscribe };
+	      }
+	    }, _ref[_symbolObservable2['default']] = function () {
+	      return this;
+	    }, _ref;
+	  }
+
+	  // 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 _ref2 = {
+	    dispatch: dispatch,
+	    subscribe: subscribe,
+	    getState: getState,
+	    replaceReducer: replaceReducer
+	  }, _ref2[_symbolObservable2['default']] = observable, _ref2;
+	}
+
+/***/ },
+/* 3 */
+/***/ function(module, exports) {
+
+	'use strict';
+
+	exports.__esModule = true;
+	exports['default'] = warning;
+	/**
+	 * Prints a warning in the console if it exists.
+	 *
+	 * @param {String} message The warning message.
+	 * @returns {void}
+	 */
+	function warning(message) {
+	  /* eslint-disable no-console */
+	  if (typeof console !== 'undefined' && typeof console.error === 'function') {
+	    console.error(message);
+	  }
+	  /* eslint-enable no-console */
+	  try {
+	    // This error was thrown as a convenience so that if you enable
+	    // "break on all exceptions" in your console,
+	    // it would pause the execution at this line.
+	    throw new Error(message);
+	    /* eslint-disable no-empty */
+	  } catch (e) {}
+	  /* eslint-enable no-empty */
+	}
+
+/***/ },
+/* 4 */
+/***/ function(module, exports, __webpack_require__) {
+
+	var getPrototype = __webpack_require__(8),
+	    isHostObject = __webpack_require__(9),
+	    isObjectLike = __webpack_require__(11);
+
+	/** `Object#toString` result references. */
+	var objectTag = '[object Object]';
+
+	/** Used for built-in method references. */
+	var funcProto = Function.prototype,
+	    objectProto = Object.prototype;
+
+	/** Used to resolve the decompiled source of functions. */
+	var funcToString = funcProto.toString;
+
+	/** Used to check objects for own properties. */
+	var hasOwnProperty = objectProto.hasOwnProperty;
+
+	/** Used to infer the `Object` constructor. */
+	var objectCtorString = funcToString.call(Object);
+
+	/**
+	 * Used to resolve the
+	 * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
+	 * of values.
+	 */
+	var objectToString = objectProto.toString;
+
+	/**
+	 * Checks if `value` is a plain object, that is, an object created by the
+	 * `Object` constructor or one with a `[[Prototype]]` of `null`.
+	 *
+	 * @static
+	 * @memberOf _
+	 * @since 0.8.0
+	 * @category Lang
+	 * @param {*} value The value to check.
+	 * @returns {boolean} Returns `true` if `value` is a plain object, else `false`.
+	 * @example
+	 *
+	 * function Foo() {
+	 *   this.a = 1;
+	 * }
+	 *
+	 * _.isPlainObject(new Foo);
+	 * // => false
+	 *
+	 * _.isPlainObject([1, 2, 3]);
+	 * // => false
+	 *
+	 * _.isPlainObject({ 'x': 0, 'y': 0 });
+	 * // => true
+	 *
+	 * _.isPlainObject(Object.create(null));
+	 * // => true
+	 */
+	function isPlainObject(value) {
+	  if (!isObjectLike(value) ||
+	      objectToString.call(value) != objectTag || isHostObject(value)) {
+	    return false;
+	  }
+	  var proto = getPrototype(value);
+	  if (proto === null) {
+	    return true;
+	  }
+	  var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor;
+	  return (typeof Ctor == 'function' &&
+	    Ctor instanceof Ctor && funcToString.call(Ctor) == objectCtorString);
+	}
+
+	module.exports = isPlainObject;
+
+
+/***/ },
+/* 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;
+
+	var _compose = __webpack_require__(1);
+
+	var _compose2 = _interopRequireDefault(_compose);
+
+	function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+	/**
+	 * 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 (createStore) {
+	    return function (reducer, preloadedState, enhancer) {
+	      var store = createStore(reducer, preloadedState, enhancer);
+	      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
+	      });
+	    };
+	  };
+	}
+
+/***/ },
+/* 6 */
+/***/ function(module, exports) {
+
+	'use strict';
+
+	exports.__esModule = true;
+	exports['default'] = bindActionCreators;
+	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) {
+	    throw new Error('bindActionCreators expected an object or a function, instead received ' + (actionCreators === null ? 'null' : typeof actionCreators) + '. ' + 'Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?');
+	  }
+
+	  var keys = Object.keys(actionCreators);
+	  var boundActionCreators = {};
+	  for (var i = 0; i < keys.length; i++) {
+	    var key = keys[i];
+	    var actionCreator = actionCreators[key];
+	    if (typeof actionCreator === 'function') {
+	      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
+	    }
+	  }
+	  return boundActionCreators;
+	}
+
+/***/ },
+/* 7 */
+/***/ function(module, exports, __webpack_require__) {
+
+	'use strict';
+
+	exports.__esModule = true;
+	exports['default'] = combineReducers;
+
+	var _createStore = __webpack_require__(2);
+
+	var _isPlainObject = __webpack_require__(4);
+
+	var _isPlainObject2 = _interopRequireDefault(_isPlainObject);
+
+	var _warning = __webpack_require__(3);
+
+	var _warning2 = _interopRequireDefault(_warning);
+
+	function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+	function getUndefinedStateErrorMessage(key, action) {
+	  var actionType = action && action.type;
+	  var actionName = actionType && '"' + actionType.toString() + '"' || 'an action';
+
+	  return 'Given action ' + actionName + ', reducer "' + key + '" returned undefined. ' + 'To ignore an action, you must explicitly return the previous state.';
+	}
+
+	function getUnexpectedStateShapeWarningMessage(inputState, reducers, action, unexpectedKeyCache) {
+	  var reducerKeys = Object.keys(reducers);
+	  var argumentName = action && action.type === _createStore.ActionTypes.INIT ? 'preloadedState argument passed to createStore' : 'previous state received by the reducer';
+
+	  if (reducerKeys.length === 0) {
+	    return 'Store does not have a valid reducer. Make sure the argument passed ' + 'to combineReducers is an object whose values are reducers.';
+	  }
+
+	  if (!(0, _isPlainObject2['default'])(inputState)) {
+	    return 'The ' + argumentName + ' has unexpected type of "' + {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] + '". Expected argument to be an object with the following ' + ('keys: "' + reducerKeys.join('", "') + '"');
+	  }
+
+	  var unexpectedKeys = Object.keys(inputState).filter(function (key) {
+	    return !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key];
+	  });
+
+	  unexpectedKeys.forEach(function (key) {
+	    unexpectedKeyCache[key] = true;
+	  });
+
+	  if (unexpectedKeys.length > 0) {
+	    return 'Unexpected ' + (unexpectedKeys.length > 1 ? 'keys' : 'key') + ' ' + ('"' + unexpectedKeys.join('", "') + '" found in ' + argumentName + '. ') + 'Expected to find one of the known reducer keys instead: ' + ('"' + reducerKeys.join('", "') + '". Unexpected keys will be ignored.');
+	  }
+	}
+
+	function assertReducerSanity(reducers) {
+	  Object.keys(reducers).forEach(function (key) {
+	    var reducer = reducers[key];
+	    var initialState = reducer(undefined, { type: _createStore.ActionTypes.INIT });
+
+	    if (typeof initialState === '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 = '@@redux/PROBE_UNKNOWN_ACTION_' + 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.');
+	    }
+	  });
+	}
+
+	/**
+	 * 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 reducerKeys = Object.keys(reducers);
+	  var finalReducers = {};
+	  for (var i = 0; i < reducerKeys.length; i++) {
+	    var key = reducerKeys[i];
+
+	    if (true) {
+	      if (typeof reducers[key] === 'undefined') {
+	        (0, _warning2['default'])('No reducer provided for key "' + key + '"');
+	      }
+	    }
+
+	    if (typeof reducers[key] === 'function') {
+	      finalReducers[key] = reducers[key];
+	    }
+	  }
+	  var finalReducerKeys = Object.keys(finalReducers);
+
+	  if (true) {
+	    var unexpectedKeyCache = {};
+	  }
+
+	  var sanityError;
+	  try {
+	    assertReducerSanity(finalReducers);
+	  } catch (e) {
+	    sanityError = e;
+	  }
+
+	  return function combination() {
+	    var state = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
+	    var action = arguments[1];
+
+	    if (sanityError) {
+	      throw sanityError;
+	    }
+
+	    if (true) {
+	      var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache);
+	      if (warningMessage) {
+	        (0, _warning2['default'])(warningMessage);
+	      }
+	    }
+
+	    var hasChanged = false;
+	    var nextState = {};
+	    for (var i = 0; i < finalReducerKeys.length; i++) {
+	      var key = finalReducerKeys[i];
+	      var reducer = finalReducers[key];
+	      var previousStateForKey = state[key];
+	      var nextStateForKey = reducer(previousStateForKey, action);
+	      if (typeof nextStateForKey === 'undefined') {
+	        var errorMessage = getUndefinedStateErrorMessage(key, action);
+	        throw new Error(errorMessage);
+	      }
+	      nextState[key] = nextStateForKey;
+	      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
+	    }
+	    return hasChanged ? nextState : state;
+	  };
+	}
+
+/***/ },
+/* 8 */
+/***/ function(module, exports, __webpack_require__) {
+
+	var overArg = __webpack_require__(10);
+
+	/** Built-in value references. */
+	var getPrototype = overArg(Object.getPrototypeOf, Object);
+
+	module.exports = getPrototype;
+
+
+/***/ },
+/* 9 */
+/***/ function(module, exports) {
+
+	/**
+	 * Checks if `value` is a host object in IE < 9.
+	 *
+	 * @private
+	 * @param {*} value The value to check.
+	 * @returns {boolean} Returns `true` if `value` is a host object, else `false`.
+	 */
+	function isHostObject(value) {
+	  // Many host objects are `Object` objects that can coerce to strings
+	  // despite having improperly defined `toString` methods.
+	  var result = false;
+	  if (value != null && typeof value.toString != 'function') {
+	    try {
+	      result = !!(value + '');
+	    } catch (e) {}
+	  }
+	  return result;
+	}
+
+	module.exports = isHostObject;
+
+
+/***/ },
+/* 10 */
+/***/ function(module, exports) {
+
+	/**
+	 * Creates a unary function that invokes `func` with its argument transformed.
+	 *
+	 * @private
+	 * @param {Function} func The function to wrap.
+	 * @param {Function} transform The argument transform.
+	 * @returns {Function} Returns the new function.
+	 */
+	function overArg(func, transform) {
+	  return function(arg) {
+	    return func(transform(arg));
+	  };
+	}
+
+	module.exports = overArg;
+
+
+/***/ },
+/* 11 */
+/***/ function(module, exports) {
+
+	/**
+	 * Checks if `value` is object-like. A value is object-like if it's not `null`
+	 * and has a `typeof` result of "object".
+	 *
+	 * @static
+	 * @memberOf _
+	 * @since 4.0.0
+	 * @category Lang
+	 * @param {*} value The value to check.
+	 * @returns {boolean} Returns `true` if `value` is object-like, else `false`.
+	 * @example
+	 *
+	 * _.isObjectLike({});
+	 * // => true
+	 *
+	 * _.isObjectLike([1, 2, 3]);
+	 * // => true
+	 *
+	 * _.isObjectLike(_.noop);
+	 * // => false
+	 *
+	 * _.isObjectLike(null);
+	 * // => false
+	 */
+	function isObjectLike(value) {
+	  return !!value && typeof value == 'object';
+	}
+
+	module.exports = isObjectLike;
+
+
+/***/ },
+/* 12 */
+/***/ function(module, exports, __webpack_require__) {
+
+	module.exports = __webpack_require__(13);
+
+
+/***/ },
+/* 13 */
+/***/ function(module, exports, __webpack_require__) {
+
+	/* WEBPACK VAR INJECTION */(function(global) {'use strict';
+
+	Object.defineProperty(exports, "__esModule", {
+		value: true
+	});
+
+	var _ponyfill = __webpack_require__(14);
+
+	var _ponyfill2 = _interopRequireDefault(_ponyfill);
+
+	function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+	var root = undefined; /* global window */
+
+	if (typeof global !== 'undefined') {
+		root = global;
+	} else if (typeof window !== 'undefined') {
+		root = window;
+	}
+
+	var result = (0, _ponyfill2['default'])(root);
+	exports['default'] = result;
+	/* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))
+
+/***/ },
+/* 14 */
+/***/ function(module, exports) {
+
+	'use strict';
+
+	Object.defineProperty(exports, "__esModule", {
+		value: true
+	});
+	exports['default'] = symbolObservablePonyfill;
+	function symbolObservablePonyfill(root) {
+		var result;
+		var _Symbol = root.Symbol;
+
+		if (typeof _Symbol === 'function') {
+			if (_Symbol.observable) {
+				result = _Symbol.observable;
+			} else {
+				result = _Symbol('observable');
+				_Symbol.observable = result;
+			}
+		} else {
+			result = '@@observable';
+		}
+
+		return result;
+	};
+
+/***/ }
+/******/ ])
+});
+;