✨ Notifications
This commit is contained in:
		| @@ -34,6 +34,8 @@ PODS: | ||||
|     - DKImagePickerController/PhotoGallery | ||||
|     - Flutter | ||||
|   - Flutter (1.0.0) | ||||
|   - flutter_local_notifications (0.0.1): | ||||
|     - Flutter | ||||
|   - flutter_secure_storage (6.0.0): | ||||
|     - Flutter | ||||
|   - image_picker_ios (0.0.1): | ||||
| @@ -41,6 +43,8 @@ PODS: | ||||
|   - path_provider_foundation (0.0.1): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
|   - permission_handler_apple (9.3.0): | ||||
|     - Flutter | ||||
|   - SDWebImage (5.19.2): | ||||
|     - SDWebImage/Core (= 5.19.2) | ||||
|   - SDWebImage/Core (5.19.2) | ||||
| @@ -51,9 +55,11 @@ PODS: | ||||
| DEPENDENCIES: | ||||
|   - file_picker (from `.symlinks/plugins/file_picker/ios`) | ||||
|   - Flutter (from `Flutter`) | ||||
|   - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) | ||||
|   - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) | ||||
|   - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) | ||||
|   - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) | ||||
|   - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) | ||||
|   - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) | ||||
|  | ||||
| SPEC REPOS: | ||||
| @@ -68,12 +74,16 @@ EXTERNAL SOURCES: | ||||
|     :path: ".symlinks/plugins/file_picker/ios" | ||||
|   Flutter: | ||||
|     :path: Flutter | ||||
|   flutter_local_notifications: | ||||
|     :path: ".symlinks/plugins/flutter_local_notifications/ios" | ||||
|   flutter_secure_storage: | ||||
|     :path: ".symlinks/plugins/flutter_secure_storage/ios" | ||||
|   image_picker_ios: | ||||
|     :path: ".symlinks/plugins/image_picker_ios/ios" | ||||
|   path_provider_foundation: | ||||
|     :path: ".symlinks/plugins/path_provider_foundation/darwin" | ||||
|   permission_handler_apple: | ||||
|     :path: ".symlinks/plugins/permission_handler_apple/ios" | ||||
|   url_launcher_ios: | ||||
|     :path: ".symlinks/plugins/url_launcher_ios/ios" | ||||
|  | ||||
| @@ -82,9 +92,11 @@ SPEC CHECKSUMS: | ||||
|   DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 | ||||
|   file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 | ||||
|   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 | ||||
|   flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 | ||||
|   flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 | ||||
|   image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 | ||||
|   path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 | ||||
|   permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 | ||||
|   SDWebImage: dfe95b2466a9823cf9f0c6d01217c06550d7b29a | ||||
|   SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 | ||||
|   url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe | ||||
|   | ||||
| @@ -199,6 +199,7 @@ | ||||
| 				9705A1C41CF9048500538489 /* Embed Frameworks */, | ||||
| 				3B06AD1E1E4923F5004D2608 /* Thin Binary */, | ||||
| 				287A33C298CA352A7E7F32A4 /* [CP] Embed Pods Frameworks */, | ||||
| 				0818E8E4321C0D7433E07576 /* [CP] Copy Pods Resources */, | ||||
| 			); | ||||
| 			buildRules = ( | ||||
| 			); | ||||
| @@ -270,6 +271,23 @@ | ||||
| /* End PBXResourcesBuildPhase section */ | ||||
|  | ||||
| /* Begin PBXShellScriptBuildPhase section */ | ||||
| 		0818E8E4321C0D7433E07576 /* [CP] Copy Pods Resources */ = { | ||||
| 			isa = PBXShellScriptBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 			); | ||||
| 			inputFileListPaths = ( | ||||
| 				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", | ||||
| 			); | ||||
| 			name = "[CP] Copy Pods Resources"; | ||||
| 			outputFileListPaths = ( | ||||
| 				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 			shellPath = /bin/sh; | ||||
| 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; | ||||
| 			showEnvVarsInLog = 0; | ||||
| 		}; | ||||
| 		259653AE41D478F4C6BAE9B2 /* [CP] Check Pods Manifest.lock */ = { | ||||
| 			isa = PBXShellScriptBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:solian/providers/account.dart'; | ||||
| import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/providers/content/attachment.dart'; | ||||
| import 'package:solian/providers/friend.dart'; | ||||
| @@ -31,6 +32,14 @@ class SolianApp extends StatelessWidget { | ||||
|         Get.lazyPut(() => AuthProvider()); | ||||
|         Get.lazyPut(() => FriendProvider()); | ||||
|         Get.lazyPut(() => AttachmentProvider()); | ||||
|         Get.lazyPut(() => AccountProvider()); | ||||
|  | ||||
|         final AuthProvider auth = Get.find(); | ||||
|         auth.isAuthorized.then((value) async { | ||||
|           if (value) { | ||||
|             Get.find<AccountProvider>().connect(); | ||||
|           } | ||||
|         }); | ||||
|       }, | ||||
|       builder: (context, child) { | ||||
|         return ScaffoldMessenger( | ||||
|   | ||||
							
								
								
									
										161
									
								
								lib/providers/account.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								lib/providers/account.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | ||||
| import 'dart:convert'; | ||||
| import 'dart:io'; | ||||
| import 'dart:math' as math; | ||||
|  | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:permission_handler/permission_handler.dart'; | ||||
| import 'package:solian/models/notification.dart'; | ||||
| import 'package:solian/models/packet.dart'; | ||||
| import 'package:solian/models/pagination.dart'; | ||||
| import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/services.dart'; | ||||
| import 'package:web_socket_channel/io.dart'; | ||||
| import 'package:flutter_local_notifications/flutter_local_notifications.dart'; | ||||
|  | ||||
| class AccountProvider extends GetxController { | ||||
|   final FlutterLocalNotificationsPlugin localNotify = | ||||
|       FlutterLocalNotificationsPlugin(); | ||||
|  | ||||
|   RxBool isConnected = false.obs; | ||||
|   RxBool isConnecting = false.obs; | ||||
|  | ||||
|   RxInt notificationUnread = 0.obs; | ||||
|   RxList<Notification> notifications = | ||||
|       List<Notification>.empty(growable: true).obs; | ||||
|  | ||||
|   IOWebSocketChannel? websocket; | ||||
|  | ||||
|   @override | ||||
|   onInit() { | ||||
|     Permission.notification.request().then((status) { | ||||
|       notifyInitialization(); | ||||
|       notifyPrefetch(); | ||||
|     }); | ||||
|  | ||||
|     super.onInit(); | ||||
|   } | ||||
|  | ||||
|   void connect({noRetry = false}) async { | ||||
|     final AuthProvider auth = Get.find(); | ||||
|     if (!await auth.isAuthorized) throw Exception('unauthorized'); | ||||
|  | ||||
|     if (auth.credentials == null) await auth.loadCredentials(); | ||||
|  | ||||
|     final uri = Uri.parse( | ||||
|       '${ServiceFinder.services['passport']}/api/ws?tk=${auth.credentials!.accessToken}' | ||||
|           .replaceFirst('http', 'ws'), | ||||
|     ); | ||||
|  | ||||
|     isConnecting.value = true; | ||||
|  | ||||
|     try { | ||||
|       websocket = IOWebSocketChannel.connect(uri); | ||||
|       await websocket?.ready; | ||||
|     } catch (e) { | ||||
|       if (!noRetry) { | ||||
|         await auth.refreshCredentials(); | ||||
|         return connect(noRetry: true); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     listen(); | ||||
|  | ||||
|     isConnected.value = true; | ||||
|     isConnecting.value = false; | ||||
|   } | ||||
|  | ||||
|   void disconnect() { | ||||
|     websocket?.sink.close(WebSocketStatus.normalClosure); | ||||
|     isConnected.value = false; | ||||
|   } | ||||
|  | ||||
|   void listen() { | ||||
|     websocket?.stream.listen( | ||||
|       (event) { | ||||
|         final packet = NetworkPackage.fromJson(jsonDecode(event)); | ||||
|         switch (packet.method) { | ||||
|           case 'notifications.new': | ||||
|             final notification = Notification.fromJson(packet.payload!); | ||||
|             notificationUnread++; | ||||
|             notifications.add(notification); | ||||
|             notifyMessage(notification.subject, notification.content); | ||||
|             break; | ||||
|         } | ||||
|       }, | ||||
|       onDone: () { | ||||
|         isConnected.value = false; | ||||
|       }, | ||||
|       onError: (err) { | ||||
|         isConnected.value = false; | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void notifyInitialization() { | ||||
|     const androidSettings = AndroidInitializationSettings('app_icon'); | ||||
|     const darwinSettings = DarwinInitializationSettings( | ||||
|       notificationCategories: [ | ||||
|         DarwinNotificationCategory('general'), | ||||
|       ], | ||||
|     ); | ||||
|     const linuxSettings = | ||||
|         LinuxInitializationSettings(defaultActionName: 'Open notification'); | ||||
|     const InitializationSettings initializationSettings = | ||||
|         InitializationSettings( | ||||
|       android: androidSettings, | ||||
|       iOS: darwinSettings, | ||||
|       macOS: darwinSettings, | ||||
|       linux: linuxSettings, | ||||
|     ); | ||||
|  | ||||
|     localNotify.initialize(initializationSettings); | ||||
|   } | ||||
|  | ||||
|   void notifyMessage(String title, String body) { | ||||
|     const androidSettings = AndroidNotificationDetails( | ||||
|       'general', | ||||
|       'General', | ||||
|       importance: Importance.high, | ||||
|       priority: Priority.high, | ||||
|       silent: true, | ||||
|     ); | ||||
|     const darwinSettings = DarwinNotificationDetails( | ||||
|       presentAlert: true, | ||||
|       presentBanner: true, | ||||
|       presentBadge: true, | ||||
|       presentSound: false, | ||||
|     ); | ||||
|     const linuxSettings = LinuxNotificationDetails(); | ||||
|  | ||||
|     localNotify.show( | ||||
|       math.max(1, math.Random().nextInt(100000000)), | ||||
|       title, | ||||
|       body, | ||||
|       const NotificationDetails( | ||||
|         android: androidSettings, | ||||
|         iOS: darwinSettings, | ||||
|         macOS: darwinSettings, | ||||
|         linux: linuxSettings, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Future<void> notifyPrefetch() async { | ||||
|     final AuthProvider auth = Get.find(); | ||||
|     if (!await auth.isAuthorized) return; | ||||
|  | ||||
|     final client = GetConnect(); | ||||
|     client.httpClient.baseUrl = ServiceFinder.services['passport']; | ||||
|     client.httpClient.addAuthenticator(auth.requestAuthenticator); | ||||
|  | ||||
|     final resp = await client.get('/api/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; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_secure_storage/flutter_secure_storage.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:get/get_connect/http/src/request/request.dart'; | ||||
| import 'package:solian/providers/account.dart'; | ||||
| import 'package:solian/services.dart'; | ||||
| import 'package:oauth2/oauth2.dart' as oauth2; | ||||
|  | ||||
| @@ -25,26 +26,30 @@ class AuthProvider extends GetConnect { | ||||
|  | ||||
|   oauth2.Credentials? credentials; | ||||
|  | ||||
|   Future<void> refreshCredentials() async { | ||||
|     final resp = await post('/api/auth/token', { | ||||
|       'refresh_token': credentials!.refreshToken, | ||||
|       'grant_type': 'refresh_token', | ||||
|     }); | ||||
|     if (resp.statusCode != 200) { | ||||
|       throw Exception(resp.bodyString); | ||||
|     } | ||||
|     credentials = oauth2.Credentials( | ||||
|       resp.body['access_token'], | ||||
|       refreshToken: resp.body['refresh_token'], | ||||
|       idToken: resp.body['access_token'], | ||||
|       tokenEndpoint: tokenEndpoint, | ||||
|       expiration: DateTime.now().add(const Duration(minutes: 3)), | ||||
|     ); | ||||
|     storage.write( | ||||
|       key: 'auth_credentials', | ||||
|       value: jsonEncode(credentials!.toJson()), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Future<Request<T?>> requestAuthenticator<T>(Request<T?> request) async { | ||||
|     if (credentials != null && credentials!.isExpired) { | ||||
|       final resp = await post('/api/auth/token', { | ||||
|         'refresh_token': credentials!.refreshToken, | ||||
|         'grant_type': 'refresh_token', | ||||
|       }); | ||||
|       if (resp.statusCode != 200) { | ||||
|         throw Exception(resp.bodyString); | ||||
|       } | ||||
|       credentials = oauth2.Credentials( | ||||
|         resp.body['access_token'], | ||||
|         refreshToken: resp.body['refresh_token'], | ||||
|         idToken: resp.body['access_token'], | ||||
|         tokenEndpoint: tokenEndpoint, | ||||
|         expiration: DateTime.now().add(const Duration(minutes: 3)), | ||||
|       ); | ||||
|       storage.write( | ||||
|         key: 'auth_credentials', | ||||
|         value: jsonEncode(credentials!.toJson()), | ||||
|       ); | ||||
|       refreshCredentials(); | ||||
|     } | ||||
|  | ||||
|     if (credentials != null) { | ||||
| @@ -91,12 +96,16 @@ class AuthProvider extends GetConnect { | ||||
|       value: jsonEncode(credentials!.toJson()), | ||||
|     ); | ||||
|  | ||||
|     Get.find<AccountProvider>().connect(); | ||||
|  | ||||
|     return credentials!; | ||||
|   } | ||||
|  | ||||
|   void signout() { | ||||
|     _cacheUserProfileResponse = null; | ||||
|  | ||||
|     Get.find<AccountProvider>().disconnect(); | ||||
|  | ||||
|     storage.deleteAll(); | ||||
|   } | ||||
|  | ||||
|   | ||||
							
								
								
									
										210
									
								
								lib/screens/account/notification.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								lib/screens/account/notification.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,210 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_animate/flutter_animate.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:solian/providers/account.dart'; | ||||
| import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/services.dart'; | ||||
| import 'package:solian/models/notification.dart' as notify; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
| import 'package:uuid/uuid.dart'; | ||||
|  | ||||
| class NotificationScreen extends StatefulWidget { | ||||
|   const NotificationScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   State<NotificationScreen> createState() => _NotificationScreenState(); | ||||
| } | ||||
|  | ||||
| class _NotificationScreenState extends State<NotificationScreen> { | ||||
|   bool _isBusy = false; | ||||
|  | ||||
|   Future<void> markAllRead() async { | ||||
|     final AuthProvider auth = Get.find(); | ||||
|     if (!await auth.isAuthorized) return; | ||||
|  | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     final AccountProvider provider = Get.find(); | ||||
|  | ||||
|     List<int> markList = List.empty(growable: true); | ||||
|     for (final element in provider.notifications) { | ||||
|       if (element.isRealtime) continue; | ||||
|       markList.add(element.id); | ||||
|     } | ||||
|  | ||||
|     if (markList.isNotEmpty) { | ||||
|       final client = GetConnect(); | ||||
|       client.httpClient.baseUrl = ServiceFinder.services['passport']; | ||||
|       client.httpClient.addAuthenticator(auth.requestAuthenticator); | ||||
|  | ||||
|       await client.put('/api/notifications/batch/read', {'messages': markList}); | ||||
|     } | ||||
|  | ||||
|     provider.notifications.clear(); | ||||
|  | ||||
|     setState(() => _isBusy = false); | ||||
|   } | ||||
|  | ||||
|   Future<void> markOneRead(notify.Notification element, int index) async { | ||||
|     final AuthProvider auth = Get.find(); | ||||
|     if (!await auth.isAuthorized) return; | ||||
|  | ||||
|     final AccountProvider provider = Get.find(); | ||||
|  | ||||
|     if (element.isRealtime) { | ||||
|       provider.notifications.removeAt(index); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     final client = GetConnect(); | ||||
|     client.httpClient.baseUrl = ServiceFinder.services['passport']; | ||||
|     client.httpClient.addAuthenticator(auth.requestAuthenticator); | ||||
|  | ||||
|     await client.put('/api/notifications/${element.id}/read', {}); | ||||
|  | ||||
|     provider.notifications.removeAt(index); | ||||
|  | ||||
|     setState(() => _isBusy = false); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final AccountProvider provider = Get.find(); | ||||
|  | ||||
|     return SizedBox( | ||||
|       height: MediaQuery.of(context).size.height * 0.85, | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           Text( | ||||
|             'notification'.tr, | ||||
|             style: Theme.of(context).textTheme.headlineSmall, | ||||
|           ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), | ||||
|           Expanded( | ||||
|             child: Obx(() { | ||||
|               return CustomScrollView( | ||||
|                 slivers: [ | ||||
|                   if (_isBusy) | ||||
|                     SliverToBoxAdapter( | ||||
|                       child: const LinearProgressIndicator().animate().scaleX(), | ||||
|                     ), | ||||
|                   if (provider.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), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                   if (provider.notifications.isNotEmpty) | ||||
|                     SliverToBoxAdapter( | ||||
|                       child: ListTile( | ||||
|                         tileColor: | ||||
|                             Theme.of(context).colorScheme.secondaryContainer, | ||||
|                         leading: const Icon(Icons.checklist), | ||||
|                         title: Text('notifyAllRead'.tr), | ||||
|                         contentPadding: | ||||
|                             const EdgeInsets.symmetric(horizontal: 28), | ||||
|                         onTap: _isBusy ? null : () => markAllRead(), | ||||
|                       ), | ||||
|                     ), | ||||
|                   SliverList.separated( | ||||
|                     itemCount: provider.notifications.length, | ||||
|                     itemBuilder: (BuildContext context, int index) { | ||||
|                       var element = provider.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.subject), | ||||
|                           subtitle: Column( | ||||
|                             children: [ | ||||
|                               Text(element.content), | ||||
|                               if (element.links != null) | ||||
|                                 Row( | ||||
|                                   children: element.links! | ||||
|                                       .map((e) => InkWell( | ||||
|                                             child: Text( | ||||
|                                               e.label, | ||||
|                                               style: TextStyle( | ||||
|                                                 color: Theme.of(context) | ||||
|                                                     .colorScheme | ||||
|                                                     .onSecondaryContainer, | ||||
|                                                 decoration: | ||||
|                                                     TextDecoration.underline, | ||||
|                                               ), | ||||
|                                             ), | ||||
|                                             onTap: () { | ||||
|                                               launchUrlString(e.url); | ||||
|                                             }, | ||||
|                                           ).paddingOnly(right: 5)) | ||||
|                                       .toList(), | ||||
|                                 ), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                         onDismissed: (_) => markOneRead(element, index), | ||||
|                       ); | ||||
|                     }, | ||||
|                     separatorBuilder: (_, __) => | ||||
|                         const Divider(thickness: 0.3, height: 0.3), | ||||
|                   ), | ||||
|                 ], | ||||
|               ); | ||||
|             }), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class NotificationButton extends StatelessWidget { | ||||
|   const NotificationButton({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final AccountProvider provider = Get.find(); | ||||
|  | ||||
|     final button = IconButton( | ||||
|       icon: const Icon(Icons.notifications), | ||||
|       onPressed: () { | ||||
|         showModalBottomSheet( | ||||
|           useRootNavigator: true, | ||||
|           isScrollControlled: true, | ||||
|           context: context, | ||||
|           builder: (context) => const NotificationScreen(), | ||||
|         ).then((_) => provider.notificationUnread.value = 0); | ||||
|       }, | ||||
|     ); | ||||
|  | ||||
|     return Obx(() { | ||||
|       if (provider.notificationUnread.value > 0) { | ||||
|         return Badge( | ||||
|           isLabelVisible: true, | ||||
|           offset: const Offset(-8, 2), | ||||
|           label: Text(provider.notificationUnread.value.toString()), | ||||
|           child: button, | ||||
|         ); | ||||
|       } else { | ||||
|         return button; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| @@ -6,6 +6,8 @@ import 'package:solian/models/post.dart'; | ||||
| import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/providers/content/post_explore.dart'; | ||||
| import 'package:solian/router.dart'; | ||||
| import 'package:solian/screens/account/notification.dart'; | ||||
| import 'package:solian/theme.dart'; | ||||
| import 'package:solian/widgets/posts/post_action.dart'; | ||||
| import 'package:solian/widgets/posts/post_item.dart'; | ||||
|  | ||||
| @@ -69,32 +71,61 @@ class _SocialScreenState extends State<SocialScreen> { | ||||
|           }), | ||||
|       body: Material( | ||||
|         color: Theme.of(context).colorScheme.surface, | ||||
|         child: RefreshIndicator( | ||||
|           onRefresh: () => Future.sync(() => _pagingController.refresh()), | ||||
|           child: PagedListView<int, Post>.separated( | ||||
|             pagingController: _pagingController, | ||||
|             builderDelegate: PagedChildBuilderDelegate<Post>( | ||||
|               itemBuilder: (context, item, index) { | ||||
|                 return GestureDetector( | ||||
|                   child: PostItem(key: Key('p${item.alias}'), item: item) | ||||
|                       .paddingSymmetric( | ||||
|                     vertical: (item.attachments?.isEmpty ?? false) ? 8 : 0, | ||||
|         child: SafeArea( | ||||
|           child: NestedScrollView( | ||||
|             headerSliverBuilder: (context, innerBoxIsScrolled) { | ||||
|               return [ | ||||
|                 SliverOverlapAbsorber( | ||||
|                   handle: | ||||
|                       NestedScrollView.sliverOverlapAbsorberHandleFor(context), | ||||
|                   sliver: SliverAppBar( | ||||
|                     title: Text('social'.tr), | ||||
|                     centerTitle: false, | ||||
|                     titleSpacing: | ||||
|                         SolianTheme.isLargeScreen(context) ? null : 24, | ||||
|                     forceElevated: innerBoxIsScrolled, | ||||
|                     actions: const [ | ||||
|                       NotificationButton(), | ||||
|                     ], | ||||
|                   ), | ||||
|                   onTap: () {}, | ||||
|                   onLongPress: () { | ||||
|                     showModalBottomSheet( | ||||
|                       useRootNavigator: true, | ||||
|                       context: context, | ||||
|                       builder: (context) => PostAction(item: item), | ||||
|                     ).then((value) { | ||||
|                       if (value == true) _pagingController.refresh(); | ||||
|                     }); | ||||
|                   }, | ||||
|                 ); | ||||
|               }, | ||||
|                 ), | ||||
|               ]; | ||||
|             }, | ||||
|             body: MediaQuery.removePadding( | ||||
|               removeTop: true, | ||||
|               context: context, | ||||
|               child: RefreshIndicator( | ||||
|                 onRefresh: () => Future.sync(() => _pagingController.refresh()), | ||||
|                 child: PagedListView<int, Post>.separated( | ||||
|                   pagingController: _pagingController, | ||||
|                   builderDelegate: PagedChildBuilderDelegate<Post>( | ||||
|                     itemBuilder: (context, item, index) { | ||||
|                       return GestureDetector( | ||||
|                         child: PostItem( | ||||
|                           key: Key('p${item.alias}'), | ||||
|                           item: item, | ||||
|                         ).paddingSymmetric( | ||||
|                           vertical: | ||||
|                               (item.attachments?.isEmpty ?? false) ? 8 : 0, | ||||
|                         ), | ||||
|                         onTap: () {}, | ||||
|                         onLongPress: () { | ||||
|                           showModalBottomSheet( | ||||
|                             useRootNavigator: true, | ||||
|                             context: context, | ||||
|                             builder: (context) => PostAction(item: item), | ||||
|                           ).then((value) { | ||||
|                             if (value == true) _pagingController.refresh(); | ||||
|                           }); | ||||
|                         }, | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                   separatorBuilder: (_, __) => | ||||
|                       const Divider(thickness: 0.3, height: 0.3), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             separatorBuilder: (_, __) => | ||||
|                 const Divider(thickness: 0.3, height: 0.3), | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|   | ||||
| @@ -18,6 +18,7 @@ class SolianMessages extends Translations { | ||||
|           'delete': 'Delete', | ||||
|           'reply': 'Reply', | ||||
|           'repost': 'Repost', | ||||
|           'notification': 'Notification', | ||||
|           'errorHappened': 'An error occurred', | ||||
|           'email': 'Email', | ||||
|           'username': 'Username', | ||||
| @@ -49,7 +50,7 @@ class SolianMessages extends Translations { | ||||
|           'signinRiskDetected': | ||||
|               'Risk detected, click Next to open a webpage and signin through it to pass security check.', | ||||
|           'signup': 'Sign up', | ||||
|           'signupGreeting': 'Welcome onboard 👋', | ||||
|           'signupGreeting': 'Welcome onboard', | ||||
|           'signupCaption': | ||||
|               'Create an account on Solarpass and then get the access of entire Solar Network!', | ||||
|           'signout': 'Sign out', | ||||
| @@ -57,6 +58,9 @@ class SolianMessages extends Translations { | ||||
|           'matureContent': 'Mature Content', | ||||
|           'matureContentCaption': | ||||
|               'The content is rated and may not suitable for everyone to view', | ||||
|           'notifyAllRead': 'Mark all as read', | ||||
|           'notifyEmpty': 'All notifications read', | ||||
|           'notifyEmptyCaption': 'It seems like nothing happened recently', | ||||
|           'postAction': 'Post', | ||||
|           'postPublishing': 'Post a post', | ||||
|           'postIdentityNotify': 'You will post this post as', | ||||
| @@ -98,6 +102,7 @@ class SolianMessages extends Translations { | ||||
|           'apply': '应用', | ||||
|           'reply': '回复', | ||||
|           'repost': '转帖', | ||||
|           'notification': '通知', | ||||
|           'errorHappened': '发生错误了', | ||||
|           'email': '邮件地址', | ||||
|           'username': '用户名', | ||||
| @@ -131,6 +136,9 @@ class SolianMessages extends Translations { | ||||
|           'riskDetection': '检测到风险', | ||||
|           'matureContent': '评级内容', | ||||
|           'matureContentCaption': '该内容已被评级为家长指导级或以上,这可能说明内容包含一系列不友好的成分', | ||||
|           'notifyAllRead': '已读所有通知', | ||||
|           'notifyEmpty': '通知箱为空', | ||||
|           'notifyEmptyCaption': '看起来最近没发生什么呢', | ||||
|           'postAction': '发表', | ||||
|           'postPublishing': '发表帖子', | ||||
|           'postIdentityNotify': '你将会以本身份发表帖子', | ||||
|   | ||||
| @@ -6,12 +6,14 @@ import FlutterMacOS | ||||
| import Foundation | ||||
|  | ||||
| import file_selector_macos | ||||
| import flutter_local_notifications | ||||
| import flutter_secure_storage_macos | ||||
| import path_provider_foundation | ||||
| import url_launcher_macos | ||||
|  | ||||
| func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { | ||||
|   FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) | ||||
|   FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) | ||||
|   FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) | ||||
|   PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) | ||||
|   UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) | ||||
|   | ||||
							
								
								
									
										154
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										154
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -89,6 +89,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.0.8" | ||||
|   dbus: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: dbus | ||||
|       sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.7.10" | ||||
|   fake_async: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -145,6 +153,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.9.3+1" | ||||
|   fixnum: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: fixnum | ||||
|       sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.1.0" | ||||
|   flutter: | ||||
|     dependency: "direct main" | ||||
|     description: flutter | ||||
| @@ -166,6 +182,30 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.0.2" | ||||
|   flutter_local_notifications: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_local_notifications | ||||
|       sha256: "40e6fbd2da7dcc7ed78432c5cdab1559674b4af035fddbfb2f9a8f9c2112fcef" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "17.1.2" | ||||
|   flutter_local_notifications_linux: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_local_notifications_linux | ||||
|       sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.0.0+1" | ||||
|   flutter_local_notifications_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_local_notifications_platform_interface | ||||
|       sha256: "340abf67df238f7f0ef58f4a26d2a83e1ab74c77ab03cd2b2d5018ac64db30b7" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "7.1.0" | ||||
|   flutter_markdown: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -186,10 +226,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_secure_storage | ||||
|       sha256: "8496a89eea74e23f92581885f876455d9d460e71201405dffe5f55dfe1155864" | ||||
|       sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "9.2.1" | ||||
|     version: "9.2.2" | ||||
|   flutter_secure_storage_linux: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -202,10 +242,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_secure_storage_macos | ||||
|       sha256: b768a7dab26d6186b68e2831b3104f8968154f0f4fdbf66e7c2dd7bdf299daaf | ||||
|       sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.1.1" | ||||
|     version: "3.1.2" | ||||
|   flutter_secure_storage_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -276,10 +316,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: go_router | ||||
|       sha256: "7685acd06244ba4be60f455c5cafe5790c63dc91fc03f7385b1e922a6b85b17c" | ||||
|       sha256: "6ad5662b014c06c20fa46ab78715c96b2222a7fe4f87bf77e0289592c2539e86" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "14.1.1" | ||||
|     version: "14.1.3" | ||||
|   http: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -300,10 +340,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: image | ||||
|       sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" | ||||
|       sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.1.7" | ||||
|     version: "4.2.0" | ||||
|   image_picker: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -536,6 +576,54 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.2.1" | ||||
|   permission_handler: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: permission_handler | ||||
|       sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "11.3.1" | ||||
|   permission_handler_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: permission_handler_android | ||||
|       sha256: "8bb852cd759488893805c3161d0b2b5db55db52f773dbb014420b304055ba2c5" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "12.0.6" | ||||
|   permission_handler_apple: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: permission_handler_apple | ||||
|       sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "9.4.4" | ||||
|   permission_handler_html: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: permission_handler_html | ||||
|       sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.1.1" | ||||
|   permission_handler_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: permission_handler_platform_interface | ||||
|       sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.2.1" | ||||
|   permission_handler_windows: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: permission_handler_windows | ||||
|       sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.2.1" | ||||
|   petitparser: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -581,6 +669,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.10.0" | ||||
|   sprintf: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: sprintf | ||||
|       sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "7.0.0" | ||||
|   stack_trace: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -629,6 +725,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.6.1" | ||||
|   timezone: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: timezone | ||||
|       sha256: a6ccda4a69a442098b602c44e61a1e2b4bf6f5516e875bbf0f427d5df14745d5 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.9.3" | ||||
|   typed_data: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -649,10 +753,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_android | ||||
|       sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" | ||||
|       sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.3.1" | ||||
|     version: "6.3.2" | ||||
|   url_launcher_ios: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -701,6 +805,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.1.1" | ||||
|   uuid: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: uuid | ||||
|       sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.4.0" | ||||
|   vector_math: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -725,14 +837,30 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.5.1" | ||||
|   web_socket: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: web_socket | ||||
|       sha256: "217f49b5213796cb508d6a942a5dc604ce1cb6a0a6b3d8cb3f0c314f0ecea712" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.1.4" | ||||
|   web_socket_channel: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: web_socket_channel | ||||
|       sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.0.0" | ||||
|   win32: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: win32 | ||||
|       sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" | ||||
|       sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "5.5.0" | ||||
|     version: "5.5.1" | ||||
|   xdg_directories: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -750,5 +878,5 @@ packages: | ||||
|     source: hosted | ||||
|     version: "6.5.0" | ||||
| sdks: | ||||
|   dart: ">=3.3.4 <4.0.0" | ||||
|   dart: ">=3.4.0 <4.0.0" | ||||
|   flutter: ">=3.19.0" | ||||
|   | ||||
| @@ -52,6 +52,10 @@ dependencies: | ||||
|   intl: ^0.19.0 | ||||
|   image: ^4.1.7 | ||||
|   font_awesome_flutter: ^10.7.0 | ||||
|   web_socket_channel: ^3.0.0 | ||||
|   flutter_local_notifications: ^17.1.2 | ||||
|   permission_handler: ^11.3.1 | ||||
|   uuid: ^4.4.0 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
| @@ -8,6 +8,7 @@ | ||||
|  | ||||
| #include <file_selector_windows/file_selector_windows.h> | ||||
| #include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h> | ||||
| #include <permission_handler_windows/permission_handler_windows_plugin.h> | ||||
| #include <url_launcher_windows/url_launcher_windows.h> | ||||
|  | ||||
| void RegisterPlugins(flutter::PluginRegistry* registry) { | ||||
| @@ -15,6 +16,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { | ||||
|       registry->GetRegistrarForPlugin("FileSelectorWindows")); | ||||
|   FlutterSecureStorageWindowsPluginRegisterWithRegistrar( | ||||
|       registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); | ||||
|   PermissionHandlerWindowsPluginRegisterWithRegistrar( | ||||
|       registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); | ||||
|   UrlLauncherWindowsRegisterWithRegistrar( | ||||
|       registry->GetRegistrarForPlugin("UrlLauncherWindows")); | ||||
| } | ||||
|   | ||||
| @@ -5,6 +5,7 @@ | ||||
| list(APPEND FLUTTER_PLUGIN_LIST | ||||
|   file_selector_windows | ||||
|   flutter_secure_storage_windows | ||||
|   permission_handler_windows | ||||
|   url_launcher_windows | ||||
| ) | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user