diff --git a/lib/activeFires.dart b/lib/activeFires.dart index 8833093..da04de6 100644 --- a/lib/activeFires.dart +++ b/lib/activeFires.dart @@ -5,17 +5,18 @@ import 'package:fires_flutter/models/yourLocation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_fab_dialer/flutter_fab_dialer.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:redux/src/store.dart'; import 'colors.dart'; import 'generated/i18n.dart'; +import 'genericMap.dart'; import 'globalFiresBottomStats.dart'; import 'locationUtils.dart'; import 'mainDrawer.dart'; import 'models/appState.dart'; import 'placesAutocompleteUtils.dart'; import 'redux/actions.dart'; -import 'genericMap.dart'; @immutable class _ViewModel { @@ -24,35 +25,36 @@ class _ViewModel { final DeleteYourLocationFunction onDelete; final ToggleSubscriptionFunction onToggleSubs; final OnLocationTapFunction onTap; + final bool isLoading; _ViewModel( {@required this.onAdd, @required this.onDelete, @required this.onToggleSubs, @required this.onTap, - @required this.yourLocations}); + @required this.yourLocations, + @required this.isLoading}); @override - bool operator ==(Object other) { - return identical(this, other) || + bool operator ==(Object other) => + identical(this, other) || other is _ViewModel && - runtimeType == other.runtimeType && - yourLocations == other.yourLocations; - } + runtimeType == other.runtimeType && + yourLocations == other.yourLocations && + isLoading == other.isLoading; @override - int get hashCode => yourLocations.hashCode; + int get hashCode => yourLocations.hashCode ^ isLoading.hashCode; } class ActiveFiresPage extends StatefulWidget { -static const String routeName = '/fires'; + static const String routeName = '/fires'; @override _ActiveFiresPageState createState() => _ActiveFiresPageState(); } class _ActiveFiresPageState extends State { - final GlobalKey _scaffoldKey = new GlobalKey(); List _fabMiniMenuItemList( @@ -171,7 +173,8 @@ class _ActiveFiresPageState extends State { onTap: (loc) { gotoLocationMap(store, loc, context); }, - yourLocations: store.state.yourLocations); + yourLocations: store.state.yourLocations, + isLoading: !store.state.isLoaded); }, builder: (context, view) { var hasLocations = view.yourLocations.length > 0; @@ -196,27 +199,29 @@ class _ActiveFiresPageState extends State { floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, bottomNavigationBar: new GlobalFiresBottomStats(), - body: hasLocations - ? new Stack(children: [ - _buildSavedLocations(context, view.yourLocations, - view.onDelete, view.onToggleSubs, view.onTap), - new FabDialer(_fabMiniMenuItemList(context, view.onAdd), - fires600, new Icon(Icons.add)) - ]) - : new Center( - child: new CenteredColumn(children: [ - new RoundedBtn( - icon: Icons.location_searching, - text: S.of(context).addYourCurrentPosition, - onPressed: () => onAddYourLocation(view.onAdd), - backColor: fires600), - const SizedBox(height: 26.0), - new RoundedBtn( - icon: Icons.edit_location, - text: S.of(context).addSomePlace, - onPressed: () => onAddOtherLocation(view.onAdd), - backColor: fires600), - ])), + body: view.isLoading + ? new SpinKitPulse(color: fires600) + : hasLocations + ? new Stack(children: [ + _buildSavedLocations(context, view.yourLocations, + view.onDelete, view.onToggleSubs, view.onTap), + new FabDialer(_fabMiniMenuItemList(context, view.onAdd), + fires600, new Icon(Icons.add)) + ]) + : new Center( + child: new CenteredColumn(children: [ + new RoundedBtn( + icon: Icons.location_searching, + text: S.of(context).addYourCurrentPosition, + onPressed: () => onAddYourLocation(view.onAdd), + backColor: fires600), + const SizedBox(height: 26.0), + new RoundedBtn( + icon: Icons.edit_location, + text: S.of(context).addSomePlace, + onPressed: () => onAddOtherLocation(view.onAdd), + backColor: fires600), + ])), ); }); } @@ -224,8 +229,8 @@ class _ActiveFiresPageState extends State { void gotoLocationMap( Store store, YourLocation loc, BuildContext context) { store.dispatch(new ShowYourLocationMapAction(loc)); - Navigator.push(context, - new MaterialPageRoute(builder: (context) => new genericMap())); + Navigator.push( + context, new MaterialPageRoute(builder: (context) => new genericMap())); } void onAddYourLocation(AddYourLocationFunction onAdd) { diff --git a/lib/fireNotificationList.dart b/lib/fireNotificationList.dart index 89483b6..4113d27 100644 --- a/lib/fireNotificationList.dart +++ b/lib/fireNotificationList.dart @@ -2,43 +2,49 @@ import 'dart:async'; import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:fires_flutter/models/fireNotification.dart'; +import 'package:fires_flutter/models/yourLocation.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 'genericMap.dart'; import 'mainDrawer.dart'; import 'models/appState.dart'; import 'redux/actions.dart'; -import 'genericMap.dart'; @immutable class _ViewModel { final List fireNotifications; + final int fireNotificationsUnread; + final List yourLocations; 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}); + @required this.fireNotifications, + @required this.yourLocations, + @required this.fireNotificationsUnread}); @override - bool operator ==(Object other) { - return identical(this, other) || - other is _ViewModel && - runtimeType == other.runtimeType && - fireNotifications == other.fireNotifications; - } + bool operator ==(Object other) => + identical(this, other) || + other is _ViewModel && + runtimeType == other.runtimeType && + fireNotifications == other.fireNotifications && + fireNotificationsUnread == other.fireNotificationsUnread && + yourLocations == other.yourLocations; @override - int get hashCode => fireNotifications.hashCode; + int get hashCode => + fireNotifications.hashCode ^ + fireNotificationsUnread.hashCode ^ + yourLocations.hashCode; } class FireNotificationList extends StatefulWidget { @@ -51,12 +57,13 @@ class FireNotificationList extends StatefulWidget { class _FireNotificationListState extends State { final GlobalKey _scaffoldKey = new GlobalKey(); - Widget _buildRow( - BuildContext context, FireNotification notif, onDeleted, onTap) { + Widget _buildRow(BuildContext context, List yourLocations, + FireNotification notif, onDeleted, onTap) { + YourLocation yl = yourLocations.singleWhere((yl) => yl.id == notif.subsId); return new ListTile( dense: true, leading: const Icon(Icons.whatshot), - title: new Text(notif.description, + title: new Text('${yl.description}. ${notif.description}', style: new TextStyle( fontWeight: notif.read ? FontWeight.normal : FontWeight.bold)), subtitle: new Text(Moment.now().from(context, notif.when)), @@ -74,13 +81,17 @@ class _FireNotificationListState extends State { )); } - Widget _buildSavedFireNotifications(BuildContext context, - List notifList, onDeleted, onTap) { + Widget _buildSavedFireNotifications( + BuildContext context, + List yourLocations, + List notifList, + onDeleted, + onTap) { return new RefreshIndicator( child: new ListView.builder( padding: const EdgeInsets.all(16.0), - reverse: true, - shrinkWrap: true, + // reverse: true, + // shrinkWrap: true, itemCount: notifList.length, itemBuilder: (BuildContext _context, int i) { final ThemeData theme = Theme.of(context); @@ -106,8 +117,8 @@ class _FireNotificationListState extends State { border: new Border( bottom: new BorderSide(color: theme.dividerColor))), - child: _buildRow( - context, notifList.elementAt(i), onDeleted, onTap))); + child: _buildRow(context, yourLocations, + notifList.elementAt(i), onDeleted, onTap))); }), onRefresh: _handleRefresh); } @@ -145,11 +156,13 @@ class _FireNotificationListState extends State { store.dispatch(new ReadFireNotificationAction( notif.copyWith(read: true))); } - new Timer(new Duration(milliseconds: 1000), () { + new Timer(new Duration(milliseconds: 500), () { gotoMap(store, notif, context); }); }, - fireNotifications: store.state.fireNotifications); + yourLocations: store.state.yourLocations, + fireNotifications: store.state.fireNotifications, + fireNotificationsUnread: store.state.fireNotificationsUnread); }, builder: (context, view) { var hasFireNotifications = view.fireNotifications.length > 0; @@ -191,16 +204,16 @@ class _FireNotificationListState extends State { style: new TextStyle( height: 1.3, color: Colors.black45)) ])))) - : _buildSavedFireNotifications(context, + : _buildSavedFireNotifications(context, view.yourLocations, view.fireNotifications, view.onDelete, view.onTap)); }); } void gotoMap( Store store, FireNotification notif, BuildContext context) { - store.dispatch(new ShowFireNotificationMapAction(notif)); - Navigator.push(context, - new MaterialPageRoute(builder: (context) => new genericMap())); + store.dispatch(new ShowFireNotificationMapAction(notif)); + Navigator.push( + context, new MaterialPageRoute(builder: (context) => new genericMap())); } _showConfirmDialog(_ViewModel view) { diff --git a/lib/firebaseMessagingConf.dart b/lib/firebaseMessagingConf.dart deleted file mode 100644 index 9f31d3c..0000000 --- a/lib/firebaseMessagingConf.dart +++ /dev/null @@ -1,46 +0,0 @@ -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'; -import 'redux/actions.dart'; - -final FirebaseMessaging _firebaseMessaging = new FirebaseMessaging(); - -// https://pub.dartlang.org/packages/firebase_messaging#-example-tab- -void firebaseConfig(Store store) { - _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("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'], - read: false, - 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 8cea877..74eb27a 100644 --- a/lib/firesApp.dart +++ b/lib/firesApp.dart @@ -1,23 +1,38 @@ +import 'package:bson_objectid/bson_objectid.dart'; import 'package:comunes_flutter/comunes_flutter.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:fires_flutter/models/fireNotification.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; +import 'package:redux/src/store.dart'; import 'activeFires.dart'; +import 'fireAlert.dart'; +import 'fireNotificationList.dart'; import 'generated/i18n.dart'; import 'homePage.dart'; import 'introPage.dart'; import 'models/appState.dart'; +import 'privacyPage.dart'; import 'redux/actions.dart'; import 'sandbox.dart'; -import 'theme.dart'; -import 'privacyPage.dart'; -import 'fireAlert.dart'; import 'supportPage.dart'; -import 'fireNotificationList.dart'; +import 'theme.dart'; -class FiresApp extends StatelessWidget { +class FiresApp extends StatefulWidget { + FiresApp(this.store); + + final Store store; + + @override + _FiresAppState createState() => _FiresAppState(store); +} + +class _FiresAppState extends State { + final GlobalKey navigatorKey = + new GlobalKey(); static final WidgetBuilder introWidget = (context) => new IntroPage(); static final WidgetBuilder continueWidget = (context) => new HomePage(); @@ -29,22 +44,23 @@ 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(), + FireNotificationList.routeName: (BuildContext context) => + new FireNotificationList(), }; final Store store; // globals.getIt.registerSingleton - FiresApp(this.store); + _FiresAppState(this.store); @override Widget build(BuildContext context) { StatefulWidget home = new MaterialAppWithIntroHome( introWidget, continueWidget, 'showInitialWizard-2018-06-27-01'); return new StoreProvider( - store: this.store, child: new MaterialApp( + navigatorKey: navigatorKey, localizationsDelegates: [ S.delegate, GlobalMaterialLocalizations.delegate, @@ -65,4 +81,97 @@ class FiresApp extends StatelessWidget { theme: firesTheme, routes: routes)); } + + @override + void initState() { + super.initState(); + _firebaseMessaging.configure(onMessage: (Map message) { + print("onMessage in fireApp: $message"); + _showItemDialog(message, store); + }, onLaunch: (Map message) { + print("onLaunch: $message"); + _navigateToItemDetail(message, store); + }, onResume: (Map message) { + print("onResume: $message"); + _navigateToItemDetail(message, store); + }); + _firebaseMessaging.requestNotificationPermissions( + const IosNotificationSettings(sound: true, badge: true, alert: true)); + _firebaseMessaging.onIosSettingsRegistered + .listen((IosNotificationSettings settings) { + print("Settings registered: $settings"); + }); + _firebaseMessaging.getToken().then((String token) { + assert(token != null); + store.dispatch(new OnUserTokenAction(token)); + setState(() {}); + }); + } + + void _showItemDialog(Map message, Store store) { + final notif = _notifForMessage(message); + showDialog( + context: navigatorKey.currentContext, + builder: (_) => _buildDialog(navigatorKey.currentContext, notif), + ).then((bool shouldNavigate) { + if (shouldNavigate == true) { + _navigateToItemDetail(message, store); + } + }).catchError((e) => print("$e")); + } + + Widget _buildDialog(BuildContext context, FireNotification item) { + return new AlertDialog( + content: new Text(item.description), + actions: [ + new FlatButton( + child: Text(S.of(context).CLOSE), + onPressed: () { + Navigator.pop(context, false); + }, + ), + new FlatButton( + child: Text(S.of(context).SHOW), + onPressed: () { + Navigator.pop(context, true); + }, + ), + ], + ); + } + + void _navigateToItemDetail(Map message, Store store) { + FireNotification notif = _notifForMessage(message); + // Clear away dialogs + Navigator.popUntil(navigatorKey.currentContext, + (Route route) => route is PageRoute); + if (!notif.getRoute(store).isCurrent) { + Navigator.push(navigatorKey.currentContext, notif.getRoute(store)); + } + } + + // https://pub.dartlang.org/packages/firebase_messaging#-example-tab- + final FirebaseMessaging _firebaseMessaging = new FirebaseMessaging(); + + FireNotification _notifForMessage(Map message) { + FireNotification notif; + print('start parse notif'); + try { + notif = new FireNotification( + id: new ObjectId.fromHexString(message['id']), + lat: double.parse(message['lat']), + lon: double.parse(message['lon']), + description: message['description'], + read: false, + when: DateTime.parse(message['when']), + sealed: message['sealed'], + subsId: new ObjectId.fromHexString(message['subsId'])); + print('end parse notif'); + debugPrint(notif.toString()); + } catch (e) { + debugPrint(e.toString()); + } + store.dispatch(new AddFireNotificationAction(notif)); + return notif; + } } diff --git a/lib/generated/i18n.dart b/lib/generated/i18n.dart index 804f286..9b0c5bd 100644 --- a/lib/generated/i18n.dart +++ b/lib/generated/i18n.dart @@ -23,8 +23,10 @@ class S implements WidgetsLocalizations { String get AvoidThisStringsIfisNotPlural => "Zero One Two Few Many Other"; String get CANCEL => "CANCEL"; + String get CLOSE => "CLOSE"; String get DELETE => "DELETE"; String get SAVE => "SAVE"; + String get SHOW => "SHOW"; String get UNDO => "UNDO"; String get aDay => "a day"; String get aF3wSeconds => "a few seconds"; @@ -111,6 +113,8 @@ class es extends S { @override String get callEmergencyServicesDescription => "Deberías llamar al 112 si no lo has hecho ya para avisar a los servicios de emergencia."; @override + String get CLOSE => "CERRAR"; + @override String get errorFirePlaceDialog => "No hemos podido obtener la ubicación del fuego"; @override String get toDeleteThisPlace => "Desliza horizontalmente para borrar este lugar"; @@ -163,6 +167,8 @@ class es extends S { @override String get SAVE => "GUARDAR"; @override + String get SHOW => "MOSTRAR"; + @override String get addedThisLocation => "Ya has añadido este lugar antes"; @override String get typeTheNameOfAPlace => "Escribe el nombre de un lugar, zona, etc"; diff --git a/lib/genericMap.dart b/lib/genericMap.dart index ec85fd1..4d413ed 100644 --- a/lib/genericMap.dart +++ b/lib/genericMap.dart @@ -1,7 +1,6 @@ import 'dart:core'; import 'package:comunes_flutter/comunes_flutter.dart'; -import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:fires_flutter/models/basicLocation.dart'; import 'package:fires_flutter/models/yourLocation.dart'; import 'package:flutter/material.dart'; @@ -9,6 +8,7 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/plugin_api.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:latlong/latlong.dart'; +import 'package:share/share.dart'; import 'colors.dart'; import 'dummyMapPlugin.dart'; @@ -25,6 +25,7 @@ import 'zoomMapPlugin.dart'; @immutable class _ViewModel { + final String serverUrl; final FireMapState mapState; final OnSubscribeFunction onSubs; final OnSubscribeConfirmedFunction onSubsConfirmed; @@ -37,6 +38,7 @@ class _ViewModel { _ViewModel( {@required this.mapState, + @required this.serverUrl, @required this.onSubs, @required this.onSubsConfirmed, @required this.onUnSubs, @@ -66,7 +68,7 @@ class _genericMapState extends State { // This needs to be stateful so when resizes don't get a new globalkey // https://github.com/flutter/flutter/issues/1632#issuecomment-180478202 final GlobalKey _scaffoldKey = new GlobalKey(); - final FirebaseMessaging _firebaseMessaging = new FirebaseMessaging(); + YourLocation _location; YourLocation _initialLocation; @@ -105,6 +107,7 @@ class _genericMapState extends State { store.dispatch(new UpdateYourLocationMapAction(loc)); store.dispatch(new EditConfirmYourLocationAction(loc)); }, + serverUrl: store.state.firesApiUrl, mapState: store.state.fireMapState); }, builder: (context, view) { @@ -223,8 +226,6 @@ class _genericMapState extends State { break; case FireMapStatus.subscriptionConfirm: view.onSubsConfirmed(_location); - // IOS specific - _firebaseMessaging.requestNotificationPermissions(); break; case FireMapStatus.unsubscribe: view.onUnSubs(_location); @@ -299,6 +300,14 @@ class _genericMapState extends State { icon: new Icon(Icons.save), onPressed: () => view.onEditConfirm(_location)) ]; + case FireMapStatus.viewFireNotification: + return [ + new IconButton( + icon: new Icon(Icons.share), + onPressed: () { + Share.share('${view.mapState.fireNotification.description}. ${view.serverUrl}fire/${view.mapState.fireNotification.sealed}'); + }) + ]; default: return []; } diff --git a/lib/mainCommon.dart b/lib/mainCommon.dart index 5e5c383..fc8ef40 100644 --- a/lib/mainCommon.dart +++ b/lib/mainCommon.dart @@ -7,7 +7,6 @@ import 'package:flutter_simple_dependency_injection/injector.dart'; import 'package:redux/redux.dart'; import 'package:sentry/sentry.dart'; -import 'firebaseMessagingConf.dart'; import 'firesApp.dart'; import 'globals.dart' as globals; import 'models/appState.dart'; @@ -27,6 +26,7 @@ void mainCommon(List> otherMiddleware) { initialState: new AppState( gmapKey: secrets['gmapKey'], firesApiKey: secrets['firesApiKey'], + serverUrl: secrets['firesApiUrl'], firesApiUrl: secrets['firesApiUrl'] + "api/v1/"), middleware: List.from(otherMiddleware) ..add(fetchDataMiddleware)); @@ -37,8 +37,6 @@ void mainCommon(List> otherMiddleware) { VoidCallback mainFn = () { loadYourLocations().then((yl) { - firebaseConfig(store); - // Run baby run! runApp(new FiresApp(store)); }); diff --git a/lib/mainDev.dart b/lib/mainDev.dart index 78d6d8a..7abc142 100644 --- a/lib/mainDev.dart +++ b/lib/mainDev.dart @@ -17,7 +17,7 @@ void main() { return ">>>>> ${action.toString().replaceAll('Instance of ', '')}"; } - LogLevel logRedux = LogLevel.actions; + LogLevel logRedux = LogLevel.full; List devMiddlewares = logRedux == LogLevel.none ? [] diff --git a/lib/mainDrawer.dart b/lib/mainDrawer.dart index 5eaa907..ca1ff46 100644 --- a/lib/mainDrawer.dart +++ b/lib/mainDrawer.dart @@ -1,16 +1,38 @@ -import 'package:flutter/material.dart'; - -import 'colors.dart'; -import 'sandbox.dart'; -import 'activeFires.dart'; -import 'globals.dart' as globals; -import 'generated/i18n.dart'; +import 'package:badge/badge.dart'; import 'package:comunes_flutter/comunes_flutter.dart'; -import 'privacyPage.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; + +import 'activeFires.dart'; +import 'colors.dart'; import 'fireAlert.dart'; -import 'supportPage.dart'; import 'fireNotificationList.dart'; -import 'package:community_material_icon/community_material_icon.dart'; +import 'generated/i18n.dart'; +import 'globals.dart' as globals; +import 'models/appState.dart'; +import 'privacyPage.dart'; +import 'sandbox.dart'; +import 'supportPage.dart'; + +@immutable +class _ViewModel { + final int unreadCount; + + _ViewModel({ + @required this.unreadCount, + }); + + @override + bool operator ==(Object other) { + return identical(this, other) || + other is _ViewModel && + runtimeType == other.runtimeType && + unreadCount == other.unreadCount; + } + + @override + int get hashCode => unreadCount.hashCode; +} class MainDrawer extends Drawer { MainDrawer(BuildContext context, {key}) @@ -18,101 +40,115 @@ class MainDrawer extends Drawer { } Widget mainDrawer(BuildContext context) { - return new ListView( - // Important: Remove any padding from the ListView. - padding: EdgeInsets.zero, - children: listWithoutNulls([ - new GestureDetector( - onTap: () { - Navigator.pop(context); - Navigator.pushNamed(context, '/'); - }, - child: new DrawerHeader( - child: new Column( - children: [ - new Image.asset( - 'images/logo-200.png', - fit: BoxFit.scaleDown, - height: 80.0, + return new StoreConnector( + distinct: true, + converter: (store) { + return new _ViewModel(unreadCount: store.state.fireNotificationsUnread); + }, + builder: (context, view) { + return new ListView( + // Important: Remove any padding from the ListView. + padding: EdgeInsets.zero, + children: listWithoutNulls([ + new GestureDetector( + onTap: () { + Navigator.pop(context); + Navigator.pushNamed(context, '/'); + }, + child: new DrawerHeader( + child: new Column( + children: [ + new Image.asset( + 'images/logo-200.png', + fit: BoxFit.scaleDown, + height: 80.0, + ), + const SizedBox(height: 20.0), + new Text(S.of(context).appName, + style: new TextStyle( + fontSize: 24.0, + color: Colors.white, + )), + ], + ), + decoration: new BoxDecoration( + color: fires300, + ), + ), ), - const SizedBox(height: 20.0), - new Text(S.of(context).appName, - style: new TextStyle( - fontSize: 24.0, - color: Colors.white, - )), - ], - ), - decoration: new BoxDecoration( - color: fires300, - ), - ), - ), - new ListTile( - leading: const Icon(Icons.whatshot), - title: new Text(S.of(context).activeFires), - onTap: () { - Navigator.pop(context); - Navigator.pushNamed(context, ActiveFiresPage.routeName); - }, - ), - new ListTile( - leading: const Icon(Icons.notifications_active), - title: new Text(S.of(context).notifyAFire), - onTap: () { - // Then close the drawer - Navigator.pop(context); - Navigator.pushNamed(context, FireAlert.routeName); - }, - ), - new ListTile( - leading: const Icon(Icons.notifications), - title: new Text(S.of(context).fireNotificationsTitleShort), - onTap: () { - // Then close the drawer - Navigator.pop(context); - Navigator.pushNamed(context, FireNotificationList.routeName); - }, - ), - new Divider(), - new ListTile( - leading: const Icon(Icons.favorite), - title: new Text(S.of(context).supportThisInitiative), - onTap: () { - // Then close the drawer - Navigator.pop(context); - Navigator.pushNamed(context, SupportPage.routeName); - }, - ), - new ListTile( - leading: const Icon(Icons.lock), - title: new Text(S.of(context).privacyPolicy), - onTap: () { - // Then close the drawer - Navigator.pop(context); - Navigator.pushNamed(context, PrivacyPage.routeName); - }, - ), - globals.isDevelopment ? - new ListTile( - leading: const Icon(Icons.bug_report), - title: new Text('Sandbox'), - onTap: () { - Navigator.pop(context); - Navigator.pushNamed(context, Sandbox.routeName); - }, - ): null, - new AboutListTile( - icon: globals.appIcon, - applicationName: S.of(context).appName, - applicationVersion: globals.appVersion, - applicationIcon: globals.appMediumIcon, - applicationLegalese: S.of(context).appLicense(DateTime.now().year.toString()), - aboutBoxChildren: [ - // new Text('What?') - ] - // FIXME - ) - ]) - ); + new ListTile( + leading: const Icon(Icons.whatshot), + title: new Text(S.of(context).activeFires), + onTap: () { + Navigator.pop(context); + Navigator.pushNamed(context, ActiveFiresPage.routeName); + }, + ), + new ListTile( + leading: const Icon(Icons.notifications_active), + title: new Text(S.of(context).notifyAFire), + onTap: () { + // Then close the drawer + Navigator.pop(context); + Navigator.pushNamed(context, FireAlert.routeName); + }, + ), + new ListTile( + leading: const Icon(Icons.notifications), + title: view.unreadCount > 0 + ? Badge.after( + spacing: 5.0, + borderColor: Colors.red, + child: Text(S.of(context).fireNotificationsTitleShort), + value: ' ${view.unreadCount.toString()} ') + : Text(S.of(context).fireNotificationsTitleShort), + onTap: () { + // Then close the drawer + Navigator.pop(context); + Navigator.pushNamed(context, FireNotificationList.routeName); + }, + ), + new Divider(), + new ListTile( + leading: const Icon(Icons.favorite), + title: new Text(S.of(context).supportThisInitiative), + onTap: () { + // Then close the drawer + Navigator.pop(context); + Navigator.pushNamed(context, SupportPage.routeName); + }, + ), + new ListTile( + leading: const Icon(Icons.lock), + title: new Text(S.of(context).privacyPolicy), + onTap: () { + // Then close the drawer + Navigator.pop(context); + Navigator.pushNamed(context, PrivacyPage.routeName); + }, + ), + globals.isDevelopment + ? new ListTile( + leading: const Icon(Icons.bug_report), + title: new Text('Sandbox'), + onTap: () { + Navigator.pop(context); + Navigator.pushNamed(context, Sandbox.routeName); + }, + ) + : null, + new AboutListTile( + icon: globals.appIcon, + applicationName: S.of(context).appName, + applicationVersion: globals.appVersion, + applicationIcon: globals.appMediumIcon, + applicationLegalese: + S.of(context).appLicense(DateTime.now().year.toString()), + aboutBoxChildren: [ + // new Text('What?') + ] + // FIXME + ) + ])); + }); } diff --git a/lib/models/appState.dart b/lib/models/appState.dart index 70ad8d0..107c108 100644 --- a/lib/models/appState.dart +++ b/lib/models/appState.dart @@ -1,6 +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:fires_flutter/models/yourLocation.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; @@ -25,12 +25,16 @@ class AppState extends Object with _$AppStateSerializerMixin { @JsonKey(ignore: true) final String gmapKey; @JsonKey(ignore: true) + final String serverUrl; + @JsonKey(ignore: true) final String firesApiKey; @JsonKey(ignore: true) final String firesApiUrl; final List yourLocations; final List fireNotifications; @JsonKey(ignore: true) + final int fireNotificationsUnread; + @JsonKey(ignore: true) final FireMapState fireMapState; @JsonKey(ignore: true) @@ -40,13 +44,15 @@ class AppState extends Object with _$AppStateSerializerMixin { AppState( {this.yourLocations: const [], this.fireNotifications: const [], + this.fireNotificationsUnread: 0, this.user: const User.initial(), this.isLoading: false, this.isLoaded: false, this.error: null, this.gmapKey, this.firesApiKey, - this.firesApiUrl, + this.firesApiUrl, + this.serverUrl, this.fireMapState: const FireMapState.initial()}); AppState copyWith( @@ -55,10 +61,12 @@ class AppState extends Object with _$AppStateSerializerMixin { String user, String error, String gmapKey, - String firesApiKey, + String firesApiKey, + String serverUrl, String firesApiUrl, List yourLocations, - List fireNotifications, + List fireNotifications, + int fireNotificationsUnread, FireMapState fireMapState}) { return new AppState( isLoading: isLoading ?? this.isLoading, @@ -67,9 +75,12 @@ class AppState extends Object with _$AppStateSerializerMixin { error: error ?? this.error, gmapKey: gmapKey ?? this.gmapKey, firesApiKey: firesApiKey ?? this.firesApiKey, - firesApiUrl: firesApiUrl ?? this.firesApiUrl, + firesApiUrl: firesApiUrl ?? this.firesApiUrl, + serverUrl: serverUrl ?? this.serverUrl, yourLocations: yourLocations ?? this.yourLocations, fireNotifications: fireNotifications ?? this.fireNotifications, + fireNotificationsUnread: + fireNotificationsUnread ?? this.fireNotificationsUnread, fireMapState: fireMapState ?? this.fireMapState); } @@ -77,8 +88,8 @@ class AppState extends Object with _$AppStateSerializerMixin { String toString() { return 'AppState{\nuser: ${user}\nisLoading: $isLoading\nisLoaded: $isLoaded\napiKey: ${ellipse( firesApiKey, 8)}\napiUrl: ${ellipse( - firesApiUrl, 8)}\nyourLocations count: ${yourLocations - .length}\nyourLocations: ${yourLocations}\nfireNotifications: ${fireNotifications}\nfireMapState: $fireMapState}'; + firesApiUrl, 8)}\nserverUrl: ${serverUrl}\nfireNotifications: ${fireNotifications}\nyourLocations count: ${yourLocations + .length}\nunread notif: ${fireNotificationsUnread}\nyourLocations: ${yourLocations}\nfireMapState: $fireMapState}'; } } diff --git a/lib/models/fireNotification.dart b/lib/models/fireNotification.dart index 12062c1..0ebbf2f 100644 --- a/lib/models/fireNotification.dart +++ b/lib/models/fireNotification.dart @@ -1,40 +1,53 @@ import 'package:bson_objectid/bson_objectid.dart'; +import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:redux/src/store.dart'; + +import '../genericMap.dart'; +import '../objectIdUtils.dart'; +import '../redux/actions.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) + @JsonKey(toJson: objectIdToJson, fromJson: objectIdFromJson) ObjectId id; final double lat; final double lon; final String description; final DateTime when; + final String sealed; + @JsonKey(toJson: objectIdToJson, fromJson: objectIdFromJson) + final ObjectId subsId; final bool read; - factory FireNotification.fromJson(Map json) => _$FireNotificationFromJson(json); + factory FireNotification.fromJson(Map json) => + _$FireNotificationFromJson(json); - FireNotification({this.id, @required this.lat, @required this.lon, @required this.description, @required this.when, @required this.read}) { + FireNotification( + {this.id, + @required this.lat, + @required this.lon, + @required this.description, + @required this.when, + @required this.read, + @required this.sealed, + @required this.subsId}) { if (this.id == null) this.id = new ObjectId(); } - FireNotification copyWith({id, lat, lon, description, read, when}) { + FireNotification copyWith( + {id, lat, lon, description, read, when, sealed, subsId}) { return new FireNotification( id: id ?? this.id, lat: lat ?? this.lat, - lon: lon ?? this.lon, - read: read ?? this.read, + lon: lon ?? this.lon, + read: read ?? this.read, description: description ?? this.description, + sealed: sealed ?? this.sealed, + subsId: subsId ?? this.subsId, when: when ?? this.when); } @@ -45,16 +58,46 @@ class FireNotification extends Object with _$FireNotificationSerializerMixin { runtimeType == other.runtimeType && id == other.id && lat == other.lat && - lon == other.lon && - read == other.read && + lon == other.lon && + read == other.read && description == other.description && + sealed == other.sealed && + subsId == other.subsId && when == other.when; @override - int get hashCode => id.hashCode ^ lat.hashCode ^ lon.hashCode ^ description.hashCode ^ when.hashCode ^ read.hashCode; + int get hashCode => + id.hashCode ^ + lat.hashCode ^ + lon.hashCode ^ + description.hashCode ^ + when.hashCode ^ + read.hashCode ^ + sealed.hashCode ^ + subsId.hashCode; @override String toString() { - return 'FireNotification {id: $id, lat: $lat, lon: $lon, description: $description, when: $when, read: $read}'; + return 'FireNotification {id: $id, lat: $lat, lon: $lon, when: $when, read: $read, subsId: $subsId, sealed ${ellipse( + sealed)}'; + } + + static final Map> routes = >{}; + + Route getRoute(Store store) { + final String routeName = '/fire/${id}'; + return routes.putIfAbsent( + routeName, + () => new MaterialPageRoute( + settings: new RouteSettings(name: routeName), + builder: (BuildContext context) { + store.dispatch(new ShowFireNotificationMapAction(this)); + Navigator.push( + context, + new MaterialPageRoute( + builder: (context) => new genericMap())); + }, + ), + ); } } diff --git a/lib/models/fireNotification.g.dart b/lib/models/fireNotification.g.dart index 14b113c..30afb18 100644 --- a/lib/models/fireNotification.g.dart +++ b/lib/models/fireNotification.g.dart @@ -10,12 +10,14 @@ part of 'fireNotification.dart'; FireNotification _$FireNotificationFromJson(Map json) => new FireNotification( - id: _objectIdFromJson(json['id'] as String), + 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), - read: json['read'] as bool); + read: json['read'] as bool, + sealed: json['sealed'] as String, + subsId: objectIdFromJson(json['subsId'] as String)); abstract class _$FireNotificationSerializerMixin { ObjectId get id; @@ -23,6 +25,8 @@ abstract class _$FireNotificationSerializerMixin { double get lon; String get description; DateTime get when; + String get sealed; + ObjectId get subsId; bool get read; Map toJson() => new _$FireNotificationJsonMapWrapper(this); } @@ -32,15 +36,23 @@ class _$FireNotificationJsonMapWrapper extends $JsonMapWrapper { _$FireNotificationJsonMapWrapper(this._v); @override - Iterable get keys => - const ['id', 'lat', 'lon', 'description', 'when', 'read']; + Iterable get keys => const [ + 'id', + 'lat', + 'lon', + 'description', + 'when', + 'sealed', + 'subsId', + 'read' + ]; @override dynamic operator [](Object key) { if (key is String) { switch (key) { case 'id': - return _objectIdToJson(_v.id); + return objectIdToJson(_v.id); case 'lat': return _v.lat; case 'lon': @@ -49,6 +61,10 @@ class _$FireNotificationJsonMapWrapper extends $JsonMapWrapper { return _v.description; case 'when': return _v.when.toIso8601String(); + case 'sealed': + return _v.sealed; + case 'subsId': + return objectIdToJson(_v.subsId); case 'read': return _v.read; } diff --git a/lib/models/yourLocation.dart b/lib/models/yourLocation.dart index 34e8fb1..b3354ba 100644 --- a/lib/models/yourLocation.dart +++ b/lib/models/yourLocation.dart @@ -1,20 +1,15 @@ import 'package:bson_objectid/bson_objectid.dart'; import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:fires_flutter/objectIdUtils.dart'; part 'yourLocation.g.dart'; -_objectIdFromJson(String json) { - return new ObjectId.fromHexString(json); -} -_objectIdToJson(ObjectId o) { - return o.toString(); -} @JsonSerializable(nullable: false) class YourLocation extends Object with _$YourLocationSerializerMixin { - @JsonKey(toJson: _objectIdToJson, fromJson: _objectIdFromJson) + @JsonKey(toJson: objectIdToJson, fromJson: objectIdFromJson) ObjectId id; final double lat; final double lon; diff --git a/lib/models/yourLocation.g.dart b/lib/models/yourLocation.g.dart index b6cb7dd..5313b32 100644 --- a/lib/models/yourLocation.g.dart +++ b/lib/models/yourLocation.g.dart @@ -10,7 +10,7 @@ part of 'yourLocation.dart'; YourLocation _$YourLocationFromJson(Map json) => new YourLocation( - id: _objectIdFromJson(json['id'] as String), + id: objectIdFromJson(json['id'] as String), lat: (json['lat'] as num).toDouble(), lon: (json['lon'] as num).toDouble(), description: json['description'] as String, @@ -40,7 +40,7 @@ class _$YourLocationJsonMapWrapper extends $JsonMapWrapper { if (key is String) { switch (key) { case 'id': - return _objectIdToJson(_v.id); + return objectIdToJson(_v.id); case 'lat': return _v.lat; case 'lon': diff --git a/lib/objectIdUtils.dart b/lib/objectIdUtils.dart new file mode 100644 index 0000000..89d398f --- /dev/null +++ b/lib/objectIdUtils.dart @@ -0,0 +1,9 @@ +import 'package:bson_objectid/bson_objectid.dart'; + +objectIdFromJson(String json) { + return new ObjectId.fromHexString(json); +} + +objectIdToJson(ObjectId o) { + return o.toString(); +} \ No newline at end of file diff --git a/lib/redux/appActions.dart b/lib/redux/appActions.dart index 4b27ff7..ba31c32 100644 --- a/lib/redux/appActions.dart +++ b/lib/redux/appActions.dart @@ -41,6 +41,7 @@ class OnUserLangAction extends AppActions { class FetchFireNotificationsSucceededAction extends AppActions { final List fetchedFireNotifications; + final int unreadCount; - FetchFireNotificationsSucceededAction(this.fetchedFireNotifications); + FetchFireNotificationsSucceededAction(this.fetchedFireNotifications, this.unreadCount); } \ No newline at end of file diff --git a/lib/redux/appReducer.dart b/lib/redux/appReducer.dart index 108ef0e..8a4ec42 100644 --- a/lib/redux/appReducer.dart +++ b/lib/redux/appReducer.dart @@ -6,7 +6,16 @@ AppState appReducer(AppState state, action) { return state.copyWith(yourLocations: action.fetchedYourLocations); } if (action is FetchFireNotificationsSucceededAction) { - return state.copyWith(fireNotifications: action.fetchedFireNotifications); + return state.copyWith(fireNotifications: action.fetchedFireNotifications, + fireNotificationsUnread: action.unreadCount); } + if (action is AddedFireNotificationAction) + return state.copyWith( + fireNotificationsUnread: state.fireNotificationsUnread + 1); + + if (action is ReadedFireNotificationAction) + return state.copyWith( + fireNotificationsUnread: state.fireNotificationsUnread - 1); + return state; -} \ No newline at end of file +} diff --git a/lib/redux/fetchDataMiddleware.dart b/lib/redux/fetchDataMiddleware.dart index 7e4314c..20f36b1 100644 --- a/lib/redux/fetchDataMiddleware.dart +++ b/lib/redux/fetchDataMiddleware.dart @@ -1,5 +1,6 @@ import 'package:bson_objectid/bson_objectid.dart'; import 'package:fires_flutter/models/yourLocation.dart'; +import 'package:fires_flutter/models/fireNotification.dart'; import 'package:flutter_simple_dependency_injection/injector.dart'; import 'package:just_debounce_it/just_debounce_it.dart'; import 'package:redux/redux.dart'; @@ -161,8 +162,12 @@ void fetchDataMiddleware(Store store, action, NextDispatcher next) { if (action is FetchFireNotificationsAction) { loadFireNotifications().then((fireNotifications) { + int unread = 0; + for (FireNotification notif in fireNotifications) { + if (!notif.read) { unread++; } + } store.dispatch( - new FetchFireNotificationsSucceededAction(fireNotifications)); + new FetchFireNotificationsSucceededAction(fireNotifications, unread)); persistFireNotifications(fireNotifications); }); } diff --git a/lib/redux/fireMapReducer.dart b/lib/redux/fireMapReducer.dart index 01e84d3..5acb58d 100644 --- a/lib/redux/fireMapReducer.dart +++ b/lib/redux/fireMapReducer.dart @@ -44,7 +44,7 @@ FireMapState _showYourLocationMap( status: action.loc.subscribed ? FireMapStatus.unsubscribe : FireMapStatus.view, - yourLocation: action.loc); + yourLocation: action.loc, fireNotication: null); } FireMapState _showFireNotificationMap( diff --git a/lib/redux/fireNotificationReducer.dart b/lib/redux/fireNotificationReducer.dart index 50b23ac..f30ed2f 100644 --- a/lib/redux/fireNotificationReducer.dart +++ b/lib/redux/fireNotificationReducer.dart @@ -16,7 +16,7 @@ final fireNotificationReducer = combineReducers>([ List _receivedFireNotification( List notifications, AddedFireNotificationAction action) { - return new List.from(notifications)..add(action.notif); + return new List.from(notifications)..insert(0, action.notif); } List _deletedFireNotification( diff --git a/lib/redux/reducers.dart b/lib/redux/reducers.dart index fb621f1..3517b81 100644 --- a/lib/redux/reducers.dart +++ b/lib/redux/reducers.dart @@ -1,29 +1,28 @@ import '../models/appState.dart'; +import 'appReducer.dart'; import 'errorReducer.dart'; import 'fireMapReducer.dart'; +import 'fireNotificationReducer.dart'; import 'loadedReducer.dart'; import 'loadingReducer.dart'; 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) { - var state = prevState; - if (action is AppActions) - state = appReducer(prevState, action); + var state = appReducer(prevState, action); return AppState( - yourLocations: yourLocationsReducer(state.yourLocations, action), - fireNotifications: fireNotificationReducer(state.fireNotifications, action), + yourLocations: yourLocationsReducer(state.yourLocations, action), + fireNotifications: + fireNotificationReducer(state.fireNotifications, action), + fireNotificationsUnread: state.fireNotificationsUnread, user: userReducer(state.user, action), isLoading: loadingReducer(state.isLoading, action), isLoaded: loadedReducer(state.isLoaded, action), error: errorReducer(state.error, action), fireMapState: fireMapReducer(state.fireMapState, action), - firesApiKey: state.firesApiKey, - firesApiUrl: state.firesApiUrl, - gmapKey: state.gmapKey - ); + firesApiKey: state.firesApiKey, + firesApiUrl: state.firesApiUrl, + serverUrl: state.serverUrl, + gmapKey: state.gmapKey); } diff --git a/pubspec.yaml b/pubspec.yaml index c8a403f..5574c31 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,6 +50,8 @@ dependencies: # https://pub.dartlang.org/packages/padder padder: "^1.0.1" flutter_fab_dialer: "^0.0.5" + badge: "^0.0.2" + flutter_spinkit: "^1.0.0" # firebase firebase_messaging: "^1.0.4" diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 1f2807f..23205c7 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -59,5 +59,7 @@ "fireNotificationsDescription": "Here you will receive fire notifications in locations you are subscribed to", "areYouSureTitle": "Are you sure?", "deleteAllFireNotificationsAlertDescription": "This will remove all your fire notifications", - "DELETE": "DELETE" + "DELETE": "DELETE", + "SHOW": "SHOW", + "CLOSE": "CLOSE" } \ No newline at end of file diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 9828c38..d3d8b9b 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -59,5 +59,7 @@ "fireNotificationsDescription": "Aquí recibirás las notificaciones de fuegos en los lugares a los que te subscribas", "areYouSureTitle": "¿Seguro?", "deleteAllFireNotificationsAlertDescription": "Esto borrará todas las notificaciones de fuegos", - "DELETE": "BORRAR" + "DELETE": "BORRAR", + "SHOW": "MOSTRAR", + "CLOSE": "CERRAR" } \ No newline at end of file