From c29cfca6fd6472a31c03a5d2e6ac704749ac3cad Mon Sep 17 00:00:00 2001 From: inventory69 Date: Thu, 4 Dec 2025 15:27:53 +0100 Subject: [PATCH 1/6] fix (test): new websocket helper --- lib/main.dart | 9 ++++- lib/services/websocket_service.dart | 23 ++++++++++++ lib/widgets/ticket_chat_widget.dart | 58 +++++++++++++++++++++++++---- 3 files changed, 81 insertions(+), 9 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index ca81947..0b37f77 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:provider/provider.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:dynamic_color/dynamic_color.dart'; @@ -80,8 +81,12 @@ class _HazeBotAdminAppState extends State if (state == AppLifecycleState.paused || state == AppLifecycleState.inactive) { // App going to background - disconnect WebSocket - debugPrint('📱 App paused/inactive - disconnecting WebSocket'); - discordAuthService.wsService.disconnect(); + // ✅ FIX: Web should NOT disconnect on inactive (tab switch) + // Only mobile needs disconnect (app minimized) + if (!kIsWeb) { + debugPrint('📱 App paused/inactive - disconnecting WebSocket'); + discordAuthService.wsService.disconnect(); + } } else if (state == AppLifecycleState.resumed) { // App coming to foreground - reconnect if authenticated if (discordAuthService.isAuthenticated) { diff --git a/lib/services/websocket_service.dart b/lib/services/websocket_service.dart index 282d0f0..9e23ad7 100644 --- a/lib/services/websocket_service.dart +++ b/lib/services/websocket_service.dart @@ -93,6 +93,29 @@ class WebSocketService { } } + /// Wait for WebSocket to connect + /// Returns true if connected, false if timeout + Future waitForConnection({Duration timeout = const Duration(seconds: 5)}) async { + if (isConnected) { + print('✅ Already connected'); + return true; + } + + print('⏳ Waiting for WebSocket connection...'); + final start = DateTime.now(); + + while (!isConnected) { + if (DateTime.now().difference(start) > timeout) { + print('❌ WebSocket connection timeout after ${timeout.inSeconds}s'); + return false; + } + await Future.delayed(const Duration(milliseconds: 100)); + } + + print('✅ WebSocket connection established'); + return true; + } + /// Disconnect WebSocket void disconnect() { if (_socket != null) { diff --git a/lib/widgets/ticket_chat_widget.dart b/lib/widgets/ticket_chat_widget.dart index 046decc..f6951ce 100644 --- a/lib/widgets/ticket_chat_widget.dart +++ b/lib/widgets/ticket_chat_widget.dart @@ -95,7 +95,7 @@ class _TicketChatWidgetState extends State // CRITICAL: Load user ID BEFORE joining WebSocket room await _loadCurrentUser(); - // Now join WebSocket with user ID for push notification suppression + // Now join WebSocket with user ID for push notification suppression (async) _setupWebSocketListener(); } @@ -170,10 +170,33 @@ class _TicketChatWidgetState extends State _wasDisconnectedWhilePaused = false; } + // ✅ FIX: Wait for WebSocket connection before joining ticket room + _rejoinTicketAfterReconnect(); + } + } + + /// Rejoin ticket room after WebSocket reconnects (with retry logic) + Future _rejoinTicketAfterReconnect() async { + final connected = await _authService!.wsService.waitForConnection(); + + if (connected) { + debugPrint('✅ WebSocket ready - joining ticket room'); _authService!.wsService.joinTicket( widget.ticket.ticketId, userId: _currentUserDiscordId, ); + } else { + debugPrint('❌ WebSocket connection timeout - retrying in 2s...'); + // Retry once after 2 seconds + await Future.delayed(const Duration(seconds: 2)); + if (_authService!.wsService.isConnected) { + _authService!.wsService.joinTicket( + widget.ticket.ticketId, + userId: _currentUserDiscordId, + ); + } else { + debugPrint('❌ Failed to rejoin ticket room - WebSocket not connected'); + } } } @@ -306,12 +329,8 @@ class _TicketChatWidgetState extends State _authService = Provider.of(context, listen: false); final wsService = _authService!.wsService; - // Join ticket room with user ID for push notification suppression - wsService.joinTicket( - widget.ticket.ticketId, - userId: - _currentUserDiscordId, // ✅ Suppress push notifications while viewing - ); + // ✅ FIX: Wait for WebSocket connection before joining ticket room + _joinTicketWhenReady(); // Listen for new messages and history wsService.onTicketUpdate(widget.ticket.ticketId, (data) { @@ -331,6 +350,31 @@ class _TicketChatWidgetState extends State }); } + /// Join ticket room once WebSocket is connected (with retry logic) + Future _joinTicketWhenReady() async { + final connected = await _authService!.wsService.waitForConnection(); + + if (connected) { + debugPrint('✅ WebSocket ready - joining ticket room (initial)'); + _authService!.wsService.joinTicket( + widget.ticket.ticketId, + userId: _currentUserDiscordId, + ); + } else { + debugPrint('⚠️ WebSocket connection timeout on initial join - retrying...'); + // Retry once after 2 seconds + await Future.delayed(const Duration(seconds: 2)); + if (_authService!.wsService.isConnected) { + _authService!.wsService.joinTicket( + widget.ticket.ticketId, + userId: _currentUserDiscordId, + ); + } else { + debugPrint('❌ Failed to join ticket room - WebSocket not connected'); + } + } + } + void _handleMessageHistory(List messagesData) { if (!mounted) return; From 1985ffea0bd84dca70d2ade404d86ca2de48afa6 Mon Sep 17 00:00:00 2001 From: inventory69 Date: Thu, 4 Dec 2025 15:36:14 +0100 Subject: [PATCH 2/6] fix again --- lib/services/websocket_service.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/services/websocket_service.dart b/lib/services/websocket_service.dart index 9e23ad7..670f671 100644 --- a/lib/services/websocket_service.dart +++ b/lib/services/websocket_service.dart @@ -57,6 +57,7 @@ class WebSocketService { _socket!.onConnect((_) { print('✅ WebSocket connected'); _isConnected = true; + print('✅ WebSocket connection established'); }); _socket!.onDisconnect((_) { @@ -153,7 +154,9 @@ class WebSocketService { /// Join a ticket room to receive updates /// [userId] - Discord user ID to suppress push notifications for this user void joinTicket(String ticketId, {String? userId}) { - if (_socket == null || !_socket!.connected) { + // ✅ FIX: Use _isConnected (same as waitForConnection) instead of _socket!.connected + // This avoids race condition where onConnect fired but socket.io internal state not yet updated + if (_socket == null || !_isConnected) { print('⚠️ Cannot join ticket: WebSocket not connected'); return; } @@ -179,7 +182,8 @@ class WebSocketService { /// Leave a ticket room /// [userId] - Discord user ID to re-enable push notifications for this user void leaveTicket(String ticketId, {String? userId}) { - if (_socket == null || !_socket!.connected) { + // ✅ FIX: Use _isConnected for consistency + if (_socket == null || !_isConnected) { return; } From d9cce0487804cf9bd4a9958ee394e6bd5aa21b16 Mon Sep 17 00:00:00 2001 From: inventory69 Date: Thu, 4 Dec 2025 15:41:59 +0100 Subject: [PATCH 3/6] fix again again --- lib/screens/admin/ticket_detail_dialog.dart | 55 +++++++++++++++++++-- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/lib/screens/admin/ticket_detail_dialog.dart b/lib/screens/admin/ticket_detail_dialog.dart index 3154465..3b0f50c 100644 --- a/lib/screens/admin/ticket_detail_dialog.dart +++ b/lib/screens/admin/ticket_detail_dialog.dart @@ -107,10 +107,33 @@ class _TicketDetailDialogState extends State _wasDisconnectedWhilePaused = false; } + // ✅ FIX: Wait for WebSocket connection before joining ticket room + _rejoinTicketAfterReconnect(); + } + } + + /// Rejoin ticket room after WebSocket reconnects (with retry logic) + Future _rejoinTicketAfterReconnect() async { + final connected = await _authService!.wsService.waitForConnection(); + + if (connected) { + debugPrint('✅ WebSocket ready - joining ticket room (dialog)'); _authService!.wsService.joinTicket( widget.ticket.ticketId, userId: _currentUserDiscordId, ); + } else { + debugPrint('❌ WebSocket connection timeout - retrying in 2s...'); + // Retry once after 2 seconds + await Future.delayed(const Duration(seconds: 2)); + if (_authService!.wsService.isConnected) { + _authService!.wsService.joinTicket( + widget.ticket.ticketId, + userId: _currentUserDiscordId, + ); + } else { + debugPrint('❌ Failed to rejoin ticket room - WebSocket not connected'); + } } } @@ -139,11 +162,8 @@ class _TicketDetailDialogState extends State _authService = Provider.of(context, listen: false); final wsService = _authService!.wsService; - // Join ticket room with user ID for push notification suppression - wsService.joinTicket( - widget.ticket.ticketId, - userId: _currentUserDiscordId, // ✅ Suppress push while viewing - ); + // ✅ FIX: Wait for WebSocket connection before joining ticket room + _joinTicketWhenReady(); // Listen for new messages wsService.onTicketUpdate(widget.ticket.ticketId, (data) { @@ -158,6 +178,31 @@ class _TicketDetailDialogState extends State }); } + /// Join ticket room once WebSocket is connected (with retry logic) + Future _joinTicketWhenReady() async { + final connected = await _authService!.wsService.waitForConnection(); + + if (connected) { + debugPrint('✅ WebSocket ready - joining ticket room (dialog initial)'); + _authService!.wsService.joinTicket( + widget.ticket.ticketId, + userId: _currentUserDiscordId, + ); + } else { + debugPrint('⚠️ WebSocket connection timeout on initial join - retrying...'); + // Retry once after 2 seconds + await Future.delayed(const Duration(seconds: 2)); + if (_authService!.wsService.isConnected) { + _authService!.wsService.joinTicket( + widget.ticket.ticketId, + userId: _currentUserDiscordId, + ); + } else { + debugPrint('❌ Failed to join ticket room - WebSocket not connected'); + } + } + } + void _handleNewMessage(Map messageData) { if (!mounted) return; From 5d4d4476bda74a6bb23d3120eaa9390295858c2d Mon Sep 17 00:00:00 2001 From: inventory69 Date: Thu, 4 Dec 2025 15:48:12 +0100 Subject: [PATCH 4/6] fix new --- lib/screens/admin/ticket_detail_dialog.dart | 188 +------------------- 1 file changed, 9 insertions(+), 179 deletions(-) diff --git a/lib/screens/admin/ticket_detail_dialog.dart b/lib/screens/admin/ticket_detail_dialog.dart index 3b0f50c..dfe2377 100644 --- a/lib/screens/admin/ticket_detail_dialog.dart +++ b/lib/screens/admin/ticket_detail_dialog.dart @@ -23,7 +23,8 @@ class TicketDetailDialog extends StatefulWidget { } class _TicketDetailDialogState extends State - with SingleTickerProviderStateMixin, WidgetsBindingObserver { + with SingleTickerProviderStateMixin { + // ✅ REMOVED WidgetsBindingObserver - TicketChatWidget handles WebSocket lifecycle late TabController _tabController; List _messages = []; bool _isLoadingMessages = false; @@ -35,19 +36,12 @@ class _TicketDetailDialogState extends State Set _seenMessageIds = {}; int _firstNewMessageIndex = -1; // Index of first new message for divider - // Store AuthService reference to avoid accessing context in dispose - AuthService? _authService; // ✅ Nullable to avoid LateInitializationError - String? _currentUserDiscordId; // ✅ For push notification suppression - bool _wasDisconnectedWhilePaused = false; // ✅ Track if we missed messages + // ✅ REMOVED: WebSocket-related variables (now handled by TicketChatWidget) @override void initState() { super.initState(); - WidgetsBinding.instance - .addObserver(this); // ✅ Observe app lifecycle changes - - // Load current user ID for push notification suppression - _loadCurrentUser(); + // ✅ REMOVED: No longer observing app lifecycle - TicketChatWidget handles this _tabController = TabController( length: 2, @@ -67,172 +61,15 @@ class _TicketDetailDialogState extends State } }); - // ✅ CRITICAL: Load user ID FIRST, then setup WebSocket - _initializeWithUser(); - // If opening to chat tab, load messages immediately if (widget.initialTab == 1) { _loadMessages(); } } - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - // ✅ Guard: Check if authService is initialized before accessing - if (_authService == null) return; - - // ✅ CRITICAL: Leave ticket room when app goes to background - // This ensures push notifications are re-enabled when user is not actively viewing - if (state == AppLifecycleState.paused || - state == AppLifecycleState.inactive) { - debugPrint( - '📱 App paused/inactive - leaving ticket room to re-enable push notifications'); - - // Track if WebSocket is disconnected during pause (we might miss messages) - _wasDisconnectedWhilePaused = !_authService!.wsService.isConnected; - - _authService!.wsService.leaveTicket( - widget.ticket.ticketId, - userId: _currentUserDiscordId, - ); - } else if (state == AppLifecycleState.resumed) { - debugPrint( - '📱 App resumed - rejoining ticket room to suppress push notifications'); - - // ✅ If WebSocket was disconnected, reload messages from API to catch missed ones - if (_wasDisconnectedWhilePaused) { - debugPrint( - '📱 WebSocket was disconnected during pause - reloading messages'); - _loadMessages(); - _wasDisconnectedWhilePaused = false; - } - - // ✅ FIX: Wait for WebSocket connection before joining ticket room - _rejoinTicketAfterReconnect(); - } - } - - /// Rejoin ticket room after WebSocket reconnects (with retry logic) - Future _rejoinTicketAfterReconnect() async { - final connected = await _authService!.wsService.waitForConnection(); - - if (connected) { - debugPrint('✅ WebSocket ready - joining ticket room (dialog)'); - _authService!.wsService.joinTicket( - widget.ticket.ticketId, - userId: _currentUserDiscordId, - ); - } else { - debugPrint('❌ WebSocket connection timeout - retrying in 2s...'); - // Retry once after 2 seconds - await Future.delayed(const Duration(seconds: 2)); - if (_authService!.wsService.isConnected) { - _authService!.wsService.joinTicket( - widget.ticket.ticketId, - userId: _currentUserDiscordId, - ); - } else { - debugPrint('❌ Failed to rejoin ticket room - WebSocket not connected'); - } - } - } - - /// Load user ID first, then setup WebSocket with user tracking - Future _initializeWithUser() async { - // CRITICAL: Load user ID BEFORE joining WebSocket room - await _loadCurrentUser(); - - // Now join WebSocket with user ID for push notification suppression - _setupWebSocketListener(); - } - - /// Load current user's Discord ID for push notification suppression - Future _loadCurrentUser() async { - try { - final authService = Provider.of(context, listen: false); - final userData = await authService.apiService.getCurrentUser(); - _currentUserDiscordId = userData['discord_id']?.toString(); - debugPrint('👤 Current user Discord ID: $_currentUserDiscordId'); - } catch (e) { - debugPrint('⚠️ Could not load current user ID: $e'); - } - } - - void _setupWebSocketListener() { - _authService = Provider.of(context, listen: false); - final wsService = _authService!.wsService; - - // ✅ FIX: Wait for WebSocket connection before joining ticket room - _joinTicketWhenReady(); - - // Listen for new messages - wsService.onTicketUpdate(widget.ticket.ticketId, (data) { - final eventType = data['event_type'] as String?; - - if (eventType == 'new_message') { - final messageData = data['data'] as Map?; - if (messageData != null) { - _handleNewMessage(messageData); - } - } - }); - } - - /// Join ticket room once WebSocket is connected (with retry logic) - Future _joinTicketWhenReady() async { - final connected = await _authService!.wsService.waitForConnection(); - - if (connected) { - debugPrint('✅ WebSocket ready - joining ticket room (dialog initial)'); - _authService!.wsService.joinTicket( - widget.ticket.ticketId, - userId: _currentUserDiscordId, - ); - } else { - debugPrint('⚠️ WebSocket connection timeout on initial join - retrying...'); - // Retry once after 2 seconds - await Future.delayed(const Duration(seconds: 2)); - if (_authService!.wsService.isConnected) { - _authService!.wsService.joinTicket( - widget.ticket.ticketId, - userId: _currentUserDiscordId, - ); - } else { - debugPrint('❌ Failed to join ticket room - WebSocket not connected'); - } - } - } - - void _handleNewMessage(Map messageData) { - if (!mounted) return; - - final newMessage = TicketMessage.fromJson(messageData); - - // Check if message already exists - if (_messages.any((m) => m.id == newMessage.id)) { - return; - } - - final oldCount = _messages.length; - - setState(() { - _messages.add(newMessage); - _previousMessageCount = _messages.length; - // Mark where new messages start (if not already set) - if (_firstNewMessageIndex < 0) { - _firstNewMessageIndex = oldCount; - } - }); - - // Scroll behavior on messages tab - if (_tabController.index == 1) { - if (_firstNewMessageIndex >= 0) { - _scrollToNewMessages(); - } else { - _scrollToBottom(); - } - } - } + // ✅ REMOVED: All WebSocket lifecycle management + // TicketChatWidget (embedded in Messages tab) now handles all WebSocket operations + // This prevents duplicate joins/leaves and multiple lifecycle observers void _markAllAsSeen() { setState(() { @@ -242,15 +79,8 @@ class _TicketDetailDialogState extends State @override void dispose() { - WidgetsBinding.instance.removeObserver(this); // ✅ Remove observer - - // Leave WebSocket ticket room with user ID to re-enable push notifications - if (_authService != null) { - _authService!.wsService.leaveTicket( - widget.ticket.ticketId, - userId: _currentUserDiscordId, // ✅ Re-enable push when leaving - ); - } + // ✅ REMOVED: No longer managing WebSocket lifecycle + // TicketChatWidget handles join/leave operations _tabController.dispose(); _messageController.dispose(); From cbc2419a3260a21b58f6463ab57017d1ca2f78da Mon Sep 17 00:00:00 2001 From: inventory69 Date: Thu, 4 Dec 2025 15:54:54 +0100 Subject: [PATCH 5/6] teeest --- lib/widgets/ticket_chat_widget.dart | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/widgets/ticket_chat_widget.dart b/lib/widgets/ticket_chat_widget.dart index f6951ce..3ea28b0 100644 --- a/lib/widgets/ticket_chat_widget.dart +++ b/lib/widgets/ticket_chat_widget.dart @@ -334,12 +334,17 @@ class _TicketChatWidgetState extends State // Listen for new messages and history wsService.onTicketUpdate(widget.ticket.ticketId, (data) { + debugPrint('🎯 TicketChatWidget received update: ${data['event_type']}'); final eventType = data['event_type'] as String?; if (eventType == 'new_message') { final messageData = data['data'] as Map?; + debugPrint('📦 Message data: ${messageData != null ? "VALID" : "NULL"}'); if (messageData != null) { + debugPrint('✅ Calling _handleNewMessage()'); _handleNewMessage(messageData); + } else { + debugPrint('❌ messageData is NULL!'); } } else if (eventType == 'message_history') { final messagesData = data['data'] as List?; @@ -441,7 +446,11 @@ class _TicketChatWidgetState extends State } void _handleNewMessage(Map messageData) { - if (!mounted) return; + debugPrint('🔍 _handleNewMessage called, mounted=$mounted'); + if (!mounted) { + debugPrint('❌ Widget not mounted, skipping'); + return; + } // ✅ FIX: Use display_content if available (for messages sent via app) // Backend sends both 'content' (formatted for Discord) and 'display_content' (original) @@ -453,6 +462,7 @@ class _TicketChatWidgetState extends State cleanedMessageData['content'] = displayContent; final newMessage = TicketMessage.fromJson(cleanedMessageData); + debugPrint('📝 New message parsed: ${newMessage.id} from ${newMessage.authorName}'); // ✅ FIX: Check if message already exists (prevent duplicates) if (_seenMessageIds.contains(newMessage.id)) { @@ -468,6 +478,9 @@ class _TicketChatWidgetState extends State '⏭️ Skipping own message from WebSocket: ${newMessage.id} (already added optimistically)'); return; } + + debugPrint('✅ Message passes all checks, adding to list'); + final oldCount = _messages.length; From 882bec761f2f6b5c350a8c8de720115afcfa83e6 Mon Sep 17 00:00:00 2001 From: inventory69 Date: Thu, 4 Dec 2025 16:00:01 +0100 Subject: [PATCH 6/6] ok last one --- lib/services/websocket_service.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/services/websocket_service.dart b/lib/services/websocket_service.dart index 670f671..621c4c2 100644 --- a/lib/services/websocket_service.dart +++ b/lib/services/websocket_service.dart @@ -147,7 +147,10 @@ class WebSocketService { _socket!.dispose(); _socket = null; _isConnected = false; - _ticketListeners.clear(); + // ✅ FIX: Do NOT clear listeners on disconnect! + // Listeners are callbacks registered by widgets and should persist across reconnects + // They will automatically work again when socket reconnects + // _ticketListeners.clear(); // ❌ REMOVED - this caused messages to not arrive after reconnect } } @@ -241,6 +244,7 @@ class WebSocketService { // Notify all listeners for this ticket if (_ticketListeners.containsKey(ticketId)) { + print('✅ Found ${_ticketListeners[ticketId]!.length} listener(s) for ticket $ticketId'); for (final listener in _ticketListeners[ticketId]!) { try { listener(updateData); @@ -248,6 +252,8 @@ class WebSocketService { print('❌ Error in ticket listener: $e'); } } + } else { + print('⚠️ No listeners found for ticket $ticketId! Available tickets: ${_ticketListeners.keys.toList()}'); } } catch (e) { print('❌ Error handling ticket update: $e');