Skip to content

Commit 71d750b

Browse files
authored
Add notifications screen (#1019)
* Add notifications screen * Add loading state, edit icon, refactor checkNodeStatus * Replace node status notification logic to notifications user data * Ignore reflectable files, remove print
1 parent 3bc046f commit 71d750b

8 files changed

Lines changed: 226 additions & 34 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:threebotlogin/app.dart';
3+
import 'package:threebotlogin/apps/farmers/farmers_user_data.dart';
4+
import 'package:threebotlogin/events/events.dart';
5+
import 'package:threebotlogin/events/go_home_event.dart';
6+
import 'package:threebotlogin/screens/notifications_screen.dart';
7+
8+
class Notifications implements App {
9+
static final Notifications _singleton = Notifications._internal();
10+
static const Widget _notificationsWidget = NotificationsScreen();
11+
12+
factory Notifications() {
13+
return _singleton;
14+
}
15+
16+
Notifications._internal();
17+
18+
@override
19+
Future<Widget> widget() async {
20+
return _notificationsWidget;
21+
}
22+
23+
@override
24+
void clearData() {
25+
clearAllData();
26+
}
27+
28+
@override
29+
bool emailVerificationRequired() {
30+
return true;
31+
}
32+
33+
@override
34+
bool pinRequired() {
35+
return true;
36+
}
37+
38+
@override
39+
void back() {
40+
Events().emit(GoHomeEvent());
41+
}
42+
} // TODO Implement this library.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import 'package:shared_preferences/shared_preferences.dart';
2+
import 'package:threebotlogin/helpers/logger.dart';
3+
4+
const String nodeStatusNotificationEnabledKey = 'nodeStatusNotificationEnabled';
5+
6+
Future<List<String>?> getNotificationSettings() async {
7+
final prefs = await SharedPreferences.getInstance();
8+
var notifications = prefs.getStringList('notifications');
9+
return notifications;
10+
}
11+
12+
Future<bool> isNodeStatusNotificationEnabled() async {
13+
try {
14+
final prefs = await SharedPreferences.getInstance();
15+
return prefs.getBool(nodeStatusNotificationEnabledKey) ?? true;
16+
} catch (e) {
17+
return true;
18+
}
19+
}
20+
21+
Future<void> setNodeStatusNotificationEnabled(bool value) async {
22+
try {
23+
final prefs = await SharedPreferences.getInstance();
24+
await prefs.setBool(nodeStatusNotificationEnabledKey, value);
25+
} catch (e) {
26+
logger.e('Error saving notification preference: $e');
27+
}
28+
}

app/lib/jrouter.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import 'package:threebotlogin/apps/wallet/wallet.dart';
66
import 'package:threebotlogin/screens/identity_verification_screen.dart';
77
import 'package:threebotlogin/screens/preference_screen.dart';
88
import 'package:threebotlogin/screens/registered_screen.dart';
9-
9+
import 'package:threebotlogin/apps/notifications/notifications.dart';
1010
import 'apps/farmers/farmers.dart';
1111
import 'apps/news/news.dart';
1212
import 'apps/sign/sign.dart';
@@ -95,6 +95,14 @@ class JRouter {
9595
view: await Sign().widget(),
9696
),
9797
app: Sign()),
98+
AppInfo(
99+
route: Route(
100+
path: '/notifications',
101+
name: 'Notifications',
102+
icon: Icons.notifications,
103+
view: await Notifications().widget(),
104+
),
105+
app: null),
98106
];
99107
}
100108

app/lib/main.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ Future<void> main() async {
7171
),
7272
(String taskId) async {
7373
logger.i('[BackgroundFetch] Task: $taskId');
74-
await checkNodeStatus();
74+
await checkNodeStatus(taskId);
7575
BackgroundFetch.finish(taskId);
7676
},
7777
(String taskId) async {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:threebotlogin/apps/notifications/notifications_user_data.dart';
3+
import 'package:threebotlogin/widgets/layout_drawer.dart';
4+
5+
class NotificationsScreen extends StatefulWidget {
6+
const NotificationsScreen({super.key});
7+
8+
@override
9+
_NotificationsScreenState createState() => _NotificationsScreenState();
10+
}
11+
12+
class _NotificationsScreenState extends State<NotificationsScreen> {
13+
bool loading = true;
14+
late bool _nodeStatusNotificationEnabled;
15+
16+
@override
17+
void initState() {
18+
super.initState();
19+
_loadNotificationPreference();
20+
}
21+
22+
void _loadNotificationPreference() async {
23+
final bool enabled = await isNodeStatusNotificationEnabled();
24+
setState(() {
25+
_nodeStatusNotificationEnabled = enabled;
26+
loading = false;
27+
});
28+
}
29+
30+
@override
31+
Widget build(BuildContext context) {
32+
return LayoutDrawer(
33+
titleText: 'Notifications',
34+
content: _buildNotificationSettings(),
35+
);
36+
}
37+
38+
Widget _buildNotificationSettings() {
39+
return Padding(
40+
padding: const EdgeInsets.symmetric(vertical: 16.0),
41+
child: loading
42+
? Center(
43+
child: Column(
44+
mainAxisAlignment: MainAxisAlignment.center,
45+
children: [
46+
const CircularProgressIndicator(),
47+
const SizedBox(height: 15),
48+
Text(
49+
'Loading notifications settings...',
50+
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
51+
color: Theme.of(context).colorScheme.onSurface,
52+
fontWeight: FontWeight.bold),
53+
),
54+
],
55+
),
56+
)
57+
: Column(
58+
mainAxisAlignment: MainAxisAlignment.start,
59+
crossAxisAlignment: CrossAxisAlignment.stretch,
60+
children: <Widget>[
61+
SwitchListTile(
62+
title: const Text('Enable node status notifications'),
63+
value: _nodeStatusNotificationEnabled,
64+
onChanged: (bool newValue) {
65+
setState(() {
66+
_nodeStatusNotificationEnabled = newValue;
67+
});
68+
setNodeStatusNotificationEnabled(newValue);
69+
},
70+
secondary: const Icon(Icons.monitor_heart),
71+
),
72+
],
73+
),
74+
);
75+
}
76+
}

app/lib/screens/registered_screen.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ class _RegisteredScreenState extends State<RegisteredScreen>
117117
children: [
118118
HomeCardWidget(
119119
name: 'Settings', icon: Icons.settings, pageNumber: 6),
120+
HomeCardWidget(
121+
name: 'Notifications',
122+
icon: Icons.notifications,
123+
pageNumber: 9),
120124
],
121125
),
122126
const SizedBox(height: 40),

app/lib/services/background_service.dart

Lines changed: 55 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import 'package:background_fetch/background_fetch.dart';
2+
import 'package:threebotlogin/apps/notifications/notifications_user_data.dart';
3+
import 'package:threebotlogin/models/farm.dart';
24
import 'package:threebotlogin/services/nodes_check_service.dart';
35
import 'notification_service.dart';
6+
import 'package:threebotlogin/helpers/logger.dart';
47

58
void backgroundFetchHeadlessTask(HeadlessTask task) async {
69
final String taskId = task.taskId;
@@ -10,50 +13,70 @@ void backgroundFetchHeadlessTask(HeadlessTask task) async {
1013
BackgroundFetch.finish(taskId);
1114
return;
1215
}
13-
await checkNodeStatus();
16+
final bool notificationsEnabled = await isNodeStatusNotificationEnabled();
1417

15-
BackgroundFetch.finish(taskId);
18+
logger.i(
19+
'Background Fetch Headless Task: $taskId, Notifications Enabled: $notificationsEnabled');
20+
21+
if (!notificationsEnabled) {
22+
logger.i(
23+
'[BackgroundFetch] Node status notifications are disabled. Finishing task: $taskId');
24+
BackgroundFetch.finish(taskId);
25+
return;
26+
}
27+
await checkNodeStatus(taskId);
1628
}
1729

18-
Future<void> checkNodeStatus() async {
19-
final offlineNodes = await NodeCheckService.pingNodesInBackground();
20-
if (offlineNodes.isEmpty) return;
30+
Future<void> checkNodeStatus(String taskId) async {
31+
try {
32+
final offlineNodes = await NodeCheckService.pingNodesInBackground();
2133

22-
final now = DateTime.now().millisecondsSinceEpoch;
23-
final sevenDaysAgoTimestamp =
24-
DateTime.now().subtract(const Duration(days: 7)).millisecondsSinceEpoch;
34+
if (offlineNodes.isEmpty) return;
2535

26-
final nodesToNotify = offlineNodes.where((node) {
27-
final nodeUpdatedAtMs = node.updatedAt! * 1000;
28-
if (nodeUpdatedAtMs <= sevenDaysAgoTimestamp) return false;
36+
final StringBuffer bodyBuffer = StringBuffer();
37+
final List<Node> nodesToNotify = [];
38+
final now = DateTime.now();
39+
final nowInMs = now.millisecondsSinceEpoch;
40+
final sevenDaysAgoTimestampMs =
41+
now.subtract(const Duration(days: 7)).millisecondsSinceEpoch;
2942

30-
final downtime = Duration(milliseconds: now - nodeUpdatedAtMs);
31-
final checkInterval = _getCheckInterval(downtime);
43+
for (final node in offlineNodes) {
44+
final nodeUpdatedAtMs = node.updatedAt! * 1000;
3245

33-
return downtime.inMinutes % checkInterval.inMinutes < 15;
34-
}).toList();
46+
if (nodeUpdatedAtMs <= sevenDaysAgoTimestampMs) continue;
3547

36-
if (nodesToNotify.isEmpty) return;
48+
final downtime = Duration(milliseconds: nowInMs - nodeUpdatedAtMs);
3749

38-
const groupKey = 'offline_nodes';
39-
final StringBuffer bodyBuffer = StringBuffer();
50+
final checkInterval = _getCheckInterval(downtime);
4051

41-
for (final node in nodesToNotify) {
42-
final nodeUpdatedAtMs = node.updatedAt! * 1000;
43-
final downtime =
44-
_formatDowntime(Duration(milliseconds: now - nodeUpdatedAtMs));
52+
bool passesIntervalCheck = false;
53+
if (downtime.inMinutes > 0 && checkInterval.inMinutes > 0) {
54+
passesIntervalCheck = downtime.inMinutes % checkInterval.inMinutes < 15;
55+
}
4556

46-
bodyBuffer.writeln('Node ${node.nodeId}: offline for $downtime');
47-
}
57+
if (passesIntervalCheck) {
58+
nodesToNotify.add(node);
59+
final formattedDowntime = _formatDowntime(downtime);
60+
bodyBuffer
61+
.writeln('Node ${node.nodeId}: offline for $formattedDowntime');
62+
}
63+
}
64+
65+
if (nodesToNotify.isEmpty) return;
4866

49-
await NotificationService().showNotification(
50-
id: nodesToNotify.hashCode,
51-
title: nodesToNotify.length == 1
52-
? 'Node Alert 🚨'
53-
: '${nodesToNotify.length} Nodes Offline 🚨',
54-
body: bodyBuffer.toString().trim(),
55-
groupKey: groupKey,
56-
);
67+
await NotificationService().showNotification(
68+
id: nodesToNotify.hashCode,
69+
title: nodesToNotify.length == 1
70+
? 'Node Alert 🚨'
71+
: '${nodesToNotify.length} Nodes Offline 🚨',
72+
body: bodyBuffer.toString().trim(),
73+
groupKey: 'offline_nodes',
74+
);
75+
} catch (e) {
76+
logger.e('Error in checkNodeStatus for task $taskId: $e');
77+
} finally {
78+
BackgroundFetch.finish(taskId);
79+
}
5780
}
5881

5982
Duration _getCheckInterval(Duration downtime) {

app/lib/widgets/layout_drawer.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,17 @@ class _LayoutDrawerState extends State<LayoutDrawer> {
198198
globals.tabController.animateTo(7);
199199
},
200200
),
201+
ListTile(
202+
minLeadingWidth: 10,
203+
leading: const Padding(
204+
padding: EdgeInsets.only(left: 10),
205+
child: Icon(Icons.notifications, size: 18)),
206+
title: const Text('Notifications'),
207+
onTap: () {
208+
Navigator.pop(context);
209+
globals.tabController.animateTo(9);
210+
},
211+
),
201212
],
202213
),
203214
),

0 commit comments

Comments
 (0)