From 223f2b5ec06814d4b103248d30f12b0fe796f1c5 Mon Sep 17 00:00:00 2001 From: Roscoe Rubin-Rottenberg Date: Sun, 17 May 2026 17:48:56 -0400 Subject: [PATCH] refactor: sprk_poptart for lexicon models --- .../repositories/onboarding_repository.dart | 4 +- .../onboarding_repository_impl.dart | 12 +- .../molecules/known_interactions_bar.dart | 11 +- .../templates/profile_page_template.dart | 2 +- .../data/adapters/bsky/actor_adapter.dart | 3 +- .../data/adapters/bsky/feed_adapter.dart | 214 ++-- .../atproto/data/models/actor_models.dart | 154 --- .../atproto/data/models/feed_models.dart | 1055 +++++------------ .../atproto/data/models/graph_models.dart | 56 - .../atproto/data/models/labeler_models.dart | 71 +- .../network/atproto/data/models/models.dart | 4 - .../data/models/notification_models.dart | 82 +- .../atproto/data/models/pref_models.dart | 386 +++--- .../atproto/data/models/record_models.dart | 100 +- .../data/models/record_write_adapters.dart | 157 +++ .../atproto/data/models/sound_models.dart | 136 +-- .../data/models/story_embed_models.dart | 158 +-- .../data/repositories/actor_repository.dart | 8 +- .../repositories/actor_repository_impl.dart | 116 +- .../data/repositories/feed_repository.dart | 2 + .../repositories/feed_repository_impl.dart | 739 ++++++------ .../data/repositories/graph_repository.dart | 21 +- .../repositories/graph_repository_impl.dart | 127 +- .../repositories/labeler_repository_impl.dart | 47 +- .../notification_repository_impl.dart | 102 +- .../repositories/pref_repository_impl.dart | 22 +- .../data/repositories/repo_repository.dart | 9 + .../repositories/repo_repository_impl.dart | 53 +- .../data/repositories/sound_repository.dart | 1 + .../repositories/sound_repository_impl.dart | 71 +- .../data/repositories/story_repository.dart | 2 + .../repositories/story_repository_impl.dart | 216 ++-- .../messages/data/models/message_models.dart | 2 +- .../pro_image_editor/story_mention_layer.dart | 4 +- .../widgets/story_mention_picker_sheet.dart | 2 +- .../models/sound_audio_track.dart | 6 +- lib/src/core/routing/app_router.dart | 3 + .../storage/cache/download_manager_impl.dart | 110 +- .../preferences/default_preferences.dart | 26 +- lib/src/core/utils/blocking_utils.dart | 10 +- lib/src/core/utils/label_utils.dart | 8 +- .../auth/providers/onboarding_providers.dart | 4 +- .../features/auth/ui/pages/login_page.dart | 2 +- .../providers/comment_input_provider.dart | 2 +- .../comments/providers/comment_provider.dart | 35 +- .../providers/comments_page_provider.dart | 1 + .../comments/ui/pages/comments_page.dart | 2 +- .../comments/ui/pages/replies_page.dart | 18 +- .../comments/ui/widgets/comment_item.dart | 9 +- .../feed/providers/feed_provider.dart | 4 +- .../feed/ui/pages/standalone_post_page.dart | 42 +- .../action_buttons/side_action_bar.dart | 1 + .../ui/widgets/post/feed_post_widget.dart | 49 +- .../feed/ui/widgets/post/post_overlay.dart | 8 +- .../providers/conversation_state.dart | 2 +- .../providers/conversations._state.dart | 2 +- .../providers/conversations_provider.dart | 2 +- .../ui/pages/new_chat_search_page.dart | 2 +- .../messages/ui/widgets/messages_list.dart | 37 +- .../models/grouped_notification.dart | 4 +- .../ui/widgets/notification_item.dart | 11 +- .../posting/models/mention_controller.dart | 2 +- .../providers/video_upload_provider.dart | 10 +- .../profile/providers/blocks_provider.dart | 1 + .../providers/edit_profile_provider.dart | 2 +- .../profile/providers/edit_profile_state.dart | 2 +- .../providers/profile_feed_provider.dart | 2 +- .../providers/profile_likes_provider.dart | 2 +- .../profile/providers/profile_provider.dart | 6 +- .../providers/profile_reposts_provider.dart | 2 +- .../profile/providers/profile_state.dart | 2 +- .../profile/providers/user_list_provider.dart | 8 +- .../profile/ui/pages/edit_profile_page.dart | 2 +- .../profile/ui/pages/profile_page.dart | 9 +- .../ui/pages/standalone_likes_feed_page.dart | 1 + .../pages/standalone_profile_feed_page.dart | 1 + .../pages/standalone_reposts_feed_page.dart | 1 + .../profile/ui/widgets/blocks_list_view.dart | 2 +- .../ui/widgets/profile_feed_post_widget.dart | 54 +- .../profile/ui/widgets/user_list_view.dart | 2 +- .../providers/actor_typeahead_state.dart | 2 +- .../providers/post_search_provider.dart | 1 + .../search/providers/search_provider.dart | 6 +- .../search/providers/search_state.dart | 2 +- .../search/ui/pages/post_results.dart | 1 + .../features/search/ui/pages/search_page.dart | 2 +- .../search/ui/pages/user_results.dart | 2 +- .../ui/widgets/suggested_feeds_list.dart | 4 +- .../settings/providers/settings_provider.dart | 115 +- .../ui/pages/labeler_label_settings_page.dart | 24 +- .../ui/pages/labeler_management_page.dart | 2 +- .../sound/providers/sound_page_provider.dart | 14 +- .../sound/providers/sound_page_state.dart | 3 + .../features/sound/ui/pages/sound_page.dart | 2 +- .../sound/ui/widgets/sound_header_card.dart | 2 +- .../stories/providers/stories_by_author.dart | 2 + .../providers/story_manager_provider.dart | 1 + .../stories/ui/pages/all_stories_page.dart | 2 +- .../stories/ui/pages/author_stories_page.dart | 2 +- .../stories/ui/pages/story_manager_page.dart | 6 +- .../features/stories/ui/pages/story_page.dart | 32 +- .../stories/utils/story_navigation.dart | 2 +- pubspec.lock | 16 + pubspec.yaml | 2 + .../data/models/story_embed_models_test.dart | 138 ++- .../story_mention_layer_test.dart | 9 +- .../ui/widgets/mention_input_field_test.dart | 12 +- .../templates/profile_page_template_test.dart | 2 +- .../data/adapters/bsky/feed_adapter_test.dart | 169 +++ .../data/models/actor_models_test.dart | 40 - .../models/record_write_adapters_test.dart | 157 +++ .../data/models/sound_models_test.dart | 4 +- .../data/models/sprk_actor_defs_test.dart | 78 ++ .../sprk_feed_response_fixtures_test.dart | 105 ++ .../sound_picker_search_provider_test.dart | 19 +- test/src/core/utils/label_utils_test.dart | 89 +- .../providers/conversation_provider_test.dart | 3 +- .../models/grouped_notification_test.dart | 8 +- 118 files changed, 2613 insertions(+), 3127 deletions(-) delete mode 100644 lib/src/core/network/atproto/data/models/actor_models.dart delete mode 100644 lib/src/core/network/atproto/data/models/graph_models.dart create mode 100644 lib/src/core/network/atproto/data/models/record_write_adapters.dart create mode 100644 test/src/core/network/atproto/data/adapters/bsky/feed_adapter_test.dart delete mode 100644 test/src/core/network/atproto/data/models/actor_models_test.dart create mode 100644 test/src/core/network/atproto/data/models/record_write_adapters_test.dart create mode 100644 test/src/core/network/atproto/data/models/sprk_actor_defs_test.dart create mode 100644 test/src/core/network/atproto/data/models/sprk_feed_response_fixtures_test.dart diff --git a/lib/src/core/auth/data/repositories/onboarding_repository.dart b/lib/src/core/auth/data/repositories/onboarding_repository.dart index 2650dc09..708fb094 100644 --- a/lib/src/core/auth/data/repositories/onboarding_repository.dart +++ b/lib/src/core/auth/data/repositories/onboarding_repository.dart @@ -1,5 +1,5 @@ import 'package:bluesky_poptart/app/bsky/actor/profile.dart'; -import 'package:spark/src/core/network/atproto/data/models/graph_models.dart'; +import 'package:sprk_poptart/so/sprk/graph/get_follows/output.dart'; abstract class OnboardingRepository { /// Checks if the current user has a Spark profile @@ -19,7 +19,7 @@ abstract class OnboardingRepository { }); /// Fetches the list of DIDs that the user follows on Bluesky - Future getBskyFollows({String? cursor}); + Future getBskyFollows({String? cursor}); /// Creates a follow record in Spark for the given subject DID Future createSparkFollow(String subject); diff --git a/lib/src/core/auth/data/repositories/onboarding_repository_impl.dart b/lib/src/core/auth/data/repositories/onboarding_repository_impl.dart index cd31bb50..06515a3c 100644 --- a/lib/src/core/auth/data/repositories/onboarding_repository_impl.dart +++ b/lib/src/core/auth/data/repositories/onboarding_repository_impl.dart @@ -11,11 +11,12 @@ import 'package:bluesky_poptart/app/bsky/actor/profile.dart'; import 'package:get_it/get_it.dart'; import 'package:spark/src/core/auth/data/repositories/auth_repository.dart'; import 'package:spark/src/core/auth/data/repositories/onboarding_repository.dart'; -import 'package:spark/src/core/network/atproto/data/models/actor_models.dart'; -import 'package:spark/src/core/network/atproto/data/models/graph_models.dart'; +import 'package:sprk_poptart/so/sprk/actor/defs.dart'; import 'package:spark/src/core/network/atproto/data/repositories/repo_repository.dart'; import 'package:spark/src/core/utils/logging/log_service.dart'; import 'package:spark/src/core/utils/logging/logger.dart'; +import 'package:sprk_poptart/so/sprk/graph/get_follows/output.dart' + as sprk_get_follows; class OnboardingRepositoryImpl implements OnboardingRepository { OnboardingRepositoryImpl({ @@ -172,7 +173,9 @@ class OnboardingRepositoryImpl implements OnboardingRepository { } @override - Future getBskyFollows({String? cursor}) async { + Future getBskyFollows({ + String? cursor, + }) async { if (_did == null || _atproto == null) { throw Exception('Not authenticated'); } @@ -202,7 +205,8 @@ class OnboardingRepositoryImpl implements OnboardingRepository { ) .toList(); - return FollowsResponse( + return sprk_get_follows.GraphGetFollowsOutput( + subject: ProfileView.fromJson(rawData['subject'] as Map), follows: follows, cursor: rawData['cursor'] as String?, ); diff --git a/lib/src/core/design_system/components/molecules/known_interactions_bar.dart b/lib/src/core/design_system/components/molecules/known_interactions_bar.dart index c8643749..2a450031 100644 --- a/lib/src/core/design_system/components/molecules/known_interactions_bar.dart +++ b/lib/src/core/design_system/components/molecules/known_interactions_bar.dart @@ -23,9 +23,14 @@ class KnownInteractionsBar extends StatelessWidget { return const SizedBox.shrink(); } - // Filter interactions by type - final reposts = interactions!.whereType().toList(); - final likes = interactions!.whereType().toList(); + final reposts = interactions! + .map((interaction) => interaction.knownRepost) + .whereType() + .toList(); + final likes = interactions! + .map((interaction) => interaction.knownLike) + .whereType() + .toList(); // If no reposts or likes, don't render anything if (reposts.isEmpty && likes.isEmpty) { 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 7a321da8..a39425cd 100644 --- a/lib/src/core/design_system/templates/profile_page_template.dart +++ b/lib/src/core/design_system/templates/profile_page_template.dart @@ -10,7 +10,7 @@ import 'package:spark/src/core/design_system/components/molecules/profile_stats. import 'package:spark/src/core/design_system/components/organisms/sticky_profile_tab_bar.dart'; import 'package:spark/src/core/design_system/tokens/typography.dart'; import 'package:spark/src/core/l10n/app_localizations.dart'; -import 'package:spark/src/core/network/atproto/data/models/actor_models.dart'; +import 'package:sprk_poptart/so/sprk/actor/defs.dart'; class ProfilePageTemplate extends StatelessWidget { const ProfilePageTemplate({ diff --git a/lib/src/core/network/atproto/data/adapters/bsky/actor_adapter.dart b/lib/src/core/network/atproto/data/adapters/bsky/actor_adapter.dart index 9a18f179..9c73a54c 100644 --- a/lib/src/core/network/atproto/data/adapters/bsky/actor_adapter.dart +++ b/lib/src/core/network/atproto/data/adapters/bsky/actor_adapter.dart @@ -3,8 +3,7 @@ import 'package:bluesky_poptart/app/bsky/actor/get_profile.dart' import 'package:bluesky_poptart/app/bsky/actor/get_profiles.dart' as bsky_actor_get_profiles; import 'package:poptart/poptart.dart'; - -import 'package:spark/src/core/network/atproto/data/models/models.dart'; +import 'package:sprk_poptart/so/sprk/actor/defs.dart'; /// Adapter for Bluesky actor models <-> Spark actor models /// diff --git a/lib/src/core/network/atproto/data/adapters/bsky/feed_adapter.dart b/lib/src/core/network/atproto/data/adapters/bsky/feed_adapter.dart index 654378ab..90b2e5c9 100644 --- a/lib/src/core/network/atproto/data/adapters/bsky/feed_adapter.dart +++ b/lib/src/core/network/atproto/data/adapters/bsky/feed_adapter.dart @@ -4,11 +4,14 @@ import 'package:poptart/poptart.dart'; import 'package:bluesky_poptart/app/bsky/embed/images.dart'; import 'package:bluesky_poptart/app/bsky/feed/defs.dart' as bsky_defs; import 'package:bluesky_poptart/app/bsky/feed/get_post_thread.dart'; -import 'package:bluesky_poptart/app/bsky/feed/post.dart'; +import 'package:bluesky_poptart/app/bsky/feed/post.dart' hide ReplyRef; +import 'package:bluesky_poptart/app/bsky/feed/post/reply_ref.dart' + as bsky_post_reply; import 'package:bluesky_poptart/app/bsky/richtext/facet.dart'; -import 'package:spark/src/core/network/atproto/data/models/models.dart' - hide ReplyRef; +import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; +import 'package:spark/src/core/network/atproto/data/models/models.dart'; import 'package:spark/src/core/utils/json_utils.dart'; +import 'package:sprk_poptart/so/sprk/feed/defs.dart' as sprk_feed_defs; /// Adapter for Bluesky feed models <-> Spark feed models /// @@ -234,19 +237,84 @@ class BskyFeedAdapter { // =========================================================================== // Bluesky -> Spark Conversions // =========================================================================== - /// Transforms Bluesky images (multiple) to Spark single image format - /// For comments/replies, only the first image should be used - void _transformBskyImagesToSingleSparkImage(Map mediaJson) { - if (mediaJson[r'$type'] == 'app.bsky.embed.images#view') { - final images = mediaJson['images'] as List?; - if (images != null && images.isNotEmpty) { - final firstImage = images.first as Map; - // Transform to Spark single image format - mediaJson[r'$type'] = 'so.sprk.media.image#view'; - mediaJson['image'] = firstImage; - mediaJson['alt'] = firstImage['alt'] ?? ''; - mediaJson.remove('images'); - } + /// Converts Bluesky app-view embeds into Spark media views. + /// + /// The rest of the app should only see `so.sprk.media.*#view` here; anything + /// Bluesky-specific is flattened or filtered at this adapter boundary. + Map? _convertBskyMediaToSparkMedia( + Map mediaJson, { + bool singleImage = false, + }) { + switch (mediaJson[r'$type']) { + case 'app.bsky.embed.images#view': + final images = mediaJson['images'] as List?; + if (images == null || images.isEmpty) return null; + + if (singleImage) { + final firstImage = Map.from( + images.first as Map, + )..[r'$type'] = 'so.sprk.media.image#view'; + return { + r'$type': 'so.sprk.media.image#view', + 'image': firstImage, + }; + } + + final sparkImages = images + .whereType>() + .map( + (image) => + Map.from(image) + ..[r'$type'] = 'so.sprk.media.image#view', + ) + .toList(); + return { + r'$type': 'so.sprk.media.images#view', + 'images': sparkImages, + }; + + case 'app.bsky.embed.video#view': + return { + ...mediaJson, + r'$type': 'so.sprk.media.video#view', + 'thumbnail': mediaJson['thumbnail'] ?? '', + }; + + case 'app.bsky.embed.recordWithMedia#view': + final nestedMedia = mediaJson['media']; + if (nestedMedia is Map) { + return _convertBskyMediaToSparkMedia( + nestedMedia, + singleImage: singleImage, + ); + } + return null; + + case 'so.sprk.media.image#view': + case 'so.sprk.media.images#view': + case 'so.sprk.media.video#view': + return mediaJson; + + default: + return null; + } + } + + void _normalizeMediaField( + Map holder, { + bool singleImage = false, + }) { + final media = holder['media']; + if (media is! Map) return; + + final normalized = _convertBskyMediaToSparkMedia( + media, + singleImage: singleImage, + ); + if (normalized == null) { + holder.remove('media'); + } else { + holder['media'] = normalized; } } @@ -291,20 +359,14 @@ class BskyFeedAdapter { final reply = record['reply'] as Map; convertReplyRefJson(reply); - // Transform media for replies - only single image allowed - if (record.containsKey('media') && record['media'] != null) { - final mediaJson = record['media'] as Map; - _transformBskyImagesToSingleSparkImage(mediaJson); - } + _normalizeMediaField(record, singleImage: true); + } else { + _normalizeMediaField(record); } } } - // Also transform post-level media if this is a reply - if (isReply && post.containsKey('media') && post['media'] != null) { - final mediaJson = post['media'] as Map; - _transformBskyImagesToSingleSparkImage(mediaJson); - } + _normalizeMediaField(post, singleImage: isReply); if (!isNestedReply && post.containsKey('reply') && post['reply'] != null) { final replyRef = post['reply'] as Map; @@ -459,7 +521,7 @@ class BskyFeedAdapter { return FeedPostRecord( text: text, createdAt: createdAt, - reply: ReplyRef(root: reply.root, parent: reply.parent), + reply: bsky_post_reply.ReplyRef(root: reply.root, parent: reply.parent), embed: embed, facets: facets, ); @@ -645,15 +707,12 @@ class BskyFeedAdapter { /// Check if a FeedViewPost has supported media bool _feedViewPostHasMedia(FeedViewPost feedViewPost) { - return feedViewPost.map( - post: (p) => p.post.hasSupportedMedia, - reply: (r) => r.reply.media != null, - ); + return feedViewPost.localPost.hasSupportedMedia; } /// Check if a FeedViewPost is a reply bool _feedViewPostIsReply(FeedViewPost feedViewPost) { - return feedViewPost.map(post: (p) => p.reply != null, reply: (r) => true); + return feedViewPost.post.record['reply'] != null; } /// Process raw Bluesky FeedViewPost list and convert to Spark format @@ -844,70 +903,7 @@ extension BskyRecordAdapter on Record { labels: labels, media: media, ), - story: (media, createdAt, sound, labels, tags, embeds) => StoryRecord( - media: media, - createdAt: createdAt, - sound: sound, - labels: labels, - tags: tags, - embeds: embeds, - ), - profile: - ( - displayName, - description, - avatar, - banner, - selfLabels, - joinedViaStarterPack, - pinnedPost, - createdAt, - ) => ProfileRecord( - displayName: displayName, - description: description, - avatar: avatar, - banner: banner, - selfLabels: selfLabels, - joinedViaStarterPack: joinedViaStarterPack, - pinnedPost: pinnedPost, - createdAt: createdAt, - ), - audio: (sound, title, createdAt, origin, details, labels) => AudioRecord( - sound: sound, - title: title, - createdAt: createdAt, - origin: origin, - details: details, - labels: labels, - ), - plyrTrack: - ( - title, - artist, - fileType, - createdAt, - audioUrl, - album, - duration, - features, - imageUrl, - supportGate, - description, - audioBlob, - ) => PlyrTrackRecord( - title: title, - artist: artist, - fileType: fileType, - createdAt: createdAt, - audioUrl: audioUrl, - album: album, - duration: duration, - features: features, - imageUrl: imageUrl, - supportGate: supportGate, - description: description, - audioBlob: audioBlob, - ), + bskyPost: (createdAt, text, facets, reply, langs, tags, selfLabels, embed) => BskyPostRecord( @@ -926,37 +922,23 @@ extension BskyRecordAdapter on Record { extension BskyPostViewAdapter on PostView { PostView toSparkPostView() { - return copyWith( - record: record.toSparkRecord() as PostRecord, - likeCount: likeCount, - replyCount: replyCount, - repostCount: repostCount, - ); + return this; } } extension BskyReplyViewAdapter on ReplyView { ReplyView toSparkReplyView() { - return copyWith( - record: record.toSparkRecord(), - media: media, - replyCount: replyCount, - likeCount: likeCount, - ); + return this; } } extension BskyFeedViewPostAdapter on FeedViewPost { FeedViewPost toSparkFeedViewPost() { - return map( - post: (postVariant) => FeedViewPost.post( - post: postVariant.post.toSparkPostView(), - reply: postVariant.reply, - ), - reply: (replyVariant) => FeedViewPost.reply( - reply: replyVariant.reply.toSparkReplyView(), - replyRef: replyVariant.replyRef, + return FeedViewPost( + post: sprk_feed_defs.PostView.fromJson( + localPost.toSparkPostView().toJson(), ), + feedContext: feedContext, ); } } diff --git a/lib/src/core/network/atproto/data/models/actor_models.dart b/lib/src/core/network/atproto/data/models/actor_models.dart deleted file mode 100644 index 43c58263..00000000 --- a/lib/src/core/network/atproto/data/models/actor_models.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'package:poptart_lex/com/atproto/label/defs.dart'; -import 'package:poptart_lex/com/atproto/repo/strong_ref.dart'; -import 'package:poptart/poptart.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:spark/src/core/utils/uri_converter.dart'; - -part 'actor_models.freezed.dart'; -part 'actor_models.g.dart'; - -@freezed -abstract class ActorViewer with _$ActorViewer { - @JsonSerializable(explicitToJson: true) - const factory ActorViewer({ - bool? muted, - // muted by list: when we add lists add this field - bool? blockedBy, - @AtUriConverter() AtUri? blocking, - // blocked by list: when we add lists add this field - @AtUriConverter() AtUri? following, - @AtUriConverter() AtUri? followedBy, - KnownFollowers? knownFollowers, - }) = _ActorViewer; - const ActorViewer._(); - - factory ActorViewer.fromJson(Map json) => - _$ActorViewerFromJson(json); -} - -@freezed -abstract class KnownFollowers with _$KnownFollowers { - @JsonSerializable(explicitToJson: true) - const factory KnownFollowers({ - required int count, - required List followers, - }) = _KnownFollowers; - const KnownFollowers._(); - - factory KnownFollowers.fromJson(Map json) => - _$KnownFollowersFromJson(json); -} - -@freezed -abstract class ProfileViewBasic with _$ProfileViewBasic { - @JsonSerializable(explicitToJson: true) - const factory ProfileViewBasic({ - required String did, - required String handle, - String? displayName, - @UriConverter() Uri? avatar, - ActorViewer? viewer, - List? stories, - }) = _ProfileViewBasic; - const ProfileViewBasic._(); - - factory ProfileViewBasic.fromJson(Map json) => - _$ProfileViewBasicFromJson(json); -} - -@freezed -abstract class ProfileView with _$ProfileView { - @JsonSerializable(explicitToJson: true) - const factory ProfileView({ - required String did, - required String handle, - String? displayName, - String? description, - @UriConverter() Uri? avatar, - // indexedAt, createdAt - ActorViewer? viewer, - List