More work with fire notifications

This commit is contained in:
Vicente J. Ruiz Jurado 2018-07-31 12:51:10 +02:00
parent 5fdaf239c0
commit 9dd8a7de97
24 changed files with 512 additions and 288 deletions

View file

@ -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,24 +25,26 @@ 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;
}
yourLocations == other.yourLocations &&
isLoading == other.isLoading;
@override
int get hashCode => yourLocations.hashCode;
int get hashCode => yourLocations.hashCode ^ isLoading.hashCode;
}
class ActiveFiresPage extends StatefulWidget {
@ -52,7 +55,6 @@ static const String routeName = '/fires';
}
class _ActiveFiresPageState extends State<ActiveFiresPage> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
List<FabMiniMenuItem> _fabMiniMenuItemList(
@ -171,7 +173,8 @@ class _ActiveFiresPageState extends State<ActiveFiresPage> {
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,7 +199,9 @@ class _ActiveFiresPageState extends State<ActiveFiresPage> {
floatingActionButtonLocation:
FloatingActionButtonLocation.centerFloat,
bottomNavigationBar: new GlobalFiresBottomStats(),
body: hasLocations
body: view.isLoading
? new SpinKitPulse(color: fires600)
: hasLocations
? new Stack(children: <Widget>[
_buildSavedLocations(context, view.yourLocations,
view.onDelete, view.onToggleSubs, view.onTap),
@ -224,8 +229,8 @@ class _ActiveFiresPageState extends State<ActiveFiresPage> {
void gotoLocationMap(
Store<AppState> 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) {

View file

@ -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<FireNotification> fireNotifications;
final int fireNotificationsUnread;
final List<YourLocation> 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) ||
bool operator ==(Object other) =>
identical(this, other) ||
other is _ViewModel &&
runtimeType == other.runtimeType &&
fireNotifications == other.fireNotifications;
}
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<FireNotificationList> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
Widget _buildRow(
BuildContext context, FireNotification notif, onDeleted, onTap) {
Widget _buildRow(BuildContext context, List<YourLocation> 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<FireNotificationList> {
));
}
Widget _buildSavedFireNotifications(BuildContext context,
List<FireNotification> notifList, onDeleted, onTap) {
Widget _buildSavedFireNotifications(
BuildContext context,
List<YourLocation> yourLocations,
List<FireNotification> 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<FireNotificationList> {
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<FireNotificationList> {
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,7 +204,7 @@ class _FireNotificationListState extends State<FireNotificationList> {
style: new TextStyle(
height: 1.3, color: Colors.black45))
]))))
: _buildSavedFireNotifications(context,
: _buildSavedFireNotifications(context, view.yourLocations,
view.fireNotifications, view.onDelete, view.onTap));
});
}
@ -199,8 +212,8 @@ class _FireNotificationListState extends State<FireNotificationList> {
void gotoMap(
Store<AppState> store, FireNotification notif, BuildContext context) {
store.dispatch(new ShowFireNotificationMapAction(notif));
Navigator.push(context,
new MaterialPageRoute(builder: (context) => new genericMap()));
Navigator.push(
context, new MaterialPageRoute(builder: (context) => new genericMap()));
}
_showConfirmDialog(_ViewModel view) {

View file

@ -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<AppState> store) {
_firebaseMessaging.configure(onMessage: (Map<String, dynamic> message) {
print("onMessage: $message");
onMessage(message, store);
//_showItemDialog(message);
}, onLaunch: (Map<String, dynamic> message) {
print("onLaunch: $message");
//_navigateToItemDetail(message);
onMessage(message, store);
}, onResume: (Map<String, dynamic> message) {
print("onResume: $message");
//_navigateToItemDetail(message);
onMessage(message, store);
});
getToken(store);
}
void onMessage(Map<String, dynamic> message, Store<AppState> 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<AppState> store) async {
String token = await _firebaseMessaging.onTokenRefresh.first;
// print(token);
store.dispatch(new OnUserTokenAction(token));
}

View file

@ -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<AppState> store;
@override
_FiresAppState createState() => _FiresAppState(store);
}
class _FiresAppState extends State<FiresApp> {
final GlobalKey<NavigatorState> navigatorKey =
new GlobalKey<NavigatorState>();
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<AppState> 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<AppState>(
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<String, dynamic> message) {
print("onMessage in fireApp: $message");
_showItemDialog(message, store);
}, onLaunch: (Map<String, dynamic> message) {
print("onLaunch: $message");
_navigateToItemDetail(message, store);
}, onResume: (Map<String, dynamic> 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<String, dynamic> message, Store store) {
final notif = _notifForMessage(message);
showDialog<bool>(
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: <Widget>[
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<String, dynamic> message, Store store) {
FireNotification notif = _notifForMessage(message);
// Clear away dialogs
Navigator.popUntil(navigatorKey.currentContext,
(Route<dynamic> 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<String, dynamic> 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;
}
}

View file

@ -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";

View file

@ -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<genericMap> {
// 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<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
final FirebaseMessaging _firebaseMessaging = new FirebaseMessaging();
YourLocation _location;
YourLocation _initialLocation;
@ -105,6 +107,7 @@ class _genericMapState extends State<genericMap> {
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<genericMap> {
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<genericMap> {
icon: new Icon(Icons.save),
onPressed: () => view.onEditConfirm(_location))
];
case FireMapStatus.viewFireNotification:
return <Widget>[
new IconButton(
icon: new Icon(Icons.share),
onPressed: () {
Share.share('${view.mapState.fireNotification.description}. ${view.serverUrl}fire/${view.mapState.fireNotification.sealed}');
})
];
default:
return <Widget>[];
}

View file

@ -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<Middleware<AppState>> 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<Middleware<AppState>> otherMiddleware) {
VoidCallback mainFn = () {
loadYourLocations().then((yl) {
firebaseConfig(store);
// Run baby run!
runApp(new FiresApp(store));
});

View file

@ -17,7 +17,7 @@ void main() {
return ">>>>> ${action.toString().replaceAll('Instance of ', '')}";
}
LogLevel logRedux = LogLevel.actions;
LogLevel logRedux = LogLevel.full;
List<Middleware> devMiddlewares = logRedux == LogLevel.none
? []

View file

@ -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,6 +40,12 @@ class MainDrawer extends Drawer {
}
Widget mainDrawer(BuildContext context) {
return new StoreConnector<AppState, _ViewModel>(
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,
@ -67,7 +95,13 @@ Widget mainDrawer(BuildContext context) {
),
new ListTile(
leading: const Icon(Icons.notifications),
title: new Text(S.of(context).fireNotificationsTitleShort),
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);
@ -93,26 +127,28 @@ Widget mainDrawer(BuildContext context) {
Navigator.pushNamed(context, PrivacyPage.routeName);
},
),
globals.isDevelopment ?
new ListTile(
globals.isDevelopment
? new ListTile(
leading: const Icon(Icons.bug_report),
title: new Text('Sandbox'),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(context, Sandbox.routeName);
},
): null,
)
: 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()),
applicationLegalese:
S.of(context).appLicense(DateTime.now().year.toString()),
aboutBoxChildren: <Widget>[
// new Text('What?')
]
// FIXME
)
])
);
]));
});
}

View file

@ -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<YourLocation> yourLocations;
final List<FireNotification> fireNotifications;
@JsonKey(ignore: true)
final int fireNotificationsUnread;
@JsonKey(ignore: true)
final FireMapState fireMapState;
@JsonKey(ignore: true)
@ -40,6 +44,7 @@ class AppState extends Object with _$AppStateSerializerMixin {
AppState(
{this.yourLocations: const <YourLocation>[],
this.fireNotifications: const <FireNotification>[],
this.fireNotificationsUnread: 0,
this.user: const User.initial(),
this.isLoading: false,
this.isLoaded: false,
@ -47,6 +52,7 @@ class AppState extends Object with _$AppStateSerializerMixin {
this.gmapKey,
this.firesApiKey,
this.firesApiUrl,
this.serverUrl,
this.fireMapState: const FireMapState.initial()});
AppState copyWith(
@ -56,9 +62,11 @@ class AppState extends Object with _$AppStateSerializerMixin {
String error,
String gmapKey,
String firesApiKey,
String serverUrl,
String firesApiUrl,
List<YourLocation> yourLocations,
List<FireNotification> fireNotifications,
int fireNotificationsUnread,
FireMapState fireMapState}) {
return new AppState(
isLoading: isLoading ?? this.isLoading,
@ -68,8 +76,11 @@ class AppState extends Object with _$AppStateSerializerMixin {
gmapKey: gmapKey ?? this.gmapKey,
firesApiKey: firesApiKey ?? this.firesApiKey,
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}';
}
}

View file

@ -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<String, dynamic> json) => _$FireNotificationFromJson(json);
factory FireNotification.fromJson(Map<String, dynamic> 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,
description: description ?? this.description,
sealed: sealed ?? this.sealed,
subsId: subsId ?? this.subsId,
when: when ?? this.when);
}
@ -48,13 +61,43 @@ class FireNotification extends Object with _$FireNotificationSerializerMixin {
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<String, Route<Null>> routes = <String, Route<Null>>{};
Route<Null> getRoute(Store store) {
final String routeName = '/fire/${id}';
return routes.putIfAbsent(
routeName,
() => new MaterialPageRoute<Null>(
settings: new RouteSettings(name: routeName),
builder: (BuildContext context) {
store.dispatch(new ShowFireNotificationMapAction(this));
Navigator.push(
context,
new MaterialPageRoute(
builder: (context) => new genericMap()));
},
),
);
}
}

View file

@ -10,12 +10,14 @@ part of 'fireNotification.dart';
FireNotification _$FireNotificationFromJson(Map<String, dynamic> 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<String, dynamic> toJson() => new _$FireNotificationJsonMapWrapper(this);
}
@ -32,15 +36,23 @@ class _$FireNotificationJsonMapWrapper extends $JsonMapWrapper {
_$FireNotificationJsonMapWrapper(this._v);
@override
Iterable<String> get keys =>
const ['id', 'lat', 'lon', 'description', 'when', 'read'];
Iterable<String> 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;
}

View file

@ -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;

View file

@ -10,7 +10,7 @@ part of 'yourLocation.dart';
YourLocation _$YourLocationFromJson(Map<String, dynamic> 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':

9
lib/objectIdUtils.dart Normal file
View file

@ -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();
}

View file

@ -41,6 +41,7 @@ class OnUserLangAction extends AppActions {
class FetchFireNotificationsSucceededAction extends AppActions {
final List<FireNotification> fetchedFireNotifications;
final int unreadCount;
FetchFireNotificationsSucceededAction(this.fetchedFireNotifications);
FetchFireNotificationsSucceededAction(this.fetchedFireNotifications, this.unreadCount);
}

View file

@ -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;
}

View file

@ -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<AppState> 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);
});
}

View file

@ -44,7 +44,7 @@ FireMapState _showYourLocationMap(
status: action.loc.subscribed
? FireMapStatus.unsubscribe
: FireMapStatus.view,
yourLocation: action.loc);
yourLocation: action.loc, fireNotication: null);
}
FireMapState _showFireNotificationMap(

View file

@ -16,7 +16,7 @@ final fireNotificationReducer = combineReducers<List<FireNotification>>([
List<FireNotification> _receivedFireNotification(
List<FireNotification> notifications, AddedFireNotificationAction action) {
return new List.from(notifications)..add(action.notif);
return new List.from(notifications)..insert(0, action.notif);
}
List<FireNotification> _deletedFireNotification(

View file

@ -1,22 +1,21 @@
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),
fireNotifications:
fireNotificationReducer(state.fireNotifications, action),
fireNotificationsUnread: state.fireNotificationsUnread,
user: userReducer(state.user, action),
isLoading: loadingReducer(state.isLoading, action),
isLoaded: loadedReducer(state.isLoaded, action),
@ -24,6 +23,6 @@ AppState appStateReducer(AppState prevState, action) {
fireMapState: fireMapReducer(state.fireMapState, action),
firesApiKey: state.firesApiKey,
firesApiUrl: state.firesApiUrl,
gmapKey: state.gmapKey
);
serverUrl: state.serverUrl,
gmapKey: state.gmapKey);
}

View file

@ -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"

View file

@ -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"
}

View file

@ -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"
}