diff --git a/lib/activeFires.dart b/lib/activeFires.dart index c9e2ba5..be6d1af 100644 --- a/lib/activeFires.dart +++ b/lib/activeFires.dart @@ -91,7 +91,7 @@ class _ActiveFiresPageState extends State { : Icons.notifications_off), onPressed: () { loc.subscribed = !loc.subscribed; - onToggle(loc.id); + onToggle(loc); /* FIXME int i = globals.yourLocations.indexOf(loc); globals.yourLocations.removeAt(i); globals.yourLocations.insert(i, loc); @@ -189,8 +189,8 @@ class _ActiveFiresPageState extends State { onDelete: (id) { store.dispatch(new DeleteYourLocationAction(id)); }, - onToggleSubs: (id) { - store.dispatch(new ToggleSubscriptionAction(id)); + onToggleSubs: (loc) { + store.dispatch(new ToggleSubscriptionAction(loc)); }, yourLocations: store.state.yourLocations); }, diff --git a/lib/mainCommon.dart b/lib/mainCommon.dart index 8989758..bc5b6ce 100644 --- a/lib/mainCommon.dart +++ b/lib/mainCommon.dart @@ -12,6 +12,7 @@ import 'models/firesApi.dart'; import 'redux/fetchDataMiddleware.dart'; import 'redux/reducers.dart'; import 'package:fires_flutter/models/yourLocationPersist.dart'; +import 'package:fires_flutter/models/yourLocation.dart'; Future> loadSecrets() async { return await SecretLoader(secretPath: 'assets/private-settings.json').load(); @@ -40,8 +41,11 @@ void mainCommon(List> otherMiddleware) { // Run baby run! runApp(new FiresApp(store)); }); + + // Listen to store changes, and re-render when the state is updated + // store.onChange.listen((state) { + // }); + }); - // Listen to store changes, and re-render when the state is updated - // store.onChange.listen(render); } diff --git a/lib/mainDev.dart b/lib/mainDev.dart index 1cbd849..a1d08a7 100644 --- a/lib/mainDev.dart +++ b/lib/mainDev.dart @@ -6,10 +6,11 @@ import 'package:redux/redux.dart'; void main() { globals.isDevelopment = true; + var logRedux = false; - List devMiddlewares = [ + List devMiddlewares = logRedux ? [ LoggingMiddleware.printer(formatter: LoggingMiddleware.multiLineFormatter) - ]; + ] : []; mainCommon(devMiddlewares); } diff --git a/lib/models/appState.dart b/lib/models/appState.dart index 333c251..796de57 100644 --- a/lib/models/appState.dart +++ b/lib/models/appState.dart @@ -79,6 +79,6 @@ class AppState extends Object with _$AppStateSerializerMixin { typedef void AddYourLocationFunction(YourLocation loc); typedef void DeleteYourLocationFunction(ObjectId id); typedef void UpdateYourLocationFunction(ObjectId id, YourLocation loc); -typedef void ToggleSubscriptionFunction(ObjectId id); +typedef void ToggleSubscriptionFunction(YourLocation loc); typedef void SubscribeFunction(ObjectId id); typedef void UnSubscribeFunction(ObjectId id); diff --git a/lib/models/firesApi.dart b/lib/models/firesApi.dart index 761631d..4436c89 100644 --- a/lib/models/firesApi.dart +++ b/lib/models/firesApi.dart @@ -1,12 +1,11 @@ import 'dart:async'; import 'dart:convert'; +import 'package:bson_objectid/bson_objectid.dart'; +import 'package:fires_flutter/models/yourLocation.dart'; import 'package:http/http.dart' as ht; import 'package:jaguar_resty/jaguar_resty.dart' as resty; -import 'package:fires_flutter/models/yourLocation.dart'; - - import 'appState.dart'; class FiresApi { @@ -14,7 +13,8 @@ class FiresApi { resty.globalClient = new ht.IOClient(); } - Future createUser(AppState state, String mobileToken, String lang) async { + Future createUser( + AppState state, String mobileToken, String lang) async { assert(state.firesApiUrl != null); assert(state.firesApiKey != null); assert(mobileToken != null); @@ -28,25 +28,87 @@ class FiresApi { final String url = '${state.firesApiUrl}mobile/users'; /* print(url); print(params); */ - String resp = await resty.post(url).json(params).go().then((response) { + return await resty.post(url).json(params).go().then((response) { if (response.statusCode == 200) { // print(response.body); return json.decode(response.body)['data']['userId']; } }); - return resp; } Future> fetchYourLocations(AppState state) async { final apiKey = state.firesApiKey; final mobileToken = state.user.token; - final String url = '${state.firesApiUrl}mobile/subscriptions/all/$apiKey/$mobileToken'; - List resp = await resty.get(url).go().then((response) { + final String url = '${state + .firesApiUrl}mobile/subscriptions/all/$apiKey/$mobileToken'; + return await resty.get(url).go().then((response) { if (response.statusCode == 200) { // print(response.body); - print(json.decode(response.body)['data']['subscriptions']); return []; + final dataSubscriptions = + json.decode(response.body)['data']['subscriptions']; + List subscribed = []; + for (int i = 0; i < dataSubscriptions.length; i++) { + var el = dataSubscriptions[i]; + var lat = el['location']['lat']; + var lon = el['location']['lon']; + var id = el['_id']['_str']; + subscribed.add(new YourLocation( + id: ObjectId.fromHexString(el['_id']['_str']), + lat: lat, + lon: lon, + subscribed: true, + distance: el['distance'])); + } + + return subscribed; + } + }); + } + + Future subscribe(AppState state, YourLocation loc) async { + assert(state.firesApiUrl != null); + assert(state.firesApiKey != null); + assert(state.user.token != null); + assert(loc != null); + assert(loc.lat != null); + assert(loc.lon != null); + assert(loc.id != null); + assert(loc.distance != null); + final params = { + "token": state.firesApiKey, + "mobileToken": state.user.token, + "id": loc.id.toHexString(), + "lat": loc.lat, + "lon": loc.lon, + "distance": loc.distance + }; + final String url = '${state + .firesApiUrl}mobile/subscriptions'; + return await resty.post(url).json(params).go().then((response) { + if (response.statusCode == 200) { + // print(response.body); + return json.decode(response.body)['data']['subsId']; + } else { + // take care of? "Unexpected error in REST call: Error: The user is already subscribed to this area [on-already-subscribed]" + print(response.body); + } + }); + } + + Future unsubscribe(AppState state, String subsId) async { + assert(state.firesApiUrl != null); + assert(state.firesApiKey != null); + assert(state.user.token != null); + final apiKey = state.firesApiKey; + final mobileToken = state.user.token; + assert(subsId != null); + final String url = '${state + .firesApiUrl}mobile/subscriptions/$apiKey/$mobileToken/$subsId'; + return await resty.delete(url).go().then((response) { + if (response.statusCode == 200) { + // print(response.body); + return true; } }); - return resp; } } diff --git a/lib/models/yourLocation.dart b/lib/models/yourLocation.dart index a3e61be..d53eece 100644 --- a/lib/models/yourLocation.dart +++ b/lib/models/yourLocation.dart @@ -20,6 +20,7 @@ class YourLocation extends Object with _$YourLocationSerializerMixin { final double lon; String description; bool subscribed; + int distance; factory YourLocation.fromJson(Map json) => _$YourLocationFromJson(json); @@ -31,14 +32,13 @@ class YourLocation extends Object with _$YourLocationSerializerMixin { @required this.lat, @required this.lon, this.description, + this.distance: 10, this.subscribed: false}) { if (this.description == null) this.description = 'Position: ${this.lat}, ${this.lon}'; if (this.id == null) this.id = new ObjectId(); } - - @override bool operator ==(Object other) => identical(this, other) || @@ -48,7 +48,8 @@ class YourLocation extends Object with _$YourLocationSerializerMixin { lat == other.lat && lon == other.lon && description == other.description && - subscribed == other.subscribed; + subscribed == other.subscribed && + distance == other.distance; @override int get hashCode => @@ -56,11 +57,11 @@ class YourLocation extends Object with _$YourLocationSerializerMixin { lat.hashCode ^ lon.hashCode ^ description.hashCode ^ - subscribed.hashCode; + subscribed.hashCode ^ + distance.hashCode; @override String toString() { - return 'YourLocation {id: $id, lat: $lat, lon: $lon, description: $description, subscribed: $subscribed}'; + return 'YourLocation {id: $id, lat: $lat, lon: $lon, description: $description, subscribed: $subscribed, distance: $distance}'; } - } diff --git a/lib/models/yourLocation.g.dart b/lib/models/yourLocation.g.dart index a5688b3..cae216b 100644 --- a/lib/models/yourLocation.g.dart +++ b/lib/models/yourLocation.g.dart @@ -14,24 +14,33 @@ YourLocation _$YourLocationFromJson(Map json) => lat: (json['lat'] as num).toDouble(), lon: (json['lon'] as num).toDouble(), description: json['description'] as String, + distance: json['distance'] as int, subscribed: json['subscribed'] as bool); abstract class _$YourLocationSerializerMixin { ObjectId get id; + double get lat; + double get lon; + String get description; + bool get subscribed; + + int get distance; + Map toJson() => new _$YourLocationJsonMapWrapper(this); } class _$YourLocationJsonMapWrapper extends $JsonMapWrapper { final _$YourLocationSerializerMixin _v; + _$YourLocationJsonMapWrapper(this._v); @override Iterable get keys => - const ['id', 'lat', 'lon', 'description', 'subscribed']; + const ['id', 'lat', 'lon', 'description', 'subscribed', 'distance']; @override dynamic operator [](Object key) { @@ -47,6 +56,8 @@ class _$YourLocationJsonMapWrapper extends $JsonMapWrapper { return _v.description; case 'subscribed': return _v.subscribed; + case 'distance': + return _v.distance; } } return null; diff --git a/lib/redux/appActions.dart b/lib/redux/appActions.dart index 4c6421a..ecd965b 100644 --- a/lib/redux/appActions.dart +++ b/lib/redux/appActions.dart @@ -4,6 +4,8 @@ abstract class AppActions {} class FetchYourLocationsAction extends AppActions {} +class PersistAppStateAction extends AppActions {} + class FetchYourLocationsSucceededAction extends AppActions { final List fetchedYourLocations; diff --git a/lib/redux/appReducer.dart b/lib/redux/appReducer.dart new file mode 100644 index 0000000..6390898 --- /dev/null +++ b/lib/redux/appReducer.dart @@ -0,0 +1,10 @@ +import 'actions.dart'; +import '../models/user.dart'; +import '../models/appState.dart'; + +AppState appReducer(AppState state, action) { + if (action is FetchYourLocationsSucceededAction) { + return state.copyWith(yourLocations: action.fetchedYourLocations); + } + return state; +} \ No newline at end of file diff --git a/lib/redux/fetchDataMiddleware.dart b/lib/redux/fetchDataMiddleware.dart index 3c7a68d..77e6747 100644 --- a/lib/redux/fetchDataMiddleware.dart +++ b/lib/redux/fetchDataMiddleware.dart @@ -1,11 +1,10 @@ +import 'package:bson_objectid/bson_objectid.dart'; +import 'package:fires_flutter/models/yourLocation.dart'; import 'package:redux/redux.dart'; import '../globals.dart' as globals; import '../models/appState.dart'; import '../models/firesApi.dart'; - -import 'package:fires_flutter/models/yourLocation.dart'; - import '../models/yourLocationPersist.dart'; import 'actions.dart'; @@ -36,28 +35,52 @@ void fetchYourLocationsMiddleware( createUser(store, store.state.user.lang, action.token); } - if (action is FetchYourLocationsAction) { - final api = globals.getIt.get(); + if (action is AddYourLocationAction) { + if (action.loc.subscribed) { + subscribeViaApi(store, action.loc, + (sub) => store.dispatch(new AddedYourLocationAction(sub))); + } else { + // No subscribed (only local) + store.dispatch(new AddedYourLocationAction(action.loc)); + persistYourLocations(store.state.yourLocations); + } + } + if (action is DeleteYourLocationAction) { + unsubsViaApi(store, action.id, + () => store.dispatch(new DeletedYourLocationAction(action.id))); + } + + if (action is ToggleSubscriptionAction) { + if (action.loc.subscribed) { + subscribeViaApi(store, action.loc, + (sub) => store.dispatch(new ToggledSubscriptionAction(sub))); + } else { + unsubsViaApi(store, action.loc.id, + () => store.dispatch(new ToggledSubscriptionAction(action.loc))); + } + } + + if (action is FetchYourLocationsAction) { // Use the api to fetch the YourLocations - api.fetchYourLocations(store.state).then((List yourSubs) { + api + .fetchYourLocations(store.state) + .then((List subscribedLocations) { // If it succeeds, dispatch a success action with the YourLocations. // Our reducer will then update the State using these YourLocations. - + // print('Subscribed to: ${subscribedLocations.length}'); loadYourLocations().then((localLocations) { // unsubscribe all locally to sync the subs state localLocations.forEach((location) => location.subscribed = false); - - yourSubs.forEach((loc) { + // print('Local persisted: ${localLocations.length}'); + subscribedLocations.forEach((subsLoc) { localLocations.firstWhere( - (localLocation) => localLocation.id == loc.id, orElse: () { - localLocations.add(loc); + (localLocation) => localLocation.id == subsLoc.id, orElse: () { + localLocations.add(subsLoc); }).subscribed = true; }); - - persistYourLocations(localLocations); - store.dispatch(new FetchYourLocationsSucceededAction(localLocations)); + persistYourLocations(localLocations); }); }).catchError((Exception error) { // If it fails, dispatch a failure action. The reducer will @@ -65,11 +88,28 @@ void fetchYourLocationsMiddleware( store.dispatch(new FetchYourLocationsFailedAction(error)); }); } - // Make sure our actions continue on to the reducer. next(action); } +void unsubsViaApi(Store store, ObjectId id, onUnsubs) { + api.unsubscribe(store.state, id.toHexString()).then((res) { + onUnsubs(); + persistYourLocations(store.state.yourLocations); + }); +} + +void subscribeViaApi(Store store, YourLocation loc, onSubs) { + api.subscribe(store.state, loc).then((subsId) { + YourLocation sub = loc; + if (loc.id != subsId) { + sub.id = new ObjectId.fromHexString(subsId); + } + onSubs(sub); + persistYourLocations(store.state.yourLocations); + }); +} + void createUser(store, lang, token) { assert(token != null, "User lang is null"); assert(token != null, "User mobile token is null"); diff --git a/lib/redux/reducers.dart b/lib/redux/reducers.dart index 3d07354..2a53994 100644 --- a/lib/redux/reducers.dart +++ b/lib/redux/reducers.dart @@ -5,9 +5,14 @@ import 'loadedReducer.dart'; import 'loadingReducer.dart'; import 'userReducer.dart'; import 'yourLocationsReducer.dart'; +import 'actions.dart'; +import 'appReducer.dart'; // We create the State reducer by combining many smaller reducers into one! -AppState appStateReducer(AppState state, action) { +AppState appStateReducer(AppState prevState, action) { + var state = prevState; + if (action is AppActions) + state = appReducer(prevState, action); return AppState( yourLocations: yourLocationsReducer(state.yourLocations, action), user: userReducer(state.user, action), diff --git a/lib/redux/yourLocationActions.dart b/lib/redux/yourLocationActions.dart index 1467b37..747f1c6 100644 --- a/lib/redux/yourLocationActions.dart +++ b/lib/redux/yourLocationActions.dart @@ -11,12 +11,25 @@ class AddYourLocationAction extends YourLocationActions { AddYourLocationAction(this.loc); } +class AddedYourLocationAction extends YourLocationActions { + YourLocation loc; + + AddedYourLocationAction(this.loc); +} + class DeleteYourLocationAction extends YourLocationActions { ObjectId id; DeleteYourLocationAction(this.id); } +class DeletedYourLocationAction extends YourLocationActions { + ObjectId id; + + DeletedYourLocationAction(this.id); +} + + class UpdateYourLocationAction extends YourLocationActions { ObjectId id; YourLocation loc; @@ -25,9 +38,15 @@ class UpdateYourLocationAction extends YourLocationActions { } class ToggleSubscriptionAction extends YourLocationActions { - ObjectId id; + YourLocation loc; - ToggleSubscriptionAction(this.id); + ToggleSubscriptionAction(this.loc); +} + +class ToggledSubscriptionAction extends YourLocationActions { + YourLocation loc; + + ToggledSubscriptionAction(this.loc); } class SubscribeAction extends YourLocationActions { diff --git a/lib/redux/yourLocationsReducer.dart b/lib/redux/yourLocationsReducer.dart index 03827fe..05c5213 100644 --- a/lib/redux/yourLocationsReducer.dart +++ b/lib/redux/yourLocationsReducer.dart @@ -4,24 +4,25 @@ import 'package:redux/redux.dart'; import 'actions.dart'; final yourLocationsReducer = combineReducers>([ - new TypedReducer, AddYourLocationAction>(_addYourLocation), - new TypedReducer, DeleteYourLocationAction>( - _deleteYourLocation), + new TypedReducer, AddedYourLocationAction>(_addedYourLocation), + new TypedReducer, DeletedYourLocationAction>( + _deletedYourLocation), new TypedReducer, UpdateYourLocationAction>( _updateYourLocation), + new TypedReducer, ToggledSubscriptionAction>(_toggledSubscriptionAction) /* new TypedReducer, ToggleSubscriptionAction>(_toggleSubscriptionAction), new TypedReducer, SubscribeAction>(_setLoadedYourLocations), new TypedReducer, UnSubscribeAction>(_setNoYourLocations), */ ]); -List _addYourLocation( - List yourLocations, AddYourLocationAction action) { +List _addedYourLocation( + List yourLocations, AddedYourLocationAction action) { return new List.from(yourLocations)..add(action.loc); } -List _deleteYourLocation( - List yourLocations, DeleteYourLocationAction action) { +List _deletedYourLocation( + List yourLocations, DeletedYourLocationAction action) { return yourLocations .where((yourLocation) => yourLocation.id != action.id) .toList(); @@ -34,13 +35,13 @@ List _updateYourLocation( yourLocation.id == action.id ? action.loc : yourLocation) .toList(); } -/* -List _toggleSubscriptionAction(List yourLocations, UpdateYourLocationAction action) { + +List _toggledSubscriptionAction(List yourLocations, ToggledSubscriptionAction action) { return yourLocations - .map((yourLocation) => yourLocation.id == action.id ? action.loc : yourLocation) + .map((yourLocation) => yourLocation.id == action.loc.id ? action.loc : yourLocation) .toList(); } - +/* List _setLoadedYourLocations(List yourLocations, YourLocationsLoadedAction action) { return action.yourLocations; }