Files
eatvio_app/lib/core/websockets/ws_manager.dart

214 lines
7.6 KiB
Dart

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:eatvio_app/core/error_reporting/error_reporter.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
class WsManager {
final WsService wsService;
final ErrorReporter _errorReporter;
WsManager({required this.wsService, required ErrorReporter errorReporter})
: _errorReporter = errorReporter;
PusherChannelsClient? client;
int? userId;
PrivateChannel? _privateNotificationChannel;
PrivateChannel? _privateMessagesChannel;
PrivateChannel? _privateDirectChatChannel;
StreamSubscription? _lifecycleSubscription;
StreamSubscription? _eventSubscription;
StreamSubscription? _connectionEstablishedSubscription;
Stream<ChannelReadEvent>? get newMessagesStream => _privateMessagesChannel?.bind('App\\Events\\ChatMessageUpdated');
Stream<ChannelReadEvent>? get newNotificationsStream => _privateNotificationChannel?.bind('Illuminate\\Notifications\\Events\\BroadcastNotificationCreated');
Stream<ChannelReadEvent>? get directChatStream => _privateDirectChatChannel?.bind('App\\Events\\ChatMessageUpdated');
Stream<ChannelReadEvent>? get clientTypingStream => _privateDirectChatChannel?.bind('client-typing');
Future<void> 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<ChatsCubit>().getChatRooms(),
);
}
});
_subscribe();
await _connectCubitsToStreams();
} catch (e, st) {
await _errorReporter.report(e, st, origin: 'WsManager');
log(e.toString(), stackTrace: st);
}
}
}
/// Подключает ChatsCubit и NotificationsCubit к стримам сокета.
/// Вызывается после успешного initialize(), чтобы уведомления и чаты работали
/// и при входе уже залогиненного пользователя (редирект на feed), не только после экрана логина.
Future<void> _connectCubitsToStreams() async {
try {
await sl<ChatsCubit>().connectToWebSocket();
await sl<NotificationsCubit>().getNotifications();
await sl<NotificationsCubit>().connectToWebSocket();
} catch (e, st) {
log('WsManager._connectCubitsToStreams: $e', stackTrace: st);
await _errorReporter.report(e, st, origin: 'WsManager');
}
}
Future<void> _initPrivateNotificationsChannel() async {
final token = await TokenStorage.getTokenFromStorage();
_privateNotificationChannel = client?.privateChannel(
'$newNotificationsChannelName.$userId',
authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel(
authorizationEndpoint: Uri.parse('${sl<AppConfig>().baseUrl}/broadcasting/auth'),
headers: {
'Authorization': 'Bearer $token',
},
),
);
}
Future<void> _initPrivateMessagesChannel() async {
final token = await TokenStorage.getTokenFromStorage();
_privateMessagesChannel = client?.privateChannel(
'$newMessagesChannelName.$userId',
authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel(
authorizationEndpoint: Uri.parse('${sl<AppConfig>().baseUrl}/broadcasting/auth'),
headers: {
'Authorization': 'Bearer $token',
},
),
);
}
Future<void> initPrivateDirectChatChannel(String chatId) async {
final token = await TokenStorage.getTokenFromStorage();
_privateDirectChatChannel = client?.privateChannel(
'$directChatChannelName.$chatId',
authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel(
authorizationEndpoint: Uri.parse('${sl<AppConfig>().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<void> 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;
}
}