Per channel skeleton
This commit is contained in:
@@ -55,7 +55,6 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
|
||||
String? _selectedAssigneeId;
|
||||
DateTimeRange? _selectedDateRange;
|
||||
late final TabController _tabController;
|
||||
bool _isSwitchingTab = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -69,16 +68,10 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
// Rebuild when tab changes so filter header can show/hide the
|
||||
// "Assigned staff" dropdown for the All Tasks tab.
|
||||
_tabController.addListener(() {
|
||||
// briefly show a skeleton when switching tabs so the UI can
|
||||
// navigate ahead and avoid a janky synchronous rebuild.
|
||||
if (!_isSwitchingTab) {
|
||||
setState(() => _isSwitchingTab = true);
|
||||
Future.delayed(const Duration(milliseconds: 150), () {
|
||||
if (!mounted) return;
|
||||
setState(() => _isSwitchingTab = false);
|
||||
});
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -103,15 +96,19 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
|
||||
final assignmentsAsync = ref.watch(taskAssignmentsProvider);
|
||||
final realtime = ref.watch(realtimeControllerProvider);
|
||||
|
||||
// Show skeleton only on initial load (no previous data) or when the
|
||||
// tasks channel is recovering. Use per-channel check so unrelated
|
||||
// channel issues (e.g. notifications) don't skeleton the tasks list.
|
||||
final showSkeleton =
|
||||
realtime.isAnyStreamRecovering ||
|
||||
tasksAsync.maybeWhen(loading: () => true, orElse: () => false) ||
|
||||
ticketsAsync.maybeWhen(loading: () => true, orElse: () => false) ||
|
||||
officesAsync.maybeWhen(loading: () => true, orElse: () => false) ||
|
||||
profilesAsync.maybeWhen(loading: () => true, orElse: () => false) ||
|
||||
assignmentsAsync.maybeWhen(loading: () => true, orElse: () => false) ||
|
||||
profileAsync.maybeWhen(loading: () => true, orElse: () => false);
|
||||
final effectiveShowSkeleton = showSkeleton || _isSwitchingTab;
|
||||
realtime.isChannelRecovering('tasks') ||
|
||||
realtime.isChannelRecovering('task_assignments') ||
|
||||
(!tasksAsync.hasValue && tasksAsync.isLoading) ||
|
||||
(!ticketsAsync.hasValue && ticketsAsync.isLoading) ||
|
||||
(!officesAsync.hasValue && officesAsync.isLoading) ||
|
||||
(!profilesAsync.hasValue && profilesAsync.isLoading) ||
|
||||
(!assignmentsAsync.hasValue && assignmentsAsync.isLoading) ||
|
||||
(!profileAsync.hasValue && profileAsync.isLoading);
|
||||
final effectiveShowSkeleton = showSkeleton;
|
||||
|
||||
final canCreate = profileAsync.maybeWhen(
|
||||
data: (profile) =>
|
||||
@@ -141,9 +138,24 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
|
||||
maxWidth: double.infinity,
|
||||
child: Skeletonizer(
|
||||
enabled: effectiveShowSkeleton,
|
||||
child: tasksAsync.when(
|
||||
data: (tasks) {
|
||||
if (tasks.isEmpty) {
|
||||
// Always render the full layout structure using valueOrNull so
|
||||
// that Skeletonizer has real widget shapes to shimmer. This
|
||||
// avoids the blank flash caused by the old `.when(loading:
|
||||
// () => SizedBox.shrink())` approach and keeps previous data
|
||||
// visible during provider refreshes.
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
// Show error only when there is genuinely no data.
|
||||
if (tasksAsync.hasError && !tasksAsync.hasValue) {
|
||||
return Center(
|
||||
child: Text('Failed to load tasks: ${tasksAsync.error}'),
|
||||
);
|
||||
}
|
||||
|
||||
final tasks = tasksAsync.valueOrNull ?? <Task>[];
|
||||
|
||||
// True empty state — data loaded but nothing returned.
|
||||
if (tasks.isEmpty && !effectiveShowSkeleton) {
|
||||
return const Center(child: Text('No tasks yet.'));
|
||||
}
|
||||
final offices = officesAsync.valueOrNull ?? <Office>[];
|
||||
@@ -527,9 +539,6 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
|
||||
],
|
||||
);
|
||||
},
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (error, _) =>
|
||||
Center(child: Text('Failed to load tasks: $error')),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user