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