From 953515d7860759c8dbcfe1426dc6ad4d51afddbb Mon Sep 17 00:00:00 2001 From: Roscoe Rubin-Rottenberg Date: Fri, 10 Jul 2026 14:30:46 -0400 Subject: [PATCH] refactor: simplify callback ownership and feature actions --- .../molecules/profile_action_buttons.dart | 10 +- .../components/molecules/profile_card.dart | 19 +- .../templates/chat_list_page_template.dart | 3 - .../templates/profile_page_template.dart | 18 +- .../data/repositories/graph_repository.dart | 12 - .../repositories/graph_repository_impl.dart | 25 -- lib/src/core/routing/app_router.dart | 12 +- lib/src/core/routing/pages.dart | 2 - .../feed/ui/pages/standalone_post_page.dart | 17 +- .../action_buttons/side_action_bar.dart | 6 - .../feed/ui/widgets/feed/feeds_bar.dart | 12 +- .../ui/widgets/post/feed_post_widget.dart | 18 -- .../feed/ui/widgets/post/post_overlay.dart | 32 ++- .../feed/ui/widgets/post_likes_sheet.dart | 2 - .../messages/ui/pages/messages_page.dart | 13 +- .../ui/pages/new_chat_search_page.dart | 2 - .../profile/providers/profile_provider.dart | 57 +++-- .../profile/ui/pages/profile_page.dart | 13 +- .../profile/ui/widgets/blocks_list_view.dart | 2 - .../ui/widgets/profile_feed_post_widget.dart | 27 +- .../profile/ui/widgets/user_list_view.dart | 18 +- .../search/ui/pages/user_results.dart | 15 +- .../settings/providers/settings_provider.dart | 59 +++-- .../settings/ui/pages/feed_list_page.dart | 83 +----- .../ui/widgets}/settings_feed_card.dart | 241 +++++++++++------- .../features/sound/ui/pages/sound_page.dart | 12 +- .../sound/ui/widgets/sound_header_card.dart | 16 +- .../stories/ui/pages/author_stories_page.dart | 1 - .../features/stories/ui/pages/story_page.dart | 1 - .../components/profile_card_test.dart | 79 ++++++ .../ui/widgets/settings_feed_card_test.dart | 42 +++ widgetbook/lib/molecules/profile_card.dart | 4 +- .../lib/templates/profile_page_template.dart | 12 +- 33 files changed, 427 insertions(+), 458 deletions(-) rename lib/src/{core/design_system/components/molecules => features/settings/ui/widgets}/settings_feed_card.dart (50%) create mode 100644 test/src/core/design_system/components/profile_card_test.dart create mode 100644 test/src/features/settings/ui/widgets/settings_feed_card_test.dart diff --git a/lib/src/core/design_system/components/molecules/profile_action_buttons.dart b/lib/src/core/design_system/components/molecules/profile_action_buttons.dart index 02b1940..f17a17c 100644 --- a/lib/src/core/design_system/components/molecules/profile_action_buttons.dart +++ b/lib/src/core/design_system/components/molecules/profile_action_buttons.dart @@ -12,8 +12,7 @@ class ProfileActionButtons extends StatelessWidget { this.isFollowing = false, this.isBlocking = false, this.onEditTap, - this.onFollowTap, - this.onUnfollowTap, + this.onFollowingChanged, this.onUnblockTap, }); @@ -21,8 +20,7 @@ class ProfileActionButtons extends StatelessWidget { final bool isFollowing; final bool isBlocking; final VoidCallback? onEditTap; - final VoidCallback? onFollowTap; - final VoidCallback? onUnfollowTap; + final ValueChanged? onFollowingChanged; final VoidCallback? onUnblockTap; @override @@ -49,10 +47,8 @@ class ProfileActionButtons extends StatelessWidget { onChanged: (isSelected) { if (isBlocking && onUnblockTap != null) { onUnblockTap!(); - } else if (isSelected) { - onFollowTap?.call(); } else { - onUnfollowTap?.call(); + onFollowingChanged?.call(isSelected); } }, width: double.infinity, diff --git a/lib/src/core/design_system/components/molecules/profile_card.dart b/lib/src/core/design_system/components/molecules/profile_card.dart index a5a38b4..f3c2fc8 100644 --- a/lib/src/core/design_system/components/molecules/profile_card.dart +++ b/lib/src/core/design_system/components/molecules/profile_card.dart @@ -12,9 +12,8 @@ class ProfileCard extends StatelessWidget { required this.userName, required this.userHandle, required this.isFollowing, - required this.onFollow, - required this.onUnfollow, this.isBlocking = false, + this.onFollowingChanged, this.onUnblock, this.showFollowButton = true, this.description, @@ -30,10 +29,9 @@ class ProfileCard extends StatelessWidget { required String userName, required String userHandle, required bool isFollowing, - required VoidCallback onFollow, - required VoidCallback onUnfollow, required String description, bool isBlocking = false, + ValueChanged? onFollowingChanged, VoidCallback? onUnblock, bool showFollowButton = true, VoidCallback? onTap, @@ -45,9 +43,8 @@ class ProfileCard extends StatelessWidget { userName: userName, userHandle: userHandle, isFollowing: isFollowing, - onFollow: onFollow, - onUnfollow: onUnfollow, isBlocking: isBlocking, + onFollowingChanged: onFollowingChanged, onUnblock: onUnblock, showFollowButton: showFollowButton, description: description, @@ -62,8 +59,7 @@ class ProfileCard extends StatelessWidget { final String userHandle; final bool isFollowing; final bool isBlocking; - final VoidCallback onFollow; - final VoidCallback onUnfollow; + final ValueChanged? onFollowingChanged; final VoidCallback? onUnblock; final bool showFollowButton; final String? description; @@ -134,7 +130,8 @@ class ProfileCard extends StatelessWidget { ], ), ), - if (showFollowButton) ...[ + if (showFollowButton && + (onFollowingChanged != null || onUnblock != null)) ...[ const SizedBox(width: 8), ToggleButton( isSelected: isBlocking && onUnblock != null @@ -150,10 +147,8 @@ class ProfileCard extends StatelessWidget { onChanged: (isSelected) { if (isBlocking && onUnblock != null) { onUnblock!(); - } else if (isSelected) { - onFollow(); } else { - onUnfollow(); + onFollowingChanged?.call(isSelected); } }, ), 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 ed041f3..8faf9a0 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 @@ -33,7 +33,6 @@ class ChatListPageTemplate extends StatelessWidget { this.loading = false, this.loadingItemCount = 8, this.onAddTap, - this.onSearchTap, this.onRefresh, }); @@ -42,7 +41,6 @@ class ChatListPageTemplate extends StatelessWidget { this.title = 'Chat', this.loadingItemCount = 8, this.onAddTap, - this.onSearchTap, this.onRefresh, }) : items = const [], onItemTap = _noopItemTap, @@ -54,7 +52,6 @@ class ChatListPageTemplate extends StatelessWidget { final bool loading; final int loadingItemCount; final VoidCallback? onAddTap; - final VoidCallback? onSearchTap; final Future Function()? onRefresh; @override 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 a39425c..dc99e5f 100644 --- a/lib/src/core/design_system/templates/profile_page_template.dart +++ b/lib/src/core/design_system/templates/profile_page_template.dart @@ -35,8 +35,7 @@ class ProfilePageTemplate extends StatelessWidget { this.onFollowersTap, this.onFollowingTap, this.onEditTap, - this.onFollowTap, - this.onUnfollowTap, + this.onFollowingChanged, this.onUnblockTap, this.onEarlySupporterTap, this.onKnownFollowersTap, @@ -71,8 +70,7 @@ class ProfilePageTemplate extends StatelessWidget { final VoidCallback? onFollowersTap; final VoidCallback? onFollowingTap; final VoidCallback? onEditTap; - final VoidCallback? onFollowTap; - final VoidCallback? onUnfollowTap; + final ValueChanged? onFollowingChanged; final VoidCallback? onUnblockTap; final VoidCallback? onEarlySupporterTap; final VoidCallback? onKnownFollowersTap; @@ -133,8 +131,7 @@ class ProfilePageTemplate extends StatelessWidget { onFollowersTap: onFollowersTap, onFollowingTap: onFollowingTap, onEditTap: onEditTap, - onFollowTap: onFollowTap, - onUnfollowTap: onUnfollowTap, + onFollowingChanged: onFollowingChanged, onUnblockTap: onUnblockTap, onEarlySupporterTap: onEarlySupporterTap, onKnownFollowersTap: onKnownFollowersTap, @@ -179,8 +176,7 @@ class _ProfileHeaderSection extends StatelessWidget { this.onFollowersTap, this.onFollowingTap, this.onEditTap, - this.onFollowTap, - this.onUnfollowTap, + this.onFollowingChanged, this.onUnblockTap, this.onEarlySupporterTap, this.onKnownFollowersTap, @@ -206,8 +202,7 @@ class _ProfileHeaderSection extends StatelessWidget { final VoidCallback? onFollowersTap; final VoidCallback? onFollowingTap; final VoidCallback? onEditTap; - final VoidCallback? onFollowTap; - final VoidCallback? onUnfollowTap; + final ValueChanged? onFollowingChanged; final VoidCallback? onUnblockTap; final VoidCallback? onEarlySupporterTap; final VoidCallback? onKnownFollowersTap; @@ -294,8 +289,7 @@ class _ProfileHeaderSection extends StatelessWidget { isFollowing: isFollowing, isBlocking: isBlocking, onEditTap: onEditTap, - onFollowTap: onFollowTap, - onUnfollowTap: onUnfollowTap, + onFollowingChanged: onFollowingChanged, onUnblockTap: onUnblockTap, ), ), diff --git a/lib/src/core/network/atproto/data/repositories/graph_repository.dart b/lib/src/core/network/atproto/data/repositories/graph_repository.dart index 05e55ac..5062d47 100644 --- a/lib/src/core/network/atproto/data/repositories/graph_repository.dart +++ b/lib/src/core/network/atproto/data/repositories/graph_repository.dart @@ -39,18 +39,6 @@ abstract class GraphRepository { /// [followUri] The URI of the follow record to delete Future unfollowUser(AtUri followUri); - /// Toggle follow status for a user - /// - /// [did] The DID of the user to toggle follow for - /// [currentFollowUri] The follow URI if following, null if not - /// [bsky] Whether to use Bluesky follow records instead of Spark - /// Returns the follow URI if now following, null if unfollowed - Future toggleFollow( - String did, - AtUri? currentFollowUri, { - bool bsky = false, - }); - /// Get blocks for a DID /// /// [did] The DID to get blocks for diff --git a/lib/src/core/network/atproto/data/repositories/graph_repository_impl.dart b/lib/src/core/network/atproto/data/repositories/graph_repository_impl.dart index e7b0b31..26fdcd5 100644 --- a/lib/src/core/network/atproto/data/repositories/graph_repository_impl.dart +++ b/lib/src/core/network/atproto/data/repositories/graph_repository_impl.dart @@ -226,31 +226,6 @@ class GraphRepositoryImpl implements GraphRepository { }); } - @override - Future toggleFollow( - String did, - AtUri? currentFollowUri, { - bool bsky = false, - }) async { - _logger.d( - 'Toggling follow for DID: $did, current URI: $currentFollowUri, ' - 'bsky: $bsky', - ); - return _client.executeWithRetry(() async { - if (currentFollowUri != null) { - // User is following, so unfollow - await unfollowUser(currentFollowUri); - _logger.i('User unfollowed via toggle'); - return null; - } else { - // User is not following, so follow - final response = await followUser(did, bsky: bsky); - _logger.i('User followed via toggle: ${response.uri}'); - return response.uri.toString(); - } - }); - } - @override Future getBlocks( String did, { diff --git a/lib/src/core/routing/app_router.dart b/lib/src/core/routing/app_router.dart index c94f2b4..83cc1e4 100644 --- a/lib/src/core/routing/app_router.dart +++ b/lib/src/core/routing/app_router.dart @@ -164,17 +164,7 @@ class AppRouter extends RootStackRouter { AutoRoute(page: RecordingRoute.page, path: '/recording'), // Stories pages - AutoRoute( - page: AllStoriesRoute.page, - path: '/stories', - children: [ - AutoRoute( - page: AuthorStoriesRoute.page, - path: 'author', - children: [AutoRoute(page: StoryRoute.page, path: 'story')], - ), - ], - ), + AutoRoute(page: AllStoriesRoute.page, path: '/stories'), // Story Manager AutoRoute(page: StoryManagerRoute.page, path: '/story-manager'), diff --git a/lib/src/core/routing/pages.dart b/lib/src/core/routing/pages.dart index 4f6ffe0..fe481c2 100644 --- a/lib/src/core/routing/pages.dart +++ b/lib/src/core/routing/pages.dart @@ -36,6 +36,4 @@ export 'package:spark/src/features/settings/ui/pages/labeler_management_page.dar export 'package:spark/src/features/settings/ui/pages/settings_page.dart'; export 'package:spark/src/features/sound/ui/pages/sound_page.dart'; export 'package:spark/src/features/stories/ui/pages/all_stories_page.dart'; -export 'package:spark/src/features/stories/ui/pages/author_stories_page.dart'; export 'package:spark/src/features/stories/ui/pages/story_manager_page.dart'; -export 'package:spark/src/features/stories/ui/pages/story_page.dart'; 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 3cef35d..ba3f440 100644 --- a/lib/src/features/feed/ui/pages/standalone_post_page.dart +++ b/lib/src/features/feed/ui/pages/standalone_post_page.dart @@ -239,24 +239,9 @@ class _StandalonePostPageState extends ConsumerState { post: postData, isLiked: postData.viewer?.like != null, labels: postData.labels ?? [], - onProfilePressed: () { - // Pause video before navigating to profile + onMediaPauseRequested: () { _videoPlayerKey.currentState?.pauseVideo(); }, - onUsernameTap: () { - // Pause video before navigating to profile - _videoPlayerKey.currentState?.pauseVideo(); - final isBskyPost = postData.uri.collection - .toString() - .startsWith('app.bsky'); - context.router.push( - ProfileRoute( - did: postData.author.did, - initialProfile: postData.author, - bsky: isBskyPost, - ), - ); - }, ), ), ], diff --git a/lib/src/features/feed/ui/widgets/action_buttons/side_action_bar.dart b/lib/src/features/feed/ui/widgets/action_buttons/side_action_bar.dart index 2d25c1e..47199ed 100644 --- a/lib/src/features/feed/ui/widgets/action_buttons/side_action_bar.dart +++ b/lib/src/features/feed/ui/widgets/action_buttons/side_action_bar.dart @@ -32,9 +32,6 @@ class SideActionBar extends ConsumerStatefulWidget { this.commentCount = '0', this.shareCount = '0', this.isLiked = false, - this.profileImageUrl, - this.isImage = false, - this.onProfilePressed, this.onMediaPauseRequested, this.showBlockOption = true, }); @@ -43,10 +40,7 @@ class SideActionBar extends ConsumerStatefulWidget { final String commentCount; final String shareCount; final bool isLiked; - final String? profileImageUrl; final PostView post; - final bool isImage; - final VoidCallback? onProfilePressed; final VoidCallback? onMediaPauseRequested; /// Whether to show the block option in the options panel. diff --git a/lib/src/features/feed/ui/widgets/feed/feeds_bar.dart b/lib/src/features/feed/ui/widgets/feed/feeds_bar.dart index c6c6860..3318fa6 100644 --- a/lib/src/features/feed/ui/widgets/feed/feeds_bar.dart +++ b/lib/src/features/feed/ui/widgets/feed/feeds_bar.dart @@ -83,15 +83,9 @@ class _FeedsBarState extends ConsumerState { ), onTap: () async { Navigator.pop(context); - if (isLiked) { - await ref - .read(settingsProvider.notifier) - .unlikeFeed(feed); - } else { - await ref - .read(settingsProvider.notifier) - .likeFeed(feed); - } + await ref + .read(settingsProvider.notifier) + .setFeedLiked(feed, liked: !isLiked); }, ), // Delete option 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 48a7521..60f5d0f 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 @@ -1,12 +1,10 @@ import 'package:poptart_lex/com/atproto/label/defs.dart'; -import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/core/design_system/tokens/colors.dart'; -import 'package:spark/src/core/routing/app_router.dart'; import 'package:spark/src/core/ui/widgets/content_warning_overlay.dart'; import 'package:spark/src/core/ui/widgets/heart_animation.dart'; import 'package:spark/src/core/utils/label_utils.dart'; @@ -258,25 +256,9 @@ class _FeedPostWidgetState extends ConsumerState { isLiked: _overrideIsLiked ?? (currentPost.viewer?.like != null), labels: labels, - onProfilePressed: () { - _mediaViewerKey.currentState?.pauseMedia(); - }, onMediaPauseRequested: () { _mediaViewerKey.currentState?.pauseMedia(); }, - onUsernameTap: () { - _mediaViewerKey.currentState?.pauseMedia(); - final isBskyPost = currentPost.uri.collection - .toString() - .startsWith('app.bsky'); - context.router.push( - ProfileRoute( - did: currentPost.author.did, - initialProfile: currentPost.author, - bsky: isBskyPost, - ), - ); - }, ), ), ], diff --git a/lib/src/features/feed/ui/widgets/post/post_overlay.dart b/lib/src/features/feed/ui/widgets/post/post_overlay.dart index a8f40b9..9750e30 100644 --- a/lib/src/features/feed/ui/widgets/post/post_overlay.dart +++ b/lib/src/features/feed/ui/widgets/post/post_overlay.dart @@ -1,8 +1,10 @@ import 'package:poptart_lex/com/atproto/label/defs.dart'; +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spark/src/core/design_system/components/molecules/known_interactions_bar.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; +import 'package:spark/src/core/routing/app_router.dart'; import 'package:spark/src/core/utils/label_utils.dart'; import 'package:spark/src/features/feed/ui/widgets/action_buttons/side_action_bar.dart'; import 'package:spark/src/features/feed/ui/widgets/post/info_bar.dart'; @@ -14,8 +16,7 @@ class PostOverlay extends ConsumerWidget { super.key, this.feed, this.isLiked = false, - this.onProfilePressed, - this.onUsernameTap, + this.onAuthorTap, this.onMediaPauseRequested, this.labels = const [], this.showBlockOption = true, @@ -24,15 +25,33 @@ class PostOverlay extends ConsumerWidget { final PostView post; final Feed? feed; final bool isLiked; - final VoidCallback? onProfilePressed; - final VoidCallback? onUsernameTap; final VoidCallback? onMediaPauseRequested; + + final VoidCallback? onAuthorTap; + final List