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 createState() => _SinglePostPageState(); } class _SinglePostPageState extends State { late EchoBoardController echoBoardController; late LikeCommentController likeCommentController; late ProfileController profileController; final ScrollController _scrollController = ScrollController(); final TextEditingController _commentController = TextEditingController(); final Set _processingLikes = {}; 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()) { Get.put(EchoBoardController()); } echoBoardController = Get.find(); if (!Get.isRegistered()) { Get.put(LikeCommentController()); } likeCommentController = Get.find(); if (!Get.isRegistered()) { Get.put(ProfileController()); } profileController = Get.find(); } 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 _loadMoreComments() async { if (!likeCommentController.isCommentPaginationLoading.value && likeCommentController.hasMoreComments.value) { await likeCommentController.fetchcomments(); } } Future _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 _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 _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 _loadSubComments(int parentCommentId) async { await likeCommentController.fetchSubComments(parentCommentId); } Future _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()) { Get.put(NavigationController()); } if (!Get.isRegistered()) { Get.put(EchoBoardController()); } if (!Get.isRegistered()) { Get.put(ProfileController()); } if (!Get.isRegistered()) { Get.put(ChatController()); } if (!Get.isRegistered()) { Get.put(RatingsReviewsController()); } if (!Get.isRegistered()) { 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( 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(), ), ], ), ); } }