import 'dart:convert'; import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:stomp_dart_client/stomp_dart_client.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:muaawana/models/chat_v2/chat_room.dart'; import 'package:muaawana/models/chat_v2/message.dart'; import 'package:muaawana/models/chat_v2/message_list.dart'; import 'package:muaawana/providers/navbar_provider/bottom_navigation_provider.dart'; import 'package:muaawana/providers/navbar_provider/client_bottom_nav_provider.dart'; import 'package:muaawana/services/chat_v2/send_message.dart'; import '../../models/auth/current_user_response.dart'; import '../../models/commun/Image.dart' as img; import '../../models/notification/notifications_model.dart'; import '../../providers/chat_v2/user_conversation.dart'; import '../../providers/notif/notification_provider.dart'; import '../../utils/utils_singleton.dart'; // NOTE: server endpoint registered in Spring is addEndpoint("/ws") // We'll try both /ws and /ws/websocket (SockJS style) if needed. const baseWs = "wss://server.muaawana.com"; const endpointPrimary = "$baseWs/ws"; // preferred const endpointSockJs = "$baseWs/ws/websocket"; // fallback if server expects SockJS path class ChatSocket extends ChangeNotifier { StompClient? stompClient; FlutterLocalNotificationsPlugin notifications = FlutterLocalNotificationsPlugin(); String token = "InvalidToken"; int statusCode = -1; bool errorToken = false; // load token from SharedPreferences (keeps your original logic) Future getToken() async { final prefs = await SharedPreferences.getInstance(); if (prefs.getString('token') == null || prefs.getString('token') == '') { prefs.setString('token', 'InvalidToken'); } if (prefs.getString('token') != null) { token = prefs.getString('token')!; } return token.toString(); } void onConnect(StompFrame frame) { //print("/user/${util.user?.id}/queue/chat"); //print('connected chat socket'); if (stompClient!.connected && util.isValidToken) { //print('connected chat socket 2'); stompClient!.subscribe( destination: "/user/${util.user!.id}/queue/chat", callback: (frame) { // //print(frame); // //print("message recieved"); onMessageReceive(frame); }, ); } } /// connectSock tries endpointPrimary first and if the handshake returns 404 /// it will automatically retry endpointSockJs once. Future connectSock() async { // ensure we have fresh token before creating client await getToken(); // if already connected, nothing to do if (stompClient != null && stompClient!.connected) return; // try primary endpoint first await _tryConnect(url: endpointPrimary, retryOn404Url: endpointSockJs); notifyListeners(); } // internal helper: try connect to given url; if 404 and retryOn404Url provided, retry once Future _tryConnect({required String url, String? retryOn404Url}) async { log("ChatSocket -> trying connect to: $url"); // ensure we close any previous client before creating new one stompClient?.deactivate(); stompClient = null; // create client with Authorization in the websocket handshake headers (critical) stompClient = StompClient( config: StompConfig( url: url, onConnect: onConnect, webSocketConnectHeaders: { 'Authorization': 'Bearer $token', }, // keep STOMP-level headers too (some servers check them) stompConnectHeaders: { 'Authorization': 'Bearer $token', }, onWebSocketError: (dynamic error) { log("ChatSocket --> connectSock --->onWebSocketError $error"); // detect 404 in the error message and retry if requested final errStr = error?.toString() ?? ''; if (retryOn404Url != null && errStr.contains('HTTP status code: 404')) { log("ChatSocket -> received 404 on $url, retrying with $retryOn404Url"); // deactivate current client and try fallback once try { stompClient?.deactivate(); } catch (e) { log("ChatSocket -> error deactivating client before retry: $e"); } // perform a single retry (no loop) to avoid infinite retries _tryConnect(url: retryOn404Url, retryOn404Url: null); } }, onStompError: (dynamic error) => log("onStompError $error"), heartbeatIncoming: const Duration(seconds: 30), heartbeatOutgoing: const Duration(seconds: 30), reconnectDelay: const Duration(seconds: 1), ), ); // activate connection try { stompClient!.activate(); // initialize local notifications only once (your original logic) var androidInitialize = const AndroidInitializationSettings('@mipmap/ic_launcher'); var iOSinitialize = const DarwinInitializationSettings(); var initilizationsSettings = InitializationSettings(android: androidInitialize, iOS: iOSinitialize); notifications.initialize(initilizationsSettings); } catch (e) { log("ChatSocket -> exception while activating client: $e"); } } onMessageReceive(StompFrame frame) async { // print("message received!!!"); //print(frame.body); Map message = jsonDecode(frame.body!); /// ***************************** if (message["senderid"] != null && message["senderid"] == util.user?.id) { return; } /// ***************************** MessageModel messageModel = MessageModel( id: message['id'], senderName: message['senderName'], timestamp: message['timestmp'], sender: null, imagePicture: message['profileimage'], content: message['message'], ); var iosChannel = const DarwinNotificationDetails(); var androidChannel = const AndroidNotificationDetails( 'channel-id', 'channel-name', importance: Importance.max, priority: Priority.max, ); var platformChannel = NotificationDetails(android: androidChannel, iOS: iosChannel); userConversationSingleton.showNotification(); var notFoundConversation = userConversationSingleton.allConversations.where((element) => element.chatroom!.chatId == message['chatId']); //this condition is for adding a message inside the current conversation if (userConversationSingleton.currentChatId == message['chatId']) { userConversationSingleton.addOneMessage(messageModel); notifyListeners(); } //this condition is for updating the conversations list else if (userConversationSingleton.currentChatId != message['chatId']) { notifications.show(0, message['senderName'], message['message'], platformChannel); if (notFoundConversation.isNotEmpty) { userConversationSingleton.updateConversation( conversation: notFoundConversation.first, message: message['message'], senderId: message['senderid']); userConversationSingleton.setConversationCount(message: notFoundConversation.first, count: notFoundConversation.first.count + 1); notifyListeners(); } //this condition is for calling the API if the notFoundConversation is empty; else if (notFoundConversation.isEmpty) { img.Image imageModel = img.Image(url: message['profileimage']); CurrentUser sender = CurrentUser(id: message['senderid'], name: message['senderName'], image: imageModel); ChatRoomModel charRoom = ChatRoomModel(chatId: message['chatId'], sender: sender); MessageListModel conversation = MessageListModel(chatroom: charRoom, lastmessage: message['message'], lastmessagesenderid: message['senderid'], lastmessagetimestmp: message['timestmp']); userConversationSingleton.addAConversation(conversation); userConversationSingleton.setConversationCount(message: conversation, count: conversation.count + 1); notifyListeners(); } } // add a message to the notification img.Image imageModel = img.Image(url: message['profileimage']); CurrentUser sender = CurrentUser(id: message['senderid'], name: message['senderName'], image: imageModel); CurrentUser user = CurrentUser(); MessageModel chat = MessageModel(sender: sender, recipient: user); notifyListeners(); NotificationsModel notification = NotificationsModel(sender: sender, recipient: user, chat: chat, timestmp: message['timestmp']); notificationsProvider.addNotification(notification); notifyListeners(); // Logic for updating the navbar var roleCheck = util.user!.role!.role!; if (roleCheck == "ROLE_USER") { clientNavProvider.updateMessageNotificationState(true); } else if (roleCheck == "ROLE_TECHNICIAN") { techNavProvider.updateMessageNotificationState(true); } } sendMessage({required String message, required int recipient}) { // Check if the client is connected before sending if (stompClient == null || !stompClient!.connected) { print("WebSocket not connected. Cannot send message."); return; } final msg = {'senderId': util.user!.id!.toString(), 'recipientId': recipient.toString(), 'content': message}; try { stompClient!.send(destination: '/app/chat', headers: {}, body: json.encode(msg)); } catch (e) { print("Error sending message: $e"); // Optionally try to reconnect connectSock(); } } // Add a method to check connection status bool get isConnected => stompClient?.connected ?? false; // Add a method to manually disconnect void disconnect() { stompClient?.deactivate(); stompClient = null; notifyListeners(); } }