import 'dart:async'; import 'dart:convert'; import 'dart:developer'; import 'package:dart_pusher_channels/dart_pusher_channels.dart'; import 'package:easy_debounce/easy_debounce.dart'; import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/websockets/ws_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; class WsManager { final WsService wsService; WsManager({required this.wsService}); PusherChannelsClient? client; int? userId; PrivateChannel? _privateNotificationChannel; PrivateChannel? _privateMessagesChannel; PrivateChannel? _privateDirectChatChannel; StreamSubscription? _lifecycleSubscription; StreamSubscription? _eventSubscription; StreamSubscription? _connectionEstablishedSubscription; Stream? get newMessagesStream => _privateMessagesChannel?.bind('App\\Events\\ChatMessageUpdated'); Stream? get newNotificationsStream => _privateNotificationChannel?.bind('Illuminate\\Notifications\\Events\\BroadcastNotificationCreated'); Stream? get directChatStream => _privateDirectChatChannel?.bind('App\\Events\\ChatMessageUpdated'); Stream? get clientTypingStream => _privateDirectChatChannel?.bind('client-typing'); Future initialize() async { if (client == null) { try { if (!await TokenStorage.haveTokenInStorage()) return; final wsUrl = await wsService.getWebsocket(); final options = PusherChannelsOptions.fromHost( scheme: 'wss', key: wsUrl.key, host: wsUrl.host, shouldSupplyMetadataQueries: true, metadata: const PusherChannelsOptionsMetadata.byDefault(), port: 443, ); client = PusherChannelsClient.websocket( options: options, connectionErrorHandler: (exception, trace, refresh) { refresh(); }, minimumReconnectDelayDuration: const Duration( seconds: 1, ), defaultActivityDuration: const Duration( seconds: 30, ), waitForPongDuration: const Duration( seconds: 30, ), ); String? extractSocketId(String? data) { if (data == null) return null; try { final decoded = jsonDecode(data); return decoded['socket_id'] as String?; } catch (_) { return null; } } _connectionEstablishedSubscription = client?.eventStream.listen((event) async { if (event.name == 'pusher:connection_established') { final data = event.data; final socketId = extractSocketId(data); await SocketIdStorage.saveSocketIdInStorage(socketId!); } }); await client?.connect(); client = client; userId = wsUrl.userId; await _initPrivateNotificationsChannel(); await _initPrivateMessagesChannel(); _lifecycleSubscription = client?.lifecycleStream.listen((data) { if (data == PusherChannelsClientLifeCycleState.establishedConnection) { _unsubscribe(); _subscribe(); } }); _eventSubscription = client?.eventStream.listen((event) { log('WsManager received event: ${event.name}'); // Только если это не системные сообщения Pusher, обновляем список чатов if (!event.name.startsWith('pusher:')) { EasyDebounce.debounce( 'searchChatRoom', const Duration(milliseconds: 300), () => sl().getChatRooms(), ); } }); _subscribe(); } catch (e, st) { await Sentry.captureException(e, stackTrace: st); log(e.toString(), stackTrace: st); } } } Future _initPrivateNotificationsChannel() async { final token = await TokenStorage.getTokenFromStorage(); _privateNotificationChannel = client?.privateChannel( '$newNotificationsChannelName.$userId', authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel( authorizationEndpoint: Uri.parse('${sl().baseUrl}/broadcasting/auth'), headers: { 'Authorization': 'Bearer $token', }, ), ); } Future _initPrivateMessagesChannel() async { final token = await TokenStorage.getTokenFromStorage(); _privateMessagesChannel = client?.privateChannel( '$newMessagesChannelName.$userId', authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel( authorizationEndpoint: Uri.parse('${sl().baseUrl}/broadcasting/auth'), headers: { 'Authorization': 'Bearer $token', }, ), ); } Future initPrivateDirectChatChannel(String chatId) async { final token = await TokenStorage.getTokenFromStorage(); _privateDirectChatChannel = client?.privateChannel( '$directChatChannelName.$chatId', authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel( authorizationEndpoint: Uri.parse('${sl().baseUrl}/broadcasting/auth'), headers: { 'Authorization': 'Bearer $token', }, ), ); } void _subscribe() { _privateMessagesChannel?.subscribe(); _privateNotificationChannel?.subscribe(); } void _unsubscribe() { _privateMessagesChannel?.unsubscribe(); _privateNotificationChannel?.unsubscribe(); } void subscribeDirectChatChannel() { _privateDirectChatChannel?.subscribe(); } void unsubscribeDirectChatChannel() { _privateDirectChatChannel?.unsubscribe(); } Future clear() async { await client?.disconnect(); await _lifecycleSubscription?.cancel(); await _eventSubscription?.cancel(); await _connectionEstablishedSubscription?.cancel(); _lifecycleSubscription = null; _eventSubscription = null; _connectionEstablishedSubscription = null; client = null; userId = null; _privateDirectChatChannel?.unsubscribe(); _privateMessagesChannel?.unsubscribe(); _privateNotificationChannel?.unsubscribe(); _privateDirectChatChannel = null; _privateMessagesChannel = null; _privateNotificationChannel = null; } }