import 'package:get/get.dart'; import 'package:onufitness/screens/echoboard/models/post_model.dart'; import 'package:signalr_netcore/signalr_client.dart'; import 'package:onufitness/screens/echoboard/controllers/echoboard_controller.dart'; import 'package:onufitness/environment/environment_config.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/models/parent_comments_response_model.dart'; import 'package:onufitness/services/local_storage_services/shared_services.dart'; import 'package:onufitness/services/logger_service.dart'; class SocketService extends GetxController { // Socket Events Constants static const String postCommentEvent = "PostCommentEvent"; static const String postReactionEvent = "PostReactionEvent"; static const String postPollVoteEvent = "PostPollVoteEvent"; static const String userDeleteFromAdminEvent = "UserDeleteFromAdminEvent"; static const String userNotificationEvent = "UserNotificationEvent"; // SignalR Configuration static String get baseUrl => EnvironmentConfigFactory.getConfig().apiBaseUrl; static const String endpoint = "/hubs/social"; late HubConnection hubConnection; final logger = LoggerService(); // Reactive variables var isConnected = false.obs; var isConnecting = false.obs; var connectionError = ''.obs; var connectionState = HubConnectionState.Disconnected.obs; @override void onInit() { super.onInit(); _initSignalR(); connect(); } @override void onClose() { disconnect(); super.onClose(); } // Initialize SignalR Connection void _initSignalR() { try { hubConnection = HubConnectionBuilder() .withUrl('$baseUrl$endpoint') .withAutomaticReconnect(retryDelays: [2000, 5000, 10000, 20000]) .build(); _setupEventListeners(); _setupCustomEventListeners(); } catch (e, stackTrace) { connectionError.value = 'Failed to initialize SignalR: $e'; logger.error( "Failed to initialize SignalR connection", error: e, stackTrace: stackTrace, ); } } // Setup SignalR Connection Event Listeners void _setupEventListeners() { // Connection state changes hubConnection.onclose(({Exception? error}) { isConnected.value = false; isConnecting.value = false; connectionState.value = HubConnectionState.Disconnected; if (error != null) { connectionError.value = 'Connection closed: $error'; logger.error( "SignalR connection closed with error", error: error, stackTrace: StackTrace.current, ); } }); hubConnection.onreconnecting(({Exception? error}) { isConnected.value = false; isConnecting.value = true; connectionState.value = HubConnectionState.Reconnecting; connectionError.value = ''; }); hubConnection.onreconnected(({String? connectionId}) { isConnected.value = true; isConnecting.value = false; connectionState.value = HubConnectionState.Connected; connectionError.value = ''; }); } // Setup Custom Event Listeners void _setupCustomEventListeners() { hubConnection.on(postCommentEvent, (arguments) { _handlePostCommentEvent( arguments?.isNotEmpty == true ? arguments![0] : null, ); }); hubConnection.on(postReactionEvent, (arguments) { _handlePostReactionEvent( arguments?.isNotEmpty == true ? arguments![0] : null, ); }); hubConnection.on(postPollVoteEvent, (arguments) { _handlePostPollVoteEvent( arguments?.isNotEmpty == true ? arguments![0] : null, ); }); hubConnection.on(userDeleteFromAdminEvent, (arguments) { _handleUserDeleteFromAdminEvent( arguments?.isNotEmpty == true ? arguments![0] : null, ); }); hubConnection.on(userNotificationEvent, (arguments) { _handleUserNotificationEvent( arguments?.isNotEmpty == true ? arguments![0] : null, ); }); } // Connect to SignalR Hub Future connect() async { if (isConnected.value || isConnecting.value) { return; } try { isConnecting.value = true; connectionError.value = ''; connectionState.value = HubConnectionState.Connecting; await hubConnection.start(); isConnected.value = true; isConnecting.value = false; connectionState.value = HubConnectionState.Connected; } catch (e, stackTrace) { isConnecting.value = false; isConnected.value = false; connectionState.value = HubConnectionState.Disconnected; connectionError.value = 'Failed to connect: $e'; logger.error( "Failed to connect to SignalR hub", error: e, stackTrace: stackTrace, ); } } // Disconnect from SignalR Hub Future disconnect() async { if (hubConnection.state != HubConnectionState.Disconnected) { await hubConnection.stop(); } isConnected.value = false; isConnecting.value = false; connectionState.value = HubConnectionState.Disconnected; } // Reconnect SignalR Future reconnect() async { await disconnect(); await Future.delayed(const Duration(seconds: 1)); await connect(); } void _handlePostCommentEvent(dynamic data) { if (data == null) return; try { final commentResponse = data['commentResponse'] as Map?; if (commentResponse == null) return; final postId = commentResponse['postID']?.toString(); if (postId == null) return; final parentCommentId = commentResponse['parentCommentID']; final isSubComment = parentCommentId != null && parentCommentId != 0; // Update EchoBoard post count try { final echoBoardController = Get.find(); final postIndex = echoBoardController.posts.indexWhere( (post) => post.postId.toString() == postId, ); if (postIndex != -1) { final currentPost = echoBoardController.posts[postIndex]; echoBoardController.posts[postIndex] = currentPost.copyWith( totalPostComments: (currentPost.totalPostComments ?? 0) + 1, ); echoBoardController.posts.refresh(); } } catch (e, stackTrace) { logger.error( "Failed to update post comment count in EchoBoardController", error: e, stackTrace: stackTrace, ); } // Update ProfileController post count try { if (!Get.isRegistered()) { Get.put(ProfileController()); } final profileController = Get.find(); profileController.updatePostCommentCount(postId, 1); } catch (e, stackTrace) { logger.error( "Failed to update post comment count in ProfileController", error: e, stackTrace: stackTrace, ); } // Update LikeComment controller only if viewing this post try { if (!Get.isRegistered()) { Get.put(LikeCommentController()); } final likeCommentController = Get.find(); if (likeCommentController.postId.value != postId) return; final newComment = CommentItem( commentId: commentResponse['commentID'], postId: commentResponse['postID'], commentText: commentResponse['commentText'], firstName: commentResponse['firstName'], lastName: commentResponse['lastName'], fullName: commentResponse['fullName'], profilePicture: commentResponse['profilePicture'], createdAt: commentResponse['createdAt'] != null ? DateTime.parse(commentResponse['createdAt']) : DateTime.now(), subComments: [], totalSubCommentCount: 0, ); if (likeCommentController.commentsResponseModel.value.data?.items == null) { likeCommentController.commentsResponseModel.value.data = CommentData( totalCount: 1, items: [newComment], ); likeCommentController.commentsResponseModel.refresh(); return; } final items = likeCommentController.commentsResponseModel.value.data!.items!; if (isSubComment) { // Add sub comment to parent final parentIndex = items.indexWhere( (comment) => comment.commentId == parentCommentId, ); if (parentIndex != -1) { items[parentIndex].subComments ??= []; // Check for duplicate if (!items[parentIndex].subComments!.any( (sub) => sub.commentId == newComment.commentId, )) { items[parentIndex].subComments!.add(newComment); items[parentIndex].totalSubCommentCount = (items[parentIndex].totalSubCommentCount ?? 0) + 1; likeCommentController.commentsResponseModel.refresh(); } } } else { // Add new parent comment if (!items.any( (comment) => comment.commentId == newComment.commentId, )) { items.insert(0, newComment); likeCommentController.commentsResponseModel.value.data!.totalCount = (likeCommentController .commentsResponseModel .value .data! .totalCount ?? 0) + 1; likeCommentController.commentsResponseModel.refresh(); } } } catch (e, stackTrace) { logger.error( "Failed to update comments in LikeCommentController", error: e, stackTrace: stackTrace, ); } } catch (e, stackTrace) { logger.error( "Failed to handle PostCommentEvent", error: e, stackTrace: stackTrace, ); } } void _handlePostReactionEvent(dynamic data) { if (data == null) return; try { // Extract reaction data from socket response final reactionResponse = data['reactionResponse'] as Map?; final userId = data['userId'] as String?; if (reactionResponse == null) return; // Get current user ID to check if this is our own reaction final currentUserId = SharedServices.getUserDetails()?.data?.userId; final isOwnReaction = currentUserId == userId; // Process all reactions including our own for consistency final postId = reactionResponse['postID']?.toString(); if (postId == null) return; final totalReactions = reactionResponse['totalPostReactions'] as int? ?? 0; final reactionTypeId = reactionResponse['postReactionTypeID']; // Update EchoBoard controller with new reaction count try { final echoBoardController = Get.find(); final postIndex = echoBoardController.posts.indexWhere( (post) => post.postId.toString() == postId, ); if (postIndex != -1) { final currentPost = echoBoardController.posts[postIndex]; final updatedPost = currentPost.copyWith( totalPostReactions: totalReactions, // Note: We don't update postReactionTypeId as that's specific to current user ); echoBoardController.posts[postIndex] = updatedPost; echoBoardController.update(); echoBoardController.posts.refresh(); } } catch (e, stackTrace) { logger.error( "Failed to update post reaction count in EchoBoardController", error: e, stackTrace: stackTrace, ); } // Update ProfileController with new reaction count try { if (!Get.isRegistered()) { Get.put(ProfileController()); } final profileController = Get.find(); final profilePostIndex = profileController.posts.indexWhere( (post) => post.postId.toString() == postId, ); if (profilePostIndex != -1) { final currentPost = profileController.posts[profilePostIndex]; final updatedPost = currentPost.copyWith( totalPostReactions: totalReactions, // Only update reaction type if it's the current user's reaction postReactionTypeId: isOwnReaction ? reactionTypeId : currentPost.postReactionTypeId, ); profileController.posts[profilePostIndex] = updatedPost; profileController.postsCache[postId] = updatedPost; profileController.posts.refresh(); profileController.update(); } } catch (e, stackTrace) { logger.error( "Failed to update post reaction in ProfileController", error: e, stackTrace: stackTrace, ); } } catch (e, stackTrace) { logger.error( "Failed to handle PostReactionEvent", error: e, stackTrace: stackTrace, ); } } void _handlePostPollVoteEvent(dynamic data) { if (data == null) return; try { // Extract vote response data from socket final voteResponse = data['voteResponse'] as Map?; final userId = data['userId'] as String?; if (voteResponse == null) return; // Get current user ID to check if this is our own vote final currentUserId = SharedServices.getUserDetails()?.data?.userId; final isOwnVote = currentUserId == userId; final pollPostId = voteResponse['pollPostID']; if (pollPostId == null) return; // Extract poll data from socket response final totalPollVotes = voteResponse['totalPollVotes'] as int? ?? 0; final options = voteResponse['options'] as List? ?? []; // Update EchoBoard controller with new poll results try { final echoBoardController = Get.find(); // Find the post containing this poll final postIndex = echoBoardController.posts.indexWhere( (post) => post.poll?.pollPostId == pollPostId, ); if (postIndex != -1) { final currentPost = echoBoardController.posts[postIndex]; // Create a new post instance with updated poll data final updatedPost = SocialPostItem( postId: currentPost.postId, userId: currentPost.userId, firstName: currentPost.firstName, lastName: currentPost.lastName, fullName: currentPost.fullName, profilePicture: currentPost.profilePicture, userTypeId: currentPost.userTypeId, userTypeName: currentPost.userTypeName, coachTypeName: currentPost.coachTypeName, coachRequestStatus: currentPost.coachRequestStatus, content: currentPost.content, postTypeId: currentPost.postTypeId, postVisibilityId: currentPost.postVisibilityId, deviceTypeId: currentPost.deviceTypeId, isTribe: currentPost.isTribe, tribeId: currentPost.tribeId, createdAt: currentPost.createdAt, updateAt: currentPost.updateAt, mediaFiles: currentPost.mediaFiles, comments: currentPost.comments, totalPostComments: currentPost.totalPostComments, totalPostReactions: currentPost.totalPostReactions, postReactionTypeId: currentPost.postReactionTypeId, ); // Update the poll with new data from socket if (currentPost.poll != null) { updatedPost.poll = Poll( pollPostId: currentPost.poll!.pollPostId, question: voteResponse['question'] ?? currentPost.poll!.question, expiryDate: voteResponse['expiryDate'] != null ? DateTime.parse(voteResponse['expiryDate']) : currentPost.poll!.expiryDate, totalPollVotes: totalPollVotes, options: [], ); // Update each poll option with new vote counts and percentages for (var socketOption in options) { final optionMap = socketOption as Map; // Find the corresponding original option to preserve any local data final originalOption = currentPost.poll!.options?.firstWhere( (opt) => opt.optionId == optionMap['optionID'], orElse: () => Option(), ); updatedPost.poll!.options!.add( Option( optionId: optionMap['optionID'], pollId: optionMap['pollID'], userId: optionMap['userID'], optionLabel: optionMap['optionLabel'] ?? originalOption?.optionLabel, totalOptionPollVotes: optionMap['totalOptionPollVotes'] ?? 0, percentageOptionPollVotes: optionMap['percentageOptionPollVotes'] ?? 0, hasUserVoted: isOwnVote ? (optionMap['hasUserVoted'] ?? false) // For own votes, use socket data : (originalOption?.hasUserVoted ?? false), // For others, preserve current user's vote status ), ); } } // Update the post in the list echoBoardController.posts[postIndex] = updatedPost; // Also update in cache if using it if (updatedPost.postId != null) { echoBoardController.postsCache[updatedPost.postId.toString()] = updatedPost; } // Force UI update echoBoardController.update(); echoBoardController.posts.refresh(); } else {} } catch (e, stackTrace) { logger.error( "Failed to update poll results in EchoBoardController", error: e, stackTrace: stackTrace, ); } // Update ProfileController with poll results try { if (!Get.isRegistered()) { Get.put(ProfileController()); } final profileController = Get.find(); profileController.updatePostPollResults(voteResponse); } catch (e, stackTrace) { logger.error( "Failed to update poll results in ProfileController", error: e, stackTrace: stackTrace, ); } // Also check if LikeCommentController is active and viewing this post try { if (!Get.isRegistered()) { Get.put(LikeCommentController()); } final likeCommentController = Get.find(); // If the controller exists and is viewing a post with this poll, trigger refresh if (likeCommentController.postId.value != null) { final post = Get.find().posts.firstWhereOrNull( (p) => p.postId.toString() == likeCommentController.postId.value && p.poll?.pollPostId == pollPostId, ); if (post != null) { likeCommentController.update(); } } } catch (e, stackTrace) { logger.error( "Failed to update poll results in LikeCommentController", error: e, stackTrace: stackTrace, ); } } catch (e, stackTrace) { logger.error( "Failed to handle PostPollVoteEvent", error: e, stackTrace: stackTrace, ); } } void _handleUserDeleteFromAdminEvent(dynamic data) { // Handle user delete from admin event // You can emit updates to other controllers or update UI state } void _handleUserNotificationEvent(dynamic data) { // Handle user notification event // You can emit updates to other controllers or update UI state } // Utility Methods bool get isSignalRConnected { return isConnected.value; } String get connectionStatus { String status; switch (connectionState.value) { case HubConnectionState.Disconnected: status = connectionError.value.isNotEmpty ? 'Error: ${connectionError.value}' : 'Disconnected'; break; case HubConnectionState.Connecting: status = 'Connecting...'; break; case HubConnectionState.Connected: status = 'Connected'; break; case HubConnectionState.Disconnecting: status = 'Disconnecting...'; break; case HubConnectionState.Reconnecting: status = 'Reconnecting...'; break; } return status; } // Get connection ID String? get connectionId { String? id = hubConnection.connectionId; return id; } // Get current connection state HubConnectionState? get currentState { HubConnectionState? state = hubConnection.state; return state; } }