From c19bb3730eeb148955d7ed288f0f94cf599fe144 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 23 Mar 2024 23:41:45 +0800 Subject: [PATCH] :sparkles: Notification links --- lib/models/notification.dart | 79 ++++++++++++++++++++++++++++ lib/screens/notifications.dart | 70 +++++++------------------ lib/widgets/notification.dart | 94 ++++++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 52 deletions(-) create mode 100644 lib/models/notification.dart create mode 100644 lib/widgets/notification.dart diff --git a/lib/models/notification.dart b/lib/models/notification.dart new file mode 100644 index 0000000..e8cd55f --- /dev/null +++ b/lib/models/notification.dart @@ -0,0 +1,79 @@ +class Notification { + int id; + DateTime createdAt; + DateTime updatedAt; + DateTime? deletedAt; + String subject; + String content; + List? links; + bool isImportant; + DateTime? readAt; + int senderId; + int recipientId; + + Notification({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.subject, + required this.content, + this.links, + required this.isImportant, + this.readAt, + required this.senderId, + required this.recipientId, + }); + + factory Notification.fromJson(Map json) => Notification( + id: json["id"], + createdAt: DateTime.parse(json["created_at"]), + updatedAt: DateTime.parse(json["updated_at"]), + deletedAt: json["deleted_at"], + subject: json["subject"], + content: json["content"], + links: json["links"] != null + ? List.from(json["links"].map((x) => Link.fromJson(x))) + : List.empty(), + isImportant: json["is_important"], + readAt: json["read_at"], + senderId: json["sender_id"], + recipientId: json["recipient_id"], + ); + + Map toJson() => { + "id": id, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + "deleted_at": deletedAt, + "subject": subject, + "content": content, + "links": links != null + ? List.from(links!.map((x) => x.toJson())) + : List.empty(), + "is_important": isImportant, + "read_at": readAt, + "sender_id": senderId, + "recipient_id": recipientId, + }; +} + +class Link { + String label; + String url; + + Link({ + required this.label, + required this.url, + }); + + factory Link.fromJson(Map json) => Link( + label: json["label"], + url: json["url"], + ); + + Map toJson() => { + "label": label, + "url": url, + }; +} diff --git a/lib/screens/notifications.dart b/lib/screens/notifications.dart index 5742b00..c107465 100644 --- a/lib/screens/notifications.dart +++ b/lib/screens/notifications.dart @@ -1,9 +1,10 @@ import 'dart:convert'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:solaragent/auth.dart'; +import 'package:solaragent/models/notification.dart' as model; +import 'package:solaragent/models/pagination.dart'; +import 'package:solaragent/widgets/notification.dart'; class NotificationScreen extends StatefulWidget { const NotificationScreen({super.key}); @@ -13,9 +14,6 @@ class NotificationScreen extends StatefulWidget { } class _NotificationScreenState extends State { - final notificationEndpoint = - Uri.parse('https://id.solsynth.dev/api/notifications?skip=0&take=25'); - List notifications = List.empty(); @override @@ -26,23 +24,21 @@ class _NotificationScreenState extends State { Future pullNotifications() async { if (await authClient.isAuthorized()) { - var res = await authClient.client!.get(notificationEndpoint); + var uri = + Uri.parse('https://id.solsynth.dev/api/notifications?skip=0&take=25'); + var res = await authClient.client!.get(uri); if (res.statusCode == 200) { + final result = + PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); setState(() { - notifications = jsonDecode(utf8.decode(res.bodyBytes))["data"]; + notifications = + result.data?.map((x) => model.Notification.fromJson(x)).toList() ?? + List.empty(); }); } } } - Future markAsRead(element) async { - if (authClient.client != null) { - var id = element['id']; - var uri = Uri.parse('https://id.solsynth.dev/api/notifications/$id/read'); - await authClient.client!.put(uri); - } - } - @override Widget build(BuildContext context) { return Scaffold( @@ -67,7 +63,7 @@ class _NotificationScreenState extends State { ? SliverToBoxAdapter( child: Container( padding: const EdgeInsets.symmetric(horizontal: 10), - color: Colors.grey[300], + color: Colors.grey[50], child: const ListTile( leading: Icon(Icons.check), title: Text('You\'re done!'), @@ -81,42 +77,12 @@ class _NotificationScreenState extends State { itemCount: notifications.length, itemBuilder: (BuildContext context, int index) { var element = notifications[index]; - return Dismissible( - key: Key('notification-$index'), - onDismissed: (direction) { - var subject = element["subject"]; - markAsRead(element).then((value) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: RichText( - text: TextSpan(children: [ - TextSpan( - text: subject, - style: const TextStyle( - fontWeight: FontWeight.bold), - ), - const TextSpan( - text: " is marked as read", - ) - ]), - ), - ), - ); - }); - setState(() { - notifications.removeAt(index); - }); - }, - background: Container( - color: Colors.green, - ), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: ListTile( - title: Text(element["subject"]), - subtitle: Text(element["content"]), - ), - ), + return NotificationItem( + index: index, + item: element, + onDismiss: () => setState(() { + notifications.removeAt(index); + }), ); }, ), diff --git a/lib/widgets/notification.dart b/lib/widgets/notification.dart new file mode 100644 index 0000000..70a9362 --- /dev/null +++ b/lib/widgets/notification.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:solaragent/models/notification.dart' as model; +import 'package:solaragent/auth.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class NotificationItem extends StatelessWidget { + final int index; + final model.Notification item; + final void Function()? onDismiss; + + const NotificationItem( + {super.key, required this.index, required this.item, this.onDismiss}); + + bool hasLinks() => item.links != null && item.links!.isNotEmpty; + + void showLinks(BuildContext context) { + if (!hasLinks()) return; + + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return ListView.builder( + padding: const EdgeInsets.all(8), + itemCount: item.links!.length, + itemBuilder: (BuildContext context, int index) { + var element = item.links![index]; + return InkWell( + borderRadius: const BorderRadius.all( + Radius.circular(64), + ), + onTap: () async { + await launchUrl(Uri.parse(element.url)); + }, + child: ListTile(title: Text(element.label)), + ); + }); + }, + ); + } + + Future markAsRead(element) async { + if (authClient.client != null) { + var id = element['id']; + var uri = Uri.parse('https://id.solsynth.dev/api/notifications/$id/read'); + await authClient.client!.put(uri); + } + } + + @override + Widget build(BuildContext context) { + return Dismissible( + key: Key('notification-$index'), + onDismissed: (direction) { + markAsRead(item).then((value) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: RichText( + text: TextSpan(children: [ + TextSpan( + text: item.subject, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const TextSpan( + text: " is marked as read", + ) + ]), + ), + ), + ); + }); + if (onDismiss != null) { + onDismiss!(); + } + }, + background: Container( + color: Colors.grey, + ), + child: Container( + padding: const EdgeInsets.only(left: 10), + child: ListTile( + title: Text(item.subject), + subtitle: Text(item.subject), + trailing: hasLinks() + ? TextButton( + onPressed: () => showLinks(context), + style: TextButton.styleFrom(shape: const CircleBorder()), + child: const Icon(Icons.more_vert), + ) + : null, + ), + ), + ); + } +}