From 5ecc3c34014e7a0d47dc281d08074331c75807fa Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Thu, 26 Jul 2018 19:13:12 +0200 Subject: [PATCH] Fire notifications --- lib/fireAlert.dart | 9 -- lib/fireNotificationList.dart | 194 +++++++++++++++++++++++ lib/firebaseMessagingConf.dart | 31 +++- lib/firesApp.dart | 2 + lib/generated/i18n.dart | 6 + lib/mainCommon.dart | 2 +- lib/mainDrawer.dart | 11 ++ lib/models/appState.dart | 13 +- lib/models/appState.g.dart | 8 +- lib/models/fireNotification.dart | 59 +++++++ lib/models/fireNotification.g.dart | 54 +++++++ lib/models/fireNotificationsPersist.dart | 36 +++++ lib/redux/actions.dart | 3 +- lib/redux/appActions.dart | 9 ++ lib/redux/appReducer.dart | 3 + lib/redux/fetchDataMiddleware.dart | 38 ++++- lib/redux/fireNotificationActions.dart | 35 ++++ lib/redux/fireNotificationReducer.dart | 28 ++++ lib/redux/reducers.dart | 4 +- pubspec.yaml | 2 +- res/values/strings_en.arb | 4 +- res/values/strings_es.arb | 4 +- 22 files changed, 524 insertions(+), 31 deletions(-) create mode 100644 lib/fireNotificationList.dart create mode 100644 lib/models/fireNotification.dart create mode 100644 lib/models/fireNotification.g.dart create mode 100644 lib/models/fireNotificationsPersist.dart create mode 100644 lib/redux/fireNotificationActions.dart create mode 100644 lib/redux/fireNotificationReducer.dart diff --git a/lib/fireAlert.dart b/lib/fireAlert.dart index c9b947a..2601ff6 100644 --- a/lib/fireAlert.dart +++ b/lib/fireAlert.dart @@ -80,15 +80,6 @@ class _FireAlertState extends State { buildTweetButton() ])), ]); - final stepInc = () { - setState(() { - if (_currentStep < fireSteps.length - 1) { - _currentStep += 1; - } else { - _currentStep = 0; - } - }); - }; return Scaffold( key: _scaffoldKey, appBar: new AppBar(title: new Text(S.of(context).notifyAFire)), diff --git a/lib/fireNotificationList.dart b/lib/fireNotificationList.dart new file mode 100644 index 0000000..c8a5124 --- /dev/null +++ b/lib/fireNotificationList.dart @@ -0,0 +1,194 @@ +import 'dart:async'; + +import 'package:comunes_flutter/comunes_flutter.dart'; +import 'package:fires_flutter/models/fireNotification.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/src/store.dart'; + +import 'customMoment.dart'; +import 'generated/i18n.dart'; +import 'mainDrawer.dart'; +import 'models/appState.dart'; +import 'redux/actions.dart'; +// import 'fireNotificationMap.dart'; + +@immutable +class _ViewModel { + final List fireNotifications; + final TapFireNotificationFunction onTap; + + //final OnReceivedFireNotificationFunction onReceived; + final DeleteFireNotificationFunction onDelete; + final DeleteAllFireNotificationFunction onDeleteAll; + + _ViewModel( + {@required this.onTap, + // @required this.onReceived, + @required this.onDelete, + @required this.onDeleteAll, + @required this.fireNotifications}); + + @override + bool operator ==(Object other) { + return identical(this, other) || + other is _ViewModel && + runtimeType == other.runtimeType && + fireNotifications == other.fireNotifications; + } + + @override + int get hashCode => fireNotifications.hashCode; +} + +class FireNotificationList extends StatefulWidget { + static const String routeName = '/fireNotifications'; + + @override + _FireNotificationListState createState() => _FireNotificationListState(); +} + +class _FireNotificationListState extends State { + final GlobalKey _scaffoldKey = new GlobalKey(); + + Widget _buildRow( + BuildContext context, FireNotification notif, onDeleted, onTap) { + return new ListTile( + dense: true, + leading: const Icon(Icons.whatshot), + title: new Text(notif.description), + subtitle: new Text(Moment.now().from(context, notif.when)), + onLongPress: () { + showSnackMsg(S.of(context).toDeleteThisPlace); + }, + onTap: () { + onTap(notif); + }); + } + + void showSnackMsg(String msg) { + _scaffoldKey.currentState.showSnackBar(new SnackBar( + content: new Text(msg), + )); + } + + Widget _buildSavedFireNotifications(BuildContext context, + List notifList, onDeleted, onTap) { + return new RefreshIndicator( + child: new ListView.builder( + padding: const EdgeInsets.all(16.0), + itemCount: notifList.length, + itemBuilder: (BuildContext _context, int i) { + final ThemeData theme = Theme.of(context); + return new Dismissible( + key: new ObjectKey(notifList.elementAt(i)), + direction: DismissDirection.horizontal, + onDismissed: (DismissDirection direction) { + onDeleted(notifList.elementAt(i)); + }, + background: new Container( + color: theme.primaryColor, + child: const ListTile( + leading: const Icon(Icons.delete, + color: Colors.white, size: 36.0))), + secondaryBackground: new Container( + color: theme.primaryColor, + child: const ListTile( + trailing: const Icon(Icons.delete, + color: Colors.white, size: 36.0))), + child: new Container( + decoration: new BoxDecoration( + color: theme.canvasColor, + border: new Border( + bottom: + new BorderSide(color: theme.dividerColor))), + child: _buildRow( + context, notifList.elementAt(i), onDeleted, onTap))); + }), + onRefresh: _handleRefresh); + } + + Future _handleRefresh() async { + await new Future.delayed(new Duration(seconds: 1)); + + setState(() {}); + + return null; + } + + @override + Widget build(BuildContext context) { + return new StoreConnector( + distinct: true, + converter: (store) { + print('New ViewModel of Fires Notifications'); + return new _ViewModel( + onDeleteAll: () { + store.dispatch(new DeleteAllFireNotificationAction()); + }, + onDelete: (notif) { + store.dispatch(new DeleteFireNotificationAction(notif)); + _scaffoldKey.currentState.showSnackBar(new SnackBar( + content: new Text(S.of(context).youDeletedThisPlace), + action: new SnackBarAction( + label: S.of(context).UNDO, + onPressed: () { + store.dispatch(new AddFireNotificationAction(notif)); + }))); + }, + onTap: (notif) {}, + fireNotifications: store.state.fireNotifications); + }, + builder: (context, view) { + var hasFireNotifications = view.fireNotifications.length > 0; + final title = S.of(context).fireNotificationsTitle; + print('Building Fire Notifications List'); + + return Scaffold( + key: _scaffoldKey, + drawer: new MainDrawer(context), + appBar: new AppBar( + title: Text(title), + leading: IconButton( + icon: Icon(Icons.menu), + onPressed: () { + _scaffoldKey.currentState.openDrawer(); + }, + ), + actions: listWithoutNulls([ + hasFireNotifications + ? IconButton( + icon: Icon(Icons.delete), + onPressed: () => view.onDeleteAll(), + ) + : null + ])), + body: !hasFireNotifications + ? Padding( + padding: const EdgeInsets.all(20.0), + child: new Card( + child: new Padding( + padding: const EdgeInsets.all(20.0), + child: new CenteredColumn(children: [ + new Icon(Icons.notifications_none, + size: 150.0, color: Colors.black26), + new SizedBox(height: 20.0), + new Text( + S.of(context).fireNotificationsDescription, + textAlign: TextAlign.center, + textScaleFactor: 1.1, + style: new TextStyle( + height: 1.5, color: Colors.black45)) + ])))) + : _buildSavedFireNotifications(context, + view.fireNotifications, view.onDelete, view.onTap)); + }); + } + + void gotoLocationMap( + Store store, FireNotification notif, BuildContext context) { + /* store.dispatch(new ShowFireNotificationMapAction(notif)); + Navigator.push(context, + new MaterialPageRoute(builder: (context) => new FireNotificationMap())); */ + } +} diff --git a/lib/firebaseMessagingConf.dart b/lib/firebaseMessagingConf.dart index b4066de..66dc085 100644 --- a/lib/firebaseMessagingConf.dart +++ b/lib/firebaseMessagingConf.dart @@ -1,4 +1,6 @@ +import 'package:bson_objectid/bson_objectid.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:fires_flutter/models/fireNotification.dart'; import 'package:redux/src/store.dart'; import 'models/appState.dart'; @@ -8,17 +10,36 @@ final FirebaseMessaging _firebaseMessaging = new FirebaseMessaging(); // https://pub.dartlang.org/packages/firebase_messaging#-example-tab- void firebaseConfig(Store store) { - _firebaseMessaging.configure(onLaunch: (Map message) { - print('On firebase launch'); - }, onMessage: (Map message) { - print('On firebase message'); + _firebaseMessaging.configure(onMessage: (Map message) { + print("onMessage: $message"); + onMessage(message, store); + //_showItemDialog(message); + }, onLaunch: (Map message) { + print("onLaunch: $message"); + //_navigateToItemDetail(message); + onMessage(message, store); }, onResume: (Map message) { - print('On firebase resume'); + print("onResume: $message"); + //_navigateToItemDetail(message); + onMessage(message, store); }); + getToken(store); } +void onMessage(Map message, Store store) { + FireNotification notif = new FireNotification( + id: new ObjectId.fromHexString(message['id']), + lat: double.parse(message['lat']), + lon: double.parse(message['lon']), + description: message['description'], + when: DateTime.parse(message['when'])); + print(notif); + store.dispatch(new AddFireNotificationAction(notif)); +} + getToken(Store store) async { String token = await _firebaseMessaging.onTokenRefresh.first; + // print(token); store.dispatch(new OnUserTokenAction(token)); } diff --git a/lib/firesApp.dart b/lib/firesApp.dart index 3410fe0..8cea877 100644 --- a/lib/firesApp.dart +++ b/lib/firesApp.dart @@ -15,6 +15,7 @@ import 'theme.dart'; import 'privacyPage.dart'; import 'fireAlert.dart'; import 'supportPage.dart'; +import 'fireNotificationList.dart'; class FiresApp extends StatelessWidget { static final WidgetBuilder introWidget = (context) => new IntroPage(); @@ -28,6 +29,7 @@ class FiresApp extends StatelessWidget { Sandbox.routeName: (BuildContext context) => new Sandbox(), FireAlert.routeName: (BuildContext context) => new FireAlert(), SupportPage.routeName: (BuildContext context) => new SupportPage(), + FireNotificationList.routeName: (BuildContext context) => new FireNotificationList(), }; final Store store; diff --git a/lib/generated/i18n.dart b/lib/generated/i18n.dart index 521b610..8218326 100644 --- a/lib/generated/i18n.dart +++ b/lib/generated/i18n.dart @@ -43,6 +43,8 @@ class S implements WidgetsLocalizations { String get chooseAWatchRadio => "Choose a watch radio"; String get confirm => "Confirm"; String get errorFirePlaceDialog => "We couldn't get the location of the fire"; + String get fireNotificationsDescription => "Here you will receive fire notifications in locations you are subscribed to"; + String get fireNotificationsTitle => "Fire Notifications"; String get firesInTheWorld => "Active fires in the world"; String get firesInYourPlaces => "Active fires in your places"; String get firesNearPlace => "Fires near other place"; @@ -91,6 +93,8 @@ class es extends S { @override String get alertWhenThereIsAFire => "Alerta cuando hay un fuego"; @override + String get fireNotificationsDescription => "Aquí recibirás las notificaciones de fuegos en los lugares a los que te subscribas"; + @override String get anHour => "una hora"; @override String get notifyAFire => "Notificar un fuego"; @@ -131,6 +135,8 @@ class es extends S { @override String get aMinute => "un minuto"; @override + String get fireNotificationsTitle => "Notificaciones de fuegos"; + @override String get firesInYourPlaces => "Fuegos en tus lugares"; @override String get isYourUbicationEnabled => "No podemos saber tu ubicación actual. ¿Están los servicios de ubicación en tu móvil activados?"; diff --git a/lib/mainCommon.dart b/lib/mainCommon.dart index 1cff9d4..5e5c383 100644 --- a/lib/mainCommon.dart +++ b/lib/mainCommon.dart @@ -29,7 +29,7 @@ void mainCommon(List> otherMiddleware) { firesApiKey: secrets['firesApiKey'], firesApiUrl: secrets['firesApiUrl'] + "api/v1/"), middleware: List.from(otherMiddleware) - ..add(fetchYourLocationsMiddleware)); + ..add(fetchDataMiddleware)); injector.map((i) => store.state.firesApiUrl, key: "firesApiUrl"); injector.map((i) => store.state.firesApiKey, key: "firesApiKey"); diff --git a/lib/mainDrawer.dart b/lib/mainDrawer.dart index 3314dc9..8dc2b74 100644 --- a/lib/mainDrawer.dart +++ b/lib/mainDrawer.dart @@ -9,6 +9,8 @@ import 'package:comunes_flutter/comunes_flutter.dart'; import 'privacyPage.dart'; import 'fireAlert.dart'; import 'supportPage.dart'; +import 'fireNotificationList.dart'; +import 'package:community_material_icon/community_material_icon.dart'; class MainDrawer extends Drawer { MainDrawer(BuildContext context, {key}) @@ -63,6 +65,15 @@ Widget mainDrawer(BuildContext context) { Navigator.pushNamed(context, FireAlert.routeName); }, ), + new ListTile( + leading: const Icon(Icons.notifications), + title: new Text(S.of(context).fireNotificationsTitle), + onTap: () { + // Then close the drawer + Navigator.pop(context); + Navigator.pushNamed(context, FireNotificationList.routeName); + }, + ), new Divider(), new ListTile( leading: const Icon(Icons.favorite), diff --git a/lib/models/appState.dart b/lib/models/appState.dart index 4c278d2..70ad8d0 100644 --- a/lib/models/appState.dart +++ b/lib/models/appState.dart @@ -1,5 +1,6 @@ import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:fires_flutter/models/yourLocation.dart'; +import 'package:fires_flutter/models/fireNotification.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; @@ -28,6 +29,7 @@ class AppState extends Object with _$AppStateSerializerMixin { @JsonKey(ignore: true) final String firesApiUrl; final List yourLocations; + final List fireNotifications; @JsonKey(ignore: true) final FireMapState fireMapState; @@ -37,6 +39,7 @@ class AppState extends Object with _$AppStateSerializerMixin { AppState( {this.yourLocations: const [], + this.fireNotifications: const [], this.user: const User.initial(), this.isLoading: false, this.isLoaded: false, @@ -55,6 +58,7 @@ class AppState extends Object with _$AppStateSerializerMixin { String firesApiKey, String firesApiUrl, List yourLocations, + List fireNotifications, FireMapState fireMapState}) { return new AppState( isLoading: isLoading ?? this.isLoading, @@ -65,6 +69,7 @@ class AppState extends Object with _$AppStateSerializerMixin { firesApiKey: firesApiKey ?? this.firesApiKey, firesApiUrl: firesApiUrl ?? this.firesApiUrl, yourLocations: yourLocations ?? this.yourLocations, + fireNotifications: fireNotifications ?? this.fireNotifications, fireMapState: fireMapState ?? this.fireMapState); } @@ -73,24 +78,26 @@ class AppState extends Object with _$AppStateSerializerMixin { return 'AppState{\nuser: ${user}\nisLoading: $isLoading\nisLoaded: $isLoaded\napiKey: ${ellipse( firesApiKey, 8)}\napiUrl: ${ellipse( firesApiUrl, 8)}\nyourLocations count: ${yourLocations - .length}\nyourLocations: ${yourLocations}\nfireMapState: $fireMapState}'; + .length}\nyourLocations: ${yourLocations}\nfireNotifications: ${fireNotifications}\nfireMapState: $fireMapState}'; } } typedef void AddYourLocationFunction(YourLocation loc); typedef void DeleteYourLocationFunction(YourLocation loc); typedef void ToggleSubscriptionFunction(YourLocation loc); - typedef void OnLocationTapFunction(YourLocation loc); typedef void OnSubscribeFunction(YourLocation loc); typedef void OnSubscribeDistanceChangeFunction(YourLocation loc); typedef void OnUnSubscribeFunction(YourLocation loc); typedef void OnSubscribeConfirmedFunction(YourLocation loc); - typedef void OnLocationEdit(YourLocation loc); typedef void OnLocationEditing(YourLocation loc); typedef void OnLocationEditConfirm(YourLocation loc); typedef void OnLocationEditCancel(YourLocation loc); +typedef void TapFireNotificationFunction(FireNotification notif); +// typedef void OnReceivedFireNotificationFunction(FireNotification notif); +typedef void DeleteFireNotificationFunction(FireNotification notif); +typedef void DeleteAllFireNotificationFunction(); // unused // typedef void UpdateYourLocationFunction(ObjectId id, YourLocation loc); diff --git a/lib/models/appState.g.dart b/lib/models/appState.g.dart index 8137ff8..49bd41e 100644 --- a/lib/models/appState.g.dart +++ b/lib/models/appState.g.dart @@ -11,10 +11,14 @@ part of 'appState.dart'; AppState _$AppStateFromJson(Map json) => new AppState( yourLocations: (json['yourLocations'] as List) .map((e) => new YourLocation.fromJson(e as Map)) + .toList(), + fireNotifications: (json['fireNotifications'] as List) + .map((e) => new FireNotification.fromJson(e as Map)) .toList()); abstract class _$AppStateSerializerMixin { List get yourLocations; + List get fireNotifications; Map toJson() => new _$AppStateJsonMapWrapper(this); } @@ -23,7 +27,7 @@ class _$AppStateJsonMapWrapper extends $JsonMapWrapper { _$AppStateJsonMapWrapper(this._v); @override - Iterable get keys => const ['yourLocations']; + Iterable get keys => const ['yourLocations', 'fireNotifications']; @override dynamic operator [](Object key) { @@ -31,6 +35,8 @@ class _$AppStateJsonMapWrapper extends $JsonMapWrapper { switch (key) { case 'yourLocations': return _v.yourLocations; + case 'fireNotifications': + return _v.fireNotifications; } } return null; diff --git a/lib/models/fireNotification.dart b/lib/models/fireNotification.dart new file mode 100644 index 0000000..0c87ce8 --- /dev/null +++ b/lib/models/fireNotification.dart @@ -0,0 +1,59 @@ +import 'package:bson_objectid/bson_objectid.dart'; +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'fireNotification.g.dart'; + +_objectIdFromJson(String json) { + return new ObjectId.fromHexString(json); +} + +_objectIdToJson(ObjectId o) { + return o.toString(); +} + +@JsonSerializable(nullable: false) +class FireNotification extends Object with _$FireNotificationSerializerMixin { + @JsonKey(toJson: _objectIdToJson, fromJson: _objectIdFromJson) + ObjectId id; + final double lat; + final double lon; + final String description; + final DateTime when; + + factory FireNotification.fromJson(Map json) => _$FireNotificationFromJson(json); + + // static FireNotification noLocation = new FireNotification(lat: 0.0, lon: 0.0, description: '', when: new DateTime(0)); + + FireNotification({this.id, @required this.lat, @required this.lon, @required this.description, @required this.when}) { + if (this.id == null) this.id = new ObjectId(); + } + + FireNotification copyWith({id, lat, lon, description, when}) { + return new FireNotification( + id: id ?? this.id, + lat: lat ?? this.lat, + lon: lon ?? this.lon, + description: description ?? this.description, + when: when ?? this.when); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FireNotification && + runtimeType == other.runtimeType && + id == other.id && + lat == other.lat && + lon == other.lon && + description == other.description && + when == other.when; + + @override + int get hashCode => id.hashCode ^ lat.hashCode ^ lon.hashCode ^ description.hashCode ^ when.hashCode; + + @override + String toString() { + return 'FireNotification {id: $id, lat: $lat, lon: $lon, description: $description, when: $when}'; + } +} diff --git a/lib/models/fireNotification.g.dart b/lib/models/fireNotification.g.dart new file mode 100644 index 0000000..b50c3c8 --- /dev/null +++ b/lib/models/fireNotification.g.dart @@ -0,0 +1,54 @@ +// Copyright (c) 2018, Comunes Association. + +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'fireNotification.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +FireNotification _$FireNotificationFromJson(Map json) => + new FireNotification( + id: _objectIdFromJson(json['id'] as String), + lat: (json['lat'] as num).toDouble(), + lon: (json['lon'] as num).toDouble(), + description: json['description'] as String, + when: DateTime.parse(json['when'] as String)); + +abstract class _$FireNotificationSerializerMixin { + ObjectId get id; + double get lat; + double get lon; + String get description; + DateTime get when; + Map toJson() => new _$FireNotificationJsonMapWrapper(this); +} + +class _$FireNotificationJsonMapWrapper extends $JsonMapWrapper { + final _$FireNotificationSerializerMixin _v; + _$FireNotificationJsonMapWrapper(this._v); + + @override + Iterable get keys => + const ['id', 'lat', 'lon', 'description', 'when']; + + @override + dynamic operator [](Object key) { + if (key is String) { + switch (key) { + case 'id': + return _objectIdToJson(_v.id); + case 'lat': + return _v.lat; + case 'lon': + return _v.lon; + case 'description': + return _v.description; + case 'when': + return _v.when.toIso8601String(); + } + } + return null; + } +} diff --git a/lib/models/fireNotificationsPersist.dart b/lib/models/fireNotificationsPersist.dart new file mode 100644 index 0000000..a68ffbe --- /dev/null +++ b/lib/models/fireNotificationsPersist.dart @@ -0,0 +1,36 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:fires_flutter/models/fireNotification.dart'; + +import '../globals.dart' as globals; + +final String fireNotificationKey = 'fireNotifications'; + +Future> loadFireNotifications() async { + return await globals.prefs.then((prefs) { + List FireNotifications = prefs.getStringList(fireNotificationKey); + List persistedList = List(); + if (FireNotifications == null) { + FireNotifications = []; + // first run, init with empty list + persistFireNotifications(persistedList); + } + FireNotifications.forEach((notificationString) { + Map notificationMap = json.decode(notificationString); + persistedList.add(FireNotification.fromJson(notificationMap)); + }); + return persistedList; + }); +} + +persistFireNotifications(List notif) { + print('Persisting $notif'); + globals.prefs.then((prefs) { + List notifAsString = []; + notif.forEach((notification) { + notifAsString.add(json.encode(notification.toJson())); + }); + prefs.setStringList(fireNotificationKey, notifAsString); + }); +} diff --git a/lib/redux/actions.dart b/lib/redux/actions.dart index 385fa38..e689e7f 100644 --- a/lib/redux/actions.dart +++ b/lib/redux/actions.dart @@ -1,3 +1,4 @@ export 'appActions.dart'; export 'yourLocationActions.dart'; -export 'fireMapActions.dart'; \ No newline at end of file +export 'fireMapActions.dart'; +export 'fireNotificationActions.dart'; \ No newline at end of file diff --git a/lib/redux/appActions.dart b/lib/redux/appActions.dart index ecd965b..4b27ff7 100644 --- a/lib/redux/appActions.dart +++ b/lib/redux/appActions.dart @@ -1,4 +1,5 @@ import 'package:fires_flutter/models/yourLocation.dart'; +import 'package:fires_flutter/models/fireNotification.dart'; abstract class AppActions {} @@ -12,6 +13,8 @@ class FetchYourLocationsSucceededAction extends AppActions { FetchYourLocationsSucceededAction(this.fetchedYourLocations); } +class FetchFireNotificationsAction extends AppActions {} + class FetchYourLocationsFailedAction extends AppActions { final Exception error; @@ -35,3 +38,9 @@ class OnUserLangAction extends AppActions { OnUserLangAction(this.lang); } + +class FetchFireNotificationsSucceededAction extends AppActions { + final List fetchedFireNotifications; + + FetchFireNotificationsSucceededAction(this.fetchedFireNotifications); +} \ No newline at end of file diff --git a/lib/redux/appReducer.dart b/lib/redux/appReducer.dart index 0e423a6..108ef0e 100644 --- a/lib/redux/appReducer.dart +++ b/lib/redux/appReducer.dart @@ -5,5 +5,8 @@ AppState appReducer(AppState state, action) { if (action is FetchYourLocationsSucceededAction) { return state.copyWith(yourLocations: action.fetchedYourLocations); } + if (action is FetchFireNotificationsSucceededAction) { + return state.copyWith(fireNotifications: action.fetchedFireNotifications); + } return state; } \ No newline at end of file diff --git a/lib/redux/fetchDataMiddleware.dart b/lib/redux/fetchDataMiddleware.dart index 4eeb2eb..17f0e0c 100644 --- a/lib/redux/fetchDataMiddleware.dart +++ b/lib/redux/fetchDataMiddleware.dart @@ -5,6 +5,7 @@ import 'package:just_debounce_it/just_debounce_it.dart'; import 'package:redux/redux.dart'; import '../models/appState.dart'; +import '../models/fireNotificationsPersist.dart'; import '../models/firesApi.dart'; import '../models/yourLocationPersist.dart'; import 'actions.dart'; @@ -20,8 +21,7 @@ import 'actions.dart'; FiresApi api = Injector.getInjector().get(); -void fetchYourLocationsMiddleware( - Store store, action, NextDispatcher next) { +void fetchDataMiddleware(Store store, action, NextDispatcher next) { // If our Middleware encounters a `FetchYourLocationAction` if (action is OnUserLangAction) { @@ -46,11 +46,10 @@ void fetchYourLocationsMiddleware( if (action is AddYourLocationAction) { if (action.loc.subscribed) { - subscribeViaApi(store, action.loc, - (sub) { - store.dispatch(new AddedYourLocationAction(sub)); - persistYourLocations(store.state.yourLocations); - }); + subscribeViaApi(store, action.loc, (sub) { + store.dispatch(new AddedYourLocationAction(sub)); + persistYourLocations(store.state.yourLocations); + }); } else { // No subscribed (only local) store.dispatch(new AddedYourLocationAction(action.loc)); @@ -69,6 +68,21 @@ void fetchYourLocationsMiddleware( } } + if (action is DeleteFireNotificationAction) { + store.dispatch(new DeletedFireNotificationAction(action.notif)); + persistFireNotifications(store.state.fireNotifications); + } + + if (action is DeleteAllFireNotificationAction) { + store.dispatch(new DeletedAllFireNotificationAction()); + persistFireNotifications(store.state.fireNotifications); + } + + if (action is AddFireNotificationAction) { + store.dispatch(new AddedFireNotificationAction(action.notif)); + persistFireNotifications(store.state.fireNotifications); + } + if (action is ShowYourLocationMapAction) { api .getYourLocationFireStats(store.state, action.loc) @@ -139,6 +153,15 @@ void fetchYourLocationsMiddleware( store.dispatch(new FetchYourLocationsFailedAction(error)); }); } + + if (action is FetchFireNotificationsAction) { + loadFireNotifications().then((fireNotifications) { + store.dispatch( + new FetchFireNotificationsSucceededAction(fireNotifications)); + persistFireNotifications(fireNotifications); + }); + } + // Make sure our actions continue on to the reducer. next(action); } @@ -167,5 +190,6 @@ void createUser(store, lang, token) { api.createUser(store.state, token, lang).then((userId) { store.dispatch(new OnUserCreatedAction(userId)); store.dispatch(new FetchYourLocationsAction()); + store.dispatch(new FetchFireNotificationsAction()); }); } diff --git a/lib/redux/fireNotificationActions.dart b/lib/redux/fireNotificationActions.dart new file mode 100644 index 0000000..18d7209 --- /dev/null +++ b/lib/redux/fireNotificationActions.dart @@ -0,0 +1,35 @@ +import 'package:fires_flutter/models/fireNotification.dart'; + +abstract class FireNotificationActions {} + +class DeleteFireNotificationAction extends FireNotificationActions { + final FireNotification notif; + + DeleteFireNotificationAction(this.notif); +} + +class DeleteAllFireNotificationAction extends FireNotificationActions { + DeleteAllFireNotificationAction(); +} + +class AddFireNotificationAction extends FireNotificationActions { + final FireNotification notif; + + AddFireNotificationAction(this.notif); +} + +class DeletedFireNotificationAction extends FireNotificationActions { + final FireNotification notif; + + DeletedFireNotificationAction(this.notif); +} + +class DeletedAllFireNotificationAction extends FireNotificationActions { + DeletedAllFireNotificationAction(); +} + +class AddedFireNotificationAction extends FireNotificationActions { + final FireNotification notif; + + AddedFireNotificationAction(this.notif); +} diff --git a/lib/redux/fireNotificationReducer.dart b/lib/redux/fireNotificationReducer.dart new file mode 100644 index 0000000..92b675e --- /dev/null +++ b/lib/redux/fireNotificationReducer.dart @@ -0,0 +1,28 @@ +import 'package:redux/redux.dart'; + +import 'actions.dart'; +import 'package:fires_flutter/models/fireNotification.dart'; + +final fireNotificationReducer = combineReducers>([ + new TypedReducer, AddedFireNotificationAction>( + _receivedFireNotification), + new TypedReducer, DeletedFireNotificationAction>( + _deletedFireNotification), + new TypedReducer, DeletedAllFireNotificationAction>( + _deletedAllFireNotifications) +]); + +List _receivedFireNotification( + List yourLocations, AddedFireNotificationAction action) { + return new List.from(yourLocations)..add(action.notif); +} + +List _deletedFireNotification( + List notifications, DeletedFireNotificationAction action) { + return new List.from(notifications)..remove(action.notif); +} + +List _deletedAllFireNotifications( + List notifications, DeletedAllFireNotificationAction action) { + return new List(); +} diff --git a/lib/redux/reducers.dart b/lib/redux/reducers.dart index 2a53994..fb621f1 100644 --- a/lib/redux/reducers.dart +++ b/lib/redux/reducers.dart @@ -7,6 +7,7 @@ import 'userReducer.dart'; import 'yourLocationsReducer.dart'; import 'actions.dart'; import 'appReducer.dart'; +import 'fireNotificationReducer.dart'; // We create the State reducer by combining many smaller reducers into one! AppState appStateReducer(AppState prevState, action) { @@ -14,7 +15,8 @@ AppState appStateReducer(AppState prevState, action) { if (action is AppActions) state = appReducer(prevState, action); return AppState( - yourLocations: yourLocationsReducer(state.yourLocations, action), + yourLocations: yourLocationsReducer(state.yourLocations, action), + fireNotifications: fireNotificationReducer(state.fireNotifications, action), user: userReducer(state.user, action), isLoading: loadingReducer(state.isLoading, action), isLoaded: loadedReducer(state.isLoaded, action), diff --git a/pubspec.yaml b/pubspec.yaml index 85b8361..c8a403f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,7 +52,7 @@ dependencies: flutter_fab_dialer: "^0.0.5" # firebase - firebase_messaging: "^1.0.2" + firebase_messaging: "^1.0.4" # icons # The following adds the Cupertino Icons font to your application. diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index d95217b..23aa8e6 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -51,5 +51,7 @@ "callEmergencyServicesDescription": "You can call 112 if you have not already done so to notify the emergency services.", "tweetAboutAFireTitle": "Tweet about", "tweetAboutAFireDescription": "Additionally if you use twitter you can share additional information with the emergency services, for example, attaching photos to the tweet if you have good visibility of the fire, when you took the photos, exact location, etc. Use #hashtags type #IFMinicipalTerminal for example #IFJumilla (Forest Fire in Jumilla).", - "supportPageDescription": "You can support this initiative, spreading the word, helping us to develop our software, translating this app to your language, etc." + "supportPageDescription": "You can support this initiative, spreading the word, helping us to develop our software, translating this app to your language, etc.", + "fireNotificationsTitle": "Fire Notifications", + "fireNotificationsDescription": "Here you will receive fire notifications in locations you are subscribed to" } \ No newline at end of file diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 7474b30..6d29ba9 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -51,5 +51,7 @@ "callEmergencyServicesDescription": "Puedes llamar al 112 si no lo has hecho ya para avisar a los servicios de emergencia.", "tweetAboutAFireTitle": "Twittea", "tweetAboutAFireDescription": "Adicionalmente si usas twitter puedes compartir información adicional con los servicios de emergencia, por ejemplo, adjuntando fotos al tweet si tienes buena visibilidad del fuego, cuando tomaste las fotos, ubicación exacta, etc. Usa #hashtags tipo #IFTerminoMunicipal por ejemplo #IFJumilla (Incendio Forestal en Jumilla).", - "supportPageDescription": "Puedes apoyar esta iniciativa, dando difusión, ayudándonos a desarrollar nuestro software, traduciendo esta aplicación a tu idioma, etc." + "supportPageDescription": "Puedes apoyar esta iniciativa, dando difusión, ayudándonos a desarrollar nuestro software, traduciendo esta aplicación a tu idioma, etc.", + "fireNotificationsTitle": "Notificaciones de fuegos", + "fireNotificationsDescription": "Aquí recibirás las notificaciones de fuegos en los lugares a los que te subscribas" } \ No newline at end of file