639 lines
21 KiB
Dart
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;
|
|
}
|
|
}
|