From ebeffbe1aa66608841bc588e7205ac57c3c89d87 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 16 Oct 2024 00:50:48 +0800 Subject: [PATCH] :recycle: Refactored notification --- lib/bootstrapper.dart | 3 +- lib/main.dart | 6 +- lib/models/notification.dart | 2 + lib/models/notification.g.dart | 4 + lib/providers/auth.dart | 7 +- lib/providers/notifications.dart | 174 ++++++++++++++++++++++++++ lib/providers/websocket.dart | 125 +----------------- lib/screens/account/notification.dart | 160 ++++++++++++----------- lib/screens/auth/signin.dart | 4 +- lib/screens/dashboard.dart | 12 +- lib/widgets/posts/post_item.dart | 5 + 11 files changed, 295 insertions(+), 207 deletions(-) create mode 100644 lib/providers/notifications.dart diff --git a/lib/bootstrapper.dart b/lib/bootstrapper.dart index 5e29d6f..538fa32 100644 --- a/lib/bootstrapper.dart +++ b/lib/bootstrapper.dart @@ -13,6 +13,7 @@ import 'package:solian/exts.dart'; import 'package:solian/platform.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/providers/content/realm.dart'; +import 'package:solian/providers/notifications.dart'; import 'package:solian/providers/relation.dart'; import 'package:solian/providers/theme_switcher.dart'; import 'package:solian/providers/websocket.dart'; @@ -214,7 +215,7 @@ class _BootstrapperShellState extends State { final AuthProvider auth = Get.find(); if (auth.isAuthorized.isTrue) { try { - Get.find().registerPushNotifications(); + Get.find().registerPushNotifications(); } catch (err) { context.showSnackbar( 'pushNotifyRegisterFailed'.trParams({'reason': err.toString()}), diff --git a/lib/main.dart b/lib/main.dart index 58eea38..85ddc32 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,6 +18,7 @@ import 'package:solian/providers/database/services/messages.dart'; import 'package:solian/providers/last_read.dart'; import 'package:solian/providers/link_expander.dart'; import 'package:solian/providers/navigation.dart'; +import 'package:solian/providers/notifications.dart'; import 'package:solian/providers/stickers.dart'; import 'package:solian/providers/subscription.dart'; import 'package:solian/providers/theme_switcher.dart'; @@ -138,11 +139,12 @@ class SolianApp extends StatelessWidget { Get.put(NavigationStateProvider()); Get.lazyPut(() => AuthProvider()); + Get.lazyPut(() => WebSocketProvider()); Get.lazyPut(() => RelationshipProvider()); Get.lazyPut(() => PostProvider()); Get.lazyPut(() => StickerProvider()); Get.lazyPut(() => AttachmentProvider()); - Get.lazyPut(() => WebSocketProvider()); + Get.lazyPut(() => NotificationProvider()); Get.lazyPut(() => StatusProvider()); Get.lazyPut(() => ChannelProvider()); Get.lazyPut(() => RealmProvider()); @@ -154,6 +156,6 @@ class SolianApp extends StatelessWidget { Get.lazyPut(() => LastReadProvider()); Get.lazyPut(() => SubscriptionProvider()); - Get.find().requestPermissions(); + Get.find().requestPermissions(); } } diff --git a/lib/models/notification.dart b/lib/models/notification.dart index 466d2bd..0b82bb1 100755 --- a/lib/models/notification.dart +++ b/lib/models/notification.dart @@ -8,6 +8,7 @@ class Notification { DateTime createdAt; DateTime updatedAt; DateTime? deletedAt; + DateTime? readAt; String title; String? subtitle; String body; @@ -21,6 +22,7 @@ class Notification { required this.createdAt, required this.updatedAt, required this.deletedAt, + required this.readAt, required this.title, required this.subtitle, required this.body, diff --git a/lib/models/notification.g.dart b/lib/models/notification.g.dart index f940126..f80239e 100644 --- a/lib/models/notification.g.dart +++ b/lib/models/notification.g.dart @@ -13,6 +13,9 @@ Notification _$NotificationFromJson(Map json) => Notification( deletedAt: json['deleted_at'] == null ? null : DateTime.parse(json['deleted_at'] as String), + readAt: json['read_at'] == null + ? null + : DateTime.parse(json['read_at'] as String), title: json['title'] as String, subtitle: json['subtitle'] as String?, body: json['body'] as String, @@ -28,6 +31,7 @@ Map _$NotificationToJson(Notification instance) => 'created_at': instance.createdAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(), + 'read_at': instance.readAt?.toIso8601String(), 'title': instance.title, 'subtitle': instance.subtitle, 'body': instance.body, diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart index 1cb22a5..09ef999 100644 --- a/lib/providers/auth.dart +++ b/lib/providers/auth.dart @@ -11,6 +11,7 @@ import 'package:solian/exceptions/request.dart'; import 'package:solian/exceptions/unauthorized.dart'; import 'package:solian/models/auth.dart'; import 'package:solian/providers/database/database.dart'; +import 'package:solian/providers/notifications.dart'; import 'package:solian/providers/websocket.dart'; import 'package:solian/services.dart'; @@ -174,7 +175,7 @@ class AuthProvider extends GetConnect { ); Get.find().connect(); - Get.find().notifyPrefetch(); + Get.find().fetchNotification(); return credentials!; } @@ -184,8 +185,8 @@ class AuthProvider extends GetConnect { userProfile.value = null; Get.find().disconnect(); - Get.find().notifications.clear(); - Get.find().notificationUnread.value = 0; + Get.find().notifications.clear(); + Get.find().notificationUnread.value = 0; AppDatabase.removeDatabase(); autoStopBackgroundNotificationService(); diff --git a/lib/providers/notifications.dart b/lib/providers/notifications.dart new file mode 100644 index 0000000..2a7b6c9 --- /dev/null +++ b/lib/providers/notifications.dart @@ -0,0 +1,174 @@ +import 'dart:developer'; + +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_udid/flutter_udid.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:solian/exceptions/request.dart'; +import 'package:solian/models/notification.dart'; +import 'package:solian/models/pagination.dart'; +import 'package:solian/platform.dart'; +import 'package:solian/providers/auth.dart'; + +class NotificationProvider extends GetxController { + RxBool isBusy = false.obs; + + RxInt notificationUnread = 0.obs; + RxList notifications = + List.empty(growable: true).obs; + + @override + void onInit() { + super.onInit(); + fetchNotification(); + } + + Future fetchNotification() async { + final AuthProvider auth = Get.find(); + if (auth.isAuthorized.isFalse) return; + + final client = await auth.configureClient('auth'); + + final resp = await client.get('/notifications?skip=0&take=100'); + if (resp.statusCode == 200) { + final result = PaginationResult.fromJson(resp.body); + final data = result.data?.map((x) => Notification.fromJson(x)).toList(); + if (data != null) { + print(data.map((x) => x.toJson())); + notifications.addAll(data); + notificationUnread.value = data.where((x) => x.readAt == null).length; + } + } + } + + Future markAllRead() async { + final AuthProvider auth = Get.find(); + if (auth.isAuthorized.isFalse) return; + + isBusy.value = true; + + final NotificationProvider nty = Get.find(); + + List markList = List.empty(growable: true); + for (final element in nty.notifications) { + if (element.id <= 0) continue; + markList.add(element.id); + } + + if (markList.isNotEmpty) { + final client = await auth.configureClient('auth'); + await client.put('/notifications/read', {'messages': markList}); + } + + nty.notifications.clear(); + + isBusy.value = false; + } + + Future markOneRead(Notification element, int index) async { + final AuthProvider auth = Get.find(); + if (auth.isAuthorized.isFalse) return; + + final NotificationProvider nty = Get.find(); + + if (element.id <= 0) { + nty.notifications.removeAt(index); + return; + } + + isBusy.value = true; + + final client = await auth.configureClient('auth'); + + await client.put('/notifications/read/${element.id}', {}); + + nty.notifications.removeAt(index); + + isBusy.value = false; + } + + void requestPermissions() { + try { + FirebaseMessaging.instance.requestPermission( + alert: true, + announcement: true, + carPlay: true, + badge: true, + sound: true); + } catch (_) { + // When firebase isn't initialized (background service) + FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.requestNotificationsPermission(); + flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + IOSFlutterLocalNotificationsPlugin>() + ?.requestPermissions( + alert: true, + badge: true, + sound: true, + ); + flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + MacOSFlutterLocalNotificationsPlugin>() + ?.requestPermissions( + alert: true, + badge: true, + sound: true, + ); + } + } + + Future registerPushNotifications() async { + if (PlatformInfo.isWeb) return; + + final prefs = await SharedPreferences.getInstance(); + if (prefs.getBool('service_background_notification') == true) { + log('Background notification service has been enabled, skip register push notifications'); + return; + } + + final AuthProvider auth = Get.find(); + if (auth.isAuthorized.isFalse) return; + + late final String? token; + late final String provider; + var deviceUuid = await _getDeviceUuid(); + + if (deviceUuid == null || deviceUuid.isEmpty) { + log("Unable to active push notifications, couldn't get device uuid"); + return; + } else { + log('Device UUID is $deviceUuid'); + } + + if (PlatformInfo.isIOS || PlatformInfo.isMacOS) { + provider = 'apple'; + token = await FirebaseMessaging.instance.getAPNSToken(); + } else { + provider = 'firebase'; + token = await FirebaseMessaging.instance.getToken(); + } + log('Device Push Token is $token'); + + final client = await auth.configureClient('auth'); + + final resp = await client.post('/notifications/subscribe', { + 'provider': provider, + 'device_token': token, + 'device_id': deviceUuid, + }); + if (resp.statusCode != 200 && resp.statusCode != 400) { + throw RequestException(resp); + } + } + + Future _getDeviceUuid() async { + if (PlatformInfo.isWeb) return null; + return await FlutterUdid.consistentUdid; + } +} diff --git a/lib/providers/websocket.dart b/lib/providers/websocket.dart index 910861d..4d0107c 100644 --- a/lib/providers/websocket.dart +++ b/lib/providers/websocket.dart @@ -3,17 +3,11 @@ import 'dart:convert'; import 'dart:developer'; import 'dart:io'; -import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:flutter_udid/flutter_udid.dart'; import 'package:get/get.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:solian/exceptions/request.dart'; import 'package:solian/models/notification.dart'; import 'package:solian/models/packet.dart'; -import 'package:solian/models/pagination.dart'; -import 'package:solian/platform.dart'; import 'package:solian/providers/auth.dart'; +import 'package:solian/providers/notifications.dart'; import 'package:solian/services.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; @@ -21,56 +15,10 @@ class WebSocketProvider extends GetxController { RxBool isConnected = false.obs; RxBool isConnecting = false.obs; - RxInt notificationUnread = 0.obs; - RxList notifications = - List.empty(growable: true).obs; - WebSocketChannel? websocket; StreamController stream = StreamController.broadcast(); - @override - onInit() { - notifyPrefetch(); - - super.onInit(); - } - - void requestPermissions() { - try { - FirebaseMessaging.instance.requestPermission( - alert: true, - announcement: true, - carPlay: true, - badge: true, - sound: true); - } catch (_) { - // When firebase isn't initialized (background service) - FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = - FlutterLocalNotificationsPlugin(); - flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() - ?.requestNotificationsPermission(); - flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation< - IOSFlutterLocalNotificationsPlugin>() - ?.requestPermissions( - alert: true, - badge: true, - sound: true, - ); - flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation< - MacOSFlutterLocalNotificationsPlugin>() - ?.requestPermissions( - alert: true, - badge: true, - sound: true, - ); - } - } - Future connect({noRetry = false}) async { if (isConnected.value) { return; @@ -119,8 +67,9 @@ class WebSocketProvider extends GetxController { log('Websocket incoming message: ${packet.method} ${packet.message}'); stream.sink.add(packet); if (packet.method == 'notifications.new') { - notifications.add(Notification.fromJson(packet.payload!)); - notificationUnread.value++; + final NotificationProvider nty = Get.find(); + nty.notifications.add(Notification.fromJson(packet.payload!)); + nty.notificationUnread.value++; } }, onDone: () { @@ -133,70 +82,4 @@ class WebSocketProvider extends GetxController { }, ); } - - Future notifyPrefetch() async { - final AuthProvider auth = Get.find(); - if (auth.isAuthorized.isFalse) return; - - final client = await auth.configureClient('auth'); - - final resp = await client.get('/notifications?skip=0&take=100'); - if (resp.statusCode == 200) { - final result = PaginationResult.fromJson(resp.body); - final data = result.data?.map((x) => Notification.fromJson(x)).toList(); - if (data != null) { - notifications.addAll(data); - notificationUnread.value = data.length; - } - } - } - - Future registerPushNotifications() async { - if (PlatformInfo.isWeb) return; - - final prefs = await SharedPreferences.getInstance(); - if (prefs.getBool('service_background_notification') == true) { - log('Background notification service has been enabled, skip register push notifications'); - return; - } - - final AuthProvider auth = Get.find(); - if (auth.isAuthorized.isFalse) return; - - late final String? token; - late final String provider; - var deviceUuid = await _getDeviceUuid(); - - if (deviceUuid == null || deviceUuid.isEmpty) { - log("Unable to active push notifications, couldn't get device uuid"); - return; - } else { - log('Device UUID is $deviceUuid'); - } - - if (PlatformInfo.isIOS || PlatformInfo.isMacOS) { - provider = 'apple'; - token = await FirebaseMessaging.instance.getAPNSToken(); - } else { - provider = 'firebase'; - token = await FirebaseMessaging.instance.getToken(); - } - log('Device Push Token is $token'); - - final client = await auth.configureClient('auth'); - - final resp = await client.post('/notifications/subscribe', { - 'provider': provider, - 'device_token': token, - 'device_id': deviceUuid, - }); - if (resp.statusCode != 200 && resp.statusCode != 400) { - throw RequestException(resp); - } - } - - Future _getDeviceUuid() async { - if (PlatformInfo.isWeb) return null; - return await FlutterUdid.consistentUdid; - } } diff --git a/lib/screens/account/notification.dart b/lib/screens/account/notification.dart index bda2267..f8514a3 100644 --- a/lib/screens/account/notification.dart +++ b/lib/screens/account/notification.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; -import 'package:solian/providers/websocket.dart'; +import 'package:solian/providers/notifications.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/models/notification.dart' as notify; +import 'package:solian/widgets/loading_indicator.dart'; import 'package:uuid/uuid.dart'; class NotificationScreen extends StatefulWidget { @@ -22,10 +22,10 @@ class _NotificationScreenState extends State { setState(() => _isBusy = true); - final WebSocketProvider provider = Get.find(); + final NotificationProvider nty = Get.find(); List markList = List.empty(growable: true); - for (final element in provider.notifications) { + for (final element in nty.notifications) { if (element.id <= 0) continue; markList.add(element.id); } @@ -35,7 +35,7 @@ class _NotificationScreenState extends State { await client.put('/notifications/read', {'messages': markList}); } - provider.notifications.clear(); + nty.notifications.clear(); setState(() => _isBusy = false); } @@ -44,10 +44,10 @@ class _NotificationScreenState extends State { final AuthProvider auth = Get.find(); if (auth.isAuthorized.isFalse) return; - final WebSocketProvider provider = Get.find(); + final NotificationProvider nty = Get.find(); if (element.id <= 0) { - provider.notifications.removeAt(index); + nty.notifications.removeAt(index); return; } @@ -57,14 +57,14 @@ class _NotificationScreenState extends State { await client.put('/notifications/read/${element.id}', {}); - provider.notifications.removeAt(index); + nty.notifications.removeAt(index); setState(() => _isBusy = false); } @override Widget build(BuildContext context) { - final WebSocketProvider ws = Get.find(); + final NotificationProvider nty = Get.find(); return SizedBox( height: MediaQuery.of(context).size.height * 0.85, @@ -77,71 +77,87 @@ class _NotificationScreenState extends State { ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), Expanded( child: Obx(() { - return CustomScrollView( - slivers: [ - if (_isBusy) - SliverToBoxAdapter( - child: const LinearProgressIndicator().animate().scaleX(), - ), - if (ws.notifications.isEmpty) - SliverToBoxAdapter( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10), - color: - Theme.of(context).colorScheme.surfaceContainerHigh, - child: ListTile( - leading: const Icon(Icons.check), - title: Text('notifyEmpty'.tr), - subtitle: Text('notifyEmptyCaption'.tr), + return RefreshIndicator( + onRefresh: () => nty.fetchNotification(), + child: CustomScrollView( + slivers: [ + Obx( + () => SliverToBoxAdapter( + child: LoadingIndicator( + isActive: _isBusy || nty.isBusy.value, ), ), ), - if (ws.notifications.isNotEmpty) - SliverToBoxAdapter( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10), - color: Theme.of(context).colorScheme.secondaryContainer, - child: ListTile( - leading: const Icon(Icons.checklist), - title: Text('notifyAllRead'.tr), - onTap: _isBusy ? null : () => _markAllRead(), + if (nty.notifications + .where((x) => x.readAt == null) + .isEmpty) + SliverToBoxAdapter( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10), + color: Theme.of(context) + .colorScheme + .surfaceContainerHigh, + child: ListTile( + leading: const Icon(Icons.check), + title: Text('notifyEmpty'.tr), + subtitle: Text('notifyEmptyCaption'.tr), + ), ), ), + if (nty.notifications + .where((x) => x.readAt == null) + .isNotEmpty) + SliverToBoxAdapter( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10), + color: + Theme.of(context).colorScheme.secondaryContainer, + child: ListTile( + leading: const Icon(Icons.checklist), + title: Text('notifyAllRead'.tr), + onTap: _isBusy ? null : () => _markAllRead(), + ), + ), + ), + SliverList.separated( + itemCount: nty.notifications.length, + itemBuilder: (BuildContext context, int index) { + var element = nty.notifications[index]; + return ClipRect( + child: Dismissible( + key: Key(const Uuid().v4()), + background: Container( + color: Colors.lightBlue, + padding: + const EdgeInsets.symmetric(horizontal: 20), + alignment: Alignment.centerLeft, + child: + const Icon(Icons.check, color: Colors.white), + ), + child: ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 8, + ), + title: Text(element.title), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (element.subtitle != null) + Text(element.subtitle!), + Text(element.body), + ], + ), + ), + onDismissed: (_) => _markOneRead(element, index), + ), + ); + }, + separatorBuilder: (_, __) => + const Divider(thickness: 0.3, height: 0.3), ), - SliverList.separated( - itemCount: ws.notifications.length, - itemBuilder: (BuildContext context, int index) { - var element = ws.notifications[index]; - return Dismissible( - key: Key(const Uuid().v4()), - background: Container( - color: Colors.lightBlue, - padding: const EdgeInsets.symmetric(horizontal: 20), - alignment: Alignment.centerLeft, - child: const Icon(Icons.check, color: Colors.white), - ), - child: ListTile( - contentPadding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 8, - ), - title: Text(element.title), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (element.subtitle != null) - Text(element.subtitle!), - Text(element.body), - ], - ), - ), - onDismissed: (_) => _markOneRead(element, index), - ); - }, - separatorBuilder: (_, __) => - const Divider(thickness: 0.3, height: 0.3), - ), - ], + ], + ), ); }), ), @@ -156,7 +172,7 @@ class NotificationButton extends StatelessWidget { @override Widget build(BuildContext context) { - final WebSocketProvider provider = Get.find(); + final NotificationProvider nty = Get.find(); final button = IconButton( icon: const Icon(Icons.notifications), @@ -166,16 +182,16 @@ class NotificationButton extends StatelessWidget { isScrollControlled: true, context: context, builder: (context) => const NotificationScreen(), - ).then((_) => provider.notificationUnread.value = 0); + ).then((_) => nty.notificationUnread.value = 0); }, ); return Obx(() { - if (provider.notificationUnread.value > 0) { + if (nty.notificationUnread.value > 0) { return Badge( isLabelVisible: true, offset: const Offset(-8, 2), - label: Text(provider.notificationUnread.value.toString()), + label: Text(nty.notificationUnread.value.toString()), child: button, ); } else { diff --git a/lib/screens/auth/signin.dart b/lib/screens/auth/signin.dart index 4794a49..10eed97 100644 --- a/lib/screens/auth/signin.dart +++ b/lib/screens/auth/signin.dart @@ -8,8 +8,8 @@ import 'package:solian/exts.dart'; import 'package:solian/models/auth.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/providers/content/realm.dart'; +import 'package:solian/providers/notifications.dart'; import 'package:solian/providers/relation.dart'; -import 'package:solian/providers/websocket.dart'; import 'package:solian/services.dart'; import 'package:solian/widgets/sized_container.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -178,7 +178,7 @@ class _SignInScreenState extends State { Get.find().refreshAvailableRealms(); Get.find().refreshRelativeList(); - Get.find().registerPushNotifications(); + Get.find().registerPushNotifications(); autoConfigureBackgroundNotificationService(); autoStartBackgroundNotificationService(); diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index 10ef854..f139d2c 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -20,7 +20,7 @@ import 'package:solian/providers/content/posts.dart'; import 'package:solian/providers/daily_sign.dart'; import 'package:solian/providers/database/services/messages.dart'; import 'package:solian/providers/last_read.dart'; -import 'package:solian/providers/websocket.dart'; +import 'package:solian/providers/notifications.dart'; import 'package:solian/router.dart'; import 'package:solian/screens/account/notification.dart'; import 'package:solian/theme.dart'; @@ -38,7 +38,7 @@ class DashboardScreen extends StatefulWidget { class _DashboardScreenState extends State { late final AuthProvider _auth = Get.find(); late final LastReadProvider _lastRead = Get.find(); - late final WebSocketProvider _ws = Get.find(); + late final NotificationProvider _nty = Get.find(); late final PostProvider _posts = Get.find(); late final DailySignProvider _dailySign = Get.find(); @@ -46,7 +46,7 @@ class _DashboardScreenState extends State { Theme.of(context).colorScheme.onSurface.withOpacity(0.75); List get _pendingNotifications => - List.from(_ws.notifications) + List.from(_nty.notifications) ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); List? _currentPosts; @@ -254,7 +254,7 @@ class _DashboardScreenState extends State { ), Text( 'notificationUnreadCount'.trParams({ - 'count': _ws.notifications.length.toString(), + 'count': _nty.notifications.length.toString(), }), ), ], @@ -267,12 +267,12 @@ class _DashboardScreenState extends State { isScrollControlled: true, context: context, builder: (context) => const NotificationScreen(), - ).then((_) => _ws.notificationUnread.value = 0); + ).then((_) => _nty.notificationUnread.value = 0); }, ), ], ).paddingOnly(left: 18, right: 18, bottom: 8), - if (_ws.notifications.isNotEmpty) + if (_nty.notifications.isNotEmpty) SizedBox( height: 76, child: ListView.separated( diff --git a/lib/widgets/posts/post_item.dart b/lib/widgets/posts/post_item.dart index 698664b..25fe291 100644 --- a/lib/widgets/posts/post_item.dart +++ b/lib/widgets/posts/post_item.dart @@ -660,6 +660,11 @@ class _PostHeaderWidget extends StatelessWidget { IconButton( color: Theme.of(context).colorScheme.primary, icon: const Icon(Icons.more_vert), + padding: const EdgeInsets.symmetric(horizontal: 4), + visualDensity: const VisualDensity( + horizontal: -4, + vertical: -2, + ), onPressed: () => onTapMore!(), ), ],