onufitness_mobile/lib/services/socket/socket_service.dart
2026-01-13 11:36:24 +05:30

639 lines
21 KiB
Dart

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<void> 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<void> disconnect() async {
if (hubConnection.state != HubConnectionState.Disconnected) {
await hubConnection.stop();
}
isConnected.value = false;
isConnecting.value = false;
connectionState.value = HubConnectionState.Disconnected;
}
// Reconnect SignalR
Future<void> 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<String, dynamic>?;
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<EchoBoardController>();
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<ProfileController>()) {
Get.put(ProfileController());
}
final profileController = Get.find<ProfileController>();
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<LikeCommentController>()) {
Get.put(LikeCommentController());
}
final likeCommentController = Get.find<LikeCommentController>();
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<String, dynamic>?;
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<EchoBoardController>();
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<ProfileController>()) {
Get.put(ProfileController());
}
final profileController = Get.find<ProfileController>();
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<String, dynamic>?;
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<dynamic>? ?? [];
// Update EchoBoard controller with new poll results
try {
final echoBoardController = Get.find<EchoBoardController>();
// 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<String, dynamic>;
// 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<ProfileController>()) {
Get.put(ProfileController());
}
final profileController = Get.find<ProfileController>();
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<LikeCommentController>()) {
Get.put(LikeCommentController());
}
final likeCommentController = Get.find<LikeCommentController>();
// If the controller exists and is viewing a post with this poll, trigger refresh
if (likeCommentController.postId.value != null) {
final post = Get.find<EchoBoardController>().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;
}
}