✨ Post reactions
This commit is contained in:
		| @@ -29,14 +29,8 @@ class SolianApp extends StatelessWidget { | |||||||
|         Get.lazyPut(() => AuthProvider()); |         Get.lazyPut(() => AuthProvider()); | ||||||
|       }, |       }, | ||||||
|       builder: (context, child) { |       builder: (context, child) { | ||||||
|         return Overlay( |         return ScaffoldMessenger( | ||||||
|           initialEntries: [ |  | ||||||
|             OverlayEntry( |  | ||||||
|               builder: (context) => ScaffoldMessenger( |  | ||||||
|           child: child ?? Container(), |           child: child ?? Container(), | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|           ], |  | ||||||
|         ); |         ); | ||||||
|       }, |       }, | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ class Post { | |||||||
|   Account author; |   Account author; | ||||||
|   int replyCount; |   int replyCount; | ||||||
|   int reactionCount; |   int reactionCount; | ||||||
|   dynamic reactionList; |   Map<String, int> reactionList; | ||||||
|  |  | ||||||
|   Post({ |   Post({ | ||||||
|     required this.id, |     required this.id, | ||||||
| @@ -75,7 +75,12 @@ class Post { | |||||||
|         author: Account.fromJson(json["author"]), |         author: Account.fromJson(json["author"]), | ||||||
|         replyCount: json["reply_count"], |         replyCount: json["reply_count"], | ||||||
|         reactionCount: json["reaction_count"], |         reactionCount: json["reaction_count"], | ||||||
|     reactionList: json["reaction_list"], |         reactionList: json["reaction_list"] != null | ||||||
|  |             ? json["reaction_list"] | ||||||
|  |                 .map((key, value) => | ||||||
|  |                     MapEntry(key, int.tryParse(value.toString()) ?? (value is double ? value.toInt() : null))) | ||||||
|  |                 .cast<String, int>() | ||||||
|  |             : {}, | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|   Map<String, dynamic> toJson() => { |   Map<String, dynamic> toJson() => { | ||||||
|   | |||||||
| @@ -13,4 +13,26 @@ final Map<String, ReactInfo> reactions = { | |||||||
|   'confuse': ReactInfo(icon: '🧐', attitude: 0), |   'confuse': ReactInfo(icon: '🧐', attitude: 0), | ||||||
|   'retard': ReactInfo(icon: '🤪', attitude: 0), |   'retard': ReactInfo(icon: '🤪', attitude: 0), | ||||||
|   'clap': ReactInfo(icon: '👏', attitude: 1), |   'clap': ReactInfo(icon: '👏', attitude: 1), | ||||||
|  |   'heart': ReactInfo(icon: '❤️', attitude: 1), | ||||||
|  |   'laugh': ReactInfo(icon: '😂', attitude: 1), | ||||||
|  |   'angry': ReactInfo(icon: '😡', attitude: 2), | ||||||
|  |   'surprise': ReactInfo(icon: '😲', attitude: 0), | ||||||
|  |   'party': ReactInfo(icon: '🎉', attitude: 1), | ||||||
|  |   'wink': ReactInfo(icon: '😉', attitude: 1), | ||||||
|  |   'scream': ReactInfo(icon: '😱', attitude: 0), | ||||||
|  |   'sleep': ReactInfo(icon: '😴', attitude: 0), | ||||||
|  |   'thinking': ReactInfo(icon: '🤔', attitude: 0), | ||||||
|  |   'blush': ReactInfo(icon: '😊', attitude: 1), | ||||||
|  |   'cool': ReactInfo(icon: '😎', attitude: 1), | ||||||
|  |   'frown': ReactInfo(icon: '☹️', attitude: 2), | ||||||
|  |   'nauseated': ReactInfo(icon: '🤢', attitude: 2), | ||||||
|  |   'facepalm': ReactInfo(icon: '🤦', attitude: 0), | ||||||
|  |   'shrug': ReactInfo(icon: '🤷', attitude: 0), | ||||||
|  |   'joy': ReactInfo(icon: '🤣', attitude: 1), | ||||||
|  |   'relieved': ReactInfo(icon: '😌', attitude: 0), | ||||||
|  |   'disappointed': ReactInfo(icon: '😞', attitude: 2), | ||||||
|  |   'smirk': ReactInfo(icon: '😏', attitude: 1), | ||||||
|  |   'astonished': ReactInfo(icon: '😮', attitude: 0), | ||||||
|  |   'hug': ReactInfo(icon: '🤗', attitude: 1), | ||||||
|  |   'pray': ReactInfo(icon: '🙏', attitude: 1), | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -29,10 +29,7 @@ class _SignInScreenState extends State<SignInScreen> { | |||||||
|       if (messages.last.contains('risk')) { |       if (messages.last.contains('risk')) { | ||||||
|         final ticketId = RegExp(r'ticketId=(\d+)').firstMatch(messages.last); |         final ticketId = RegExp(r'ticketId=(\d+)').firstMatch(messages.last); | ||||||
|         if (ticketId == null) { |         if (ticketId == null) { | ||||||
|           Get.showSnackbar(GetSnackBar( |           Get.snackbar('errorHappened'.tr, 'Requested to multi-factor authenticate, but the ticket id was not found'); | ||||||
|             title: 'errorHappened'.tr, |  | ||||||
|             message: 'requested to multi-factor authenticate, but the ticket id was not found', |  | ||||||
|           )); |  | ||||||
|         } |         } | ||||||
|         showDialog( |         showDialog( | ||||||
|           context: context, |           context: context, | ||||||
|   | |||||||
| @@ -28,14 +28,14 @@ class _SignUpScreenState extends State<SignUpScreen> { | |||||||
|  |  | ||||||
|     final client = GetConnect(); |     final client = GetConnect(); | ||||||
|     client.httpClient.baseUrl = ServiceFinder.services['passport']; |     client.httpClient.baseUrl = ServiceFinder.services['passport']; | ||||||
|     final res = await client.post('/api/users', { |     final resp = await client.post('/api/users', { | ||||||
|       'name': username, |       'name': username, | ||||||
|       'nick': nickname, |       'nick': nickname, | ||||||
|       'email': email, |       'email': email, | ||||||
|       'password': password, |       'password': password, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     if (res.statusCode == 200) { |     if (resp.statusCode == 200) { | ||||||
|       showDialog( |       showDialog( | ||||||
|         context: context, |         context: context, | ||||||
|         builder: (context) { |         builder: (context) { | ||||||
| @@ -54,10 +54,7 @@ class _SignUpScreenState extends State<SignUpScreen> { | |||||||
|         AppRouter.instance.replaceNamed('auth.sign-in'); |         AppRouter.instance.replaceNamed('auth.sign-in'); | ||||||
|       }); |       }); | ||||||
|     } else { |     } else { | ||||||
|       Get.showSnackbar(GetSnackBar( |       Get.snackbar('errorHappened'.tr, resp.bodyString!); | ||||||
|         title: 'errorHappened'.tr, |  | ||||||
|         message: res.bodyString, |  | ||||||
|       )); |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -34,10 +34,7 @@ class _PostPublishingScreenState extends State<PostPublishingScreen> { | |||||||
|       'content': _contentController.value.text, |       'content': _contentController.value.text, | ||||||
|     }); |     }); | ||||||
|     if (resp.statusCode != 200) { |     if (resp.statusCode != 200) { | ||||||
|       Get.showSnackbar(GetSnackBar( |       Get.snackbar('errorHappened'.tr, resp.bodyString!); | ||||||
|         title: 'errorHappened'.tr, |  | ||||||
|         message: resp.bodyString, |  | ||||||
|       )); |  | ||||||
|     } else { |     } else { | ||||||
|       AppRouter.instance.pop(resp.body); |       AppRouter.instance.pop(resp.body); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -29,6 +29,10 @@ class SolianMessages extends Translations { | |||||||
|           'postPublishing': 'Post a post', |           'postPublishing': 'Post a post', | ||||||
|           'postIdentityNotify': 'You will post this post as', |           'postIdentityNotify': 'You will post this post as', | ||||||
|           'postContentPlaceholder': 'What\'s happened?!', |           'postContentPlaceholder': 'What\'s happened?!', | ||||||
|  |           'postReaction': 'Reactions of the Post', | ||||||
|  |           'reactAdd': 'React', | ||||||
|  |           'reactCompleted': 'Your reaction has been added', | ||||||
|  |           'reactUncompleted': 'Your reaction has been removed' | ||||||
|         }, |         }, | ||||||
|         'zh_CN': { |         'zh_CN': { | ||||||
|           'next': '下一步', |           'next': '下一步', | ||||||
| @@ -55,6 +59,10 @@ class SolianMessages extends Translations { | |||||||
|           'postPublishing': '发表帖子', |           'postPublishing': '发表帖子', | ||||||
|           'postIdentityNotify': '你将会以本身份发表帖子', |           'postIdentityNotify': '你将会以本身份发表帖子', | ||||||
|           'postContentPlaceholder': '发生什么事了?!', |           'postContentPlaceholder': '发生什么事了?!', | ||||||
|  |           'postReaction': '帖子的反应', | ||||||
|  |           'reactAdd': '作出反应', | ||||||
|  |           'reactCompleted': '你的反应已被添加', | ||||||
|  |           'reactUncompleted': '你的反应已被移除' | ||||||
|         } |         } | ||||||
|       }; |       }; | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										115
									
								
								lib/widgets/posts/post_action.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								lib/widgets/posts/post_action.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | |||||||
|  | import 'package:flutter/cupertino.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:get/get.dart'; | ||||||
|  | import 'package:solian/models/post.dart'; | ||||||
|  | import 'package:solian/models/reaction.dart'; | ||||||
|  | import 'package:solian/providers/auth.dart'; | ||||||
|  | import 'package:solian/services.dart'; | ||||||
|  | import 'package:solian/widgets/posts/post_reaction.dart'; | ||||||
|  |  | ||||||
|  | class PostQuickAction extends StatefulWidget { | ||||||
|  |   final Post item; | ||||||
|  |   final void Function(String symbol, int num) onReact; | ||||||
|  |  | ||||||
|  |   const PostQuickAction({super.key, required this.item, required this.onReact}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   State<PostQuickAction> createState() => _PostQuickActionState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _PostQuickActionState extends State<PostQuickAction> { | ||||||
|  |   bool _isSubmitting = false; | ||||||
|  |  | ||||||
|  |   void showReactMenu() { | ||||||
|  |     showModalBottomSheet( | ||||||
|  |       useRootNavigator: true, | ||||||
|  |       isScrollControlled: true, | ||||||
|  |       context: context, | ||||||
|  |       builder: (context) => PostReactionPopup( | ||||||
|  |         item: widget.item, | ||||||
|  |         onReact: (key, value) { | ||||||
|  |           doWidgetReact(key, value.attitude); | ||||||
|  |         }, | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> doWidgetReact(String symbol, int attitude) async { | ||||||
|  |     final AuthProvider auth = Get.find(); | ||||||
|  |  | ||||||
|  |     if (_isSubmitting) return; | ||||||
|  |     if (!await auth.isAuthorized) return; | ||||||
|  |  | ||||||
|  |     final client = GetConnect(); | ||||||
|  |     client.httpClient.baseUrl = ServiceFinder.services['interactive']; | ||||||
|  |     client.httpClient.addAuthenticator(auth.reqAuthenticator); | ||||||
|  |  | ||||||
|  |     setState(() => _isSubmitting = true); | ||||||
|  |  | ||||||
|  |     final resp = await client.post('/api/posts/${widget.item.alias}/react', { | ||||||
|  |       'symbol': symbol, | ||||||
|  |       'attitude': attitude, | ||||||
|  |     }); | ||||||
|  |     if (resp.statusCode == 201) { | ||||||
|  |       widget.onReact(symbol, 1); | ||||||
|  |       Get.snackbar('', 'reactCompleted'.tr); | ||||||
|  |     } else if (resp.statusCode == 204) { | ||||||
|  |       widget.onReact(symbol, -1); | ||||||
|  |       Get.snackbar('', 'reactUncompleted'.tr); | ||||||
|  |     } else { | ||||||
|  |       Get.snackbar('errorHappened'.tr, resp.bodyString!); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     setState(() => _isSubmitting = false); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     const density = VisualDensity(horizontal: -4, vertical: -3); | ||||||
|  |  | ||||||
|  |     return SizedBox( | ||||||
|  |       height: 32, | ||||||
|  |       width: double.infinity, | ||||||
|  |       child: Row( | ||||||
|  |         mainAxisAlignment: MainAxisAlignment.start, | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |         children: [ | ||||||
|  |           ActionChip( | ||||||
|  |             avatar: const Icon(Icons.comment), | ||||||
|  |             label: Text(widget.item.replyCount.toString()), | ||||||
|  |             visualDensity: density, | ||||||
|  |             onPressed: () {}, | ||||||
|  |           ), | ||||||
|  |           const VerticalDivider(thickness: 0.3, width: 0.3, indent: 8, endIndent: 8).paddingOnly(left: 8), | ||||||
|  |           Expanded( | ||||||
|  |             child: ListView( | ||||||
|  |               shrinkWrap: true, | ||||||
|  |               scrollDirection: Axis.horizontal, | ||||||
|  |               children: [ | ||||||
|  |                 ...widget.item.reactionList.entries.map((x) { | ||||||
|  |                   final info = reactions[x.key]; | ||||||
|  |                   return Padding( | ||||||
|  |                     padding: const EdgeInsets.only(right: 8), | ||||||
|  |                     child: ActionChip( | ||||||
|  |                       avatar: Text(info!.icon), | ||||||
|  |                       label: Text(x.value.toString()), | ||||||
|  |                       tooltip: ':${x.key}:', | ||||||
|  |                       visualDensity: density, | ||||||
|  |                       onPressed: _isSubmitting ? null : () => doWidgetReact(x.key, info.attitude), | ||||||
|  |                     ), | ||||||
|  |                   ); | ||||||
|  |                 }), | ||||||
|  |                 ActionChip( | ||||||
|  |                   avatar: const Icon(Icons.add_reaction, color: Colors.teal), | ||||||
|  |                   label: Text('reactAdd'.tr), | ||||||
|  |                   visualDensity: density, | ||||||
|  |                   onPressed: () => showReactMenu(), | ||||||
|  |                 ), | ||||||
|  |               ], | ||||||
|  |             ).paddingOnly(left: 8), | ||||||
|  |           ) | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -5,6 +5,7 @@ import 'package:get/get_utils/get_utils.dart'; | |||||||
| import 'package:solian/models/post.dart'; | import 'package:solian/models/post.dart'; | ||||||
| import 'package:solian/widgets/account/account_avatar.dart'; | import 'package:solian/widgets/account/account_avatar.dart'; | ||||||
| import 'package:solian/widgets/attachments/attachment_list.dart'; | import 'package:solian/widgets/attachments/attachment_list.dart'; | ||||||
|  | import 'package:solian/widgets/posts/post_action.dart'; | ||||||
| import 'package:timeago/timeago.dart' show format; | import 'package:timeago/timeago.dart' show format; | ||||||
|  |  | ||||||
| class PostItem extends StatefulWidget { | class PostItem extends StatefulWidget { | ||||||
| @@ -17,30 +18,40 @@ class PostItem extends StatefulWidget { | |||||||
| } | } | ||||||
|  |  | ||||||
| class _PostItemState extends State<PostItem> { | class _PostItemState extends State<PostItem> { | ||||||
|  |   late final Post item; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void initState() { | ||||||
|  |     item = widget.item; | ||||||
|  |     super.initState(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|  |     final hasAttachment = item.attachments?.isNotEmpty ?? false; | ||||||
|  |  | ||||||
|     return Column( |     return Column( | ||||||
|       children: [ |       children: [ | ||||||
|         Row( |         Row( | ||||||
|           crossAxisAlignment: CrossAxisAlignment.start, |           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|           children: [ |           children: [ | ||||||
|             AccountAvatar(content: widget.item.author.avatar), |             AccountAvatar(content: item.author.avatar), | ||||||
|             Expanded( |             Expanded( | ||||||
|               child: Column( |               child: Column( | ||||||
|                 children: [ |                 children: [ | ||||||
|                   Row( |                   Row( | ||||||
|                     children: [ |                     children: [ | ||||||
|                       Text( |                       Text( | ||||||
|                         widget.item.author.nick, |                         item.author.nick, | ||||||
|                         style: const TextStyle(fontWeight: FontWeight.bold), |                         style: const TextStyle(fontWeight: FontWeight.bold), | ||||||
|                       ).paddingOnly(left: 12), |                       ).paddingOnly(left: 12), | ||||||
|                       Text(format(widget.item.createdAt, locale: 'en_short')).paddingOnly(left: 4), |                       Text(format(item.createdAt, locale: 'en_short')).paddingOnly(left: 4), | ||||||
|                     ], |                     ], | ||||||
|                   ), |                   ), | ||||||
|                   Markdown( |                   Markdown( | ||||||
|                     shrinkWrap: true, |                     shrinkWrap: true, | ||||||
|                     physics: const NeverScrollableScrollPhysics(), |                     physics: const NeverScrollableScrollPhysics(), | ||||||
|                     data: widget.item.content, |                     data: item.content, | ||||||
|                     padding: const EdgeInsets.all(0), |                     padding: const EdgeInsets.all(0), | ||||||
|                   ).paddingOnly(left: 12, right: 8), |                   ).paddingOnly(left: 12, right: 8), | ||||||
|                 ], |                 ], | ||||||
| @@ -49,11 +60,24 @@ class _PostItemState extends State<PostItem> { | |||||||
|           ], |           ], | ||||||
|         ).paddingOnly( |         ).paddingOnly( | ||||||
|           top: 18, |           top: 18, | ||||||
|           bottom: (widget.item.attachments?.isNotEmpty ?? false) ? 10 : 18, |           bottom: hasAttachment ? 10 : 0, | ||||||
|           right: 16, |           right: 16, | ||||||
|           left: 16, |           left: 16, | ||||||
|         ), |         ), | ||||||
|         AttachmentList(attachmentsId: widget.item.attachments ?? List.empty()), |         AttachmentList(attachmentsId: item.attachments ?? List.empty()), | ||||||
|  |         PostQuickAction( | ||||||
|  |           item: widget.item, | ||||||
|  |           onReact: (symbol, changes) { | ||||||
|  |             setState(() { | ||||||
|  |               item.reactionList[symbol] = (item.reactionList[symbol] ?? 0) + changes; | ||||||
|  |             }); | ||||||
|  |           }, | ||||||
|  |         ).paddingOnly( | ||||||
|  |           top: hasAttachment ? 10 : 6, | ||||||
|  |           left: hasAttachment ? 16 : 60, | ||||||
|  |           right: 16, | ||||||
|  |           bottom: 10, | ||||||
|  |         ), | ||||||
|       ], |       ], | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
							
								
								
									
										53
									
								
								lib/widgets/posts/post_reaction.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								lib/widgets/posts/post_reaction.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:get/get.dart'; | ||||||
|  | import 'package:solian/models/post.dart'; | ||||||
|  | import 'package:solian/models/reaction.dart'; | ||||||
|  |  | ||||||
|  | class PostReactionPopup extends StatelessWidget { | ||||||
|  |   final Post item; | ||||||
|  |   final void Function(String key, ReactInfo info) onReact; | ||||||
|  |  | ||||||
|  |   const PostReactionPopup({super.key, required this.item, required this.onReact}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return SizedBox( | ||||||
|  |       height: MediaQuery.of(context).size.height * 0.85, | ||||||
|  |       child: Column( | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |         children: [ | ||||||
|  |           Text( | ||||||
|  |             'postReaction'.tr, | ||||||
|  |             style: Theme.of(context).textTheme.headlineSmall, | ||||||
|  |           ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), | ||||||
|  |           Expanded( | ||||||
|  |             child: SingleChildScrollView( | ||||||
|  |               child: Wrap( | ||||||
|  |                 runSpacing: 4.0, | ||||||
|  |                 spacing: 8.0, | ||||||
|  |                 children: reactions.entries.map((e) { | ||||||
|  |                   return ActionChip( | ||||||
|  |                     avatar: Text(e.value.icon), | ||||||
|  |                     label: Row( | ||||||
|  |                       mainAxisSize: MainAxisSize.min, | ||||||
|  |                       children: [ | ||||||
|  |                         Text(e.key, style: const TextStyle(fontFamily: 'monospace')), | ||||||
|  |                         const SizedBox(width: 6), | ||||||
|  |                         Text('x${item.reactionList[e.key]?.toString() ?? '0'}', | ||||||
|  |                             style: const TextStyle(fontWeight: FontWeight.bold)), | ||||||
|  |                       ], | ||||||
|  |                     ), | ||||||
|  |                     onPressed: () { | ||||||
|  |                       onReact(e.key, e.value); | ||||||
|  |                       Navigator.pop(context); | ||||||
|  |                     }, | ||||||
|  |                   ); | ||||||
|  |                 }).toList(), | ||||||
|  |               ).paddingSymmetric(horizontal: 24), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user