diff --git a/lib/src/core/design_system/templates/chat_list_page_template.dart b/lib/src/core/design_system/templates/chat_list_page_template.dart index 6dae19b4..ed041f32 100644 --- a/lib/src/core/design_system/templates/chat_list_page_template.dart +++ b/lib/src/core/design_system/templates/chat_list_page_template.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spark/src/core/design_system/components/atoms/icons.dart'; -import 'package:spark/src/core/design_system/tokens/typography.dart'; import 'package:spark/src/core/design_system/components/atoms/user_avatar.dart'; +import 'package:spark/src/core/design_system/tokens/typography.dart'; class ChatListItemData { const ChatListItemData({ @@ -29,14 +30,29 @@ class ChatListPageTemplate extends StatelessWidget { required this.onItemTap, super.key, this.title = 'Chat', + this.loading = false, + this.loadingItemCount = 8, this.onAddTap, this.onSearchTap, this.onRefresh, }); + const ChatListPageTemplate.loading({ + super.key, + this.title = 'Chat', + this.loadingItemCount = 8, + this.onAddTap, + this.onSearchTap, + this.onRefresh, + }) : items = const [], + onItemTap = _noopItemTap, + loading = true; + final String title; final List items; final void Function(int index) onItemTap; + final bool loading; + final int loadingItemCount; final VoidCallback? onAddTap; final VoidCallback? onSearchTap; final Future Function()? onRefresh; @@ -65,18 +81,20 @@ class ChatListPageTemplate extends StatelessWidget { body: Column( children: [ Expanded( - child: RefreshIndicator( - onRefresh: onRefresh ?? () async {}, - child: ListView.separated( - padding: EdgeInsets.zero, - itemCount: items.length, - separatorBuilder: (_, _) => const SizedBox.shrink(), - itemBuilder: (context, index) => _ChatTile( - data: items[index], - onTap: () => onItemTap(index), - ), - ), - ), + child: loading + ? _ChatListSkeleton(itemCount: loadingItemCount) + : RefreshIndicator( + onRefresh: onRefresh ?? () async {}, + child: ListView.separated( + padding: EdgeInsets.zero, + itemCount: items.length, + separatorBuilder: (_, _) => const SizedBox.shrink(), + itemBuilder: (context, index) => _ChatTile( + data: items[index], + onTap: () => onItemTap(index), + ), + ), + ), ), ], ), @@ -84,6 +102,8 @@ class ChatListPageTemplate extends StatelessWidget { } } +void _noopItemTap(int _) {} + class _ChatTile extends StatelessWidget { const _ChatTile({required this.data, this.onTap}); final ChatListItemData data; @@ -156,3 +176,84 @@ class _ChatTile extends StatelessWidget { ); } } + +class _ChatListSkeleton extends StatelessWidget { + const _ChatListSkeleton({required this.itemCount}); + + final int itemCount; + + @override + Widget build(BuildContext context) { + return Skeletonizer( + child: ListView.separated( + padding: EdgeInsets.zero, + itemCount: itemCount, + separatorBuilder: (_, _) => const SizedBox.shrink(), + itemBuilder: (context, index) => const _ChatTileSkeleton(), + ), + ); + } +} + +class _ChatTileSkeleton extends StatelessWidget { + const _ChatTileSkeleton(); + + @override + Widget build(BuildContext context) { + final skeletonColor = Theme.of(context).colorScheme.surfaceContainerHighest; + + return ListTile( + horizontalTitleGap: 12, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + leading: Skeleton.leaf( + child: Container( + width: 50.45, + height: 50.45, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: skeletonColor, + ), + ), + ), + title: Row( + children: [ + Expanded( + child: Skeleton.leaf( + child: Container( + height: 17, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: skeletonColor, + ), + ), + ), + ), + const SizedBox(width: 44), + Skeleton.leaf( + child: Container( + width: 36, + height: 10, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: skeletonColor, + ), + ), + ), + ], + ), + subtitle: FractionallySizedBox( + widthFactor: 0.72, + alignment: Alignment.centerLeft, + child: Skeleton.leaf( + child: Container( + height: 13, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: skeletonColor, + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/features/messages/ui/pages/messages_page.dart b/lib/src/features/messages/ui/pages/messages_page.dart index ef7b5a63..871d6183 100644 --- a/lib/src/features/messages/ui/pages/messages_page.dart +++ b/lib/src/features/messages/ui/pages/messages_page.dart @@ -4,9 +4,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; import 'package:spark/src/core/design_system/templates/chat_list_page_template.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/routing/app_router.dart'; import 'package:spark/src/core/utils/logging/logging.dart'; -import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/features/messages/providers/conversations_provider.dart'; @RoutePage() @@ -85,10 +85,8 @@ class _MessagesPageState extends ConsumerState { ); }, loading: () { - final theme = Theme.of(context); - return Scaffold( - backgroundColor: theme.colorScheme.surface, - body: const Center(child: CircularProgressIndicator()), + return ChatListPageTemplate.loading( + onAddTap: () => context.router.push(const NewChatSearchRoute()), ); }, error: (error, stack) { diff --git a/widgetbook/lib/templates/page_templates.dart b/widgetbook/lib/templates/page_templates.dart index de96c0aa..f0041eb2 100644 --- a/widgetbook/lib/templates/page_templates.dart +++ b/widgetbook/lib/templates/page_templates.dart @@ -39,6 +39,11 @@ Widget buildChatListPageTemplateUseCase(BuildContext context) { ); } +@UseCase(name: 'loading', type: ChatListPageTemplate) +Widget buildChatListPageTemplateLoadingUseCase(BuildContext context) { + return const ChatListPageTemplate.loading(); +} + @UseCase(name: 'default', type: ChatThreadPageTemplate) Widget buildChatThreadPageTemplateUseCase(BuildContext context) { return ChatThreadPageTemplate(