onufitness_mobile/lib/screens/echoboard/views/single_post_screen.dart
2026-01-13 11:36:24 +05:30

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(),
),
],
),
);
}
}