From bbb23453b53f2ab57dc09cdc335e22175cc65a5b Mon Sep 17 00:00:00 2001 From: Roscoe Rubin-Rottenberg Date: Tue, 16 Dec 2025 00:00:55 -0500 Subject: [PATCH] feat(ui): profile skeleton loading --- .../atoms/buttons/action_button.dart | 21 +++- .../atoms/buttons/primary_button.dart | 2 +- .../components/atoms/buttons/text_button.dart | 38 +++--- .../components/molecules/post_tile.dart | 14 +-- .../components/molecules/profile_stats.dart | 13 +- .../templates/profile_page_template.dart | 92 ++++++++------ .../comments/ui/widgets/comment_item.dart | 8 +- .../feed/ui/pages/standalone_post_page.dart | 7 +- .../ui/widgets/post/feed_post_widget.dart | 56 +++++---- .../profile/ui/pages/profile_page.dart | 115 ++++++++++++------ .../ui/widgets/profile_feed_post_widget.dart | 48 ++++---- .../ui/widgets/profile_grid_widget.dart | 21 +++- .../profile/ui/widgets/user_list_view.dart | 12 +- .../search/ui/pages/user_results.dart | 13 +- .../features/sound/ui/pages/sound_page.dart | 7 +- .../stories/ui/pages/author_stories_page.dart | 7 +- pubspec.lock | 8 ++ pubspec.yaml | 1 + 18 files changed, 312 insertions(+), 171 deletions(-) diff --git a/lib/src/core/design_system/components/atoms/buttons/action_button.dart b/lib/src/core/design_system/components/atoms/buttons/action_button.dart index ad595ba1..02e2e200 100644 --- a/lib/src/core/design_system/components/atoms/buttons/action_button.dart +++ b/lib/src/core/design_system/components/atoms/buttons/action_button.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:sparksocial/src/core/design_system/components/atoms/buttons/interactive_pressable.dart'; +import 'package:sparksocial/src/core/design_system/tokens/colors.dart'; import 'package:sparksocial/src/core/design_system/tokens/typography.dart'; class ActionButton extends StatelessWidget { @@ -20,6 +21,7 @@ class ActionButton extends StatelessWidget { @override Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; const baseSize = 45.0; const pressedScale = 0.9; const iconSize = 34.0; @@ -45,10 +47,21 @@ class ActionButton extends StatelessWidget { child: Container( width: baseSize, height: baseSize, - decoration: BoxDecoration( - color: Colors.white.withAlpha(25), - shape: BoxShape.circle, - ), + decoration: isActive + ? const BoxDecoration( + color: AppColors.primary600, + shape: BoxShape.circle, + ) + : BoxDecoration( + color: isDark ? AppColors.darkGreyButton : AppColors.lightGreyButton, + shape: BoxShape.circle, + border: const Border.fromBorderSide( + BorderSide( + color: AppColors.greyBorder, + width: 1.14667, + ), + ), + ), child: Center(child: sizedIcon), ), ), diff --git a/lib/src/core/design_system/components/atoms/buttons/primary_button.dart b/lib/src/core/design_system/components/atoms/buttons/primary_button.dart index 6dd0d8cc..251bde2e 100644 --- a/lib/src/core/design_system/components/atoms/buttons/primary_button.dart +++ b/lib/src/core/design_system/components/atoms/buttons/primary_button.dart @@ -45,7 +45,7 @@ class PrimaryButton extends StatelessWidget { minHeight: minHeight ?? 60, ), decoration: BoxDecoration( - color: AppColors.primary500, + color: AppColors.primary600, borderRadius: BorderRadius.circular(16), ), child: Row( diff --git a/lib/src/core/design_system/components/atoms/buttons/text_button.dart b/lib/src/core/design_system/components/atoms/buttons/text_button.dart index 0c4603ef..f621d30c 100644 --- a/lib/src/core/design_system/components/atoms/buttons/text_button.dart +++ b/lib/src/core/design_system/components/atoms/buttons/text_button.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:flutter/material.dart'; import 'package:sparksocial/src/core/design_system/components/atoms/buttons/interactive_pressable.dart'; import 'package:sparksocial/src/core/design_system/tokens/colors.dart'; @@ -20,24 +18,26 @@ class TextButton extends StatelessWidget { final isDark = Theme.of(context).brightness == Brightness.dark; return InteractivePressable( onTap: onTap, - borderRadius: BorderRadius.circular(8), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 25, sigmaY: 25), - child: Container( - width: double.infinity, - constraints: const BoxConstraints(minHeight: 36), - padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 11), - decoration: BoxDecoration( - color: isDark ? AppColors.darkGreyButton : AppColors.lightGreyButton, - borderRadius: BorderRadius.circular(8), - border: BoxBorder.all( - color: AppColors.greyBorder, - width: 1.25, - ), + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: Container( + width: double.infinity, + constraints: const BoxConstraints(minHeight: 36), + padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 11), + decoration: BoxDecoration( + color: isDark ? AppColors.darkGreyButton : AppColors.lightGreyButton, + borderRadius: const BorderRadius.all(Radius.circular(8)), + border: const Border.fromBorderSide( + BorderSide( + color: AppColors.greyBorder, + width: 1.14667, ), - child: Text(label, textAlign: TextAlign.center, style: AppTypography.textSmallMedium), + ), + ), + child: Align( + child: Text( + label, + textAlign: TextAlign.center, + style: AppTypography.textSmallMedium, ), ), ), diff --git a/lib/src/core/design_system/components/molecules/post_tile.dart b/lib/src/core/design_system/components/molecules/post_tile.dart index 7bcb29d3..a53a5593 100644 --- a/lib/src/core/design_system/components/molecules/post_tile.dart +++ b/lib/src/core/design_system/components/molecules/post_tile.dart @@ -56,14 +56,9 @@ class PostTile extends StatelessWidget { child: CachedNetworkImage( imageUrl: thumbnailUrl, fit: BoxFit.cover, + fadeInDuration: const Duration(milliseconds: 150), placeholder: (context, url) => const ColoredBox( color: AppColors.grey800, - child: Center( - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(AppColors.primary500), - ), - ), ), errorWidget: (context, url, error) => const ColoredBox( color: AppColors.grey800, @@ -78,14 +73,9 @@ class PostTile extends StatelessWidget { CachedNetworkImage( imageUrl: thumbnailUrl, fit: BoxFit.cover, + fadeInDuration: const Duration(milliseconds: 150), placeholder: (context, url) => const ColoredBox( color: AppColors.grey800, - child: Center( - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(AppColors.primary500), - ), - ), ), errorWidget: (context, url, error) => const ColoredBox( color: AppColors.grey800, diff --git a/lib/src/core/design_system/components/molecules/profile_stats.dart b/lib/src/core/design_system/components/molecules/profile_stats.dart index ee7ca6f8..d09020f5 100644 --- a/lib/src/core/design_system/components/molecules/profile_stats.dart +++ b/lib/src/core/design_system/components/molecules/profile_stats.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:sparksocial/src/core/design_system/tokens/typography.dart'; class ProfileStats extends StatelessWidget { @@ -61,11 +62,13 @@ class _StatItem extends StatelessWidget { ), ), const SizedBox(height: 4), - Text( - label, - style: AppTypography.textSmallThin.copyWith( - color: theme.textTheme.bodyMedium?.color ?? theme.colorScheme.onSurfaceVariant, - fontSize: 14, + Skeleton.keep( + child: Text( + label, + style: AppTypography.textSmallThin.copyWith( + color: theme.textTheme.bodyMedium?.color ?? theme.colorScheme.onSurfaceVariant, + fontSize: 14, + ), ), ), ], diff --git a/lib/src/core/design_system/templates/profile_page_template.dart b/lib/src/core/design_system/templates/profile_page_template.dart index 00f8594f..e17492ed 100644 --- a/lib/src/core/design_system/templates/profile_page_template.dart +++ b/lib/src/core/design_system/templates/profile_page_template.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:sparksocial/src/core/design_system/components/atoms/buttons/app_leading_button.dart'; import 'package:sparksocial/src/core/design_system/components/atoms/icons.dart'; import 'package:sparksocial/src/core/design_system/components/molecules/profile_action_buttons.dart'; @@ -41,6 +42,7 @@ class ProfilePageTemplate extends StatelessWidget { this.onRefresh, this.selectedTabIndex = 0, this.onTabChanged, + this.isLoading = false, }); final String displayName; @@ -72,6 +74,7 @@ class ProfilePageTemplate extends StatelessWidget { final Function(int)? onTabChanged; final Widget contentWidget; final Future Function()? onRefresh; + final bool isLoading; @override Widget build(BuildContext context) { @@ -92,29 +95,32 @@ class ProfilePageTemplate extends StatelessWidget { child: CustomScrollView( slivers: [ SliverToBoxAdapter( - child: _ProfileHeaderSection( - displayName: displayName, - handle: handle, - postsCount: postsCount, - followersCount: followersCount, - followingCount: followingCount, - avatarUrl: avatarUrl, - description: description, - links: links, - hasStories: hasStories, - isCurrentUser: isCurrentUser, - isFollowing: isFollowing, - isEarlySupporter: isEarlySupporter, - onAvatarTap: onAvatarTap, - onFollowersTap: onFollowersTap, - onFollowingTap: onFollowingTap, - onEditTap: onEditTap, - onFollowTap: onFollowTap, - onUnfollowTap: onUnfollowTap, - onShareTap: onShareTap, - onEarlySupporterTap: onEarlySupporterTap, - onMentionTap: onMentionTap, - onAddStoryTap: onAddStoryTap, + child: Skeletonizer( + enabled: isLoading, + child: _ProfileHeaderSection( + displayName: displayName, + handle: handle, + postsCount: postsCount, + followersCount: followersCount, + followingCount: followingCount, + avatarUrl: avatarUrl, + description: description, + links: links, + hasStories: hasStories, + isCurrentUser: isCurrentUser, + isFollowing: isFollowing, + isEarlySupporter: isEarlySupporter, + onAvatarTap: onAvatarTap, + onFollowersTap: onFollowersTap, + onFollowingTap: onFollowingTap, + onEditTap: onEditTap, + onFollowTap: onFollowTap, + onUnfollowTap: onUnfollowTap, + onShareTap: onShareTap, + onEarlySupporterTap: onEarlySupporterTap, + onMentionTap: onMentionTap, + onAddStoryTap: onAddStoryTap, + ), ), ), SliverPersistentHeader( @@ -221,13 +227,15 @@ class _ProfileHeaderSection extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ProfileAvatar( - avatarUrl: avatarUrl, - displayName: displayName, - hasStories: hasStories, - onTap: onAvatarTap, - showAddButton: isCurrentUser, - onAddTap: onAddStoryTap, + Skeleton.keep( + child: ProfileAvatar( + avatarUrl: avatarUrl, + displayName: displayName, + hasStories: hasStories, + onTap: onAvatarTap, + showAddButton: isCurrentUser, + onAddTap: onAddStoryTap, + ), ), const SizedBox(width: 16), Expanded( @@ -236,7 +244,9 @@ class _ProfileHeaderSection extends StatelessWidget { children: [ Row( children: [ - Text(displayName, overflow: TextOverflow.ellipsis, style: AppTypography.textLargeBold), + Skeleton.keep( + child: Text(displayName, overflow: TextOverflow.ellipsis, style: AppTypography.textLargeBold), + ), if (isEarlySupporter) GestureDetector( onTap: onEarlySupporterTap, @@ -248,7 +258,9 @@ class _ProfileHeaderSection extends StatelessWidget { ], ), const SizedBox(height: 4), - _buildHandleText(context, handle), + Skeleton.keep( + child: _buildHandleText(context, handle), + ), const SizedBox(height: 10), ProfileStats( postsCount: postsCount, @@ -273,13 +285,15 @@ class _ProfileHeaderSection extends StatelessWidget { ), ], const SizedBox(height: 16), - ProfileActionButtons( - isCurrentUser: isCurrentUser, - isFollowing: isFollowing, - onEditTap: onEditTap, - onFollowTap: onFollowTap, - onUnfollowTap: onUnfollowTap, - onShareTap: onShareTap, + Skeleton.leaf( + child: ProfileActionButtons( + isCurrentUser: isCurrentUser, + isFollowing: isFollowing, + onEditTap: onEditTap, + onFollowTap: onFollowTap, + onUnfollowTap: onUnfollowTap, + onShareTap: onShareTap, + ), ), ], ), diff --git a/lib/src/features/comments/ui/widgets/comment_item.dart b/lib/src/features/comments/ui/widgets/comment_item.dart index 04309d73..1ccb7a11 100644 --- a/lib/src/features/comments/ui/widgets/comment_item.dart +++ b/lib/src/features/comments/ui/widgets/comment_item.dart @@ -36,7 +36,13 @@ class _CommentItemState extends ConsumerState { } void _navigateToProfile() { - context.router.push(ProfileRoute(did: commentState.thread.post.author.did)); + final author = commentState.thread.post.author; + context.router.push( + ProfileRoute( + did: author.did, + initialProfile: author, + ), + ); } void _handleReportComment() { diff --git a/lib/src/features/feed/ui/pages/standalone_post_page.dart b/lib/src/features/feed/ui/pages/standalone_post_page.dart index 85e0b2b8..b095c87e 100644 --- a/lib/src/features/feed/ui/pages/standalone_post_page.dart +++ b/lib/src/features/feed/ui/pages/standalone_post_page.dart @@ -166,7 +166,12 @@ class _StandalonePostPageState extends ConsumerState { onUsernameTap: () { // Pause video before navigating to profile _videoPlayerKey.currentState?.pauseVideo(); - context.router.push(ProfileRoute(did: postData.author.did)); + context.router.push( + ProfileRoute( + did: postData.author.did, + initialProfile: postData.author, + ), + ); }, ), ), diff --git a/lib/src/features/feed/ui/widgets/post/feed_post_widget.dart b/lib/src/features/feed/ui/widgets/post/feed_post_widget.dart index 1944ba53..01dc48c4 100644 --- a/lib/src/features/feed/ui/widgets/post/feed_post_widget.dart +++ b/lib/src/features/feed/ui/widgets/post/feed_post_widget.dart @@ -199,14 +199,15 @@ class _FeedPostWidgetState extends ConsumerState { _isAnimatingHeart = false; }); }, - child: GestureDetector( - onDoubleTap: () => _handleDoubleTapLike(postData), - child: Stack( - alignment: Alignment.center, - children: [ - // Main content - Positioned.fill( - bottom: 0 + MediaQuery.of(context).padding.bottom, + child: Stack( + alignment: Alignment.center, + children: [ + // Main content - only this part should detect double-tap for likes + Positioned.fill( + bottom: 0 + MediaQuery.of(context).padding.bottom, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onDoubleTap: () => _handleDoubleTapLike(postData), child: switch (postData.media) { MediaViewVideo() => PostVideoPlayer( key: _videoPlayerKey, @@ -244,25 +245,30 @@ class _FeedPostWidgetState extends ConsumerState { _ => const DecoratedBox(decoration: BoxDecoration(color: AppColors.black)), }, ), + ), - // Overlay controls - Positioned.fill( - child: PostOverlay( - post: postData, - feed: widget.feed, - isLiked: _overrideIsLiked ?? (postData.viewer?.like != null), - labels: labels, - onProfilePressed: () { - _videoPlayerKey.currentState?.pauseVideo(); - }, - onUsernameTap: () { - _videoPlayerKey.currentState?.pauseVideo(); - context.router.push(ProfileRoute(did: postData.author.did)); - }, - ), + // Overlay controls - no double-tap detection, so buttons respond immediately + Positioned.fill( + child: PostOverlay( + post: postData, + feed: widget.feed, + isLiked: _overrideIsLiked ?? (postData.viewer?.like != null), + labels: labels, + onProfilePressed: () { + _videoPlayerKey.currentState?.pauseVideo(); + }, + onUsernameTap: () { + _videoPlayerKey.currentState?.pauseVideo(); + context.router.push( + ProfileRoute( + did: postData.author.did, + initialProfile: postData.author, + ), + ); + }, ), - ], - ), + ), + ], ), ); diff --git a/lib/src/features/profile/ui/pages/profile_page.dart b/lib/src/features/profile/ui/pages/profile_page.dart index 241a787a..760cba90 100644 --- a/lib/src/features/profile/ui/pages/profile_page.dart +++ b/lib/src/features/profile/ui/pages/profile_page.dart @@ -25,9 +25,17 @@ import 'package:sparksocial/src/features/profile/ui/widgets/early_supporter_shee @RoutePage() class ProfilePage extends ConsumerStatefulWidget { - const ProfilePage({@PathParam('did') required this.did, super.key}); + const ProfilePage({ + @PathParam('did') required this.did, + this.initialProfile, + super.key, + }); final String did; + /// Optional initial profile data to show while loading. + /// Can be partially filled - only did and handle are required in ProfileViewBasic. + final actor_models.ProfileViewBasic? initialProfile; + @override ConsumerState createState() => _ProfilePageState(); } @@ -86,33 +94,34 @@ class _ProfilePageState extends ConsumerState { final theme = Theme.of(context); final colorScheme = theme.colorScheme; - return profileStateAsync.when( - data: (state) { - if (state.showAuthPrompt) { - context.router.push(AuthPromptRoute(onClose: notifier.hideAuthPrompt)); - } + // Always render the AutoTabsRouter so the grid starts loading immediately + return AutoTabsRouter( + routes: [ + ProfileVideosRoute(did: widget.did), + ], + builder: (context, child) { + final tabsRouter = AutoTabsRouter.of(context); - final profile = state.profile; - if (profile == null) { - return ErrorScreen( - context: context, - message: 'Profile not found', - stackTrace: null, - onRetry: notifier.refreshProfile, - theme: theme, - ); - } - final isCurrentUser = notifier.isCurrentUser(); - final description = profile.description ?? ''; - final links = TextFormatter.extractUrls(description); - final uniqueLinks = links.toSet().toList(); + return profileStateAsync.when( + data: (state) { + if (state.showAuthPrompt) { + context.router.push(AuthPromptRoute(onClose: notifier.hideAuthPrompt)); + } - return AutoTabsRouter( - routes: [ - ProfileVideosRoute(did: widget.did), - ], - builder: (context, child) { - final tabsRouter = AutoTabsRouter.of(context); + final profile = state.profile; + if (profile == null) { + return ErrorScreen( + context: context, + message: 'Profile not found', + stackTrace: null, + onRetry: notifier.refreshProfile, + theme: theme, + ); + } + final isCurrentUser = notifier.isCurrentUser(); + final description = profile.description ?? ''; + final links = TextFormatter.extractUrls(description); + final uniqueLinks = links.toSet().toList(); return ProfilePageTemplate( displayName: profile.displayName ?? profile.handle, @@ -273,19 +282,51 @@ class _ProfilePageState extends ConsumerState { }, ); }, + loading: () { + final initial = widget.initialProfile; + + return ProfilePageTemplate( + isLoading: true, + displayName: initial?.displayName ?? initial?.handle ?? 'Loading...', + handle: initial?.handle ?? 'loading', + avatarUrl: initial?.avatar?.toString(), + postsCount: '0', + followersCount: '0', + followingCount: '0', + isCurrentUser: false, + appBarTitle: initial?.displayName ?? initial?.handle, + appBarActions: [ + Padding( + padding: const EdgeInsets.only(right: 8), + child: Container( + padding: const EdgeInsets.all(8), + child: AppIcons.moreHoriz(color: colorScheme.onSurface), + ), + ), + ], + tabsWidget: ProfileTabBar( + selectedIndex: tabsRouter.activeIndex, + tabs: [ + ProfileTabItem( + icon: AppIcons.grid(), + filledIcon: AppIcons.gridFilled(), + isSelected: tabsRouter.activeIndex == 0, + onTap: () => tabsRouter.setActiveIndex(0), + ), + ], + ), + contentWidget: child, + ); + }, + error: (error, stackTrace) => ErrorScreen( + context: context, + message: error.toString(), + stackTrace: stackTrace, + onRetry: notifier.refreshProfile, + theme: theme, + ), ); }, - loading: () => Scaffold( - backgroundColor: theme.scaffoldBackgroundColor, - body: const Center(child: CircularProgressIndicator()), - ), - error: (error, stackTrace) => ErrorScreen( - context: context, - message: error.toString(), - stackTrace: stackTrace, - onRetry: notifier.refreshProfile, - theme: theme, - ), ); } diff --git a/lib/src/features/profile/ui/widgets/profile_feed_post_widget.dart b/lib/src/features/profile/ui/widgets/profile_feed_post_widget.dart index af5a24d2..f8786b52 100644 --- a/lib/src/features/profile/ui/widgets/profile_feed_post_widget.dart +++ b/lib/src/features/profile/ui/widgets/profile_feed_post_widget.dart @@ -151,12 +151,13 @@ class _ProfileFeedPostWidgetState extends ConsumerState { _isAnimatingHeart = false; }); }, - child: GestureDetector( - onDoubleTap: () => _handleDoubleTapLike(post), - child: Stack( - children: [ - // Main content - Positioned.fill( + child: Stack( + children: [ + // Main content - only this part should detect double-tap for likes + Positioned.fill( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onDoubleTap: () => _handleDoubleTapLike(post), child: switch (post.media) { MediaViewVideo() => PostVideoPlayer(videoUrl: post.videoUrl, thumbnail: post.thumbnailUrl), MediaViewBskyVideo() => PostVideoPlayer(videoUrl: post.videoUrl, thumbnail: post.thumbnailUrl), @@ -170,23 +171,28 @@ class _ProfileFeedPostWidgetState extends ConsumerState { _ => const DecoratedBox(decoration: BoxDecoration(color: AppColors.black)), }, ), + ), - // Overlay controls - Positioned.fill( - child: PostOverlay( - post: post, - isLiked: _overrideIsLiked ?? (post.viewer?.like != null), - labels: post.labels ?? [], - onProfilePressed: () { - // No special handling needed for profile navigation in standalone feed - }, - onUsernameTap: () { - context.router.push(ProfileRoute(did: post.author.did)); - }, - ), + // Overlay controls - no double-tap detection, so buttons respond immediately + Positioned.fill( + child: PostOverlay( + post: post, + isLiked: _overrideIsLiked ?? (post.viewer?.like != null), + labels: post.labels ?? [], + onProfilePressed: () { + // No special handling needed for profile navigation in standalone feed + }, + onUsernameTap: () { + context.router.push( + ProfileRoute( + did: post.author.did, + initialProfile: post.author, + ), + ); + }, ), - ], - ), + ), + ], ), ); diff --git a/lib/src/features/profile/ui/widgets/profile_grid_widget.dart b/lib/src/features/profile/ui/widgets/profile_grid_widget.dart index c41ae615..9dc4450f 100644 --- a/lib/src/features/profile/ui/widgets/profile_grid_widget.dart +++ b/lib/src/features/profile/ui/widgets/profile_grid_widget.dart @@ -4,6 +4,7 @@ import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:sparksocial/src/core/design_system/components/molecules/post_tile.dart'; import 'package:sparksocial/src/core/network/atproto/data/models/feed_models.dart'; import 'package:sparksocial/src/core/routing/app_router.dart'; @@ -103,7 +104,6 @@ class _ProfileGridWidgetState extends ConsumerState { if (index >= filteredUris.length) { return ColoredBox( color: Theme.of(context).colorScheme.surfaceContainerHighest, - child: const Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))), ); } @@ -123,7 +123,24 @@ class _ProfileGridWidgetState extends ConsumerState { }, ); }, - loading: () => const Center(child: CircularProgressIndicator()), + loading: () => Skeletonizer( + child: GridView.builder( + padding: const EdgeInsets.all(5), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 5, + mainAxisSpacing: 5, + childAspectRatio: 9 / 16, + ), + itemCount: 12, + itemBuilder: (context, index) => Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(15), + ), + ), + ), + ), error: (error, stack) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/src/features/profile/ui/widgets/user_list_view.dart b/lib/src/features/profile/ui/widgets/user_list_view.dart index 66c07c81..3d8065cb 100644 --- a/lib/src/features/profile/ui/widgets/user_list_view.dart +++ b/lib/src/features/profile/ui/widgets/user_list_view.dart @@ -59,7 +59,17 @@ class UserListView extends ConsumerWidget { ref.read(userListProvider(did: did, type: type).notifier).unfollowUser(user.did); }, showFollowButton: !ref.read(userListProvider(did: did, type: type).notifier).isCurrentUser(user.did), - onTap: () => context.router.push(ProfileRoute(did: user.did)), + onTap: () => context.router.push( + ProfileRoute( + did: user.did, + initialProfile: ProfileViewBasic( + did: user.did, + handle: user.handle, + displayName: user.displayName, + avatar: user.avatar, + ), + ), + ), ), ); }, diff --git a/lib/src/features/search/ui/pages/user_results.dart b/lib/src/features/search/ui/pages/user_results.dart index 052b3f96..3e5c0334 100644 --- a/lib/src/features/search/ui/pages/user_results.dart +++ b/lib/src/features/search/ui/pages/user_results.dart @@ -3,6 +3,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sparksocial/src/core/design_system/components/molecules/profile_card.dart'; +import 'package:sparksocial/src/core/network/atproto/data/models/actor_models.dart'; import 'package:sparksocial/src/core/routing/app_router.dart'; import 'package:sparksocial/src/features/search/providers/search_provider.dart'; @@ -185,7 +186,17 @@ class _UserResultsState extends ConsumerState with AutomaticKeepAli showFollowButton: !ref.read(searchProvider.notifier).isCurrentUser(actor.did), onTap: () { if (actor.did.isNotEmpty) { - context.router.push(ProfileRoute(did: actor.did)); + context.router.push( + ProfileRoute( + did: actor.did, + initialProfile: ProfileViewBasic( + did: actor.did, + handle: actor.handle, + displayName: actor.displayName, + avatar: actor.avatar, + ), + ), + ); } }, ), diff --git a/lib/src/features/sound/ui/pages/sound_page.dart b/lib/src/features/sound/ui/pages/sound_page.dart index 20fb6f0a..fa3a65ff 100644 --- a/lib/src/features/sound/ui/pages/sound_page.dart +++ b/lib/src/features/sound/ui/pages/sound_page.dart @@ -67,7 +67,12 @@ class _SoundPageState extends ConsumerState { SliverToBoxAdapter( child: SoundHeaderCard( audio: state.audio, - onAuthorTap: () => context.router.push(ProfileRoute(did: state.audio.author.did)), + onAuthorTap: () => context.router.push( + ProfileRoute( + did: state.audio.author.did, + initialProfile: state.audio.author, + ), + ), ), ), diff --git a/lib/src/features/stories/ui/pages/author_stories_page.dart b/lib/src/features/stories/ui/pages/author_stories_page.dart index 08da5565..898ea43e 100644 --- a/lib/src/features/stories/ui/pages/author_stories_page.dart +++ b/lib/src/features/stories/ui/pages/author_stories_page.dart @@ -291,7 +291,12 @@ class _AuthorStoriesPageState extends ConsumerState with Tick children: [ GestureDetector( onTap: () { - context.router.push(ProfileRoute(did: widget.author.did)); + context.router.push( + ProfileRoute( + did: widget.author.did, + initialProfile: widget.author, + ), + ); }, child: Container( width: 40, diff --git a/pubspec.lock b/pubspec.lock index 0ca02e01..eed0991d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1406,6 +1406,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + skeletonizer: + dependency: "direct main" + description: + name: skeletonizer + sha256: "83157d8e2e41f0252079cfec496281c16e4c63660052dab8d4cd72a206bb7109" + url: "https://pub.dev" + source: hosted + version: "2.1.2" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 0df0678d..4f13eba4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -67,6 +67,7 @@ dependencies: ref: dev audioplayers: ^6.5.1 audio_waveforms: ^2.0.0 + skeletonizer: ^2.1.2 dev_dependencies: flutter_test: -- 2.51.2