From 6822c4be822d0e552036ef28c2b47f67268e7eeb Mon Sep 17 00:00:00 2001 From: Bretton <36870434+BrettM86@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:23:01 -0700 Subject: [PATCH] fix(embeds): parse appview #view embed types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The appview now serves embeds with a `#view` suffix on `$type` (`social.coves.embed.external#view`, `social.coves.embed.post#view`) to distinguish resolved view data from the raw record form. The client only matched the bare record types, so appview-served embeds silently dropped to a null external/blueskyPost. Accept both spellings so the client keeps working across the appview deploy in either order. This must ship before the appview wire change reaches production — store rollout lags by days. Co-Authored-By: Claude Opus 5 (1M context) --- lib/constants/embed_types.dart | 6 +++ lib/models/post.dart | 5 ++- test/models/post_embed_test.dart | 77 ++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 test/models/post_embed_test.dart diff --git a/lib/constants/embed_types.dart b/lib/constants/embed_types.dart index 83abf8c..809c36b 100644 --- a/lib/constants/embed_types.dart +++ b/lib/constants/embed_types.dart @@ -8,6 +8,12 @@ class EmbedTypes { /// External link embed (URLs, articles, etc.) static const external = 'social.coves.embed.external'; + /// External link embed as served by the appview (thumb resolved to a URL). + static const externalView = 'social.coves.embed.external#view'; + /// Embedded Bluesky post static const post = 'social.coves.embed.post'; + + /// Embedded Bluesky post as served by the appview (with resolved data). + static const postView = 'social.coves.embed.post#view'; } diff --git a/lib/models/post.dart b/lib/models/post.dart index 6a621aa..6e00fc4 100644 --- a/lib/models/post.dart +++ b/lib/models/post.dart @@ -320,14 +320,15 @@ class PostEmbed { ExternalEmbed? externalEmbed; BlueskyPostEmbed? blueskyPostEmbed; - if (embedType == EmbedTypes.external && + if ((embedType == EmbedTypes.external || + embedType == EmbedTypes.externalView) && json['external'] != null) { externalEmbed = ExternalEmbed.fromJson( json['external'] as Map, ); } - if (embedType == EmbedTypes.post) { + if (embedType == EmbedTypes.post || embedType == EmbedTypes.postView) { blueskyPostEmbed = BlueskyPostEmbed.fromJson(json); } diff --git a/test/models/post_embed_test.dart b/test/models/post_embed_test.dart new file mode 100644 index 0000000..ff8abfb --- /dev/null +++ b/test/models/post_embed_test.dart @@ -0,0 +1,77 @@ +import 'package:coves_flutter/constants/embed_types.dart'; +import 'package:coves_flutter/models/post.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('PostEmbed.fromJson', () { + final externalPayload = { + 'external': { + 'uri': 'https://example.com/article', + 'title': 'Example article', + 'thumb': 'https://pds.example/xrpc/com.atproto.sync.getBlob?cid=abc', + }, + }; + + final postPayload = { + 'post': { + 'uri': 'at://did:plc:xyz/app.bsky.feed.post/abc', + 'cid': 'bafyrei123', + }, + }; + + test('parses record-form external embed', () { + final embed = PostEmbed.fromJson({ + r'$type': EmbedTypes.external, + ...externalPayload, + }); + + expect(embed.external, isNotNull); + expect(embed.external!.uri, 'https://example.com/article'); + expect(embed.blueskyPost, isNull); + }); + + test('parses view-form external embed served by the appview', () { + final embed = PostEmbed.fromJson({ + r'$type': EmbedTypes.externalView, + ...externalPayload, + }); + + expect(embed.external, isNotNull); + expect(embed.external!.uri, 'https://example.com/article'); + expect(embed.external!.thumb, contains('getBlob')); + expect(embed.blueskyPost, isNull); + }); + + test('parses record-form post embed', () { + final embed = PostEmbed.fromJson({ + r'$type': EmbedTypes.post, + ...postPayload, + }); + + expect(embed.blueskyPost, isNotNull); + expect(embed.blueskyPost!.uri, 'at://did:plc:xyz/app.bsky.feed.post/abc'); + expect(embed.external, isNull); + }); + + test('parses view-form post embed served by the appview', () { + final embed = PostEmbed.fromJson({ + r'$type': EmbedTypes.postView, + ...postPayload, + }); + + expect(embed.blueskyPost, isNotNull); + expect(embed.blueskyPost!.uri, 'at://did:plc:xyz/app.bsky.feed.post/abc'); + expect(embed.external, isNull); + }); + + test('falls back to external for unrecognized type with top-level uri', () { + final embed = PostEmbed.fromJson({ + r'$type': 'social.coves.embed.unknown', + 'uri': 'https://example.com/bare-link', + }); + + expect(embed.external, isNotNull); + expect(embed.external!.uri, 'https://example.com/bare-link'); + }); + }); +} -- 2.51.2