From 504e4d55ad16585e46a8fbd190cd94c067837594 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 6 Dec 2025 13:31:17 +0800 Subject: [PATCH] :lipstick: Post list skeleton --- lib/screens/explore.dart | 338 +++++++++--------- lib/widgets/paging/pagination_list.dart | 51 +-- lib/widgets/post/post_item_skeleton.dart | 427 +++++++++++++++++++++++ lib/widgets/post/post_list.dart | 2 + 4 files changed, 625 insertions(+), 193 deletions(-) create mode 100644 lib/widgets/post/post_item_skeleton.dart diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index 8c4e90f4..eea56569 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -26,6 +26,7 @@ import 'package:island/widgets/navigation/fab_menu.dart'; import 'package:island/widgets/paging/pagination_list.dart'; import 'package:island/widgets/post/post_featured.dart'; import 'package:island/widgets/post/post_item.dart'; +import 'package:island/widgets/post/post_item_skeleton.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:island/widgets/realm/realm_card.dart'; import 'package:island/widgets/publisher/publisher_card.dart'; @@ -137,10 +138,9 @@ class ExploreScreen extends HookConsumerWidget { ), tooltip: 'explore'.tr(), isSelected: currentFilter.value == null, - color: - currentFilter.value == null - ? Theme.of(context).colorScheme.primary - : null, + color: currentFilter.value == null + ? Theme.of(context).colorScheme.primary + : null, ), IconButton( onPressed: () => handleFilterChange('subscriptions'), @@ -150,10 +150,9 @@ class ExploreScreen extends HookConsumerWidget { ), tooltip: 'exploreFilterSubscriptions'.tr(), isSelected: currentFilter.value == 'subscriptions', - color: - currentFilter.value == 'subscriptions' - ? Theme.of(context).colorScheme.primary - : null, + color: currentFilter.value == 'subscriptions' + ? Theme.of(context).colorScheme.primary + : null, ), IconButton( onPressed: () => handleFilterChange('friends'), @@ -163,10 +162,9 @@ class ExploreScreen extends HookConsumerWidget { ), tooltip: 'exploreFilterFriends'.tr(), isSelected: currentFilter.value == 'friends', - color: - currentFilter.value == 'friends' - ? Theme.of(context).colorScheme.primary - : null, + color: currentFilter.value == 'friends' + ? Theme.of(context).colorScheme.primary + : null, ), ], ), @@ -179,57 +177,56 @@ class ExploreScreen extends HookConsumerWidget { tooltip: 'webArticlesStand'.tr(), ), PopupMenuButton( - itemBuilder: - (context) => [ - PopupMenuItem( - child: Row( - children: [ - const Icon(Symbols.category), - const Gap(12), - Text('categories').tr(), - ], - ), - onTap: () { - context.pushNamed('postCategories'); - }, - ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Symbols.label), - const Gap(12), - Text('tags').tr(), - ], - ), - onTap: () { - context.pushNamed('postTags'); - }, - ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Symbols.shuffle), - const Gap(12), - Text('postShuffle').tr(), - ], - ), - onTap: () { - context.pushNamed('postShuffle'); - }, - ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Symbols.search), - const Gap(12), - Text('search').tr(), - ], - ), - onTap: () { - context.pushNamed('postSearch'); - }, - ), - ], + itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Symbols.category), + const Gap(12), + Text('categories').tr(), + ], + ), + onTap: () { + context.pushNamed('postCategories'); + }, + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Symbols.label), + const Gap(12), + Text('tags').tr(), + ], + ), + onTap: () { + context.pushNamed('postTags'); + }, + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Symbols.shuffle), + const Gap(12), + Text('postShuffle').tr(), + ], + ), + onTap: () { + context.pushNamed('postShuffle'); + }, + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Symbols.search), + const Gap(12), + Text('search').tr(), + ], + ), + onTap: () { + context.pushNamed('postSearch'); + }, + ), + ], icon: Icon(Symbols.action_key), tooltip: 'search'.tr(), ), @@ -237,10 +234,9 @@ class ExploreScreen extends HookConsumerWidget { ).padding(horizontal: 8, vertical: 4), ); - final appBar = - isWide - ? null - : _buildAppBar(currentFilter.value, handleFilterChange, context); + final appBar = isWide + ? null + : _buildAppBar(currentFilter.value, handleFilterChange, context); final dragging = useState(false); @@ -263,19 +259,18 @@ class ExploreScreen extends HookConsumerWidget { AppScaffold( isNoBackground: false, appBar: appBar, - body: - isWide - ? _buildWideBody( - context, - ref, - filterBar, - user, - notificationCount, - query, - events, - selectedDay, - ) - : _buildNarrowBody(context, ref, currentFilter.value), + body: isWide + ? _buildWideBody( + context, + ref, + filterBar, + user, + notificationCount, + query, + events, + selectedDay, + ) + : _buildNarrowBody(context, ref, currentFilter.value), ), if (dragging.value) Positioned.fill( @@ -295,12 +290,11 @@ class ExploreScreen extends HookConsumerWidget { const Gap(16), Text( 'dropToShare'.tr(), - style: Theme.of( - context, - ).textTheme.headlineMedium?.copyWith( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold, - ), + style: Theme.of(context).textTheme.headlineMedium + ?.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + ), ), ], ), @@ -321,9 +315,9 @@ class ExploreScreen extends HookConsumerWidget { // Sliver list cannot provide refresh handled by the pagination list isRefreshable: false, isSliver: true, - contentBuilder: - (data, footer) => - _ActivityListView(data: data, isWide: isWide, footer: footer), + footerSkeletonChild: const PostItemSkeleton(), + contentBuilder: (data, footer) => + _ActivityListView(data: data, isWide: isWide, footer: footer), ); } @@ -393,39 +387,38 @@ class ExploreScreen extends HookConsumerWidget { else Flexible( flex: 2, - child: - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon(Symbols.emoji_people_rounded, size: 40), - const Gap(8), - Text( - 'Welcome to\nthe Solar Network', - style: Theme.of(context).textTheme.titleLarge, - textAlign: TextAlign.center, - ).bold(), - const Gap(2), - Text( - 'Login to explore more!', - style: Theme.of(context).textTheme.bodyLarge, - textAlign: TextAlign.center, - ), - const Gap(4), - TextButton.icon( - onPressed: () { - showModalBottomSheet( - context: context, - useRootNavigator: true, - isScrollControlled: true, - builder: (context) => LoginModal(), - ); - }, - icon: const Icon(Symbols.login), - label: Text('login').tr(), - ), - ], - ).padding(horizontal: 36, vertical: 16).center(), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Symbols.emoji_people_rounded, size: 40), + const Gap(8), + Text( + 'Welcome to\nthe Solar Network', + style: Theme.of(context).textTheme.titleLarge, + textAlign: TextAlign.center, + ).bold(), + const Gap(2), + Text( + 'Login to explore more!', + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + ), + const Gap(4), + TextButton.icon( + onPressed: () { + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + builder: (context) => LoginModal(), + ); + }, + icon: const Icon(Symbols.login), + label: Text('login').tr(), + ), + ], + ).padding(horizontal: 36, vertical: 16).center(), ), ], ).padding(horizontal: 12); @@ -491,57 +484,56 @@ class ExploreScreen extends HookConsumerWidget { tooltip: 'webArticlesStand'.tr(), ), PopupMenuButton( - itemBuilder: - (context) => [ - PopupMenuItem( - child: Row( - children: [ - const Icon(Symbols.category), - const Gap(12), - Text('categories').tr(), - ], - ), - onTap: () { - context.pushNamed('postCategories'); - }, - ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Symbols.label), - const Gap(12), - Text('tags').tr(), - ], - ), - onTap: () { - context.pushNamed('postTags'); - }, - ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Symbols.shuffle), - const Gap(12), - Text('postShuffle').tr(), - ], - ), - onTap: () { - context.pushNamed('postShuffle'); - }, - ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Symbols.search), - const Gap(12), - Text('search').tr(), - ], - ), - onTap: () { - context.pushNamed('postSearch'); - }, - ), - ], + itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Symbols.category), + const Gap(12), + Text('categories').tr(), + ], + ), + onTap: () { + context.pushNamed('postCategories'); + }, + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Symbols.label), + const Gap(12), + Text('tags').tr(), + ], + ), + onTap: () { + context.pushNamed('postTags'); + }, + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Symbols.shuffle), + const Gap(12), + Text('postShuffle').tr(), + ], + ), + onTap: () { + context.pushNamed('postShuffle'); + }, + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Symbols.search), + const Gap(12), + Text('search').tr(), + ], + ), + onTap: () { + context.pushNamed('postSearch'); + }, + ), + ], icon: Icon(Symbols.action_key, color: foregroundColor), tooltip: 'search'.tr(), ), diff --git a/lib/widgets/paging/pagination_list.dart b/lib/widgets/paging/pagination_list.dart index 3fd6d3d7..7326640f 100644 --- a/lib/widgets/paging/pagination_list.dart +++ b/lib/widgets/paging/pagination_list.dart @@ -21,6 +21,7 @@ class PaginationList extends HookConsumerWidget { final bool isSliver; final bool showDefaultWidgets; final EdgeInsets? padding; + final Widget? footerSkeletonChild; const PaginationList({ super.key, required this.provider, @@ -30,6 +31,7 @@ class PaginationList extends HookConsumerWidget { this.isSliver = false, this.showDefaultWidgets = true, this.padding, + this.footerSkeletonChild, }); @override @@ -55,7 +57,11 @@ class PaginationList extends HookConsumerWidget { itemCount: (data.value?.length ?? 0) + 1, itemBuilder: (context, idx) { if (idx == data.value?.length) { - return PaginationListFooter(noti: noti, data: data); + return PaginationListFooter( + noti: noti, + data: data, + skeletonChild: footerSkeletonChild, + ); } final entry = data.value?[idx]; if (entry != null) return itemBuilder(context, idx, entry); @@ -67,7 +73,11 @@ class PaginationList extends HookConsumerWidget { itemCount: (data.value?.length ?? 0) + 1, itemBuilder: (context, idx) { if (idx == data.value?.length) { - return PaginationListFooter(noti: noti, data: data); + return PaginationListFooter( + noti: noti, + data: data, + skeletonChild: footerSkeletonChild, + ); } final entry = data.value?[idx]; if (entry != null) return itemBuilder(context, idx, entry); @@ -88,6 +98,7 @@ class PaginationWidget extends HookConsumerWidget { final bool isRefreshable; final bool isSliver; final bool showDefaultWidgets; + final Widget? footerSkeletonChild; const PaginationWidget({ super.key, required this.provider, @@ -96,6 +107,7 @@ class PaginationWidget extends HookConsumerWidget { this.isRefreshable = true, this.isSliver = false, this.showDefaultWidgets = true, + this.footerSkeletonChild, }); @override @@ -116,7 +128,11 @@ class PaginationWidget extends HookConsumerWidget { return isSliver ? SliverFillRemaining(child: content) : content; } - final footer = PaginationListFooter(noti: noti, data: data); + final footer = PaginationListFooter( + noti: noti, + data: data, + skeletonChild: footerSkeletonChild, + ); final content = contentBuilder(data.value ?? [], footer); return isRefreshable @@ -153,23 +169,18 @@ class PaginationListFooter extends HookConsumerWidget { trailing: const Icon(Icons.ac_unit), ), ); - final child = SizedBox( - height: 64, - child: Center( - child: hasBeenVisible.value - ? data.isLoading - ? placeholder - : Row( - spacing: 8, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Symbols.close, size: 16), - Text('noFurtherData').tr().fontSize(13), - ], - ).opacity(0.9) - : placeholder, - ).padding(all: 8), - ); + final child = hasBeenVisible.value + ? data.isLoading + ? placeholder + : Row( + spacing: 8, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Symbols.close, size: 16), + Text('noFurtherData').tr().fontSize(13), + ], + ).opacity(0.9).height(64).center() + : placeholder; return VisibilityDetector( key: Key("pagination-list-${noti.hashCode}"), diff --git a/lib/widgets/post/post_item_skeleton.dart b/lib/widgets/post/post_item_skeleton.dart new file mode 100644 index 00000000..f7abc663 --- /dev/null +++ b/lib/widgets/post/post_item_skeleton.dart @@ -0,0 +1,427 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:skeletonizer/skeletonizer.dart'; +import 'package:styled_widget/styled_widget.dart'; + +class PostItemSkeleton extends StatelessWidget { + final EdgeInsets? padding; + final bool isFullPost; + final bool isShowReference; + final bool isEmbedReply; + final bool isCompact; + final double? borderRadius; + + const PostItemSkeleton({ + super.key, + this.padding, + this.isFullPost = false, + this.isShowReference = false, + this.isEmbedReply = false, + this.isCompact = false, + this.borderRadius, + }); + + @override + Widget build(BuildContext context) { + final renderingPadding = + padding ?? const EdgeInsets.symmetric(horizontal: 8, vertical: 8); + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Gap(renderingPadding.vertical), + _PostHeaderSkeleton( + isFullPost: isFullPost, + isCompact: isCompact, + renderingPadding: renderingPadding, + ), + _PostBodySkeleton( + isFullPost: isFullPost, + renderingPadding: renderingPadding, + ), + if (isShowReference) + _ReferencedPostWidgetSkeleton(renderingPadding: renderingPadding), + if (isEmbedReply) + _PostReplyPreviewSkeleton( + renderingPadding: renderingPadding, + ).padding(horizontal: renderingPadding.horizontal, top: 8), + Gap(renderingPadding.vertical), + ], + ); + } +} + +class _PostHeaderSkeleton extends StatelessWidget { + final bool isFullPost; + final bool isCompact; + final EdgeInsets renderingPadding; + + const _PostHeaderSkeleton({ + required this.isFullPost, + required this.isCompact, + required this.renderingPadding, + }); + + @override + Widget build(BuildContext context) { + return Skeletonizer( + enabled: true, + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 12, + children: [ + // Profile picture skeleton + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + shape: BoxShape.circle, + ), + ), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 4, + children: [ + // Name skeleton + Container( + height: 16, + width: 120, + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(4), + ), + ), + if (!isCompact) + Container( + height: 12, + width: 80, + margin: const EdgeInsets.only(left: 4), + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(4), + ), + ), + ], + ), + const Gap(4), + // Timestamp skeleton + Container( + height: 12, + width: 60, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(4), + ), + ), + ], + ), + ), + // Reaction button skeleton + if (!isCompact) + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(18), + ), + ), + ], + ), + ], + ).padding(horizontal: renderingPadding.horizontal, bottom: 4), + ); + } +} + +class _PostBodySkeleton extends StatelessWidget { + final bool isFullPost; + final EdgeInsets renderingPadding; + + const _PostBodySkeleton({ + required this.isFullPost, + required this.renderingPadding, + }); + + @override + Widget build(BuildContext context) { + return Skeletonizer( + enabled: true, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title skeleton (if applicable) + if (isFullPost) + Container( + height: 20, + width: 200, + margin: EdgeInsets.only( + left: renderingPadding.horizontal, + right: renderingPadding.horizontal, + bottom: 8, + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(4), + ), + ), + // Content skeleton + Container( + height: 16, + margin: EdgeInsets.only( + left: renderingPadding.horizontal, + right: renderingPadding.horizontal, + bottom: 4, + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(4), + ), + ), + Container( + height: 16, + width: 250, + margin: EdgeInsets.only( + left: renderingPadding.horizontal, + right: renderingPadding.horizontal, + bottom: 4, + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(4), + ), + ), + Container( + height: 16, + width: 180, + margin: EdgeInsets.only( + left: renderingPadding.horizontal, + right: renderingPadding.horizontal, + bottom: 8, + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(4), + ), + ), + // Metadata skeleton + Row( + spacing: 8, + children: [ + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(8), + ), + ), + Container( + height: 12, + width: 80, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(4), + ), + ), + ], + ).padding(horizontal: renderingPadding.horizontal + 4, top: 4), + ], + ), + ); + } +} + +class _ReferencedPostWidgetSkeleton extends StatelessWidget { + final EdgeInsets renderingPadding; + + const _ReferencedPostWidgetSkeleton({required this.renderingPadding}); + + @override + Widget build(BuildContext context) { + return Skeletonizer( + enabled: true, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: renderingPadding.horizontal, + vertical: 8, + ), + margin: EdgeInsets.only( + top: 8, + left: renderingPadding.vertical, + right: renderingPadding.vertical, + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Theme.of(context).dividerColor.withOpacity(0.5), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(8), + ), + ), + const SizedBox(width: 6), + Container( + height: 12, + width: 60, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(4), + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 14, + width: 100, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(height: 4), + Container( + height: 12, + width: 150, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(height: 4), + Container( + height: 12, + width: 120, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(4), + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ); + } +} + +class _PostReplyPreviewSkeleton extends StatelessWidget { + final EdgeInsets renderingPadding; + + const _PostReplyPreviewSkeleton({required this.renderingPadding}); + + @override + Widget build(BuildContext context) { + return Skeletonizer( + enabled: true, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerLow, + border: Border.all( + color: Theme.of(context).dividerColor.withOpacity(0.5), + ), + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 4, + children: [ + Container( + height: 14, + width: 80, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(4), + ), + ), + const Gap(8), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8, + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + shape: BoxShape.circle, + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 12, + width: 150, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(height: 4), + Container( + height: 10, + width: 100, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(4), + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/post/post_list.dart b/lib/widgets/post/post_list.dart index e453a63a..3e2be9e4 100644 --- a/lib/widgets/post/post_list.dart +++ b/lib/widgets/post/post_list.dart @@ -7,6 +7,7 @@ import 'package:island/pods/paging.dart'; import 'package:island/widgets/paging/pagination_list.dart'; import 'package:island/widgets/post/post_item.dart'; import 'package:island/widgets/post/post_item_creator.dart'; +import 'package:island/widgets/post/post_item_skeleton.dart'; part 'post_list.freezed.dart'; @@ -159,6 +160,7 @@ class SliverPostList extends HookConsumerWidget { notifier: notifier, isRefreshable: false, isSliver: true, + footerSkeletonChild: const PostItemSkeleton(), itemBuilder: (context, index, post) { if (maxWidth != null) { return Center(