Fix flickering issues due to reconnecting loop

This commit is contained in:
2026-03-02 22:09:29 +08:00
parent 5713581992
commit 7115e2df05
8 changed files with 171 additions and 45 deletions
+15 -21
View File
@@ -192,15 +192,24 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
@override
Widget build(BuildContext context) {
final tasksAsync = ref.watch(tasksProvider);
final ticketsAsync = ref.watch(ticketsProvider);
// Use per-item providers to avoid rebuilding when unrelated tasks/tickets
// change. Only the specific task/ticket for this screen triggers rebuilds.
final task = ref.watch(taskByIdProvider(widget.taskId));
final officesAsync = ref.watch(officesProvider);
final profileAsync = ref.watch(currentProfileProvider);
final assignmentsAsync = ref.watch(taskAssignmentsProvider);
final taskMessagesAsync = ref.watch(taskMessagesProvider(widget.taskId));
final profilesAsync = ref.watch(profilesProvider);
final task = _findTask(tasksAsync, widget.taskId);
// Loading state: use .select() so we only rebuild when the loading flag
// itself changes, not when list data changes.
final isTasksLoading = ref.watch(
tasksProvider.select((a) => !a.hasValue && a.isLoading),
);
final isTicketsLoading = ref.watch(
ticketsProvider.select((a) => !a.hasValue && a.isLoading),
);
if (task == null) {
return const ResponsiveBody(
child: Center(child: Text('Task not found.')),
@@ -210,7 +219,7 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
final typingChannelId = task.id;
final ticket = ticketId == null
? null
: _findTicket(ticketsAsync, ticketId);
: ref.watch(ticketByIdProvider(ticketId));
final officeById = {
for (final office in officesAsync.valueOrNull ?? []) office.id: office,
};
@@ -255,8 +264,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
final isRetrieving =
realtime.isChannelRecovering('tasks') ||
realtime.isChannelRecovering('task_assignments') ||
(!tasksAsync.hasValue && tasksAsync.isLoading) ||
(!ticketsAsync.hasValue && ticketsAsync.isLoading) ||
isTasksLoading ||
isTicketsLoading ||
(!officesAsync.hasValue && officesAsync.isLoading) ||
(!profileAsync.hasValue && profileAsync.isLoading) ||
(!assignmentsAsync.hasValue && assignmentsAsync.isLoading) ||
@@ -3464,21 +3473,6 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
);
}
Task? _findTask(AsyncValue<List<Task>> tasksAsync, String taskId) {
return tasksAsync.maybeWhen(
data: (tasks) => tasks.where((task) => task.id == taskId).firstOrNull,
orElse: () => null,
);
}
Ticket? _findTicket(AsyncValue<List<Ticket>> ticketsAsync, String ticketId) {
return ticketsAsync.maybeWhen(
data: (tickets) =>
tickets.where((ticket) => ticket.id == ticketId).firstOrNull,
orElse: () => null,
);
}
bool _canAssignStaff(String role) {
return role == 'admin' || role == 'dispatcher' || role == 'it_staff';
}
+4 -21
View File
@@ -6,7 +6,6 @@ import 'package:supabase_flutter/supabase_flutter.dart';
import '../../models/office.dart';
import '../../models/profile.dart';
import '../../models/task.dart';
import '../../models/ticket.dart';
import '../../models/ticket_message.dart';
import '../../providers/notifications_provider.dart';
@@ -57,12 +56,13 @@ class _TicketDetailScreenState extends ConsumerState<TicketDetailScreen> {
@override
Widget build(BuildContext context) {
final ticket = _findTicket(ref, widget.ticketId);
// Use per-item providers to avoid rebuilding when unrelated tickets/tasks
// change. Only the specific ticket for this screen triggers rebuilds.
final ticket = ref.watch(ticketByIdProvider(widget.ticketId));
final messagesAsync = ref.watch(ticketMessagesProvider(widget.ticketId));
final profilesAsync = ref.watch(profilesProvider);
final officesAsync = ref.watch(officesProvider);
final currentProfileAsync = ref.watch(currentProfileProvider);
final tasksAsync = ref.watch(tasksProvider);
final typingState = ref.watch(typingIndicatorProvider(widget.ticketId));
final canPromote = currentProfileAsync.maybeWhen(
data: (profile) => profile != null && _canPromote(profile.role),
@@ -76,7 +76,7 @@ class _TicketDetailScreenState extends ConsumerState<TicketDetailScreen> {
final showAssign = canAssign && ticket?.status != 'closed';
final taskForTicket = ticket == null
? null
: _findTaskForTicket(tasksAsync, ticket.id);
: ref.watch(taskByTicketIdProvider(ticket.id));
final hasStaffMessage = _hasStaffMessage(
messagesAsync.valueOrNull ?? const [],
profilesAsync.valueOrNull ?? const [],
@@ -477,15 +477,6 @@ class _TicketDetailScreenState extends ConsumerState<TicketDetailScreen> {
return 'Filed by: $name';
}
Ticket? _findTicket(WidgetRef ref, String ticketId) {
final ticketsAsync = ref.watch(ticketsProvider);
return ticketsAsync.maybeWhen(
data: (tickets) =>
tickets.where((ticket) => ticket.id == ticketId).firstOrNull,
orElse: () => null,
);
}
bool _hasStaffMessage(List<TicketMessage> messages, List<Profile> profiles) {
if (messages.isEmpty || profiles.isEmpty) {
return false;
@@ -508,14 +499,6 @@ class _TicketDetailScreenState extends ConsumerState<TicketDetailScreen> {
);
}
Task? _findTaskForTicket(AsyncValue<List<Task>> tasksAsync, String ticketId) {
return tasksAsync.maybeWhen(
data: (tasks) =>
tasks.where((task) => task.ticketId == ticketId).firstOrNull,
orElse: () => null,
);
}
Future<void> _handleSendMessage(
WidgetRef ref,
List<Profile> profiles,