diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 1537689c..8e71ae6e 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -10,13 +10,14 @@
@@ -24,6 +25,16 @@
+
+
+
+
+
+
+
+
+
data, Callback callback)
if (match == null) {
callback.reject("no matching route");
+ return;
}
match.delete(path, data, callback);
diff --git a/android/app/src/main/java/org/mozilla/magnet/magnetapi/ApiMagnetReact.java b/android/app/src/main/java/org/mozilla/magnet/magnetapi/ApiMagnetReact.java
index aec87548..8932cd1e 100644
--- a/android/app/src/main/java/org/mozilla/magnet/magnetapi/ApiMagnetReact.java
+++ b/android/app/src/main/java/org/mozilla/magnet/magnetapi/ApiMagnetReact.java
@@ -6,7 +6,9 @@
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.bridge.ReadableNativeArray;
import com.facebook.react.bridge.ReadableNativeMap;
import org.json.JSONArray;
@@ -15,6 +17,8 @@
import org.mozilla.magnet.api.Utils;
import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
public class ApiMagnetReact extends ReactContextBaseJavaModule {
private static final String TAG = "APIMagnetReact";
@@ -46,11 +50,39 @@ public void reject(String error) {
}
@ReactMethod
- public void post(String path, ReadableMap data, final Promise promise) {
+ public void post(String path, ReadableMap map, final Promise promise) {
Log.d(TAG, "post");
- HashMap map = ((ReadableNativeMap) data).toHashMap();
+ Object data = fromReactArgument(map);
+
+ if (data == null) {
+ promise.reject("invalid-data-type", "invalid data type");
+ return;
+ }
- mApiMagnet.post(path, map, new Api.Callback() {
+ mApiMagnet.post(path, data, new Api.Callback() {
+ @Override
+ public void resolve(Object result) {
+ promise.resolve(toReactArgument(result));
+ }
+
+ @Override
+ public void reject(String error) {
+ promise.reject(error, error);
+ }
+ });
+ }
+
+ @ReactMethod
+ public void postArray(String path, ReadableArray array, final Promise promise) {
+ Log.d(TAG, "post");
+ Object data = fromReactArgument(array);
+
+ if (data == null) {
+ promise.reject("invalid-data-type", "invalid data type");
+ return;
+ }
+
+ mApiMagnet.post(path, data, new Api.Callback() {
@Override
public void resolve(Object result) {
promise.resolve(toReactArgument(result));
@@ -86,4 +118,10 @@ static private Object toReactArgument(Object object) {
else if (object instanceof JSONObject) return Utils.jsonToWritableMap((JSONObject) object);
else return null;
}
+
+ static private Object fromReactArgument(Object object) {
+ if (object instanceof ReadableNativeArray) return ((ReadableNativeArray) object).toArrayList();
+ else if (object instanceof ReadableNativeMap) return ((ReadableNativeMap) object).toHashMap();
+ else return null;
+ }
}
diff --git a/config.js b/config.js
index 6b490b06..ce7afa7d 100644
--- a/config.js
+++ b/config.js
@@ -16,11 +16,6 @@ module.exports = {
title: 'Show distance',
},
- 'removeOldItems': {
- value: false, // default
- title: 'Remove old items',
- },
-
'enableTelemetry': {
value: true, // default
title: 'Telemetry',
@@ -40,6 +35,7 @@ module.exports = {
'itemExpiring': 10000, // 10 secs
'metadataServiceUrl': 'https://tengam.org/api/v1/metadata',
+ 'searchServiceUrl': 'https://tengam.org/content/v1/search/url',
'theme': {
'colorBackground': '#f2f2f2',
diff --git a/ios/Api.swift b/ios/Api.swift
index 9b63783c..b9ba7f2d 100644
--- a/ios/Api.swift
+++ b/ios/Api.swift
@@ -12,6 +12,7 @@ import SwiftyJSON
protocol Api {
func get(path: String, callback: ApiCallback)
func post(path: String, data: NSDictionary, callback: ApiCallback)
+ func post(path: String, data: NSArray, callback: ApiCallback)
func put(path: String, data: NSDictionary, callback: ApiCallback)
func delete(path: String, data: NSDictionary, callback: ApiCallback)
func mount(path: String, api: Api)
@@ -60,6 +61,17 @@ class ApiBase: Api {
api!.post(path, data: data, callback: callback)
}
+ func post(path: String, data: NSArray, callback: ApiCallback) {
+ let api = find(path)
+
+ guard api != nil else {
+ callback.onError("Could not find route \(path)")
+ return
+ }
+
+ api!.post(path, data: data, callback: callback)
+ }
+
func put(path: String, data: NSDictionary, callback: ApiCallback) {
let api = find(path)
diff --git a/ios/ApiMagnetReact.m b/ios/ApiMagnetReact.m
index c7809ac3..d13e9cf8 100644
--- a/ios/ApiMagnetReact.m
+++ b/ios/ApiMagnetReact.m
@@ -21,6 +21,12 @@ @interface RCT_EXTERN_MODULE(ApiMagnetReact, NSObject);
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
+RCT_EXTERN_METHOD(
+ postArray:(NSString *)path
+ data:(NSArray *)data
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject)
+
RCT_EXTERN_METHOD(
put:(NSString *)path
data:(NSDictionary *)data
diff --git a/ios/ApiMagnetReact.swift b/ios/ApiMagnetReact.swift
index 585c5a61..f7ad1f3a 100644
--- a/ios/ApiMagnetReact.swift
+++ b/ios/ApiMagnetReact.swift
@@ -31,6 +31,16 @@ import Foundation
}))
}
+ @objc func postArray(path: String, data: NSArray, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
+ api.post(path, data: data, callback: ApiCallback(success: { result in
+ resolve(result.rawValue)
+ },
+ error: { (error) in
+ let err = NSError(coder: NSCoder())
+ reject("get_error", "Error resolving \(path) with \(data)", err)
+ }))
+ }
+
@objc func put(path: String, data: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
api.put(path, data: data, callback: ApiCallback(success: { result in
resolve(result.rawValue)
diff --git a/ios/ApiMetadata.swift b/ios/ApiMetadata.swift
index 6593089a..e662674b 100644
--- a/ios/ApiMetadata.swift
+++ b/ios/ApiMetadata.swift
@@ -35,20 +35,25 @@ class ApiMetadata: ApiBase {
//
// data["objects"] = elems
- override func post(path: String, data: NSDictionary, callback: ApiCallback) {
+ override func post(path: String, data: NSArray, callback: ApiCallback) {
guard System.connectedToNetwork() else {
callback.onError("No internet connection")
return
}
let url = NSURL(string: ApiMetadata.SERVER_URL)
+ var urls = Array>();
- let parameters = ["objects": (data.valueForKey("objects"))!]
+ for url in data {
+ urls.append(["url": url as! String]);
+ }
+ let body = ["objects": urls]
let request = NSMutableURLRequest(URL: url!)
+
request.HTTPMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
- request.HTTPBody = try! NSJSONSerialization.dataWithJSONObject(parameters, options: [])
+ request.HTTPBody = try! NSJSONSerialization.dataWithJSONObject(body, options: [])
Alamofire.request(request).responseJSON { response in
guard response.result.value != nil else {
diff --git a/ios/Magnet.xcodeproj/project.pbxproj b/ios/Magnet.xcodeproj/project.pbxproj
index 072f1a0b..8a505498 100644
--- a/ios/Magnet.xcodeproj/project.pbxproj
+++ b/ios/Magnet.xcodeproj/project.pbxproj
@@ -989,6 +989,7 @@
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/../node_modules/react-native/React/**",
"$(SRCROOT)/../node_modules/react-native-linear-gradient/BVLinearGradient",
+ "$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS",
);
INFOPLIST_FILE = Magnet/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
@@ -1027,6 +1028,7 @@
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/../node_modules/react-native/React/**",
"$(SRCROOT)/../node_modules/react-native-linear-gradient/BVLinearGradient",
+ "$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS",
);
INFOPLIST_FILE = Magnet/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
@@ -1086,6 +1088,7 @@
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/../node_modules/react-native/React/**",
"$(SRCROOT)/../node_modules/react-native-linear-gradient/BVLinearGradient",
+ "$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS/RCTLinkingManager.h",
);
IPHONEOS_DEPLOYMENT_TARGET = 7.0;
MTL_ENABLE_DEBUG_INFO = YES;
@@ -1128,6 +1131,7 @@
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/../node_modules/react-native/React/**",
"$(SRCROOT)/../node_modules/react-native-linear-gradient/BVLinearGradient",
+ "$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS/RCTLinkingManager.h",
);
IPHONEOS_DEPLOYMENT_TARGET = 7.0;
MTL_ENABLE_DEBUG_INFO = NO;
diff --git a/ios/Magnet/AppDelegate.m b/ios/Magnet/AppDelegate.m
index 5bb70019..88794bea 100644
--- a/ios/Magnet/AppDelegate.m
+++ b/ios/Magnet/AppDelegate.m
@@ -11,6 +11,7 @@
#import "RCTBundleURLProvider.h"
#import "RCTRootView.h"
#import "RCTBridge.h"
+#import "RCTLinkingManager.h"
// Include the project headers for swift code. (-Swift.h)
#import "magnet-Swift.h"
@@ -70,4 +71,9 @@ -(void)application:(UIApplication *)application didReceiveLocalNotification:(UIL
[self.bridge.eventDispatcher sendDeviceEventWithName:@"notification:applaunch" body:nil];
}
+- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
+ return [RCTLinkingManager application:application openURL:url sourceApplication:sourceApplication annotation:annotation];
+}
+
+
@end
diff --git a/ios/Magnet/Info.plist b/ios/Magnet/Info.plist
index df5bbb35..90138c64 100644
--- a/ios/Magnet/Info.plist
+++ b/ios/Magnet/Info.plist
@@ -18,6 +18,17 @@
1.2.1
CFBundleSignature
????
+ CFBundleURLTypes
+
+
+ CFBundleURLName
+ org.mozilla.magnet
+ CFBundleURLSchemes
+
+ mozilla-magnet
+
+
+
CFBundleVersion
7
LSRequiresIPhoneOS
diff --git a/ios/NotificationsHelperIOS10.swift b/ios/NotificationsHelperIOS10.swift
index 7d1e1df7..99779142 100644
--- a/ios/NotificationsHelperIOS10.swift
+++ b/ios/NotificationsHelperIOS10.swift
@@ -48,11 +48,9 @@ class NotificationsHelperIOS10: NSObject, UNUserNotificationCenterDelegate {
private func fetchData(url: String, callback: ((JSON) -> Void)) {
let api = ApiMetadata()
+ let urls: NSArray = [url]
- let item: Dictionary = ["url": url]
- let objects: Dictionary = ["objects": [item]]
-
- api.post("metadata", data: objects, callback: ApiCallback(success: { json in
+ api.post("metadata", data: urls, callback: ApiCallback(success: { json in
callback(json)
}, error: { (err) in
debugPrint("Could not get metadata for \(url): \(err)")
diff --git a/lib/api/fetch-item.js b/lib/api/fetch-item.js
new file mode 100644
index 00000000..c8344a1f
--- /dev/null
+++ b/lib/api/fetch-item.js
@@ -0,0 +1,50 @@
+'use strict';
+
+/**
+ * Dependencies
+ */
+
+const { searchServiceUrl } = require('../../config');
+const debug = require('../debug')('get-item');
+const api = require('.');
+
+/**
+ * Exports
+ */
+
+module.exports = function(url) {
+ return Promise.all([
+ fetchBeaconData(url),
+ fetchMetadata(url),
+ ])
+
+ .then(results => {
+ var item = results[0][0];
+ var metadata = results[1][0];
+
+ return {
+ ...item,
+ metadata,
+ };
+ });
+};
+
+function fetchMetadata(url) {
+ return api.postArray('metadata', [url]);
+}
+
+function fetchBeaconData(url) {
+ debug('fetch beacon data', url);
+ const request = new Request(searchServiceUrl, {
+ method: 'post',
+ headers: new Headers({ 'Content-Type': 'application/json;charset=utf-8' }),
+ body: JSON.stringify([url]),
+ });
+
+ return fetch(request)
+ .then(res => res.json())
+ .catch(() => {
+ debug('no beacon data', url);
+ return [];
+ });
+}
diff --git a/lib/api.js b/lib/api/index.js
similarity index 100%
rename from lib/api.js
rename to lib/api/index.js
diff --git a/lib/app.js b/lib/app.js
index 8095d3af..ae041311 100644
--- a/lib/app.js
+++ b/lib/app.js
@@ -4,20 +4,22 @@
* Dependencies
*/
-const api = require('./api');
const SubscriptionsScene = require('./views/subscriptions-scene');
const SettingsScene = require('./views/settings-scene');
+const ItemScene = require('./views/item/item-scene');
+const ListScene = require('./views/list/list-scene');
+const { HEALTHY } = require('./store/constants');
const notification = require('./notifications');
-const ItemScene = require('./views/item-scene');
-const ListScene = require('./views/list-scene');
+const { bindActionCreators } = require('redux');
const ReactNative = require('react-native');
const { connect } = require('react-redux');
const actions = require('./store/actions');
const track = require('./utils/tracker');
const debug = require('./debug')('App');
-const { flags } = require('../config');
const Scanner = require('./scanner');
const React = require('react');
+const api = require('./api');
+const URL = require('url');
const {
BackAndroid,
@@ -59,13 +61,13 @@ class App extends React.Component {
constructor(props) {
super(props);
+ // pre-bind context
+ this.onDeepLink = this.onDeepLink.bind(this);
+
// create the scanner
this.scanner = new Scanner({
- onUpdate: this.props.updateItem,
- onLost: this.props.removeItem,
- onMetadata: this.props.setItemMetadata,
- expiryEnabled: this.enabled.bind(this, 'removeOldItems'),
- getItems: () => this.props.items,
+ onFound: this.props.itemFound,
+ onLost: this.props.itemLost,
});
// we need to use telemetry after we
@@ -75,9 +77,6 @@ class App extends React.Component {
this.setupNotificationListeners();
});
- // when network is bad, the scanner errors
- this.scanner.on('networkerror', this.onScannerNetworkError.bind(this));
-
// respond to app background/foreground changes
AppState.addEventListener('change', this.onAppStateChanged.bind(this));
@@ -86,9 +85,15 @@ class App extends React.Component {
componentDidMount() {
debug('mounted');
+ Linking.getInitialURL().then(url => this.onDeepLink({ url }));
+ Linking.addEventListener('url', this.onDeepLink);
this.startScanning();
}
+ componentWillUnmount() {
+ Linking.removeEventListener('url', this.onDeepLink);
+ }
+
/**
* Triggered whenever the redux store updates.
* We use this hook to channel the event to more
@@ -97,6 +102,7 @@ class App extends React.Component {
componentWillReceiveProps(next) {
var current = this.props;
if (next.userFlags !== current.userFlags) this.onUserFlagsChange(next.userFlags);
+ if (next.network !== current.network) this.onNetworkStatusChange(next.network);
}
render() {
@@ -112,25 +118,18 @@ class App extends React.Component {
}
/**
- * Called by `Navigator` to determin which
+ * Called by `Navigator` to determine which
* 'scene` gets rendered.
*/
- renderScene({ type }, navigator) {
- debug('render scene', type, this.props.openItem);
+ renderScene(route, navigator) {
+ debug('render scene', route.type);
- switch (type) {
+ switch (route.type) {
case 'home': return ;
- case 'item':
- return ;
+ case 'item': return ;
case 'settings': return 1) {
- this.refs.navigator.pop();
+ navigator.pop();
return true;
}
@@ -376,19 +375,16 @@ App.propTypes = {
// actions
setIndicateScanning: React.PropTypes.func,
- setChannels: React.PropTypes.func,
- setOpenItem: React.PropTypes.func,
- setItemMetadata: React.PropTypes.func,
setUserFlag: React.PropTypes.func,
- updateItem: React.PropTypes.func,
- removeItem: React.PropTypes.func,
- clearItems: React.PropTypes.func,
+ itemFound: React.PropTypes.func,
+ itemLost: React.PropTypes.func,
+ itemOpened: React.PropTypes.func,
+ dispatch: React.PropTypes.func,
// state
indicateScanning: React.PropTypes.bool,
userFlags: React.PropTypes.object,
- openItem: React.PropTypes.object,
- items: React.PropTypes.array,
+ items: React.PropTypes.object,
};
/**
@@ -408,7 +404,7 @@ function mapStateToProps(store) {
return {
indicateScanning: store.indicateScanning,
userFlags: store.userFlags,
- openItem: store.openItem,
+ network: store.network,
items: store.items,
};
}
@@ -420,8 +416,19 @@ function mapStateToProps(store) {
* @param {function} dispatch
* @return {Object}
*/
-function mapDispatchToProps() {
- return actions;
+function mapDispatchToProps(dispatch) {
+ const bound = bindActionCreators({
+ itemFound: actions.itemFound,
+ itemLost: actions.itemLost,
+ itemOpened: actions.itemOpened,
+ setUserFlag: actions.setUserFlag,
+ setIndicateScanning: actions.setIndicateScanning,
+ }, dispatch);
+
+ return {
+ ...bound,
+ dispatch,
+ };
}
/**
diff --git a/lib/scanner/index.js b/lib/scanner/index.js
index 86aa43f5..532c5b34 100644
--- a/lib/scanner/index.js
+++ b/lib/scanner/index.js
@@ -7,18 +7,16 @@
const Emitter = require('events').EventEmitter;
const debug = require('../debug')('Scanner');
const ReactNative = require('react-native');
-const fetchMetadata = require('./metadata');
-const track = require('../utils/tracker');
const config = require('../../config');
const {
Alert,
NativeModules,
DeviceEventEmitter,
+ InteractionManager,
} = ReactNative;
const MagnetScanner = NativeModules.MagnetScannerReact;
-const EXPIRE_CHECK_INTERVAL = 5000; // every 5 secs
/**
* A JS interface for the Native `magnet-scanner-android`
@@ -34,10 +32,7 @@ class Scanner extends Emitter {
constructor(options) {
super();
this.started = false;
- this.expiryEnabled = options.expiryEnabled;
- this.onMetadata = options.onMetadata;
- this.onUpdate = options.onUpdate;
- this.getItems = options.getItems;
+ this.onFound = options.onFound;
this.onLost = options.onLost;
this.listener = DeviceEventEmitter
@@ -50,53 +45,15 @@ class Scanner extends Emitter {
.then(() => {
debug('started');
this.injectTestUrls();
- this.startExpireCheck();
});
}
stop() {
debug('stop');
MagnetScanner.stop();
- this.stopExpireCheck();
return Promise.resolve();
}
- startExpireCheck() {
- this.nextExpireCheck = setTimeout(() => {
- this.checkExpired();
- this.startExpireCheck();
- }, EXPIRE_CHECK_INTERVAL);
- }
-
- stopExpireCheck() {
- clearTimeout(this.nextExpireCheck);
- }
-
- checkExpired() {
- if (!this.expiryEnabled()) return;
-
- var now = Date.now();
- this.getItems().forEach(item => {
-
- // mdns and test urls don't have a distance and
- // don't expire in the same way as ble beacons
- if (typeof item.distance != 'number') return;
-
- var age = now - item.timeLastSeen;
-
- // when an item expires,
- // it is removed from the list
- if (age > config.itemExpires) {
- debug('item expired', item.id);
- return this.onLost(item.id);
- }
-
- // when an item is 'expiring' it is greyed out
- var expiring = age > config.itemExpiring;
- this.onUpdate(item.id, { expiring: expiring });
- });
- }
-
/**
* Show a dialog to warn the user of the
* consequences of not having bluetooth
@@ -133,67 +90,25 @@ class Scanner extends Emitter {
injectTestUrls() {
if (!config.flags.injectTestUrls) return;
config.testUrls.forEach(url => {
- this.onItemFound({
- distance: -1,
- url,
- });
+ this.onItemFound({ url });
});
}
/**
- * When a URL is found we fetch its
- * associated metadata then notify
- * the listener.
+ * Called when an item is found by the scanner.
+ * Can be called several times per second.
*
- * @param {String} url
- * @private
+ * We don't fire the callback until all interactions
+ * (touches/animations) are complete to prevent
+ * re-rendering and droppping frames.
*/
onItemFound({ url, distance }) {
- debug('item found', url);
- const isNew = this.itemIsNew(url);
-
- this.onUpdate(url, {
- id: url,
- url,
- distance,
- timeLastSeen: Date.now(),
+ InteractionManager.runAfterInteractions(() => {
+ debug('item found', url);
+ const id = url;
+ this.onFound(id, { distance });
});
-
- // don't populate if item already found
- if (!isNew) return;
-
- track.itemFound(url);
- const start = Date.now();
-
- debug('fetching metadata ...');
- fetchMetadata(url)
- .then((metadata) => {
- if (!metadata) return debug('metadata fetch failed', url);
- debug('got metadata: %s', url, metadata);
- track.timing('system', 'fetch-metadata', start);
- track.itemPopulated(url, metadata.unadaptedUrl);
- this.onMetadata(url, metadata);
- })
-
- .catch(err => {
- console.info(err);
- if (!isNetworkError(err)) return;
- this.emit('networkerror', err);
- });
}
-
- itemIsNew(id) {
- return !this.getItems().some(item => item.id === id);
- }
-}
-
-/**
- * Utils
- */
-
-function isNetworkError(err) {
- return err.name === 'NetworkError'
- || err.message === 'Network request failed';
}
/**
diff --git a/lib/scanner/metadata/clientside.js b/lib/scanner/metadata/clientside.js
deleted file mode 100644
index d44c1beb..00000000
--- a/lib/scanner/metadata/clientside.js
+++ /dev/null
@@ -1,50 +0,0 @@
-
-/**
- * Dependencies
- */
-
-var debug = require('../../debug')('Clientside');
-var parser = require('magnet-html-parser');
-
-/**
- * Exports
- */
-
-module.exports = function(url) {
- debug('fetch and parse', url);
- return getContent(url)
- .then(({ endUrl, content }) => {
- return parser.parse(content, endUrl)
- .then(metadata => {
-
- // copied from magnet-metadata-service
- metadata.id
- = metadata.url
- = metadata.displayUrl
- = metadata.unadaptedUrl
- = endUrl;
-
- metadata.originalUrl = url;
- return metadata;
- });
- });
-};
-
-function getContent(url) {
- return fetch(url, {
- method: 'GET',
- headers: {
- 'Accept': 'text/html',
- 'Content-Type': 'text/html',
- },
- })
-
- .then(res => {
- return res.text().then(text => {
- return {
- content: text,
- endUrl: res.url,
- };
- });
- });
-}
diff --git a/lib/scanner/metadata/index.js b/lib/scanner/metadata/index.js
deleted file mode 100644
index b608783b..00000000
--- a/lib/scanner/metadata/index.js
+++ /dev/null
@@ -1,46 +0,0 @@
-'use strict';
-
-/**
- * Dependencies
- */
-
-var debug = require('../../debug')('metadata');
-var clientside = require('./clientside');
-var serverside = require('./serverside');
-
-/**
- * Get the metadata for a given URL.
- *
- * @param {String} url
- * @return {Promise}
- */
-module.exports = function(url) {
- debug('get metadata', url);
- return (isInternal(url))
- ? clientside(url)
- : serverside(url);
-};
-
-/**
- * A crude test for internal urls.
- *
- * We should find the local IP of
- * the device and test for a matching
- * subnet mask.
- *
- * Worst case scenario is that an external
- * URI is fetched clientside. Better that
- * than an internal URL failing to be
- * fetched serverside.
- *
- * @param {String} url
- * @return {Boolean}
- */
-function isInternal(url) {
- return /\.local/.test(url)
- || isIp(url);
-}
-
-function isIp(string) {
- return /^https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/.test(string);
-}
diff --git a/lib/scanner/metadata/serverside.js b/lib/scanner/metadata/serverside.js
deleted file mode 100644
index 3e00fddb..00000000
--- a/lib/scanner/metadata/serverside.js
+++ /dev/null
@@ -1,86 +0,0 @@
-
-/**
- * Dependencies
- */
-
-var { metadataServiceUrl } = require('../../../config');
-var debug = require('../../debug')('Metadata');
-
-function Metadata() {
- this.batch = [];
-}
-
-Metadata.prototype = {
- get(url) {
- debug('get');
-
- var index = this.batch.length;
- this.batch.push({ url: url });
-
- return this.schedule(url)
- .then(responses => {
- var response = responses[index];
- debug('response', response);
- if (response.error) return Promise.reject(error(response.error));
- return response;
- });
- },
-
- schedule() {
- if (this.pending) return this.pending.promise;
- this.pending = new Deferred();
-
- setTimeout(() => {
- var promise = this.pending;
-
- request(this.batch)
- .then(json => promise.resolve(json))
- .catch(err => promise.reject(err));
-
- delete this.pending;
- this.batch = [];
- }, 200);
-
- return this.pending.promise;
- },
-};
-
-/**
- * Exports
- */
-
-var metadata = new Metadata();
-module.exports = function(url) {
- return metadata.get(url);
-};
-
-/**
- * Utils
- */
-
-function request(urls) {
- debug('request', metadataServiceUrl, urls);
-
- var request = new Request(metadataServiceUrl, {
- method: 'post',
- headers: new Headers({
- 'Content-Type': 'application/json;charset=utf-8',
- }),
-
- body: JSON.stringify({ objects: urls }),
- });
-
- return fetch(request)
- .then(res => res.json());
-}
-
-function error(message) {
- return new Error(message);
-}
-
-function Deferred() {
- this.promise = new Promise(function(resolve, reject) {
- this.resolve = resolve;
- this.reject = reject;
- }.bind(this));
-}
diff --git a/lib/store/actions.js b/lib/store/actions.js
index 5cf86aab..35dbe323 100644
--- a/lib/store/actions.js
+++ b/lib/store/actions.js
@@ -5,36 +5,113 @@
*/
const { bindActionCreators } = require('redux');
-const store = require('./');
+const fetchItem = require('../api/fetch-item');
+const { isNetworkError } = require('../utils');
+const debug = require('../debug')('actions');
+const track = require('../utils/tracker');
+const { dispatch } = require('.');
+const api = require('../api');
+
+const {
+ INCOMPLETE,
+ COMPLETE,
+ ERROR,
+ HEALTHY,
+} = require('./constants');
/**
* Exports
*/
const actions = {
- updateItem(id, update) {
+ itemFound(id, value) {
+ return (dispatch, getState) => {
+ const exists = getState().itemsNearby.indexOf(id) > -1;
+ if (!exists) dispatch(actions.newItemFound(id, value));
+ else dispatch(actions.itemUpdated(id, value));
+ };
+ },
+
+ newItemFound(id, value) {
+ track.itemFound(id);
+ return {
+ type: 'ITEM_FOUND',
+ id,
+ value,
+ };
+ },
+
+ itemUpdated(id, value) {
+ return {
+ type: 'ITEM_UPDATED',
+ id,
+ value,
+ };
+ },
+
+ itemLost(id) {
return {
- type: 'UPDATE_ITEM',
+ type: 'ITEM_LOST',
id,
- update,
};
},
- setItemMetadata(id, metadata) {
+ itemOpened(id) {
return {
- type: 'SET_ITEM_METADATA',
- metadata,
+ type: 'ITEM_OPENED',
id,
};
},
- setOpenItem(id) {
+ itemClosed(id) {
return {
- type: 'SET_OPEN_ITEM',
+ type: 'ITEM_CLOSED',
id,
};
},
+ fetchItemIfNeeded(id) {
+ return (dispatch, getState) => {
+ if (!id) return Promise.resolve();
+ if (!shouldFetchItem(getState(), id)) return Promise.resolve();
+
+ dispatch(actions.itemFetching(id));
+
+ return fetchItem(id)
+ .then(item => {
+ dispatch(actions.networkStatusUpdate(HEALTHY));
+ dispatch(actions.itemFetched(id, item));
+ })
+
+ .catch(e => {
+ dispatch(actions.itemFetchErrored(id, e));
+ if (isNetworkError(e)) dispatch(actions.networkStatusUpdate(ERROR));
+ });
+ };
+ },
+
+ itemFetching(id) {
+ return {
+ type: 'ITEM_FETCHING',
+ id,
+ };
+ },
+
+ itemFetched(id, value) {
+ return {
+ type: 'ITEM_FETCHED',
+ id,
+ value,
+ };
+ },
+
+ itemFetchErrored(id, value) {
+ return {
+ type: 'ITEM_FETCH_ERRORED',
+ value,
+ };
+ },
+
removeItem(id) {
return {
type: 'REMOVE_ITEM',
@@ -42,9 +119,9 @@ const actions = {
};
},
- clearItems() {
+ refreshItems() {
return {
- type: 'CLEAR_ITEMS',
+ type: 'REFRESH_ITEMS',
};
},
@@ -63,38 +140,74 @@ const actions = {
};
},
+ fetchChannelsIfNeeded() {
+ return (dispatch, getState) => {
+ if (!shouldFetchChannels(getState())) return Promise.resolve();
+
+ // update local state
+ dispatch(actions.channelsFetching());
+ debug('fetching channels ...');
+
+ // fetch remote data
+ return api.get('channels')
+ .then(result => dispatch(actions.channelsFetched(result)))
+ .catch(err => dispatch(actions.channelsFetchErrored(err)));
+ };
+ },
+
channelsFetching() {
return {
type: 'CHANNELS_FETCHING',
};
},
- channelsFetched(responseType, value) {
+ channelsFetched(value) {
return {
type: 'CHANNELS_FETCHED',
- responseType,
value,
};
},
- setChannels(state, value) {
+ channelsFetchErrored(value) {
return {
- type: 'SET_CHANNELS',
- state,
+ type: 'CHANNELS_FETCH_ERRORED',
value,
};
},
+ fetchSubscriptionsIfNeeded() {
+ return (dispatch, getState) => {
+ if (!shouldFetchSubscriptions(getState())) return Promise.resolve();
+
+ // update local state
+ dispatch(actions.subscriptionsFetching());
+ debug('fetching subscriptions ...');
+
+ // fetch remote data
+ return api.get('subscriptions')
+ .then(result => dispatch(actions.subscriptionsFetched(result)))
+ .catch(err => dispatch(actions.subscriptionsFetchErrored(err)));
+ };
+ },
+
+ // TODO: Migrate to async action like `items`
subscriptionsFetching() {
return {
type: 'SUBSCRIPTIONS_FETCHING',
};
},
- subscriptionsFetched(responseType, value) {
+ // TODO: Migrate to async action like `items`
+ subscriptionsFetched(value) {
return {
type: 'SUBSCRIPTIONS_FETCHED',
- responseType,
+ value,
+ };
+ },
+
+ subscriptionsFetchErrored(value) {
+ return {
+ type: 'SUBSCRIPTIONS_FETCH_ERRORED',
value,
};
},
@@ -119,7 +232,53 @@ const actions = {
subscription,
};
},
+
+ networkStatusUpdate(value) {
+ return {
+ type: 'NETWORK_STATUS_UPDATE',
+ value: {
+ status: value,
+ timestamp: Date.now(),
+ },
+ };
+ },
};
-module.exports = bindActionCreators(actions, store.dispatch);
-module.exports.actions = actions;
+function shouldFetchItem(state, id) {
+ const item = state.items[id];
+ if (!item) return true;
+
+ switch (item.status) {
+ case COMPLETE:
+ case INCOMPLETE:
+ case ERROR:
+ return false;
+ default:
+ return true;
+ }
+}
+
+function shouldFetchChannels({ channels }) {
+ switch (channels.status) {
+ case COMPLETE:
+ case INCOMPLETE:
+ return false;
+ case ERROR:
+ default:
+ return true;
+ }
+}
+
+function shouldFetchSubscriptions({ subscriptions }) {
+ switch (subscriptions.status) {
+ case COMPLETE:
+ case INCOMPLETE:
+ return false;
+ case ERROR:
+ default:
+ return true;
+ }
+}
+
+module.exports = actions;
+module.exports.bound = bindActionCreators(actions, dispatch);
diff --git a/lib/store/constants.js b/lib/store/constants.js
new file mode 100644
index 00000000..025b01f7
--- /dev/null
+++ b/lib/store/constants.js
@@ -0,0 +1,7 @@
+
+module.exports = {
+ INCOMPLETE: 'incomplete',
+ COMPLETE: 'complete',
+ ERROR: 'error',
+ HEALTHY: 'healthy',
+};
diff --git a/lib/store/index.js b/lib/store/index.js
index dc7b77a2..1f503dd9 100644
--- a/lib/store/index.js
+++ b/lib/store/index.js
@@ -4,11 +4,15 @@
* Dependencies
*/
-const { createStore } = require('redux');
-const reducer = require('./reducer');
+import { createStore, applyMiddleware } from 'redux';
+import reducer from './reducer';
+import thunk from 'redux-thunk';
/**
* Exports
*/
-module.exports = createStore(reducer);
+module.exports = createStore(
+ reducer,
+ applyMiddleware(thunk)
+);
diff --git a/lib/store/reducer.js b/lib/store/reducer.js
index 5f534941..8b4d22f6 100644
--- a/lib/store/reducer.js
+++ b/lib/store/reducer.js
@@ -7,14 +7,32 @@
const debug = require('../debug')('reducer');
const config = require('../../config');
+const {
+ INCOMPLETE,
+ COMPLETE,
+ ERROR,
+ HEALTHY,
+} = require('./constants');
+
const initialState = {
- items: [],
+ items: {},
+ itemsNearby: [],
openItem: null,
+ network: {
+ status: HEALTHY,
+ timestamp: Date.now(),
+ },
+
indicateScanning: undefined,
userFlags: config.userFlags,
- channels: {},
+
+ channels: {
+ status: null,
+ value: null,
+ },
+
subscriptions: {
- state: null,
+ status: null,
value: null,
},
};
@@ -24,18 +42,28 @@ module.exports = (state, action) => {
debug('action', action.type);
switch (action.type) {
- case 'UPDATE_ITEM': return updateItem(state, action.id, action.update);
- case 'REMOVE_ITEM': return removeItem(state, action.id);
- case 'SET_ITEM_METADATA': return setItemMetadata(state, action.id, action.metadata);
+ case 'ITEM_FOUND': return itemFound(state, action.id, action.value);
+ case 'ITEM_UPDATED': return itemUpdated(state, action.id, action.value);
+ case 'ITEM_LOST': return itemLost(state, action.id);
+ case 'ITEM_OPENED': return itemOpened(state, action.id);
+ case 'ITEM_CLOSED': return itemClosed(state, action.id);
+ case 'ITEM_FETCHING': return itemFetching(state, action.id);
+ case 'ITEM_FETCHED': return itemFetched(state, action.id, action.value);
+ case 'ITEM_FETCH_ERROR': return itemFetchError(state, action.id, action.value);
+ case 'REFRESH_ITEMS': return refreshItems(state);
+
case 'SET_USER_FLAG': return setUserFlag(state, action.key, action.value);
- case 'CLEAR_ITEMS': return { ...state, items: [] };
- case 'SET_OPEN_ITEM': return setOpenItem(state, action.id);
- case 'CLOSE_ITEM': return { ...state, openItem: null };
case 'SET_INDICATE_SCANNING': return { ...state, indicateScanning: action.value };
+ case 'NETWORK_STATUS_UPDATE': return networkStatusUpdate(state, action.value);
+
case 'CHANNELS_FETCHING': return channelsFetching(state);
- case 'CHANNELS_FETCHED': return channelsFetched(state, action);
+ case 'CHANNELS_FETCHED': return channelsFetched(state, action.value);
+ case 'CHANNELS_FETCH_ERRORED': return channelsFetchErrored(state, action.value);
+
case 'SUBSCRIPTIONS_FETCHING': return subscriptionsFetching(state);
- case 'SUBSCRIPTIONS_FETCHED': return subscriptionsFetched(state, action);
+ case 'SUBSCRIPTIONS_FETCHED': return subscriptionsFetched(state, action.value);
+ case 'SUBSCRIPTIONS_FETCH_ERRORED': return subscriptionsFetchErrored(state, action.value);
+
case 'SUBSCRIBED': return subscribed(state, action);
case 'UNSUBSCRIBED': return unsubscribed(state, action);
case 'SUBSCRIPTION_UPDATED': return subscriptionUpdated(state, action);
@@ -43,105 +71,142 @@ module.exports = (state, action) => {
}
};
-function updateItem(state, id, update) {
- var existing = findItem(state.items, id);
- if (!existing) return addItem(state, update);
- debug('update item', id);
+function itemFetching(state, id) {
+ debug('item fetching', id);
+ var item = findItem(state.items, id) || createItem(id, {});
- var updated = {
- ...existing.item,
- ...update,
+ return {
+ ...state,
+ items: {
+ ...state.items,
+ [id]: {
+ ...item,
+ status: INCOMPLETE,
+ },
+ },
};
+}
- setDistance(updated, update.distance);
+function itemFetched(state, id, value) {
+ var item = findItem(state.items, id);
+ if (!item) return state;
+ debug('item fetched', id, item, value);
- // don't mutate if objects are equal (shallow)
- if (equal(existing.item, updated)) {
- debug('item didnt change', id);
- return state;
- }
+ return {
+ ...state,
+ items: {
+ ...state.items,
+ [id]: {
+ ...item,
+ status: COMPLETE,
+ value: {
+ ...item.value,
+ ...value,
+ },
+ },
+ },
+ };
+}
+
+function itemFetchError(state, id, value) {
+ debug('item fetch error', id, value);
+ var item = findItem(state.items, id);
+ if (!item) return state;
return {
...state,
- items: [
- ...state.items.slice(0, existing.index),
- updated,
- ...state.items.slice(existing.index + 1),
- ],
+ items: {
+ ...state.items,
+ [id]: {
+ status: ERROR,
+ value: {
+ ...item.value,
+ error: value,
+ },
+ },
+ },
};
}
-function setDistance(item, newDistance) {
- if (typeof newDistance != 'number' || newDistance === -1) return;
- var distances = item.distances;
- distances.push(newDistance);
- var length = distances.length;
- if (length > 5) distances.shift();
- item.regulatedDistance = roundDistance(getAverage(distances));
- debug('regulated distance', distances, item.regulatedDistance);
+function itemFound(state, id, { distance }) {
+ debug('item found', id);
+ const items = state.items;
+ const item = items[id] || createItem(id, { distance });
+
+ return {
+ ...state,
+ items: {
+ ...items,
+ [id]: { ...item },
+ },
+
+ itemsNearby: [
+ ...state.itemsNearby,
+ id,
+ ],
+ };
}
-function setItemMetadata(state, id, metadata) {
- var existing = findItem(state.items, id);
- if (!existing) return state;
+function itemUpdated(state, id, { distance }) {
+ debug('item found', id);
+ const previous = findItem(state.items, id);
+ const items = state.items;
+
+ if (!previous) return state;
- var updated = {
- ...existing.item,
- metadata: metadata,
+ const next = {
+ ...previous,
+ value: {
+ ...previous.value,
+ ...normalizeDistance(distance, previous.value),
+ },
};
return {
...state,
- items: [
- ...state.items.slice(0, existing.index),
- updated,
- ...state.items.slice(existing.index + 1),
- ],
+ items: {
+ ...items,
+ [id]: next,
+ },
};
}
-function addItem(state, item) {
- debug('add item', item.id);
- var copy = {
- ...item,
- id: item.id,
- url: item.url,
- expiring: false,
- distance: null,
- distances: [],
- regulatedDistance: null,
- };
+function itemLost(state, id) {
+ debug('item lost', id);
+ const { itemsNearby } = state;
- setDistance(copy, item.distance);
+ var index = itemsNearby.indexOf(id);
+ if (!~index) return state;
return {
...state,
- items: [
- copy,
- ...state.items,
+ itemsNearby: [
+ ...itemsNearby.slice(0, index),
+ ...itemsNearby.slice(index + 1),
],
};
}
-function removeItem(state, id) {
- debug('remove item', id);
- var existing = findItem(state.items, id);
- if (!existing) return state;
+function itemOpened(state, id) {
+ debug('item opened', id);
+ return {
+ ...state,
+ openItem: id,
+ };
+}
+function itemClosed(state) {
+ debug('item closed');
return {
...state,
- items: [
- ...state.items.slice(0, existing.index),
- ...state.items.slice(existing.index + 1),
- ],
+ openItem: null,
};
}
-function setOpenItem(state, id) {
- var found = findItem(state.items, id);
+function refreshItems(state) {
return {
...state,
- openItem: found && found.item,
+ itemsNearby: [],
};
}
@@ -168,22 +233,38 @@ function setUserFlag(state, key, value) {
}
function channelsFetching(state) {
+ debug('channels fetching');
+
return {
...state,
channels: {
...state.channels,
- isFetching: true,
+ status: INCOMPLETE,
},
};
}
-function channelsFetched(state, { responseType, value }) {
+function channelsFetched(state, value) {
+ debug('channels fetched');
+
return {
...state,
channels: {
...state.channels,
- isFetching: false,
- responseType,
+ status: COMPLETE,
+ value,
+ },
+ };
+}
+
+function channelsFetchErrored(state, value) {
+ debug('channels fetch errored');
+
+ return {
+ ...state,
+ channels: {
+ ...state.channels,
+ status: ERROR,
value,
},
};
@@ -196,20 +277,32 @@ function subscriptionsFetching(state) {
...state,
subscriptions: {
...state.subscriptions,
- isFetching: true,
+ status: INCOMPLETE,
},
};
}
-function subscriptionsFetched(state, { responseType, value }) {
+function subscriptionsFetched(state, value) {
debug('subscriptions fetched');
return {
...state,
subscriptions: {
...state.subscriptions,
- isFetching: false,
- responseType,
+ status: COMPLETE,
+ value,
+ },
+ };
+}
+
+function subscriptionsFetchErrored(state, value) {
+ debug('subscriptions fetch errored');
+
+ return {
+ ...state,
+ subscriptions: {
+ ...state.subscriptions,
+ status: ERROR,
value,
},
};
@@ -269,17 +362,60 @@ function subscriptionUpdated(state, { subscription }) {
};
}
+function networkStatusUpdate(state, { status, timestamp }) {
+ return {
+ ...state,
+ network: {
+ status,
+ timestamp,
+ },
+ };
+}
+
+/**
+ * Utils
+ */
+
function findItem(items, id) {
debug('find item', id);
- if (!id) return null;
+ return items[id];
+}
+
+function createItem(id, { distance }) {
+ return {
+ id,
+ status: null,
+ value: {
+ url: id,
+ ...normalizeDistance(distance, {}),
+ },
+ };
+}
+
+function normalizeDistance(newDistance, { _distanceHistory=[] }) {
+ if (typeof newDistance != 'number' || newDistance === -1) {
+ return {
+ distance: null,
+ _distanceHistory: [],
+ };
+ }
+
+ // copy
+ _distanceHistory = _distanceHistory.slice();
+ _distanceHistory.push(newDistance);
+
+ // enforce max-length
+ var length = _distanceHistory.length;
+ if (length > 5) _distanceHistory.shift();
- var index;
- var item = items.find((item, i) => {
- index = i;
- return item.id === id;
- });
+ // calculate normalized distance
+ const distance = roundDistance(getAverage(_distanceHistory));
+ debug('regulated distance', _distanceHistory, distance);
- return item ? { item, index } : null;
+ return {
+ distance,
+ _distanceHistory,
+ };
}
function roundDistance(value) {
@@ -288,15 +424,7 @@ function roundDistance(value) {
}
function getAverage(array) {
+ if (array.length === 1) return array[0];
var sum = array.reduce((sum, distance) => sum + distance, 0);
return sum / array.length;
}
-
-function equal(a, b) {
- for (var key in b) {
- if (!b.hasOwnProperty(key)) continue;
- if (a[key] !== b[key]) return false;
- }
-
- return true;
-}
diff --git a/lib/utils/index.js b/lib/utils/index.js
new file mode 100644
index 00000000..48ebf4ba
--- /dev/null
+++ b/lib/utils/index.js
@@ -0,0 +1,6 @@
+
+export function isNetworkError(err) {
+ return err.name === 'NetworkError'
+ || err.message === 'Network request failed'
+ || err.code.indexOf('NoConnectionError') > -1;
+}
diff --git a/lib/views/item-scene.js b/lib/views/item/item-detail.js
similarity index 74%
rename from lib/views/item-scene.js
rename to lib/views/item/item-detail.js
index 9458c166..46f039bd 100644
--- a/lib/views/item-scene.js
+++ b/lib/views/item/item-detail.js
@@ -1,43 +1,72 @@
+'use strict';
+
/**
* Dependencies
*/
-const debug = require('../debug')('ItemScene');
-const theme = require('../../config').theme;
+const debug = require('../../debug')('ItemDetail');
+const theme = require('../../../config').theme;
const ReactNative = require('react-native');
const tinycolor = require('tinycolor2');
const React = require('react');
const {
+ ActivityIndicator,
TouchableOpacity,
StyleSheet,
- View,
ScrollView,
- Text,
- Image,
Linking,
+ Image,
+ View,
+ Text,
} = ReactNative;
-class ItemScene extends React.Component {
+const {
+ INCOMPLETE,
+ ERROR,
+} = require('../../store/constants');
+
+class ItemDetail extends React.Component {
constructor(props) {
super(props);
+ this.state = { animating: true };
debug('created');
}
- render() {
- const metadata = this.props.item.metadata || {};
- console.log('render', this.props.item);
+ shouldComponentUpdate(nextProps) {
+ if (this.dragging) return false;
+ return nextProps.item !== this.props.item;
+ }
+ render() {
return (
-
- {this.renderMedia(metadata)}
- {this.renderUrlBar(metadata)}
- {this.renderText(metadata)}
+
+ {this.renderContent(this.props.item)}
{this.renderHeader()}
-
+
);
}
+ renderContent(item) {
+ if (!item || item.status === INCOMPLETE) return this.renderLoading();
+ if (item.status === ERROR) return this.goBack();
+ const { metadata } = item.value;
+ return
+ {this.renderMedia(metadata)}
+ {this.renderUrlBar(metadata)}
+ {this.renderText(metadata)}
+ ;
+ }
+
+ renderLoading() {
+ return
+
+ ;
+ }
+
renderMedia({ image, title, themeColor }) {
debug('render media', image, title, themeColor);
if (image) return this.renderMediaImage(image);
@@ -104,7 +133,7 @@ class ItemScene extends React.Component {
+ source={require('../../images/item-scene-room.png')}/>
{label}
);
@@ -119,27 +148,31 @@ class ItemScene extends React.Component {
onPress={this.onClosePress.bind(this)}>
+ source={require('../../images/item-scene-close.png')}/>
);
}
onClosePress() {
+ this.goBack();
+ }
+
+ goBack() {
this.props.navigator.pop();
}
}
-ItemScene.propTypes = {
+ItemDetail.propTypes = {
navigator: React.PropTypes.object.isRequired,
- item: React.PropTypes.object.isRequired,
+ item: React.PropTypes.object,
};
/**
* Exports
*/
-module.exports = ItemScene;
+module.exports = ItemDetail;
const VERTICAL_MARGIN = 20;
@@ -165,6 +198,10 @@ const styles = StyleSheet.create({
height: 20,
},
+ scroller: {
+ flex: 1,
+ },
+
mediaImage: {
height: 220,
},
@@ -240,4 +277,9 @@ const styles = StyleSheet.create({
height: 20,
marginRight: 5,
},
+
+ loading: {
+ flex: 1,
+ justifyContent: 'center',
+ },
});
diff --git a/lib/views/item/item-scene.js b/lib/views/item/item-scene.js
new file mode 100644
index 00000000..6bd7162e
--- /dev/null
+++ b/lib/views/item/item-scene.js
@@ -0,0 +1,69 @@
+'use strict';
+
+/**
+ * Dependencies
+ */
+
+import { fetchItemIfNeeded } from '../../store/actions';
+import React, { PropTypes, Component } from 'react';
+import { connect } from 'react-redux';
+import ItemDetail from './item-detail';
+
+class ItemScene extends Component {
+ componentDidMount() {
+ const { openItem, dispatch } = this.props;
+ dispatch(fetchItemIfNeeded(openItem));
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.openItem !== this.props.openItem) {
+ const { dispatch, openItem } = nextProps;
+ dispatch(fetchItemIfNeeded(openItem));
+ }
+ }
+
+ render() {
+ const { openItem, items, navigator } = this.props;
+ const item = items[openItem];
+
+ return ;
+ }
+}
+
+ItemScene.propTypes = {
+ navigator: PropTypes.object.isRequired,
+ itemUrl: PropTypes.string,
+ openItem: PropTypes.string,
+ items: PropTypes.object,
+ setOpenItem: PropTypes.func,
+ dispatch: PropTypes.func.isRequired,
+};
+
+/**
+ * Takes the redux `store` (passed down by
+ * the parent `Provider`) view and maps
+ * specific properties onto the App's
+ * `this.props` object.
+ *
+ * This means the app never touches the
+ * redux store directly and prevents
+ * hacky code being written.
+ *
+ * @param {ReduxStore} store
+ * @return {Object}
+ */
+function mapStateToProps({ openItem, items }) {
+ return {
+ items,
+ openItem,
+ };
+}
+
+/**
+ * Exports
+ */
+
+module.exports = connect(mapStateToProps)(ItemScene);
diff --git a/lib/views/list-scene.js b/lib/views/list-scene.js
deleted file mode 100644
index d9a89505..00000000
--- a/lib/views/list-scene.js
+++ /dev/null
@@ -1,142 +0,0 @@
-
-/**
- * Dependencies
- */
-
-const debug = require('../debug')('ListScene');
-const ReactNative = require('react-native');
-const HeaderBar = require('./header-bar');
-const ListView = require('./list');
-const React = require('react');
-
-const {
- TouchableOpacity,
- StyleSheet,
- Image,
- View,
-} = ReactNative;
-
-class ListScene extends React.Component {
- constructor(props) {
- super(props);
- }
-
- render() {
- debug('render', this.props.scanning);
- var { sortByDistance, showDistance } = this.props.userFlags;
-
- return (
-
- {this.renderHeader()}
-
-
- );
- }
-
- renderHeader() {
- return
-
- }
-
- right={[
- this.renderSubscriptionsButton(),
-
-
- ,
- ]}
- />;
- }
-
- renderSubscriptionsButton() {
- return
-
- ;
- }
-
- onRefresh() {}
-
- onSubscriptionsPress() {
- debug('on subscriptions press');
- this.props.navigator.push({ type: 'subscriptions' });
- }
-
- onSettingsPress() {
- debug('on settings press');
- this.props.navigator.push({ type: 'settings' });
- }
-}
-
-ListScene.propTypes = {
- items: React.PropTypes.array,
- scanning: React.PropTypes.bool,
- navigator: React.PropTypes.object,
- userFlags: React.PropTypes.object,
- onItemPress: React.PropTypes.func.isRequired,
- onItemSwiped: React.PropTypes.func.isRequired,
- onRefresh: React.PropTypes.func.isRequired,
-};
-
-/**
- * Exports
- */
-
-module.exports = ListScene;
-
-const styles = StyleSheet.create({
- root: {
- flex: 1,
- backgroundColor: '#fff',
- },
-
- logo: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- paddingTop: 1,
- },
-
- logoImage: {
- width: 141.76,
- height: 22.8,
- },
-
- settings: {
- width: 37,
- paddingTop: 1,
- paddingLeft: 2,
- justifyContent: 'center',
- alignItems: 'center',
- },
-
- settingImage: {
- width: 22,
- height: 22,
- },
-
- subscriptionsImage: {
- width: 22,
- height: 23,
- },
-});
diff --git a/lib/views/list-item.js b/lib/views/list/list-item.js
similarity index 93%
rename from lib/views/list-item.js
rename to lib/views/list/list-item.js
index 997a7d90..7c6de9fd 100644
--- a/lib/views/list-item.js
+++ b/lib/views/list/list-item.js
@@ -5,8 +5,8 @@
*/
const LinearGradient = require('react-native-linear-gradient').default;
-const debug = require('../debug')('ListItem');
-const theme = require('../../config').theme;
+const debug = require('../../debug')('ListItem');
+const theme = require('../../../config').theme;
const tinycolor = require('tinycolor2');
const React = require('react');
@@ -68,12 +68,13 @@ class ListItem extends React.Component {
shouldComponentUpdate(nextProps) {
if (this.dragging) return false;
return nextProps.item !== this.props.item
- || nextProps.showDistance !== this.props.showDistance;
+ || nextProps.showDistance !== this.props.showDistance
+ || nextProps.item.distance !== this.props.item.distance;
}
render() {
- debug('render');
- var { item } = this.props;
+ var item = this.props.item.value;
+ debug('render', item);
var opacity = this.state.opacity;
var pan = this.state.pan;
var translateX = pan.x;
@@ -104,14 +105,10 @@ class ListItem extends React.Component {
? ['rgba(0,0,0,0)', 'rgba(0,0,0,0.5)']
: ['transparent', 'transparent'];
- var style = [styles.content, {
- opacity: item.expiring ? 0.5 : 1,
- }];
-
return (
{this.renderBackground(item)}
{this.renderDistance(item)}
@@ -131,16 +128,15 @@ class ListItem extends React.Component {
);
}
- renderDistance({ regulatedDistance, expiring }) {
- debug('render distance', regulatedDistance);
- var ignore = !regulatedDistance
+ renderDistance({ distance }) {
+ debug('render distance', distance);
+ var ignore = !distance
|| !this.props.showDistance
- || regulatedDistance === Infinity
- || regulatedDistance === -1
- || expiring;
+ || distance === Infinity
+ || distance === -1;
if (ignore) return;
- var text = `${regulatedDistance}m`;
+ var text = `${distance}m`;
return
dispatch(fetchItemIfNeeded(item.id)));
+ }
+
+ render() {
+ debug('render');
+
+ return (
+
+ {this.renderHeader()}
+
+
+ );
+ }
+
+ renderHeader() {
+ return
+
+ }
+
+ right={[
+ this.renderSubscriptionsButton(),
+
+
+ ,
+ ]}
+ />;
+ }
+
+ renderSubscriptionsButton() {
+ return
+
+ ;
+ }
+
+ onRefresh() {}
+
+ /**
+ * Called when a list-item is pressed.
+ *
+ * Depending on what prefs are set, this
+ * could open the expanded view, or simply
+ * navigate to the item's url.
+ */
+ onItemPress(id) {
+ track.tapListItem(id);
+ if (!flags.itemsExpandable) return Linking.openURL(id);
+ this.props.itemOpened(id);
+ this.props.navigator.push({ type: 'item' });
+ }
+
+ onSubscriptionsPress() {
+ debug('on subscriptions press');
+ this.props.navigator.push({ type: 'subscriptions' });
+ }
+
+ onSettingsPress() {
+ debug('on settings press');
+ this.props.navigator.push({ type: 'settings' });
+ }
+}
+
+ListScene.propTypes = {
+ indicateScanning: PropTypes.bool,
+ navigator: PropTypes.object,
+ userFlags: PropTypes.object,
+ showDistance: PropTypes.bool,
+ sortByDistance: PropTypes.bool,
+ items: PropTypes.array,
+
+ onRefresh: PropTypes.func.isRequired,
+ itemOpened: PropTypes.func.isRequired,
+ dispatch: PropTypes.func.isRequired,
+};
+
+/**
+ * Takes the redux `store` (passed down by
+ * the parent `Provider`) view and maps
+ * specific properties onto the App's
+ * `this.props` object.
+ *
+ * This means the app never touches the
+ * redux store directly and prevents
+ * hacky code being written.
+ *
+ * @param {ReduxStore} store
+ * @return {Object}
+ */
+function mapStateToProps({ items, itemsNearby, userFlags, indicateScanning }) {
+ const { sortByDistance, showDistance } = userFlags;
+
+ return {
+ items: itemsNearby.map(id => items[id]),
+ sortByDistance: sortByDistance.value,
+ showDistance: showDistance.value,
+ userFlags,
+ indicateScanning,
+ };
+}
+/**
+ * Maps the methods exported from `action-creators.js`
+ * to `this.props.`.
+ *
+ * @param {function} dispatch
+ * @return {Object}
+ */
+function mapDispatchToProps(dispatch) {
+ const { itemOpened } = actions;
+
+ return {
+ ...bindActionCreators({ itemOpened }, dispatch),
+ dispatch,
+ };
+}
+
+/**
+ * Exports
+ */
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(ListScene);
+
+const styles = StyleSheet.create({
+ root: {
+ flex: 1,
+ backgroundColor: '#fff',
+ },
+
+ logo: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingTop: 1,
+ },
+
+ logoImage: {
+ width: 141.76,
+ height: 22.8,
+ },
+
+ settings: {
+ width: 37,
+ paddingTop: 1,
+ paddingLeft: 2,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+
+ settingImage: {
+ width: 22,
+ height: 22,
+ },
+
+ subscriptionsImage: {
+ width: 22,
+ height: 23,
+ },
+});
diff --git a/lib/views/list.js b/lib/views/list/list.js
similarity index 90%
rename from lib/views/list.js
rename to lib/views/list/list.js
index ca9e4257..e1837464 100644
--- a/lib/views/list.js
+++ b/lib/views/list/list.js
@@ -4,23 +4,25 @@
* Dependencies
*/
-const debug = require('../debug')('ListView');
-const ListItem = require('./list-item');
-const React = require('react');
+import ListItem from './list-item';
+import Debug from '../../debug';
+import React from 'react';
-const {
+import {
StyleSheet,
ScrollView,
View,
Text,
RefreshControl,
LayoutAnimation,
-} = require('react-native');
+} from 'react-native';
-const {
+import {
theme,
flags,
-} = require('../../config');
+} from '../../../config';
+
+const debug = Debug('ListView');
class ListView extends React.Component {
constructor(props) {
@@ -89,14 +91,14 @@ class ListView extends React.Component {
renderItems() {
debug('render list items');
- var items = this.props.items.filter(item => !item.hidden);
+ var { items} = this.props;
// only sort if preffed on
if (this.props.sortByDistance) {
items = items.sort(this.sort);
}
- return items.map((item) => {
+ return items.map(item => {
debug('render id', item.id);
return ;
});
}
@@ -126,8 +128,11 @@ class ListView extends React.Component {
}
sort(item1, item2) {
- var distance1 = item1.regulatedDistance;
- var distance2 = item2.regulatedDistance;
+ item1 = item1.value;
+ item2 = item2.value;
+
+ var distance1 = item1.distance;
+ var distance2 = item2.distance;
if (distance1 === distance2) return 0;
@@ -161,11 +166,6 @@ class ListView extends React.Component {
this.props.onRefresh();
}
- onItemPress({props}) {
- debug('on item press');
- this.props.onItemPress(props.item);
- }
-
onScroll({nativeEvent:{contentOffset}}) {
this.scrollY = contentOffset.y;
}
diff --git a/lib/views/subscriptions-scene.js b/lib/views/subscriptions-scene.js
index 6bd57225..d98f3bce 100644
--- a/lib/views/subscriptions-scene.js
+++ b/lib/views/subscriptions-scene.js
@@ -4,16 +4,24 @@
* Dependencies
*/
-const debug = require('../debug')('SubscriptionsScene', 1);
+const debug = require('../debug')('SubscriptionsScene');
+const { COMPLETE } = require('../store/constants');
const SubscribeButton = require('./subscribe-button');
const ReactNative = require('react-native');
const { connect } = require('react-redux');
const HeaderBar = require('./header-bar');
-const actions = require('../store/actions');
const { theme } = require('../../config');
const React = require('react');
const api = require('../api');
+const {
+ subscribed,
+ unsubscribed,
+ subscriptionUpdated,
+ fetchChannelsIfNeeded,
+ fetchSubscriptionsIfNeeded,
+} = require('../store/actions');
+
const {
TouchableOpacity,
InteractionManager,
@@ -26,9 +34,6 @@ const {
} = ReactNative;
class SubscriptionsScene extends React.Component {
- constructor(props) {
- super(props);
- }
/**
* Fetches the data for the view after
@@ -40,8 +45,9 @@ class SubscriptionsScene extends React.Component {
*/
componentDidMount() {
InteractionManager.runAfterInteractions(() => {
- this.updateChannels();
- this.updateSubscriptions();
+ const { dispatch } = this.props;
+ dispatch(fetchChannelsIfNeeded());
+ dispatch(fetchSubscriptionsIfNeeded());
});
}
@@ -119,36 +125,9 @@ class SubscriptionsScene extends React.Component {
return subscriptions.value && !!subscriptions.value[channelId];
}
- updateChannels() {
- if (this.props.channels.isFetching) return;
- if (this.props.channels.value) return;
-
- debug('fetching channels ...', this.props.channels);
-
- // update local state
- this.props.channelsFetching();
-
- // fetch remote data
- api.get('channels')
- .then(result => this.props.channelsFetched('success', result))
- .catch(err => this.props.channelsFetched('error', err));
- }
-
- updateSubscriptions() {
- if (this.props.subscriptions.state) return;
- debug('update subscriptions', this.props.subscriptions);
-
- // update local state
- this.props.subscriptionsFetching();
-
- // fetch remote data
- api.get('subscriptions')
- .then(result => this.props.subscriptionsFetched('success', result))
- .catch(err => this.props.subscriptionsFetched('error', err));
- }
-
dataReady() {
- return !!this.props.channels.value;
+ return this.props.channels.status === COMPLETE
+ && this.props.subscriptions.status === COMPLETE;
}
onSubscriptionChange(channel_id, value) {
@@ -158,9 +137,10 @@ class SubscriptionsScene extends React.Component {
onSubscribed(channel_id) {
debug('on unsubscribed', channel_id);
+ const { dispatch } = this.props;
// update local state
- this.props.subscribed(channel_id);
+ dispatch(subscribed(channel_id));
// persist it
api.post('subscriptions', {
@@ -170,12 +150,13 @@ class SubscriptionsScene extends React.Component {
// update local state with latest model
.then(subscription => {
- this.props.subscriptionUpdated(subscription);
+ dispatch(subscriptionUpdated(subscription));
});
}
onUnsubscribed(channel_id) {
- this.props.unsubscribed(channel_id);
+ const { dispatch } = this.props;
+ dispatch(unsubscribed(channel_id));
api.delete('subscriptions', { channel_id });
}
@@ -199,6 +180,7 @@ SubscriptionsScene.propTypes = {
subscriptionUpdated: React.PropTypes.func,
subscriptionsFetching: React.PropTypes.func,
subscriptionsFetched: React.PropTypes.func,
+ dispatch: React.PropTypes.func,
};
/**
@@ -221,30 +203,11 @@ function mapStateToProps(store) {
};
}
-/**
- * Maps the methods exported from `action-creators.js`
- * to `this.props.`.
- *
- * @param {function} dispatch
- * @return {Object}
- */
-function mapDispatchToProps() {
- return {
- channelsFetching: actions.channelsFetching,
- channelsFetched: actions.channelsFetched,
- subscriptionsFetching: actions.subscriptionsFetching,
- subscriptionsFetched: actions.subscriptionsFetched,
- subscribed: actions.subscribed,
- unsubscribed: actions.unsubscribed,
- subscriptionUpdated: actions.subscriptionUpdated,
- };
-}
-
/**
* Exports
*/
-module.exports = connect(mapStateToProps, mapDispatchToProps)(SubscriptionsScene);
+module.exports = connect(mapStateToProps)(SubscriptionsScene);
const styles = StyleSheet.create({
root: {
diff --git a/package.json b/package.json
index 35b80510..eafc9107 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"react-native-linear-gradient": "^1.5.13",
"react-redux": "^4.4.5",
"redux": "^3.5.2",
+ "redux-thunk": "^2.1.0",
"stream": "0.0.2",
"tinycolor2": "^1.3.0"
},
diff --git a/test/__mocks__/react-native-google-analytics-bridge.js b/test/__mocks__/react-native-google-analytics-bridge.js
new file mode 100644
index 00000000..5295bf74
--- /dev/null
+++ b/test/__mocks__/react-native-google-analytics-bridge.js
@@ -0,0 +1,5 @@
+
+export default {
+ setTrackerId: () => {},
+ trackEvent: () => {},
+};
diff --git a/test/lib/scanner/metadata/index.test.js b/test/lib/scanner/metadata/index.test.js
deleted file mode 100644
index 3b580f17..00000000
--- a/test/lib/scanner/metadata/index.test.js
+++ /dev/null
@@ -1,93 +0,0 @@
-
-/**
- * Dependencies
- */
-
-const metadata = require('../../../../lib/scanner/metadata');
-const assert = require('assert');
-
-jest.useFakeTimers();
-
-describe('metadata', () => {
-
- describe('serverside', function() {
- beforeEach(function() {
- global.Request = jest.fn((url, config) => {
- return Object.assign({}, config, { url });
- });
-
- global.Headers = jest.fn(config => config);
-
- global.fetch = jest.fn(() => {
- this.lastFetch = new Deferred;
- return this.lastFetch.promise;
- });
- });
-
- afterEach(function() {
-
- });
-
- describe('batches', function() {
- beforeEach(function() {
- this.calls = [
- metadata('http://bbc.co.uk/news'),
- metadata('http://google.com'),
- ];
-
- // tick past batch window
- jest.runTimersToTime(200);
- });
-
- it('batches requests', function() {
- var request = global.fetch.mock.calls[0][0];
- var body = JSON.parse(request.body);
- var urls = body.objects;
-
- assert.equal(urls[0].url, 'http://bbc.co.uk/news');
- assert.equal(urls[1].url, 'http://google.com');
- });
-
- describe('error', function() {
- beforeEach(function() {
- var result = Promise.resolve([
- {
- error: 'bad thing',
- },
- {
- title: 'Google',
- },
- ]);
-
- this.lastFetch.resolve({
- json: () => result,
- });
-
- // wait till initial call has resolved
- return this.calls[1];
- });
-
- it('resolves the successful items', function() {
- return this.calls[1]
- .then(result => {
- assert.equal(result.title, 'Google');
- });
- });
-
- it('rejects the errored items', function() {
- return this.calls[0]
- .catch(err => {
- assert.equal(err.message, 'bad thing');
- });
- });
- });
- });
- });
-
- function Deferred() {
- this.promise = new Promise((resolve, reject) => {
- this.resolve = resolve;
- this.reject = reject;
- });
- }
-});
diff --git a/test/lib/store.test.js b/test/lib/store.test.js
index b5eadd9d..85cc86b2 100644
--- a/test/lib/store.test.js
+++ b/test/lib/store.test.js
@@ -3,14 +3,15 @@
* Dependencies
*/
-const actions = require('../../lib/store/actions').actions;
-const { createStore, bindActionCreators } = require('redux');
-const reducer = require('../../lib/store/reducer');
-const assert = require('assert');
+import { createStore, bindActionCreators, applyMiddleware } from 'redux';
+import actions from '../../lib/store/actions';
+import reducer from '../../lib/store/reducer';
+import thunk from 'redux-thunk';
+import assert from 'assert';
describe('store', function() {
beforeEach(function() {
- this.store = createStore(reducer);
+ this.store = createStore(reducer, applyMiddleware(thunk));
this.actions = bindActionCreators(actions, this.store.dispatch);
});
@@ -19,7 +20,7 @@ describe('store', function() {
describe('first', function() {
beforeEach(function() {
- this.actions.updateItem(url, {
+ this.actions.itemFound(url, {
url,
id: url,
distance: 3,
@@ -27,80 +28,80 @@ describe('store', function() {
});
it('sets correct distance', function() {
- var item = getItem(this.store.getState().items, url);
+ var item = getItem(this.store.getState().items, url).value;
var expected = [3];
- assert.deepEqual(item.distances, expected);
- assert.equal(item.regulatedDistance, meanToNearest(expected, 2));
+ assert.deepEqual(item._distanceHistory, expected);
+ assert.equal(item.distance, meanToNearest(expected, 2));
});
describe('second', function() {
beforeEach(function() {
- this.actions.updateItem(url, { distance: 4 });
+ this.actions.itemFound(url, { distance: 4 });
});
it('sets correct distance', function() {
- var item = getItem(this.store.getState().items, url);
+ var item = getItem(this.store.getState().items, url).value;
var expected = [3, 4];
- assert.deepEqual(item.distances, expected);
- assert.equal(item.regulatedDistance, meanToNearest(expected, 2));
+ assert.deepEqual(item._distanceHistory, expected);
+ assert.equal(item.distance, meanToNearest(expected, 2));
});
describe('third', function() {
beforeEach(function() {
- this.actions.updateItem(url, {
+ this.actions.itemFound(url, {
distance: 3,
});
});
it('sets correct distance', function() {
- var item = getItem(this.store.getState().items, url);
+ var item = getItem(this.store.getState().items, url).value;
var expected = [3, 4, 3];
- assert.deepEqual(item.distances, expected);
- assert.equal(item.regulatedDistance, meanToNearest(expected, 2));
+ assert.deepEqual(item._distanceHistory, expected);
+ assert.equal(item.distance, meanToNearest(expected, 2));
});
describe('fourth', function() {
beforeEach(function() {
- this.actions.updateItem(url, {
+ this.actions.itemFound(url, {
distance: 7,
});
});
it('sets correct distance', function() {
- var item = getItem(this.store.getState().items, url);
+ var item = getItem(this.store.getState().items, url).value;
var expected = [3, 4, 3, 7];
- assert.deepEqual(item.distances, expected);
- assert.equal(item.regulatedDistance, meanToNearest(expected, 2));
+ assert.deepEqual(item._distanceHistory, expected);
+ assert.equal(item.distance, meanToNearest(expected, 2));
});
describe('fifth', function() {
beforeEach(function() {
- this.actions.updateItem(url, { distance: 4 });
+ this.actions.itemFound(url, { distance: 4 });
});
it('sets correct distance', function() {
- var item = getItem(this.store.getState().items, url);
+ var item = getItem(this.store.getState().items, url).value;
var expected = [3, 4, 3, 7, 4];
- assert.deepEqual(item.distances, expected);
- assert.equal(item.regulatedDistance, meanToNearest(expected, 2));
+ assert.deepEqual(item._distanceHistory, expected);
+ assert.equal(item.distance, meanToNearest(expected, 2));
});
describe('sixth', function() {
beforeEach(function() {
- this.actions.updateItem(url, { distance: 4 });
+ this.actions.itemFound(url, { distance: 4 });
});
it('sets correct distance', function() {
- var item = getItem(this.store.getState().items, url);
+ var item = getItem(this.store.getState().items, url).value;
var expected = [4, 3, 7, 4, 4];
- assert.deepEqual(item.distances, expected);
- assert.equal(item.regulatedDistance, meanToNearest(expected, 2));
+ assert.deepEqual(item._distanceHistory, expected);
+ assert.equal(item.distance, meanToNearest(expected, 2));
});
});
});
@@ -111,7 +112,7 @@ describe('store', function() {
});
function getItem(items, id) {
- return items.find(item => item.id === id);
+ return items[id];
}
function meanToNearest(numbers, nearest) {
diff --git a/test/lib/views/item-scene.test.js b/test/lib/views/item-scene.test.js
index 0e1355dc..68c24195 100644
--- a/test/lib/views/item-scene.test.js
+++ b/test/lib/views/item-scene.test.js
@@ -1,9 +1,12 @@
-import 'react-native';
-import React from 'react';
-import ItemScene from '../../../lib/views/item-scene';
-
+import ItemScene from '../../../lib/views/item/item-scene';
+import { createStore, applyMiddleware } from 'redux';
+import reducer from '../../../lib/store/reducer';
import renderer from 'react-test-renderer';
+import thunk from 'redux-thunk';
+import React from 'react';
+import 'react-native';
it('renders without crashing', () => {
- renderer.create();
+ const store = createStore(reducer, applyMiddleware(thunk));
+ renderer.create();
});
diff --git a/yarn.lock b/yarn.lock
index c9da7fb8..16cb9d75 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3537,10 +3537,9 @@ lru-cache@^4.0.0, lru-cache@^4.0.1:
url "^0.11.0"
xml2js "^0.4.16"
-magnet-scanner-android@mozilla-magnet/magnet-scanner-android#v4.0.1, mozilla-magnet/magnet-scanner-android#v4.0.1:
- name magnet-scanner-android
- version "4.0.1"
- resolved "https://codeload.github.com/mozilla-magnet/magnet-scanner-android/tar.gz/ebed682910b46e646afe385df5c2397cc3f6a6e5"
+magnet-scanner-android@mozilla-magnet/magnet-scanner-android#v4.0.3:
+ version "4.0.3"
+ resolved "https://codeload.github.com/mozilla-magnet/magnet-scanner-android/tar.gz/155c7d338bd4d5b54d94beb582e11b24c6441fa4"
makeerror@1.0.x:
version "1.0.11"
@@ -4192,9 +4191,9 @@ react-deep-force-update@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/react-deep-force-update/-/react-deep-force-update-1.0.1.tgz#f911b5be1d2a6fe387507dd6e9a767aa2924b4c7"
-react-native-google-analytics-bridge@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/react-native-google-analytics-bridge/-/react-native-google-analytics-bridge-3.1.0.tgz#4869db16e6e18d59f1f4ab6a33e483cc71872421"
+react-native-google-analytics-bridge@mozilla-magnet/react-native-google-analytics-bridge#master:
+ version "4.0.0"
+ resolved "https://codeload.github.com/mozilla-magnet/react-native-google-analytics-bridge/tar.gz/eb4543509e9ab96b1bcc9b7e19942e2716330259"
react-native-linear-gradient@^1.5.13:
version "1.5.14"
@@ -4305,7 +4304,7 @@ react-transform-hmr@^1.0.4:
global "^4.3.0"
react-proxy "^1.1.7"
-react@^15.3.1:
+react@15.3.x:
version "15.3.2"
resolved "https://registry.yarnpkg.com/react/-/react-15.3.2.tgz#a7bccd2fee8af126b0317e222c28d1d54528d09e"
dependencies:
@@ -4436,6 +4435,10 @@ reduce-component@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/reduce-component/-/reduce-component-1.0.1.tgz#e0c93542c574521bea13df0f9488ed82ab77c5da"
+redux-thunk:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.1.0.tgz#c724bfee75dbe352da2e3ba9bc14302badd89a98"
+
redux@^3.5.2:
version "3.6.0"
resolved "https://registry.yarnpkg.com/redux/-/redux-3.6.0.tgz#887c2b3d0b9bd86eca2be70571c27654c19e188d"