942 lines
30 KiB
Dart
942 lines
30 KiB
Dart
import 'dart:convert';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:onufitness/constants/color_constant.dart';
|
|
import 'package:onufitness/constants/text_constant.dart';
|
|
import 'package:onufitness/environment/environment_config.dart';
|
|
import 'package:onufitness/controller/notification_controller.dart';
|
|
import 'package:onufitness/screens/accounts/Controllers/coach_rating_controller.dart';
|
|
import 'package:onufitness/screens/chat/controllers/chat_controller.dart';
|
|
import 'package:onufitness/screens/echoboard/controllers/like_comment_controller.dart';
|
|
import 'package:onufitness/screens/echoboard/controllers/profile_controller.dart';
|
|
import 'package:onufitness/screens/echoboard/controllers/echoboard_controller.dart';
|
|
import 'package:onufitness/screens/echoboard/models/parent_comments_response_model.dart';
|
|
import 'package:onufitness/screens/echoboard/models/post_model.dart';
|
|
import 'package:onufitness/screens/echoboard/widget/Like/reaction_popup.dart';
|
|
import 'package:onufitness/screens/echoboard/widget/post_card.dart';
|
|
import 'package:onufitness/screens/navbar/bottom_nav_bar.dart';
|
|
import 'package:onufitness/services/logger_service.dart';
|
|
import 'package:share_plus/share_plus.dart';
|
|
import 'package:shimmer/shimmer.dart';
|
|
import 'package:timeago/timeago.dart' as timeago;
|
|
|
|
class SinglePostPage extends StatefulWidget {
|
|
final String postId;
|
|
final int tribeId;
|
|
final bool isComingFromShare;
|
|
|
|
const SinglePostPage({
|
|
super.key,
|
|
required this.postId,
|
|
this.tribeId = 0,
|
|
this.isComingFromShare = false,
|
|
});
|
|
|
|
@override
|
|
State<SinglePostPage> createState() => _SinglePostPageState();
|
|
}
|
|
|
|
class _SinglePostPageState extends State<SinglePostPage> {
|
|
late EchoBoardController echoBoardController;
|
|
late LikeCommentController likeCommentController;
|
|
late ProfileController profileController;
|
|
|
|
final ScrollController _scrollController = ScrollController();
|
|
final TextEditingController _commentController = TextEditingController();
|
|
final Set<String> _processingLikes = <String>{};
|
|
final logger = LoggerService();
|
|
|
|
final RxBool _isInitializing = true.obs;
|
|
final RxBool _hasError = false.obs;
|
|
|
|
bool get isTribeEchoboard => widget.tribeId != 0;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
initializeControllers();
|
|
setupScrollListener();
|
|
|
|
// Defer loading to after build
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
loadPostAndComments();
|
|
});
|
|
}
|
|
|
|
void initializeControllers() {
|
|
if (!Get.isRegistered<EchoBoardController>()) {
|
|
Get.put(EchoBoardController());
|
|
}
|
|
echoBoardController = Get.find<EchoBoardController>();
|
|
|
|
if (!Get.isRegistered<LikeCommentController>()) {
|
|
Get.put(LikeCommentController());
|
|
}
|
|
likeCommentController = Get.find<LikeCommentController>();
|
|
|
|
if (!Get.isRegistered<ProfileController>()) {
|
|
Get.put(ProfileController());
|
|
}
|
|
profileController = Get.find<ProfileController>();
|
|
}
|
|
|
|
void loadPostAndComments() async {
|
|
try {
|
|
_isInitializing.value = true;
|
|
_hasError.value = false;
|
|
|
|
// Load the specific post
|
|
await echoBoardController.loadSpecificPost(
|
|
postId: widget.postId,
|
|
tribeId: widget.tribeId,
|
|
);
|
|
|
|
// Load comments for this post
|
|
likeCommentController.postId.value = widget.postId;
|
|
await likeCommentController.fetchcomments(refresh: true);
|
|
|
|
_isInitializing.value = false;
|
|
} catch (e, stackTrace) {
|
|
logger.error(
|
|
"Failed to load post and comments for post ID: ${widget.postId}",
|
|
error: e,
|
|
stackTrace: stackTrace,
|
|
);
|
|
_hasError.value = true;
|
|
_isInitializing.value = false;
|
|
}
|
|
}
|
|
|
|
void setupScrollListener() {
|
|
_scrollController.addListener(() {
|
|
if (_scrollController.position.pixels >=
|
|
_scrollController.position.maxScrollExtent - 200) {
|
|
_loadMoreComments();
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> _loadMoreComments() async {
|
|
if (!likeCommentController.isCommentPaginationLoading.value &&
|
|
likeCommentController.hasMoreComments.value) {
|
|
await likeCommentController.fetchcomments();
|
|
}
|
|
}
|
|
|
|
Future<void> _handleRefresh() async {
|
|
try {
|
|
_hasError.value = false;
|
|
await echoBoardController.loadSpecificPost(
|
|
postId: widget.postId,
|
|
tribeId: widget.tribeId,
|
|
);
|
|
await likeCommentController.fetchcomments(refresh: true);
|
|
} catch (e, stackTrace) {
|
|
logger.error(
|
|
"Failed to refresh post and comments for post ID: ${widget.postId}",
|
|
error: e,
|
|
stackTrace: stackTrace,
|
|
);
|
|
_hasError.value = true;
|
|
}
|
|
}
|
|
|
|
Future<void> _handleLikePressed(SocialPostItem post) async {
|
|
final postIdStr = post.postId.toString();
|
|
|
|
if (_processingLikes.contains(postIdStr)) return;
|
|
_processingLikes.add(postIdStr);
|
|
|
|
final wasLiked = post.postReactionTypeId != null;
|
|
|
|
likeCommentController.updatePostReaction(postIdStr, 1, wasLiked);
|
|
|
|
likeCommentController
|
|
.submitPostReaction(
|
|
postId: postIdStr,
|
|
reactionTypeId: 1,
|
|
isDeleteReaction: wasLiked,
|
|
)
|
|
.then((success) {
|
|
if (!success) {
|
|
likeCommentController.updatePostReaction(postIdStr, 1, !wasLiked);
|
|
}
|
|
})
|
|
.whenComplete(() {
|
|
_processingLikes.remove(postIdStr);
|
|
});
|
|
}
|
|
|
|
Future<void> _handleLongPress(
|
|
SocialPostItem post,
|
|
GlobalKey likeButtonKey,
|
|
) async {
|
|
final postIdStr = post.postId.toString();
|
|
|
|
if (_processingLikes.contains(postIdStr)) return;
|
|
|
|
showReactions(
|
|
context: context,
|
|
buttonKey: likeButtonKey,
|
|
onReactionSelected: (reaction, reactionTypeID) async {
|
|
if (_processingLikes.contains(postIdStr)) return;
|
|
|
|
_processingLikes.add(postIdStr);
|
|
|
|
likeCommentController.updatePostReaction(
|
|
postIdStr,
|
|
reactionTypeID,
|
|
false,
|
|
);
|
|
|
|
likeCommentController
|
|
.submitPostReaction(
|
|
postId: postIdStr,
|
|
reactionTypeId: reactionTypeID,
|
|
isDeleteReaction: false,
|
|
)
|
|
.then((success) {
|
|
if (!success) {
|
|
likeCommentController.updatePostReaction(
|
|
postIdStr,
|
|
post.postReactionTypeId ?? 1,
|
|
false,
|
|
);
|
|
}
|
|
})
|
|
.whenComplete(() {
|
|
_processingLikes.remove(postIdStr);
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
void _submitComment() async {
|
|
if (_commentController.text.trim().isEmpty) return;
|
|
|
|
final String commentText = _commentController.text.trim();
|
|
|
|
int? parentId;
|
|
if (likeCommentController.replyingTo.value != null &&
|
|
likeCommentController.parentOfReply.value != null) {
|
|
parentId = likeCommentController.parentOfReply.value!.commentId;
|
|
} else if (likeCommentController.replyingTo.value != null) {
|
|
parentId = likeCommentController.replyingTo.value!.commentId;
|
|
}
|
|
|
|
_commentController.clear();
|
|
likeCommentController.resetReplyState();
|
|
|
|
await likeCommentController.submitComment(
|
|
postId: widget.postId,
|
|
commentText: commentText,
|
|
parentCommentId: parentId,
|
|
);
|
|
}
|
|
|
|
void _startReply(CommentItem comment, {CommentItem? parentComment}) {
|
|
likeCommentController.startReply(comment, parentComment);
|
|
_commentController.clear();
|
|
FocusScope.of(context).unfocus();
|
|
|
|
Future.delayed(const Duration(milliseconds: 100), () {
|
|
if (!mounted) return;
|
|
FocusScope.of(context).requestFocus(FocusNode());
|
|
});
|
|
}
|
|
|
|
Future<void> _loadSubComments(int parentCommentId) async {
|
|
await likeCommentController.fetchSubComments(parentCommentId);
|
|
}
|
|
|
|
Future<bool> _handleBackNavigation() async {
|
|
if (widget.isComingFromShare == true) {
|
|
// Navigate immediately without deferring
|
|
Get.offAll(() => DashboardScreen());
|
|
|
|
// Initialize controllers after navigation in next frame
|
|
Future.delayed(Duration.zero, () {
|
|
if (!Get.isRegistered<NavigationController>()) {
|
|
Get.put(NavigationController());
|
|
}
|
|
if (!Get.isRegistered<EchoBoardController>()) {
|
|
Get.put(EchoBoardController());
|
|
}
|
|
if (!Get.isRegistered<ProfileController>()) {
|
|
Get.put(ProfileController());
|
|
}
|
|
if (!Get.isRegistered<ChatController>()) {
|
|
Get.put(ChatController());
|
|
}
|
|
if (!Get.isRegistered<RatingsReviewsController>()) {
|
|
Get.put(RatingsReviewsController());
|
|
}
|
|
if (!Get.isRegistered<NotificationController>()) {
|
|
Get.put(NotificationController());
|
|
}
|
|
});
|
|
} else {
|
|
Get.back();
|
|
}
|
|
return true; // Allow navigation
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PopScope(
|
|
canPop: false,
|
|
onPopInvokedWithResult: (didPop, result) async {
|
|
if (!didPop) {
|
|
await _handleBackNavigation();
|
|
}
|
|
},
|
|
child: Scaffold(
|
|
resizeToAvoidBottomInset: true,
|
|
backgroundColor: pageBackGroundColor,
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.white,
|
|
leading: IconButton(
|
|
icon: Icon(Icons.arrow_back_ios, color: Colors.black),
|
|
onPressed: () async {
|
|
await _handleBackNavigation();
|
|
},
|
|
),
|
|
title: Text(
|
|
'Post',
|
|
style: TextStyle(
|
|
fontSize: regularSizeText,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.black,
|
|
),
|
|
),
|
|
),
|
|
body: Obx(() {
|
|
// Initial loading state
|
|
if (_isInitializing.value) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
CircularProgressIndicator(color: Colors.black),
|
|
SizedBox(height: 16.h),
|
|
Text(
|
|
'Loading post...',
|
|
style: TextStyle(
|
|
fontSize: smallSizeText,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Error state
|
|
if (_hasError.value) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.error_outline, size: 60, color: Colors.red),
|
|
SizedBox(height: 16.h),
|
|
Text(
|
|
'Failed to load post',
|
|
style: TextStyle(
|
|
fontSize: mediumSizeText,
|
|
color: Colors.grey[700],
|
|
),
|
|
),
|
|
SizedBox(height: 24.h),
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
loadPostAndComments();
|
|
});
|
|
},
|
|
icon: Icon(Icons.refresh),
|
|
label: Text('Retry'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.black,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Find the post
|
|
final post = echoBoardController.posts.firstWhereOrNull(
|
|
(p) => p.postId.toString() == widget.postId,
|
|
);
|
|
|
|
if (post == null) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.post_add, size: 60, color: Colors.grey),
|
|
SizedBox(height: 16.h),
|
|
Text(
|
|
'Post not found',
|
|
style: TextStyle(
|
|
fontSize: mediumSizeText,
|
|
color: Colors.grey[700],
|
|
),
|
|
),
|
|
SizedBox(height: 8.h),
|
|
Text(
|
|
'This post may have been deleted',
|
|
style: TextStyle(
|
|
fontSize: smallSizeText,
|
|
color: Colors.grey[500],
|
|
),
|
|
),
|
|
SizedBox(height: 24.h),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
await _handleBackNavigation();
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.black,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: Text('Go Back'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return Column(
|
|
children: [
|
|
Expanded(
|
|
child: RefreshIndicator(
|
|
color: Colors.black,
|
|
backgroundColor: Colors.white,
|
|
onRefresh: _handleRefresh,
|
|
child: CustomScrollView(
|
|
controller: _scrollController,
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
slivers: [
|
|
// Post Card
|
|
SliverToBoxAdapter(child: _buildPostSection(post)),
|
|
|
|
// Comments Header
|
|
SliverToBoxAdapter(child: _buildCommentsHeader()),
|
|
|
|
// Comments List
|
|
_buildCommentsList(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Obx(() {
|
|
if (_isInitializing.value || _hasError.value) {
|
|
return SizedBox.shrink();
|
|
}
|
|
return _buildCommentInputSection();
|
|
}),
|
|
],
|
|
);
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPostSection(SocialPostItem post) {
|
|
final GlobalKey likeButtonKey = GlobalKey();
|
|
|
|
return PostCard(
|
|
key: ValueKey('post_${post.postId}_${post.poll?.pollPostId}'),
|
|
post: post,
|
|
isTribeEchoboard: isTribeEchoboard,
|
|
onProfilePressed: () {},
|
|
onLikePressed: () => _handleLikePressed(post),
|
|
likeButtonKey: likeButtonKey,
|
|
onLikeLongPressed: () => _handleLongPress(post, likeButtonKey),
|
|
onCommentPressed: () {
|
|
// Scroll to comments section
|
|
_scrollController.animateTo(
|
|
_scrollController.position.maxScrollExtent,
|
|
duration: Duration(milliseconds: 300),
|
|
curve: Curves.easeOut,
|
|
);
|
|
},
|
|
onSharePressed: () async {
|
|
final encodedPostId = base64Url.encode(
|
|
utf8.encode(post.postId.toString()),
|
|
);
|
|
final encodedTribeId = base64Url.encode(
|
|
utf8.encode(widget.tribeId.toString()),
|
|
);
|
|
final webUrl = EnvironmentConfigFactory.getConfig().webUrl;
|
|
final shareUrl =
|
|
widget.tribeId != 0
|
|
? "https://$webUrl/tribe-post-link?tribeId=$encodedTribeId&postId=$encodedPostId"
|
|
: "https://$webUrl/post-link?postId=$encodedPostId";
|
|
|
|
await SharePlus.instance.share(
|
|
ShareParams(text: shareUrl, subject: 'Check out this post!'),
|
|
);
|
|
},
|
|
isThreeDotButtonEnabled: false,
|
|
onMorePressed: () {
|
|
// final bool isMyPost =
|
|
// post.userId == SharedServices.getUserDetails()?.data?.userId;
|
|
|
|
// PostMoreOptionsBottomSheet.show(
|
|
// context: context,
|
|
// postId: post.postId.toString(),
|
|
// isMyPost: isMyPost,
|
|
// post: post,
|
|
// tribeId: widget.tribeId,
|
|
// );
|
|
},
|
|
onPollOptionSelected: (pollId, optionId) async {
|
|
echoBoardController.submitPollVote(pollId, optionId).then((response) {
|
|
if (response != null) {
|
|
echoBoardController.updatePostWithPollResults(
|
|
response,
|
|
selectedOptionId: optionId,
|
|
);
|
|
}
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildCommentsHeader() {
|
|
return Obx(() {
|
|
final totalComments =
|
|
likeCommentController.commentsResponseModel.value.data?.totalCount ??
|
|
0;
|
|
|
|
return Container(
|
|
color: Colors.white,
|
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
|
|
child: Row(
|
|
children: [
|
|
Text(
|
|
'Comments',
|
|
style: TextStyle(
|
|
fontSize: mediumSizeText,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
SizedBox(width: 8.w),
|
|
Text(
|
|
'($totalComments)',
|
|
style: TextStyle(
|
|
fontSize: smallSizeText,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
Widget _buildCommentsList() {
|
|
return Obx(() {
|
|
if (likeCommentController.isCommentsFetchLoading.value) {
|
|
return SliverList(
|
|
delegate: SliverChildBuilderDelegate(
|
|
(context, index) => _buildLoadingShimmer(),
|
|
childCount: 3,
|
|
),
|
|
);
|
|
}
|
|
|
|
final comments =
|
|
likeCommentController.commentsResponseModel.value.data?.items ?? [];
|
|
|
|
if (comments.isEmpty) {
|
|
return SliverToBoxAdapter(
|
|
child: Container(
|
|
color: Colors.white,
|
|
padding: EdgeInsets.symmetric(vertical: 40.h),
|
|
child: Center(
|
|
child: Column(
|
|
children: [
|
|
Icon(Icons.comment_outlined, size: 48, color: Colors.grey),
|
|
SizedBox(height: 12.h),
|
|
Text(
|
|
'No comments yet',
|
|
style: TextStyle(
|
|
fontSize: mediumSizeText,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
SizedBox(height: 4.h),
|
|
Text(
|
|
'Be the first to comment!',
|
|
style: TextStyle(
|
|
fontSize: smallSizeText,
|
|
color: Colors.grey[500],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return SliverList(
|
|
delegate: SliverChildBuilderDelegate((context, index) {
|
|
if (index == comments.length) {
|
|
if (likeCommentController.hasMoreComments.value) {
|
|
return Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 12.h),
|
|
child: Center(
|
|
child: Obx(
|
|
() =>
|
|
likeCommentController.isCommentPaginationLoading.value
|
|
? CircularProgressIndicator(color: Colors.black)
|
|
: ElevatedButton(
|
|
onPressed: _loadMoreComments,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.white,
|
|
foregroundColor: Colors.black,
|
|
elevation: 0,
|
|
side: BorderSide(color: Colors.grey.shade300),
|
|
),
|
|
child: Text('Load More Comments'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return SizedBox.shrink();
|
|
}
|
|
|
|
final comment = comments[index];
|
|
return Container(
|
|
color: Colors.white,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildCommentItem(comment, isSubComment: false),
|
|
if ((comment.subComments?.isNotEmpty ?? false) ||
|
|
(comment.totalSubCommentCount ?? 0) > 0)
|
|
Padding(
|
|
padding: EdgeInsets.only(left: 56.w),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (comment.subComments != null)
|
|
...comment.subComments!.map(
|
|
(subComment) => _buildCommentItem(
|
|
subComment,
|
|
isSubComment: true,
|
|
parentComment: comment,
|
|
),
|
|
),
|
|
if ((comment.totalSubCommentCount ?? 0) >
|
|
(comment.subComments?.length ?? 0))
|
|
_buildLoadMoreRepliesButton(comment),
|
|
],
|
|
),
|
|
),
|
|
Divider(height: 1, color: Colors.grey[200]),
|
|
],
|
|
),
|
|
);
|
|
}, childCount: comments.length + 1),
|
|
);
|
|
});
|
|
}
|
|
|
|
Widget _buildCommentItem(
|
|
CommentItem comment, {
|
|
required bool isSubComment,
|
|
CommentItem? parentComment,
|
|
}) {
|
|
return Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (isSubComment)
|
|
Padding(
|
|
padding: EdgeInsets.only(right: 8.w),
|
|
child: Icon(
|
|
Icons.subdirectory_arrow_right,
|
|
size: 16.sp,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
CircleAvatar(
|
|
radius: 18.r,
|
|
backgroundColor: Colors.grey.shade300,
|
|
child:
|
|
(comment.profilePicture != null &&
|
|
comment.profilePicture!.isNotEmpty)
|
|
? ClipOval(
|
|
child: Image.network(
|
|
comment.profilePicture!,
|
|
fit: BoxFit.cover,
|
|
width: 36.r,
|
|
height: 36.r,
|
|
errorBuilder: (context, error, stackTrace) {
|
|
return Icon(Icons.person, color: Colors.white);
|
|
},
|
|
),
|
|
)
|
|
: Icon(Icons.person, color: Colors.white),
|
|
),
|
|
SizedBox(width: 12.w),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Text(
|
|
comment.fullName ?? 'Unknown User',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: smallSizeText,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(width: 8.w),
|
|
|
|
Text(
|
|
comment.createdAt != null
|
|
? timeago.format(comment.createdAt!)
|
|
: '',
|
|
style: TextStyle(
|
|
fontSize: verySmallSizeText,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
SizedBox(height: 4.h),
|
|
Text(
|
|
comment.commentText ?? '',
|
|
style: TextStyle(fontSize: smallSizeText),
|
|
),
|
|
if (!isSubComment) ...[
|
|
SizedBox(height: 4.h),
|
|
GestureDetector(
|
|
onTap:
|
|
() =>
|
|
_startReply(comment, parentComment: parentComment),
|
|
child: Text(
|
|
'Reply',
|
|
style: TextStyle(
|
|
fontSize: verySmallSizeText,
|
|
fontWeight: FontWeight.w600,
|
|
color: Color(primaryColor),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLoadMoreRepliesButton(CommentItem comment) {
|
|
return InkWell(
|
|
onTap: () => _loadSubComments(comment.commentId!),
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 16.w),
|
|
child: Row(
|
|
children: [
|
|
Obx(
|
|
() =>
|
|
likeCommentController.loadingSubCommentIds.contains(
|
|
comment.commentId,
|
|
)
|
|
? SizedBox(
|
|
width: 16.w,
|
|
height: 16.h,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
Colors.grey,
|
|
),
|
|
),
|
|
)
|
|
: Icon(
|
|
Icons.forum_outlined,
|
|
size: 14.sp,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
SizedBox(width: 4.w),
|
|
Text(
|
|
'View ${(comment.totalSubCommentCount ?? 0) - (comment.subComments?.length ?? 0)} more ${((comment.totalSubCommentCount ?? 0) - (comment.subComments?.length ?? 0)) == 1 ? 'reply' : 'replies'}',
|
|
style: TextStyle(
|
|
fontSize: verySmallSizeText,
|
|
color: Color(primaryColor),
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLoadingShimmer() {
|
|
return Shimmer.fromColors(
|
|
baseColor: Colors.grey[300]!,
|
|
highlightColor: Colors.grey[100]!,
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
width: 36.r,
|
|
height: 36.r,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
SizedBox(width: 12.w),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
width: 120.w,
|
|
height: 14.h,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
SizedBox(height: 8.h),
|
|
Container(
|
|
width: double.infinity,
|
|
height: 12.h,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCommentInputSection() {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.05),
|
|
blurRadius: 10,
|
|
offset: Offset(0, -2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Obx(() => _buildReplyBar()),
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[100],
|
|
borderRadius: BorderRadius.circular(24.r),
|
|
),
|
|
child: TextField(
|
|
controller: _commentController,
|
|
maxLines: 4,
|
|
minLines: 1,
|
|
decoration: InputDecoration(
|
|
hintText: 'Add a comment...',
|
|
border: InputBorder.none,
|
|
hintStyle: TextStyle(fontSize: smallSizeText),
|
|
),
|
|
style: TextStyle(fontSize: smallSizeText),
|
|
textCapitalization: TextCapitalization.sentences,
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 8.w),
|
|
Container(
|
|
width: 40.w,
|
|
height: 40.h,
|
|
decoration: BoxDecoration(
|
|
color: yellowColor,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: IconButton(
|
|
icon: Icon(Icons.send, size: 20.sp, color: Colors.black),
|
|
onPressed: _submitComment,
|
|
padding: EdgeInsets.zero,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
padding: EdgeInsetsDirectional.only(
|
|
bottom: MediaQuery.of(context).viewPadding.bottom,
|
|
),
|
|
|
|
color: Colors.white,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildReplyBar() {
|
|
if (likeCommentController.replyingTo.value == null) {
|
|
return SizedBox.shrink();
|
|
}
|
|
|
|
return Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
|
|
color: Colors.grey.shade200,
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.reply, size: 16.sp),
|
|
SizedBox(width: 8.w),
|
|
Expanded(
|
|
child: Text(
|
|
'Replying to ${likeCommentController.replyingTo.value!.fullName}',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w500,
|
|
fontSize: smallSizeText,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: Icon(Icons.close, size: 16.sp),
|
|
onPressed: () => likeCommentController.resetReplyState(),
|
|
padding: EdgeInsets.zero,
|
|
constraints: BoxConstraints(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|