From 4f4a709562eae5c8a0b2fbae0dd74fd3046123fd Mon Sep 17 00:00:00 2001 From: MohammadTaha Basiri Date: Mon, 24 Jan 2022 22:06:04 +0330 Subject: [PATCH] D1APP-61 comments --- lib/pages/home/comments/comments.dart | 213 +++++++++++++- lib/pages/home/comments/comments_state.dart | 107 ++++++- .../home/comments/widgets/comment_item.dart | 262 ++++++++++++++++++ 3 files changed, 573 insertions(+), 9 deletions(-) create mode 100644 lib/pages/home/comments/widgets/comment_item.dart diff --git a/lib/pages/home/comments/comments.dart b/lib/pages/home/comments/comments.dart index 5846b44..9aafbac 100644 --- a/lib/pages/home/comments/comments.dart +++ b/lib/pages/home/comments/comments.dart @@ -1,6 +1,15 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/models/view/app_bar_data.dart'; import 'package:didvan/pages/home/comments/comments_state.dart'; +import 'package:didvan/pages/home/comments/widgets/comment_item.dart'; +import 'package:didvan/widgets/animated_visibility.dart'; +import 'package:didvan/widgets/didvan/icon_button.dart'; import 'package:didvan/widgets/didvan/scaffold.dart'; +import 'package:didvan/widgets/didvan/text.dart'; +import 'package:didvan/widgets/shimmer_placeholder.dart'; +import 'package:didvan/widgets/state_handlers/sliver_state_handler.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -19,8 +28,9 @@ class _CommentsState extends State { @override void initState() { final state = context.read(); - state.id = widget.pageData['id']; + state.itemId = widget.pageData['id']; state.isRadar = widget.pageData['isRadar']; + state.onCommentAdded = widget.pageData['onCommentAdded']; Future.delayed( Duration.zero, () => state.getComments(), @@ -30,8 +40,205 @@ class _CommentsState extends State { @override Widget build(BuildContext context) { - return DidvanScaffold( - appBarData: AppBarData(), + return Material( + child: Stack( + children: [ + DidvanScaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + appBarData: AppBarData( + hasBack: true, + title: 'نظرات', + subtitle: widget.pageData['title'], + ), + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 92), + slivers: [ + Consumer( + builder: (context, state, child) => + SliverStateHandler( + onRetry: state.getComments, + state: state, + itemPadding: const EdgeInsets.symmetric(vertical: 16), + childCount: state.comments.length, + placeholder: const _CommentPlaceholder(), + builder: (context, state, index) => Comment( + comment: state.comments[index], + ), + ), + ), + ], + ), + Positioned( + bottom: MediaQuery.of(context).viewInsets.bottom, + left: 0, + right: 0, + child: const _MessageBox(), + ), + ], + ), + ); + } +} + +class _MessageBox extends StatefulWidget { + const _MessageBox({Key? key}) : super(key: key); + + @override + State<_MessageBox> createState() => _MessageBoxState(); +} + +class _MessageBoxState extends State<_MessageBox> { + final _controller = TextEditingController(); + + @override + Widget build(BuildContext context) { + final state = context.watch(); + return Column( + children: [ + AnimatedVisibility( + duration: DesignConfig.lowAnimationDuration, + isVisible: state.replyingTo != null, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border( + top: BorderSide( + color: Theme.of(context).colorScheme.border, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (state.replyingTo != null) + DidvanText( + 'پاسخ به ${state.replyingTo!.fullName}:', + color: Theme.of(context).colorScheme.caption, + style: Theme.of(context).textTheme.caption, + ), + const Spacer(), + DidvanIconButton( + gestureSize: 24, + color: Theme.of(context).colorScheme.caption, + icon: DidvanIcons.close_regular, + onPressed: () { + state.commentId = null; + state.replyingTo = null; + state.update(); + }, + ), + ], + ), + ), + ), + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border( + top: BorderSide( + color: Theme.of(context).colorScheme.border, + ), + ), + ), + child: Row( + children: [ + DidvanIconButton( + onPressed: () => _onSend(state), + icon: DidvanIcons.send_solid, + size: 24, + color: Theme.of(context).colorScheme.focusedBorder, + ), + Expanded( + child: TextField( + controller: _controller, + textInputAction: TextInputAction.send, + style: Theme.of(context).textTheme.bodyText2, + onEditingComplete: () {}, + onSubmitted: (value) => _onSend(state), + decoration: InputDecoration( + border: InputBorder.none, + hintText: 'پیام خود را ارسال کنید', + hintStyle: Theme.of(context).textTheme.caption!.copyWith( + color: Theme.of(context).colorScheme.disabledText), + ), + onChanged: (value) => state.text = value, + ), + ), + ], + ), + ), + ], + ); + } + + void _onSend(CommentsState state) { + if (state.text.replaceAll(' ', '').isNotEmpty) { + state.addComment(); + _controller.text = ''; + } + } +} + +class _CommentPlaceholder extends StatelessWidget { + const _CommentPlaceholder({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ShimmerPlaceholder( + height: 24, + width: 24, + borderRadius: DesignConfig.highBorderRadius, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + ShimmerPlaceholder( + height: 20, + width: 100, + ), + ShimmerPlaceholder( + height: 14, + width: 100, + ), + ], + ), + const SizedBox(height: 12), + const ShimmerPlaceholder( + height: 16, + width: double.infinity, + ), + const SizedBox(height: 8), + const ShimmerPlaceholder( + height: 16, + width: 200, + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + ShimmerPlaceholder( + height: 24, + width: 48, + ), + SizedBox(width: 8), + ShimmerPlaceholder( + height: 24, + width: 48, + ), + ], + ), + ], + ), + ), + ], ); } } diff --git a/lib/pages/home/comments/comments_state.dart b/lib/pages/home/comments/comments_state.dart index 5f48e8a..d079c57 100644 --- a/lib/pages/home/comments/comments_state.dart +++ b/lib/pages/home/comments/comments_state.dart @@ -1,31 +1,126 @@ +import 'dart:math'; + +import 'package:didvan/config/design_config.dart'; import 'package:didvan/models/comment/comment.dart'; +import 'package:didvan/models/comment/feedback.dart'; +import 'package:didvan/models/comment/reply.dart'; +import 'package:didvan/models/comment/user.dart'; import 'package:didvan/models/enums.dart'; import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/providers/user_provider.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:provider/provider.dart'; class CommentsState extends CoreProvier { - final List comments = []; + String text = ''; + int? commentId; + UserOverview? replyingTo; + late VoidCallback onCommentAdded; + + final List comments = []; + final Map> _feedbackQueue = {}; bool isRadar = true; - int id = 0; + int itemId = 0; Future getComments() async { appState = AppState.busy; final service = RequestService( isRadar - ? RequestHelper.radarComments(id) - : RequestHelper.newsComments(id), + ? RequestHelper.radarComments(itemId) + : RequestHelper.newsComments(itemId), ); await service.httpGet(); if (service.isSuccess) { - final messages = service.result['messages']; + final messages = service.result['comments']; for (var i = 0; i < messages.length; i++) { - comments.add(Comment.fromJson(messages[i])); + comments.add(CommentData.fromJson(messages[i])); } appState = AppState.idle; return; } appState = AppState.failed; } + + Future feedback(int id, bool like, bool dislike) async { + _feedbackQueue.addAll({id: MapEntry(like, dislike)}); + Future.delayed(const Duration(milliseconds: 500), () async { + if (!_feedbackQueue.containsKey(id)) return; + final service = RequestService( + isRadar + ? RequestHelper.feedbackRadarComment(itemId, id) + : RequestHelper.feedbackNewsComment(itemId, id), + body: { + 'like': _feedbackQueue[id]!.key, + 'dislike': _feedbackQueue[id]!.value, + }); + await service.put(); + _feedbackQueue.remove(id); + }); + } + + Future addComment() async { + final user = DesignConfig.context.read().user; + if (replyingTo != null) { + final coment = comments.firstWhere((comment) => comment.id == commentId); + coment.replies.add( + Reply( + id: Random().nextInt(1000), + text: text, + createdAt: DateTime.now().toString(), + liked: false, + disliked: false, + feedback: const FeedbackData(like: 0, dislike: 0), + toUser: replyingTo!, + user: UserOverview( + id: user.id, + fullName: user.fullName, + photo: user.photo, + ), + ), + ); + } else { + comments.insert( + 0, + CommentData( + id: Random().nextInt(1000), + text: text, + createdAt: DateTime.now().toString(), + liked: false, + disliked: false, + feedback: const FeedbackData(like: 0, dislike: 0), + user: UserOverview( + id: user.id, + fullName: user.fullName, + photo: user.photo, + ), + replies: [], + ), + ); + } + + onCommentAdded(); + + final body = {}; + + if (commentId != null) { + body.addAll({'commentId': commentId}); + } + if (replyingTo != null) { + body.addAll({'replyUserId': replyingTo!.id}); + } + body.addAll({'text': text}); + final service = RequestService( + isRadar + ? RequestHelper.addRadarComment(itemId) + : RequestHelper.addNewsComment(itemId), + body: body); + commentId = null; + replyingTo = null; + update(); + await service.post(); + update(); + } } diff --git a/lib/pages/home/comments/widgets/comment_item.dart b/lib/pages/home/comments/widgets/comment_item.dart new file mode 100644 index 0000000..96e0a33 --- /dev/null +++ b/lib/pages/home/comments/widgets/comment_item.dart @@ -0,0 +1,262 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/comment/comment.dart'; +import 'package:didvan/pages/home/comments/comments_state.dart'; +import 'package:didvan/utils/date_time.dart'; +import 'package:didvan/widgets/animated_visibility.dart'; +import 'package:didvan/widgets/didvan/icon_button.dart'; +import 'package:didvan/widgets/didvan/text.dart'; +import 'package:didvan/widgets/ink_wrapper.dart'; +import 'package:didvan/widgets/skeleton_image.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class Comment extends StatefulWidget { + final CommentData comment; + const Comment({ + Key? key, + required this.comment, + }) : super(key: key); + + @override + State createState() => CommentState(); +} + +class CommentState extends State { + late final CommentsState state; + + bool _showSubComments = false; + + CommentData get _comment => widget.comment; + + @override + void initState() { + state = context.read(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + _commentBuilder(comment: _comment), + if (_comment.replies.isNotEmpty) const SizedBox(height: 16), + for (var i = 0; i < _comment.replies.length; i++) + AnimatedVisibility( + duration: DesignConfig.lowAnimationDuration, + isVisible: _showSubComments, + child: _commentBuilder( + isSubComment: true, + comment: _comment.replies[i], + ), + ), + ], + ); + } + + Widget _commentBuilder({required comment, bool isSubComment = false}) => + Container( + decoration: BoxDecoration( + border: Border( + right: isSubComment + ? BorderSide(color: Theme.of(context).colorScheme.caption) + : BorderSide.none, + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (isSubComment) const SizedBox(width: 12), + if (comment.user.photo == null) + const Icon(DidvanIcons.avatar_light), + if (comment.user.photo != null) + SkeletonImage( + imageUrl: comment.user.photo, + height: 24, + width: 24, + borderRadius: DesignConfig.highBorderRadius, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + DidvanText( + comment.user.fullName, + style: Theme.of(context).textTheme.bodyText1, + ), + DidvanText( + DateTimeUtils.momentGenerator(comment.createdAt), + style: Theme.of(context).textTheme.caption, + color: Theme.of(context).colorScheme.caption, + ), + ], + ), + const SizedBox(height: 8), + if (isSubComment) + DidvanText( + 'پاسخ به ${comment.toUser.fullName}', + style: Theme.of(context).textTheme.caption, + color: Theme.of(context).colorScheme.caption, + ), + const SizedBox(height: 8), + DidvanText(comment.text), + const SizedBox(height: 8), + Row( + children: [ + InkWrapper( + onPressed: () { + state.commentId = _comment.id; + state.replyingTo = comment.user; + state.update(); + }, + child: DidvanText( + 'پاسخ', + style: Theme.of(context).textTheme.bodyText1, + color: Theme.of(context).colorScheme.primary, + ), + ), + if (!isSubComment) const SizedBox(width: 20), + if (!isSubComment && comment.replies.isNotEmpty) + InkWrapper( + onPressed: () => setState( + () => _showSubComments = !_showSubComments, + ), + child: Row( + children: [ + DidvanText( + 'پاسخ‌ها(${comment.replies.length})', + style: Theme.of(context).textTheme.bodyText1, + color: Theme.of(context).colorScheme.primary, + ), + AnimatedRotation( + duration: DesignConfig.lowAnimationDuration, + turns: _showSubComments ? 0.5 : 0, + child: Icon( + DidvanIcons.angle_down_regular, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + ), + const Spacer(), + _FeedbackButtons( + likeCount: comment.feedback.like, + dislikeCount: comment.feedback.dislike, + likeValue: comment.liked, + dislikeValue: comment.disliked, + onFeedback: (like, dislike) => + state.feedback(comment.id, like, dislike), + ), + ], + ), + ], + ), + ), + ], + ), + ); +} + +class _FeedbackButtons extends StatefulWidget { + final int likeCount; + final int dislikeCount; + final bool likeValue; + final bool dislikeValue; + final void Function(bool like, bool dislike) onFeedback; + const _FeedbackButtons({ + Key? key, + required this.onFeedback, + required this.likeCount, + required this.dislikeCount, + required this.likeValue, + required this.dislikeValue, + }) : super(key: key); + + @override + State<_FeedbackButtons> createState() => _FeedbackButtonsState(); +} + +class _FeedbackButtonsState extends State<_FeedbackButtons> { + late bool _likeValue; + late bool _dislikeValue; + late int _likeCount; + late int _dislikeCount; + + @override + void initState() { + _likeValue = widget.likeValue; + _dislikeValue = widget.dislikeValue; + _likeCount = widget.likeCount; + _dislikeCount = widget.dislikeCount; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + DidvanText( + _likeCount.toString(), + style: Theme.of(context).textTheme.caption, + color: Theme.of(context).colorScheme.caption, + ), + const SizedBox(width: 4), + DidvanIconButton( + icon: _likeValue ? DidvanIcons.like_solid : DidvanIcons.like_regular, + color: _likeValue ? Theme.of(context).colorScheme.primary : null, + gestureSize: 24, + onPressed: () { + setState(() { + if (_likeValue) { + _likeCount--; + } else { + _likeCount++; + if (_dislikeValue) { + _dislikeValue = false; + _dislikeCount--; + } + } + _likeValue = !_likeValue; + }); + widget.onFeedback(_likeValue, _dislikeValue); + }, + ), + const SizedBox(width: 16), + DidvanText( + _dislikeCount.toString(), + style: Theme.of(context).textTheme.caption, + color: Theme.of(context).colorScheme.caption, + ), + const SizedBox(width: 4), + DidvanIconButton( + icon: _dislikeValue + ? DidvanIcons.dislike_solid + : DidvanIcons.dislike_regular, + color: _dislikeValue ? Theme.of(context).colorScheme.secondary : null, + gestureSize: 24, + onPressed: () { + setState(() { + if (_dislikeValue) { + _dislikeCount--; + } else { + _dislikeCount++; + if (_likeValue) { + _likeValue = false; + _likeCount--; + } + } + _dislikeValue = !_dislikeValue; + }); + widget.onFeedback(_likeValue, _dislikeValue); + }, + ), + ], + ); + } +}