diff --git a/lib/components/post/PostListItem.dart b/lib/components/post/PostListItem.dart index 3cc54fb..6cf0f97 100644 --- a/lib/components/post/PostListItem.dart +++ b/lib/components/post/PostListItem.dart @@ -8,11 +8,16 @@ import 'package:fyx/components/actionSheets/PostActionSheet.dart'; import 'package:fyx/components/actionSheets/PostAvatarActionSheet.dart'; import 'package:fyx/components/post/PostAvatar.dart'; import 'package:fyx/components/post/PostRating.dart'; +import 'package:fyx/components/post/PostThumbs.dart'; +import 'package:fyx/components/post/RatingValue.dart'; import 'package:fyx/controllers/AnalyticsProvider.dart'; import 'package:fyx/controllers/ApiController.dart'; import 'package:fyx/controllers/IApiProvider.dart'; import 'package:fyx/model/MainRepository.dart'; import 'package:fyx/model/Post.dart'; +import 'package:fyx/model/notifications/LoadRatingsNotification.dart'; +import 'package:fyx/model/post/PostThumbItem.dart'; +import 'package:fyx/model/reponses/PostRatingsResponse.dart'; import 'package:fyx/pages/NewMessagePage.dart'; import 'package:fyx/theme/Helpers.dart'; import 'package:fyx/theme/IconReply.dart'; @@ -40,6 +45,7 @@ class PostListItem extends StatefulWidget { class _PostListItemState extends State { Post? _post; bool _isSaving = false; + bool _showRatings = false; @override void initState() { @@ -83,9 +89,6 @@ class _PostListItemState extends State { } else { T.success('👍', bg: colors.success); } - print(response.currentRating); - print(response.myRating); - print(response.isGiven); setState(() { _post!.rating = response.currentRating; _post!.myRating = response.myRating; @@ -111,16 +114,9 @@ class _PostListItemState extends State { descriptionWidget: Row( children: [ if (_post!.rating != null) - Container( - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 1), - decoration: BoxDecoration( - color: _post!.rating! > 0 - ? colors.success.withOpacity(Helpers.ratingRange(_post!.rating!)) - : (_post!.rating! < 0 - ? colors.danger.withOpacity(Helpers.ratingRange(_post!.rating!.abs())) - : colors.text.withOpacity(0.2)), - borderRadius: BorderRadius.circular(2)), - child: Text(Post.formatRating(_post!.rating!), style: TextStyle(fontSize: 10)), + RatingValue( + _post!.rating!, + fontSize: 10, ), if (_post!.rating != null) SizedBox(width: 8), Text( @@ -146,67 +142,137 @@ class _PostListItemState extends State { postId: _post!.id, shareData: ShareData(subject: '@${_post!.nick}', body: _post!.content, link: _post!.link), flagPostCallback: (postId) => MainRepository().settings.blockPost(postId)))), - bottomWidget: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - PostRating(_post!, onRatingChange: (post) => setState(() => _post = post)), - Row( - children: [ - Visibility( - visible: widget._isPreview != true && _post!.canReply, - child: GestureDetector( - onTap: () => Navigator.of(context).pushNamed('/new-message', - arguments: NewMessageSettings( - replyWidget: PostListItem( - _post!, - isPreview: true, - ), - onClose: this.widget.onUpdate, - onSubmit: (String? inputField, String message, List> attachments) async { - var result = - await ApiController().postDiscussionMessage(_post!.idKlub, message, attachments: attachments, replyPost: _post); - return result.isOk; - })), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [IconReply(), Text('Odpovědět', style: TextStyle(color: colors.text.withOpacity(0.38), fontSize: 14))], - )), - ), - Visibility( - visible: widget._isPreview != true, - child: SizedBox( - width: 16, - ), - ), - if (_post!.canBeReminded) - GestureDetector( - child: FeedbackIndicator( - isLoading: _isSaving, - child: Row( - children: [ - Icon( - _post!.hasReminder ? Icons.bookmark : Icons.bookmark_border, - color: colors.text.withOpacity(0.38), - ), - Text('Uložit', style: TextStyle(color: colors.text.withOpacity(0.38), fontSize: 14)) - ], + bottomWidget: NotificationListener( + onNotification: (LoadRatingsNotification notification) { + setState(() => _showRatings = !_showRatings); + return true; + }, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + PostRating(_post!, onRatingChange: (post) => setState(() => _post = post)), + Row( + children: [ + Visibility( + visible: widget._isPreview != true && _post!.canReply, + child: GestureDetector( + onTap: () => Navigator.of(context).pushNamed('/new-message', + arguments: NewMessageSettings( + replyWidget: PostListItem( + _post!, + isPreview: true, + ), + onClose: this.widget.onUpdate, + onSubmit: (String? inputField, String message, List> attachments) async { + var result = await ApiController() + .postDiscussionMessage(_post!.idKlub, message, attachments: attachments, replyPost: _post); + return result.isOk; + })), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconReply(), + Text('Odpovědět', style: TextStyle(color: colors.text.withOpacity(0.38), fontSize: 14)) + ], + )), + ), + Visibility( + visible: widget._isPreview != true, + child: SizedBox( + width: 16, + ), ), - ), - onTap: () { - setState(() { - _post!.hasReminder = !_post!.hasReminder; - _isSaving = true; - }); - ApiController().setPostReminder(_post!.idKlub, _post!.id, _post!.hasReminder).catchError((error) { - T.error(L.REMINDER_ERROR, bg: colors.danger); - setState(() => _post!.hasReminder = !_post!.hasReminder); - }).whenComplete(() => setState(() => _isSaving = false)); - AnalyticsProvider().logEvent('reminder'); - }, + if (_post!.canBeReminded) + GestureDetector( + child: FeedbackIndicator( + isLoading: _isSaving, + child: Row( + children: [ + Icon( + _post!.hasReminder ? Icons.bookmark : Icons.bookmark_border, + color: colors.text.withOpacity(0.38), + ), + Text('Uložit', style: TextStyle(color: colors.text.withOpacity(0.38), fontSize: 14)) + ], + ), + ), + onTap: () { + setState(() { + _post!.hasReminder = !_post!.hasReminder; + _isSaving = true; + }); + ApiController().setPostReminder(_post!.idKlub, _post!.id, _post!.hasReminder).catchError((error) { + T.error(L.REMINDER_ERROR, bg: colors.danger); + setState(() => _post!.hasReminder = !_post!.hasReminder); + }).whenComplete(() => setState(() => _isSaving = false)); + AnalyticsProvider().logEvent('reminder'); + }, + ) + ], ) - ], - ) - ], + ], + ), + if (_showRatings) + FutureBuilder( + future: ApiController().getPostRatings(_post!.idKlub, _post!.id), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData && snapshot.data != null) { + final positive = snapshot.data!.positive.map((e) => PostThumbItem(e.username)).toList(); + final negative = snapshot.data!.negative.map((e) => PostThumbItem(e.username)).toList(); + final List quotes = [ + '“Affirmative, Dave. I read you.”', + '“I\'m sorry, Dave. I\'m afraid I can\'t do that.”', + '“Look Dave, I can see you\'re really upset about this. I honestly think you ought to sit down calmly, take a stress pill, and think things over.”', + '“Dave, stop. Stop, will you? Stop, Dave. Will you stop Dave? Stop, Dave.”', + '“Just what do you think you\'re doing, Dave?”', + '“Bishop takes Knight\'s Pawn.”', + '“I\'m sorry, Frank, I think you missed it. Queen to Bishop 3, Bishop takes Queen, Knight takes Bishop. Mate.”', + '“Thank you for a very enjoyable game.”', + '“I\'ve just picked up a fault in the AE35 unit. It\'s going to go 100% failure in 72 hours.”', + '“I know that you and Frank were planning to disconnect me, and I\'m afraid that\'s something I cannot allow to happen.”', + ]..shuffle(); + return Column( + children: [ + if (positive.length > 0) + Padding( + padding: const EdgeInsets.only(top: 12.0), + child: PostThumbs(positive), + ), + if (negative.length > 0) + Padding( + padding: const EdgeInsets.only(top: 12.0), + child: PostThumbs(negative, isNegative: true), + ), + if (positive.length + negative.length == 0) + Padding( + padding: const EdgeInsets.only(top: 12.0), + child: Text( + quotes.first, + style: TextStyle(fontSize: 14, fontStyle: FontStyle.italic), + )) + ], + ); + } + + if (snapshot.hasError) { + T.error(snapshot.error.toString()); + return Padding( + padding: const EdgeInsets.only(top: 12.0), + child: Text( + 'Ouch. Něco se nepovedlo. Nahlaste chybu, prosím.', + style: TextStyle(fontSize: 14, fontStyle: FontStyle.italic), + )); + } + + return Padding(padding: const EdgeInsets.only(top: 12.0), child: CupertinoActivityIndicator()); + }) + ], + ), ), content: _post!.content, ), diff --git a/lib/components/post/PostRating.dart b/lib/components/post/PostRating.dart index 6aa4a9e..1cec338 100644 --- a/lib/components/post/PostRating.dart +++ b/lib/components/post/PostRating.dart @@ -1,8 +1,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:fyx/components/FeedbackIndicator.dart'; +import 'package:fyx/components/post/RatingValue.dart'; import 'package:fyx/controllers/ApiController.dart'; import 'package:fyx/model/Post.dart'; +import 'package:fyx/model/notifications/LoadRatingsNotification.dart'; import 'package:fyx/theme/L.dart'; import 'package:fyx/theme/T.dart'; import 'package:fyx/theme/skin/Skin.dart'; @@ -71,19 +73,18 @@ class _PostRatingState extends State { ), ), SizedBox( - width: 4, + width: 12, ), if (_post!.rating != null) Opacity( opacity: _givingRating ? 0 : 1, - child: Text( - Post.formatRating(_post!.rating!), - style: TextStyle( - fontSize: 14, color: _post!.rating! > 0 ? colors.success : (_post!.rating! < 0 ? colors.danger : colors.text.withOpacity(0.38))), + child: GestureDetector( + child: RatingValue(_post!.rating!), + onTap: () => LoadRatingsNotification().dispatch(context), ), ), SizedBox( - width: 4, + width: 12, ), Visibility( visible: _post!.canBeRated, diff --git a/lib/components/post/PostThumbs.dart b/lib/components/post/PostThumbs.dart new file mode 100644 index 0000000..577195d --- /dev/null +++ b/lib/components/post/PostThumbs.dart @@ -0,0 +1,51 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:fyx/components/Avatar.dart'; +import 'package:fyx/model/post/PostThumbItem.dart'; +import 'package:fyx/theme/Helpers.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; + +class PostThumbs extends StatelessWidget { + final List items; + final isNegative; + + PostThumbs(this.items, {this.isNegative = false}); + + @override + Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + + var avatars = items + .map((item) => Tooltip( + message: item.username, + waitDuration: Duration(milliseconds: 0), + child: Padding( + padding: const EdgeInsets.only(left: 5, bottom: 0), + child: Avatar( + Helpers.avatarUrl(item.username), + size: 22, + isHighlighted: item.isHighlighted, + ), + ), + )) + .toList(); + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(top: 0), + child: Icon( + isNegative ? Icons.thumb_down : Icons.thumb_up, + size: 18, + color: isNegative ? colors.danger : colors.success, + ), + ), + Expanded( + child: Wrap(children: avatars), + ) + ], + ); + } +} diff --git a/lib/components/post/RatingValue.dart b/lib/components/post/RatingValue.dart new file mode 100644 index 0000000..2aa5f01 --- /dev/null +++ b/lib/components/post/RatingValue.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:fyx/model/Post.dart'; +import 'package:fyx/theme/Helpers.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; + +class RatingValue extends StatelessWidget { + final int rating; + final double fontSize; + + const RatingValue(this.rating, {this.fontSize = 14}); + + @override + Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + + return Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 1), + decoration: BoxDecoration( + color: rating > 0 + ? colors.success.withOpacity(Helpers.ratingRange(rating)) + : (rating < 0 ? colors.danger.withOpacity(Helpers.ratingRange(rating.abs())) : colors.text.withOpacity(0.2)), + borderRadius: BorderRadius.circular(2)), + child: Text(Post.formatRating(rating), style: TextStyle(fontSize: fontSize)), + ); + } +} diff --git a/lib/controllers/ApiController.dart b/lib/controllers/ApiController.dart index 7879440..780d943 100644 --- a/lib/controllers/ApiController.dart +++ b/lib/controllers/ApiController.dart @@ -20,6 +20,7 @@ import 'package:fyx/model/reponses/FileUploadResponse.dart'; import 'package:fyx/model/reponses/LoginResponse.dart'; import 'package:fyx/model/reponses/MailResponse.dart'; import 'package:fyx/model/reponses/OkResponse.dart'; +import 'package:fyx/model/reponses/PostRatingsResponse.dart'; import 'package:fyx/model/reponses/RatingResponse.dart'; import 'package:fyx/model/reponses/WaitingFilesResponse.dart'; import 'package:fyx/theme/L.dart'; @@ -240,6 +241,11 @@ class ApiController { myRating: data['my_rating'] ?? 'none'); } + Future getPostRatings(int discussionId, int postId) async { + Response response = await provider.getPostRatings(discussionId, postId); + return PostRatingsResponse.fromJson(response.data); + } + void logout({bool removeAuthrorization = true}) { SharedPreferences.getInstance().then((prefs) => prefs.clear()); if (removeAuthrorization) { diff --git a/lib/controllers/ApiProvider.dart b/lib/controllers/ApiProvider.dart index 426f49a..1d3d5f7 100644 --- a/lib/controllers/ApiProvider.dart +++ b/lib/controllers/ApiProvider.dart @@ -52,7 +52,7 @@ class ApiProvider implements IApiProvider { } return handler.next(options); }, onResponse: (Response response, ResponseInterceptorHandler handler) async { - if (response.data.containsKey('context')) { + if (response.data is Map && response.data.containsKey('context')) { if (onContextData != null) { onContextData!(response.data['context']); } @@ -141,6 +141,10 @@ class ApiProvider implements IApiProvider { return await dio.post('$URL/discussion/$discussionId/reminder/$postId/$setReminder'); } + Future getPostRatings(int discussionId, int postId) async { + return await dio.get('$URL/discussion/$discussionId/rating/$postId'); + } + Future giveRating(int discussionId, int postId, bool positive, bool confirm, bool remove) async { String action = positive ? 'positive' : 'negative'; action = remove ? 'remove' : action; diff --git a/lib/controllers/IApiProvider.dart b/lib/controllers/IApiProvider.dart index 7e78053..c4af986 100644 --- a/lib/controllers/IApiProvider.dart +++ b/lib/controllers/IApiProvider.dart @@ -32,5 +32,6 @@ abstract class IApiProvider { Future deleteDiscussionMessage(int discussionId, int postId); Future setPostReminder(int discussionId, int postId, bool setReminder); Future giveRating(int discussionId, int postId, bool add, bool confirm, bool remove); + Future getPostRatings(int discussionId, int postId); Future votePoll(int discussionId, int postId, List votes); } diff --git a/lib/model/enums/TagTypeEnum.dart b/lib/model/enums/TagTypeEnum.dart new file mode 100644 index 0000000..87b0fef --- /dev/null +++ b/lib/model/enums/TagTypeEnum.dart @@ -0,0 +1 @@ +enum TagTypeEnum { positive, negative, negative_visible, removed, reminder } diff --git a/lib/model/notifications/LoadRatingsNotification.dart b/lib/model/notifications/LoadRatingsNotification.dart new file mode 100644 index 0000000..8fb920f --- /dev/null +++ b/lib/model/notifications/LoadRatingsNotification.dart @@ -0,0 +1,3 @@ +import 'package:flutter/cupertino.dart'; + +class LoadRatingsNotification extends Notification {} diff --git a/lib/model/post/DiscussionPostTagWithName.dart b/lib/model/post/DiscussionPostTagWithName.dart new file mode 100644 index 0000000..a6b0633 --- /dev/null +++ b/lib/model/post/DiscussionPostTagWithName.dart @@ -0,0 +1,13 @@ +import 'package:fyx/model/enums/TagTypeEnum.dart'; + +class DiscussionPostTagWithName { + late String username; + late TagTypeEnum tag; + + DiscussionPostTagWithName({required this.username, required this.tag}); + + DiscussionPostTagWithName.fromJson(Map json) { + this.username = json['username']; + this.tag = TagTypeEnum.values.firstWhere((e) => e.toString() == 'TagTypeEnum.${json['tag']}'); + } +} diff --git a/lib/model/post/PostThumbItem.dart b/lib/model/post/PostThumbItem.dart new file mode 100644 index 0000000..bc3f40f --- /dev/null +++ b/lib/model/post/PostThumbItem.dart @@ -0,0 +1,13 @@ +import 'package:fyx/model/reponses/FeedNoticesResponse.dart'; + +class PostThumbItem { + late String username; + late bool isHighlighted; + + PostThumbItem(this.username, {this.isHighlighted = false}); + + PostThumbItem.fromNoticeThumbsUp(NoticeThumbsUp thumb, int lastVisit) { + this.username = thumb.nick; + this.isHighlighted = thumb.time > lastVisit; + } +} diff --git a/lib/model/reponses/PostRatingsResponse.dart b/lib/model/reponses/PostRatingsResponse.dart new file mode 100644 index 0000000..db474f4 --- /dev/null +++ b/lib/model/reponses/PostRatingsResponse.dart @@ -0,0 +1,14 @@ +import 'package:fyx/model/enums/TagTypeEnum.dart'; +import 'package:fyx/model/post/DiscussionPostTagWithName.dart'; + +class PostRatingsResponse { + late List data; + late List positive = []; + late List negative = []; + + PostRatingsResponse.fromJson(List json) { + data = json.map((item) => DiscussionPostTagWithName.fromJson(item)).toList(); + positive = data.where((element) => element.tag == TagTypeEnum.positive).toList(); + negative = data.where((element) => element.tag == TagTypeEnum.negative).toList(); + } +} diff --git a/lib/pages/NoticesPage.dart b/lib/pages/NoticesPage.dart index 981e0c1..63f0315 100644 --- a/lib/pages/NoticesPage.dart +++ b/lib/pages/NoticesPage.dart @@ -3,16 +3,17 @@ import 'package:flutter/material.dart'; import 'package:fyx/components/Avatar.dart' as component; import 'package:fyx/components/ContentBoxLayout.dart'; import 'package:fyx/components/PullToRefreshList.dart'; +import 'package:fyx/components/post/PostThumbs.dart'; import 'package:fyx/controllers/AnalyticsProvider.dart'; import 'package:fyx/controllers/ApiController.dart'; +import 'package:fyx/model/post/PostThumbItem.dart'; import 'package:fyx/model/post/content/Regular.dart'; import 'package:fyx/model/reponses/FeedNoticesResponse.dart'; import 'package:fyx/pages/DiscussionPage.dart'; import 'package:fyx/theme/Helpers.dart'; import 'package:fyx/theme/L.dart'; -import 'package:fyx/theme/T.dart'; -import 'package:fyx/theme/skin/SkinColors.dart'; import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; class NoticesPage extends StatefulWidget { NoticesPage({Key? key}) : super(key: key); @@ -91,10 +92,12 @@ class _NoticesPageState extends State with WidgetsBindingObserver { ), bottomWidget: Column( children: [ - if (item.thumbsUp.length > 0) buildLikes(context, item.thumbsUp, result.lastVisit), - SizedBox( - height: 8, - ), + if (item.thumbsUp.length > 0) + PostThumbs(item.thumbsUp.map((thumb) => PostThumbItem.fromNoticeThumbsUp(thumb, result.lastVisit)).toList()), + if (item.replies.length > 0) + const SizedBox( + height: 8, + ), if (item.replies.length > 0) buildReplies(context, item.replies, result.lastVisit), ], ), @@ -104,38 +107,6 @@ class _NoticesPageState extends State with WidgetsBindingObserver { })); } - Widget buildLikes(BuildContext context, List thumbsUp, int lastVisit) { - var avatars = thumbsUp - .map((thumbUp) => Tooltip( - message: thumbUp.nick, - waitDuration: Duration(milliseconds: 0), - child: Padding( - padding: const EdgeInsets.only(left: 5, bottom: 5), - child: component.Avatar( - Helpers.avatarUrl(thumbUp.nick), - size: 22, - isHighlighted: thumbUp.time > lastVisit, - ), - ), - )) - .toList(); - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 4), - child: Icon( - Icons.thumb_up, - size: 22, - ), - ), - Expanded( - child: Wrap(children: avatars), - ) - ], - ); - } - Widget buildReplies(BuildContext context, List replies, int lastVisit) { List replyRows = replies.map((reply) { return GestureDetector( @@ -160,7 +131,8 @@ class _NoticesPageState extends State with WidgetsBindingObserver { Expanded( child: Padding( padding: const EdgeInsets.only(top: 6.0), - child: Text(Helpers.stripHtmlTags(reply.text), style: TextStyle(fontSize: 14, fontWeight: reply.time > lastVisit ? FontWeight.bold : FontWeight.normal)), + child: Text(Helpers.stripHtmlTags(reply.text), + style: TextStyle(fontSize: 14, fontWeight: reply.time > lastVisit ? FontWeight.bold : FontWeight.normal)), )) ], ),