Pagination
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
@@ -57,16 +58,38 @@ class TypingIndicatorController extends StateNotifier<TypingIndicatorState> {
|
||||
RealtimeChannel? _channel;
|
||||
Timer? _typingTimer;
|
||||
final Map<String, Timer> _remoteTimeouts = {};
|
||||
// Marked when dispose() starts to prevent late async callbacks mutating state.
|
||||
bool _disposed = false;
|
||||
|
||||
void _initChannel() {
|
||||
final channel = _client.channel('typing:$_ticketId');
|
||||
channel.onBroadcast(
|
||||
event: 'typing',
|
||||
callback: (payload) {
|
||||
// Prevent any work if we're already disposing. Log stack for diagnostics.
|
||||
if (_disposed || !mounted) {
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'TypingIndicatorController: onBroadcast skipped (disposed|unmounted)',
|
||||
);
|
||||
debugPrint(StackTrace.current.toString());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final Map<String, dynamic> data = _extractPayload(payload);
|
||||
final userId = data['user_id'] as String?;
|
||||
final rawType = data['type']?.toString();
|
||||
final currentUserId = _client.auth.currentUser?.id;
|
||||
if (_disposed || !mounted) {
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'TypingIndicatorController: payload received but controller disposed/unmounted',
|
||||
);
|
||||
debugPrint(StackTrace.current.toString());
|
||||
}
|
||||
return;
|
||||
}
|
||||
state = state.copyWith(lastPayload: data);
|
||||
if (userId == null || userId == currentUserId) {
|
||||
return;
|
||||
@@ -79,6 +102,15 @@ class TypingIndicatorController extends StateNotifier<TypingIndicatorState> {
|
||||
},
|
||||
);
|
||||
channel.subscribe((status, error) {
|
||||
if (_disposed || !mounted) {
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'TypingIndicatorController: subscribe callback skipped (disposed|unmounted)',
|
||||
);
|
||||
debugPrint(StackTrace.current.toString());
|
||||
}
|
||||
return;
|
||||
}
|
||||
state = state.copyWith(channelStatus: status.name);
|
||||
});
|
||||
_channel = channel;
|
||||
@@ -100,36 +132,83 @@ class TypingIndicatorController extends StateNotifier<TypingIndicatorState> {
|
||||
}
|
||||
|
||||
void userTyping() {
|
||||
if (_disposed || !mounted) {
|
||||
if (kDebugMode)
|
||||
debugPrint(
|
||||
'TypingIndicatorController.userTyping() ignored after dispose',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_client.auth.currentUser?.id == null) return;
|
||||
_sendTypingEvent('start');
|
||||
_typingTimer?.cancel();
|
||||
_typingTimer = Timer(const Duration(milliseconds: 150), () {
|
||||
if (_disposed || !mounted) {
|
||||
if (kDebugMode)
|
||||
debugPrint(
|
||||
'TypingIndicatorController._typingTimer callback ignored after dispose',
|
||||
);
|
||||
return;
|
||||
}
|
||||
_sendTypingEvent('stop');
|
||||
});
|
||||
}
|
||||
|
||||
void stopTyping() {
|
||||
if (_disposed || !mounted) {
|
||||
if (kDebugMode)
|
||||
debugPrint(
|
||||
'TypingIndicatorController.stopTyping() ignored after dispose',
|
||||
);
|
||||
return;
|
||||
}
|
||||
_typingTimer?.cancel();
|
||||
_sendTypingEvent('stop');
|
||||
}
|
||||
|
||||
void _markRemoteTyping(String userId) {
|
||||
if (_disposed || !mounted) {
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'TypingIndicatorController._markRemoteTyping ignored after dispose for user: $userId',
|
||||
);
|
||||
debugPrint(StackTrace.current.toString());
|
||||
}
|
||||
return;
|
||||
}
|
||||
final updated = {...state.userIds, userId};
|
||||
if (_disposed || !mounted) return;
|
||||
state = state.copyWith(userIds: updated);
|
||||
_remoteTimeouts[userId]?.cancel();
|
||||
_remoteTimeouts[userId] = Timer(const Duration(milliseconds: 400), () {
|
||||
if (_disposed || !mounted) {
|
||||
if (kDebugMode)
|
||||
debugPrint(
|
||||
'TypingIndicatorController.remote timeout callback ignored after dispose for user: $userId',
|
||||
);
|
||||
return;
|
||||
}
|
||||
_clearRemoteTyping(userId);
|
||||
});
|
||||
}
|
||||
|
||||
void _clearRemoteTyping(String userId) {
|
||||
if (_disposed || !mounted) {
|
||||
if (kDebugMode)
|
||||
debugPrint(
|
||||
'TypingIndicatorController._clearRemoteTyping ignored after dispose for user: $userId',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final updated = {...state.userIds}..remove(userId);
|
||||
if (_disposed || !mounted) return;
|
||||
state = state.copyWith(userIds: updated);
|
||||
_remoteTimeouts[userId]?.cancel();
|
||||
_remoteTimeouts.remove(userId);
|
||||
}
|
||||
|
||||
void _sendTypingEvent(String type) {
|
||||
if (_disposed || !mounted) return;
|
||||
final userId = _client.auth.currentUser?.id;
|
||||
if (userId == null || _channel == null) return;
|
||||
_channel!.sendBroadcastMessage(
|
||||
@@ -138,15 +217,36 @@ class TypingIndicatorController extends StateNotifier<TypingIndicatorState> {
|
||||
);
|
||||
}
|
||||
|
||||
// Exposed for tests only: simulate a remote typing broadcast.
|
||||
@visibleForTesting
|
||||
void debugSimulateRemoteTyping(String userId, {bool stop = false}) {
|
||||
if (_disposed || !mounted) return;
|
||||
final data = {'user_id': userId, 'type': stop ? 'stop' : 'start'};
|
||||
state = state.copyWith(lastPayload: data);
|
||||
if (stop) {
|
||||
_clearRemoteTyping(userId);
|
||||
} else {
|
||||
_markRemoteTyping(userId);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
stopTyping();
|
||||
// Mark disposed first so any late async callbacks will no-op.
|
||||
_disposed = true;
|
||||
|
||||
// Cancel local timers and remote timeouts; do NOT send network events during
|
||||
// dispose (prevents broadcasts from re-entering callbacks after disposal).
|
||||
_typingTimer?.cancel();
|
||||
for (final timer in _remoteTimeouts.values) {
|
||||
timer.cancel();
|
||||
}
|
||||
_remoteTimeouts.clear();
|
||||
|
||||
// Unsubscribe from realtime channel.
|
||||
_channel?.unsubscribe();
|
||||
_channel = null;
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user