diff --git a/lib/main.dart b/lib/main.dart index 0b37f77..3244a01 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -80,17 +80,14 @@ class _HazeBotAdminAppState extends State if (state == AppLifecycleState.paused || state == AppLifecycleState.inactive) { - // App going to background - disconnect WebSocket - // โœ… FIX: Web should NOT disconnect on inactive (tab switch) - // Only mobile needs disconnect (app minimized) + // Web should NOT disconnect on inactive (tab switch) + // Only mobile disconnects when app goes to background if (!kIsWeb) { - debugPrint('๐Ÿ“ฑ App paused/inactive - disconnecting WebSocket'); discordAuthService.wsService.disconnect(); } } else if (state == AppLifecycleState.resumed) { - // App coming to foreground - reconnect if authenticated + // Reconnect if authenticated if (discordAuthService.isAuthenticated) { - debugPrint('๐Ÿ“ฑ App resumed - reconnecting WebSocket'); final baseUrl = dotenv.env['API_BASE_URL'] ?? ''; if (baseUrl.isNotEmpty) { discordAuthService.wsService.connect(baseUrl); diff --git a/lib/services/websocket_service.dart b/lib/services/websocket_service.dart index 621c4c2..7576571 100644 --- a/lib/services/websocket_service.dart +++ b/lib/services/websocket_service.dart @@ -17,10 +17,7 @@ class WebSocketService { /// Initialize WebSocket connection void connect(String baseUrl) { - print('๐Ÿ”Œ WebSocket connect() called with baseUrl: $baseUrl'); - if (_socket != null && _socket!.connected) { - print('๐Ÿ”Œ WebSocket already connected - skipping'); return; } @@ -30,20 +27,13 @@ class WebSocketService { if (kIsWeb) { // WEB: Use current origin (admin.haze.pro) - // WebSocket connects to same domain as the web app - wsUrl = Uri.base.origin; // Gets https://admin.haze.pro - print('๐ŸŒ WEB: Using current origin for WebSocket: $wsUrl'); + wsUrl = Uri.base.origin; } else { // MOBILE: Direct URL - remove trailing /api if present - // baseUrl is like: https://api.haze.pro/api - // We need: https://api.haze.pro wsUrl = baseUrl.endsWith('/api') ? baseUrl.substring(0, baseUrl.length - 4) : baseUrl; - print('๐Ÿ“ฑ MOBILE: Using direct API URL for WebSocket: $wsUrl'); } - - print('๐Ÿ”Œ Connecting to WebSocket: $wsUrl'); _socket = IO.io( wsUrl, @@ -55,13 +45,10 @@ class WebSocketService { ); _socket!.onConnect((_) { - print('โœ… WebSocket connected'); _isConnected = true; - print('โœ… WebSocket connection established'); }); _socket!.onDisconnect((_) { - print('โŒ WebSocket disconnected'); _isConnected = false; }); @@ -71,16 +58,14 @@ class WebSocketService { }); _socket!.on('connected', (data) { - print('๐Ÿ“ก Server confirmed connection: $data'); + // Server confirmed connection }); _socket!.on('ticket_update', (data) { - print('๐Ÿ“จ Received ticket update: $data'); _handleTicketUpdate(data); }); _socket!.on('message_history', (data) { - print('๐Ÿ“œ Received message history: ${data}'); _handleMessageHistory(data); }); @@ -120,13 +105,8 @@ class WebSocketService { /// Disconnect WebSocket void disconnect() { if (_socket != null) { - print('๐Ÿ”Œ Disconnecting WebSocket'); - - // โœ… CRITICAL: Leave all joined tickets BEFORE disconnecting - // This ensures backend receives leave_ticket events and clears active_ticket_viewers + // Leave all joined tickets BEFORE disconnecting if (_joinedTickets.isNotEmpty) { - print( - '๐Ÿงน Leaving ${_joinedTickets.length} ticket room(s) before disconnect...'); for (var entry in _joinedTickets.entries) { final ticketId = entry.key; final userId = entry.value; @@ -137,8 +117,6 @@ class WebSocketService { } _socket!.emit('leave_ticket', data); - print( - '๐ŸŽซ Left ticket room: $ticketId${userId != null ? " (user: $userId)" : ""}'); } _joinedTickets.clear(); } @@ -157,53 +135,33 @@ 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}) { - // โœ… 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; } - print( - '๐ŸŽซ Joining ticket room: $ticketId${userId != null ? " (user: $userId)" : ""}'); - final data = {'ticket_id': ticketId}; if (userId != null) { - data['user_id'] = userId; // โœ… Send user_id to suppress push notifications + data['user_id'] = userId; } - // โœ… Track this ticket as joined (for cleanup on disconnect) _joinedTickets[ticketId] = userId; - _socket!.emit('join_ticket', data); - - _socket!.once('joined_ticket', (data) { - print('โœ… Joined ticket room: $data'); - }); } /// Leave a ticket room /// [userId] - Discord user ID to re-enable push notifications for this user void leaveTicket(String ticketId, {String? userId}) { - // โœ… FIX: Use _isConnected for consistency if (_socket == null || !_isConnected) { return; } - print( - '๐ŸŽซ Leaving ticket room: $ticketId${userId != null ? " (user: $userId)" : ""}'); - final data = {'ticket_id': ticketId}; if (userId != null) { - data['user_id'] = - userId; // โœ… Send user_id to re-enable push notifications + data['user_id'] = userId; } _socket!.emit('leave_ticket', data); - - // โœ… Remove from joined tickets tracking _joinedTickets.remove(ticketId); - _ticketListeners.remove(ticketId); } @@ -214,7 +172,6 @@ class WebSocketService { _ticketListeners[ticketId] = []; } _ticketListeners[ticketId]!.add(callback); - print('๐Ÿ‘‚ Added listener for ticket: $ticketId'); } /// Remove ticket update listener @@ -236,15 +193,11 @@ class WebSocketService { final eventType = updateData['event_type'] as String?; if (ticketId == null || eventType == null) { - print('โš ๏ธ Invalid ticket update data: $updateData'); return; } - print('๐Ÿ“ก Processing $eventType for ticket $ticketId'); - // 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); @@ -252,8 +205,6 @@ 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'); @@ -268,13 +219,9 @@ class WebSocketService { final messages = historyData['messages'] as List?; if (ticketId == null || messages == null) { - print('โš ๏ธ Invalid message history data: $historyData'); return; } - print( - '๐Ÿ“œ Processing message history for ticket $ticketId: ${messages.length} messages'); - // Convert to proper format and notify listeners final updateData = { 'ticket_id': ticketId, diff --git a/lib/widgets/ticket_chat_widget.dart b/lib/widgets/ticket_chat_widget.dart index 3ea28b0..ab525c0 100644 --- a/lib/widgets/ticket_chat_widget.dart +++ b/lib/widgets/ticket_chat_widget.dart @@ -141,17 +141,10 @@ class _TicketChatWidgetState extends State @override void didChangeAppLifecycleState(AppLifecycleState state) { - // โœ… Guard: Check if authService is initialized 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( @@ -159,18 +152,12 @@ class _TicketChatWidgetState extends State 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 + // Reload messages if we were disconnected if (_wasDisconnectedWhilePaused) { - debugPrint( - '๐Ÿ“ฑ WebSocket was disconnected during pause - reloading messages to catch up'); _loadMessages(); _wasDisconnectedWhilePaused = false; } - // โœ… FIX: Wait for WebSocket connection before joining ticket room _rejoinTicketAfterReconnect(); } } @@ -180,13 +167,11 @@ class _TicketChatWidgetState extends State 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) { @@ -194,8 +179,6 @@ class _TicketChatWidgetState extends State widget.ticket.ticketId, userId: _currentUserDiscordId, ); - } else { - debugPrint('โŒ Failed to rejoin ticket room - WebSocket not connected'); } } } @@ -334,17 +317,12 @@ 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?; @@ -360,13 +338,11 @@ class _TicketChatWidgetState extends State 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) { @@ -374,8 +350,6 @@ class _TicketChatWidgetState extends State widget.ticket.ticketId, userId: _currentUserDiscordId, ); - } else { - debugPrint('โŒ Failed to join ticket room - WebSocket not connected'); } } } @@ -446,40 +420,26 @@ class _TicketChatWidgetState extends State } void _handleNewMessage(Map messageData) { - debugPrint('๐Ÿ” _handleNewMessage called, mounted=$mounted'); - if (!mounted) { - debugPrint('โŒ Widget not mounted, skipping'); - return; - } + if (!mounted) return; - // โœ… FIX: Use display_content if available (for messages sent via app) - // Backend sends both 'content' (formatted for Discord) and 'display_content' (original) final displayContent = messageData['display_content'] as String? ?? messageData['content'] as String; - // Create message with cleaned content for display final cleanedMessageData = Map.from(messageData); 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) + // Check if message already exists (prevent duplicates) if (_seenMessageIds.contains(newMessage.id)) { - debugPrint('โš ๏ธ Duplicate message ignored: ${newMessage.id}'); return; } - // โœ… FIX: Skip own messages from WebSocket (already added optimistically) - // This prevents duplicate messages when user sends a message + // Skip own messages from WebSocket (already added optimistically) if (_currentUserDiscordId != null && newMessage.authorId == _currentUserDiscordId) { - debugPrint( - 'โญ๏ธ Skipping own message from WebSocket: ${newMessage.id} (already added optimistically)'); return; } - - debugPrint('โœ… Message passes all checks, adding to list'); final oldCount = _messages.length; @@ -498,16 +458,9 @@ class _TicketChatWidgetState extends State // โœ… Update cache (fire-and-forget) _cacheService.appendMessage(widget.ticket.ticketId, newMessage); - // โœ… IMPROVED: Smart scroll behavior based on user position - // - If user is at bottom: auto-scroll to new message - // - If user scrolled up: don't interrupt their reading + // Smart scroll behavior based on user position if (_isUserAtBottom) { - debugPrint('๐Ÿ“ฉ New message arrived, user at bottom โ†’ auto-scrolling'); _scrollToBottom(animate: true); - } else { - debugPrint( - '๐Ÿ“ฉ New message arrived, user scrolled up โ†’ not auto-scrolling'); - // Optional: Could show a "New messages" badge here } }