taxgilde/lib/consts/notification_webscoket.dart
2026-04-11 10:21:31 +05:30

329 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'dart:convert';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:taxglide/services/notification_service.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:taxglide/controller/api_contoller.dart';
class NotificationWebSocket {
static final NotificationWebSocket _instance =
NotificationWebSocket._internal();
factory NotificationWebSocket() => _instance;
NotificationWebSocket._internal();
WebSocketChannel? _channel;
StreamSubscription? _subscription;
Timer? _reconnectTimer;
bool _isConnected = false;
bool _isIntentionalClose = false;
String? _userId;
String? _wsUrl;
ProviderContainer? _container;
int _reconnectAttempts = 0;
static const int _maxReconnectAttempts = 10;
/// ChatId the user is currently VIEWING.
String? _activeChatId;
/// ChatId whose `chat.<id>` channel we are subscribed to on this WS.
String? _subscribedChatChannelId;
// ─────────────────────────── Public API ────────────────────────────────────
/// Call from LiveChatScreen.initState().
void setActiveChatId(String chatId) {
_activeChatId = chatId;
debugPrint('NotificationWS: viewing chat $chatId');
_subscribeToChatChannel(chatId);
}
/// Call from LiveChatScreen.dispose()
void clearActiveChatId() {
debugPrint('NotificationWS: left chat $_activeChatId');
_activeChatId = null;
}
/// Subscribe to a chat channel for updates.
void watchChatChannel(String chatId) {
debugPrint('Eye NotificationWS: watchChatChannel($chatId)');
_subscribeToChatChannel(chatId);
}
/// Connect (or skip if already connected for the same user).
Future<void> connect({
required String userId,
required ProviderContainer container,
String baseUrl = 'wss://taxglide.amrithaa.net:443/reverb',
String appKey = '2yj0lyzc9ylw2h03ts6i',
}) async {
if (_isConnected && _userId == userId) {
debugPrint('⚡ NotificationWS: already connected for user $userId');
return;
}
await disconnect();
_userId = userId;
_container = container;
_wsUrl = '$baseUrl/app/$appKey?protocol=7&client=js&version=1.0';
_isIntentionalClose = false;
_reconnectAttempts = 0;
_openSocket();
}
// ──────────────────────── Socket lifecycle ──────────────────────────────────
void _openSocket() {
try {
debugPrint('🔌 NotificationWS connecting…');
_channel = WebSocketChannel.connect(Uri.parse(_wsUrl!));
_subscription = _channel!.stream.listen(
_handleMessage,
onError: _handleError,
onDone: _handleDone,
);
_isConnected = true;
_subscribeToUserChannel();
// Re-subscribe to chat channel after reconnect if we had one before.
final chatId = _subscribedChatChannelId;
if (chatId != null) {
_sendRaw({
'event': 'pusher:subscribe',
'data': {'channel': 'chat.$chatId'},
});
debugPrint('📡 NotificationWS: re-subscribed to chat.$chatId');
}
} catch (e) {
debugPrint('❌ NotificationWS connect error: $e');
_isConnected = false;
_scheduleReconnect();
}
}
void _subscribeToUserChannel() {
if (_userId == null) return;
_sendRaw({
'event': 'pusher:subscribe',
'data': {'channel': 'user-chat-notification.$_userId'},
});
debugPrint(
'📡 NotificationWS: subscribed to user-chat-notification.$_userId',
);
}
void _subscribeToChatChannel(String chatId) {
if (_subscribedChatChannelId == chatId) return; // already subscribed
// Unsubscribe previous chat channel
if (_subscribedChatChannelId != null) {
_sendRaw({
'event': 'pusher:unsubscribe',
'data': {'channel': 'chat.$_subscribedChatChannelId'},
});
debugPrint(
'📡 NotificationWS: unsubscribed from chat.$_subscribedChatChannelId',
);
}
_subscribedChatChannelId = chatId;
_sendRaw({
'event': 'pusher:subscribe',
'data': {'channel': 'chat.$chatId'},
});
debugPrint('📡 NotificationWS: subscribed to chat.$chatId');
}
// ──────────────────────── Message handler ──────────────────────────────────
void _handleMessage(dynamic raw) {
debugPrint('🔔 NotificationWS raw: $raw');
try {
final msg = jsonDecode(raw) as Map<String, dynamic>;
final event = msg['event']?.toString() ?? '';
// ── Pusher connection established → re-subscribe ──────────────────────
if (event == 'pusher:connection_established') {
debugPrint('🔗 NotificationWS: connection established');
_subscribeToUserChannel();
final chatId = _subscribedChatChannelId;
if (chatId != null) {
_sendRaw({
'event': 'pusher:subscribe',
'data': {'channel': 'chat.$chatId'},
});
}
return;
}
// ── Skip all other Pusher internal events ─────────────────────────────
if (event.startsWith('pusher')) return;
if (event.startsWith('pusher_internal')) return;
// ── Refresh Riverpod badge count ──────────────────────────────────────
try {
_container?.read(notificationTriggerProvider.notifier).state++;
} catch (_) {}
// ── Determine which chat this message belongs to ──────────────────────
String? incomingChatId;
final channelStr = msg['channel']?.toString() ?? '';
if (channelStr.startsWith('chat.')) {
incomingChatId = channelStr.split('.').last;
}
// ── Parse the payload ─────────────────────────────────────────────────
final rawData = msg['data'];
if (rawData == null) return;
dynamic data;
try {
data = rawData is String ? jsonDecode(rawData) : rawData;
} catch (_) {
data = rawData;
}
// Flatten list → use first element
if (data is List && data.isNotEmpty) {
data = data[0];
}
if (data is! Map) {
debugPrint('⚠️ NotificationWS: unrecognised data format, skipping');
return;
}
final dataMap = Map<String, dynamic>.from(data);
debugPrint('🔔 NotificationWS data keys: ${dataMap.keys.toList()}');
// ── Skip ping-only payloads {status, reload} ──────────────────────────
// The backend first fires a lightweight notification-trigger event that
// only contains `status` and `reload`. There is nothing to display yet;
// the full message list arrives in the next event on the same channel.
final onlyStatusReload =
dataMap.containsKey('status') &&
dataMap.containsKey('reload') &&
!dataMap.containsKey('message') &&
!dataMap.containsKey('chat_by');
if (onlyStatusReload) {
debugPrint('🔔 NotificationWS: reload ping received, badge refreshed');
return;
}
// ── Skip user's own messages ──────────────────────────────────────────
final chatBy = dataMap['chat_by']?.toString().toLowerCase() ?? '';
if (chatBy == 'user' || chatBy == 'customer') {
debugPrint('🔕 NotificationWS: own message, skipping');
return;
}
// ── Extract routing info if needed for internal state ─────────────
final String? page =
dataMap['page']?.toString() ?? dataMap['type']?.toString();
final String? pageId =
dataMap['page_id']?.toString() ?? dataMap['pageId']?.toString();
if (page == null || page.isEmpty || pageId == null || pageId.isEmpty) {
// Chat Notification fallback
incomingChatId ??=
dataMap['chat_id']?.toString() ??
dataMap['chatId']?.toString() ??
(dataMap['parent_tag'] != null && dataMap['parent_tag'] is Map
? dataMap['parent_tag']['id']?.toString()
: null) ??
dataMap['id']?.toString();
if (incomingChatId != null && incomingChatId != '0') {
_subscribeToChatChannel(incomingChatId);
}
}
// ── Show Foreground Notification ─────────────────────────────────────
// Only show if it's NOT a message for the currently active chat screen.
// Pass incomingChatId as the dedup tag so that if FCM already fired a
// notification for this same chat, this one *replaces* it on Android
// instead of showing a second notification.
if (incomingChatId != _activeChatId) {
// Prefer the sender name from dataMap for the title.
final String senderTitle =
dataMap['sender_name']?.toString() ??
dataMap['name']?.toString() ??
dataMap['chat_by']?.toString() ??
'New Message';
final RemoteMessage fcmLikeMessage = RemoteMessage(
data: dataMap,
notification: RemoteNotification(
title: senderTitle,
body: dataMap['message']?.toString() ?? 'You have a new message',
),
);
Get.find<NotificationService>().showForegroundNotification(
fcmLikeMessage,
tag: incomingChatId, // dedup key same chat collapses into 1 notif
);
}
} catch (e, st) {
debugPrint('❌ NotificationWS _handleMessage error: $e\n$st');
}
}
void _sendRaw(Map<String, dynamic> message) {
try {
_channel?.sink.add(jsonEncode(message));
} catch (e) {
debugPrint('❌ NotificationWS send error: $e');
}
}
// ──────────────────────── Reconnect ────────────────────────────────────────
void _handleError(dynamic error) {
debugPrint('❌ NotificationWS error: $error');
_isConnected = false;
_scheduleReconnect();
}
void _handleDone() {
debugPrint('🔌 NotificationWS: connection closed');
_isConnected = false;
if (!_isIntentionalClose) _scheduleReconnect();
}
void _scheduleReconnect() {
if (_isIntentionalClose || _reconnectAttempts >= _maxReconnectAttempts) {
return;
}
_reconnectAttempts++;
final delay = Duration(seconds: _reconnectAttempts * 2);
debugPrint(
'🔄 NotificationWS: reconnect #$_reconnectAttempts in ${delay.inSeconds}s',
);
_reconnectTimer = Timer(delay, () {
if (!_isIntentionalClose) _openSocket();
});
}
Future<void> disconnect() async {
_isIntentionalClose = true;
_reconnectTimer?.cancel();
await _subscription?.cancel();
await _channel?.sink.close();
_isConnected = false;
_subscribedChatChannelId = null;
debugPrint('🔌 NotificationWS: disconnected');
}
}