D1APP-61 comments

This commit is contained in:
MohammadTaha Basiri 2022-01-24 22:06:04 +03:30
parent 33dabd7ab5
commit 4f4a709562
3 changed files with 573 additions and 9 deletions

View File

@ -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<Comments> {
@override
void initState() {
final state = context.read<CommentsState>();
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<Comments> {
@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<CommentsState>(
builder: (context, state, child) =>
SliverStateHandler<CommentsState>(
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<CommentsState>();
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,
),
],
),
],
),
),
],
);
}
}

View File

@ -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<Comment> comments = [];
String text = '';
int? commentId;
UserOverview? replyingTo;
late VoidCallback onCommentAdded;
final List<CommentData> comments = [];
final Map<int, MapEntry<bool, bool>> _feedbackQueue = {};
bool isRadar = true;
int id = 0;
int itemId = 0;
Future<void> 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<void> 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<void> addComment() async {
final user = DesignConfig.context.read<UserProvider>().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();
}
}

View File

@ -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<Comment> createState() => CommentState();
}
class CommentState extends State<Comment> {
late final CommentsState state;
bool _showSubComments = false;
CommentData get _comment => widget.comment;
@override
void initState() {
state = context.read<CommentsState>();
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);
},
),
],
);
}
}