forked from eatvio/eatvio_app
214 lines
7.6 KiB
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;
|
|
}
|
|
}
|