BlueSky & more on desktop lazurite.stormlightlabs.org/
tauri rust typescript bluesky appview atproto solid
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: post & embed relationships

+118 -32
+16 -11
src/components/feeds/embeds/ContentEmbed.tsx
··· 125 125 ); 126 126 } 127 127 case "recordWithMedia": { 128 - const quotedWithMedia: QuotedRecordPresentation = { 129 - ...embed.quoted, 130 - normalizedEmbeds: embed.media 131 - ? [embed.media, ...embed.quoted.normalizedEmbeds] 132 - : embed.quoted.normalizedEmbeds, 133 - }; 134 128 return ( 135 - <RenderQuotedPreview 136 - depth={depth()} 137 - post={props.post} 138 - quoted={quotedWithMedia} 139 - onOpenPost={props.onOpenPost} /> 129 + <div class="grid gap-3"> 130 + <Show when={embed.media}> 131 + {(mediaEmbed) => ( 132 + <EmbedContent 133 + depth={depth() + 1} 134 + embed={mediaEmbed()} 135 + onOpenPost={props.onOpenPost} 136 + post={props.post} /> 137 + )} 138 + </Show> 139 + <RenderQuotedPreview 140 + depth={depth()} 141 + post={props.post} 142 + quoted={embed.quoted} 143 + onOpenPost={props.onOpenPost} /> 144 + </div> 140 145 ); 141 146 } 142 147 default: {
+49 -1
src/components/feeds/tests/PostCard.test.tsx
··· 278 278 expect(screen.getByText("Quoted body")).toBeInTheDocument(); 279 279 const quotedCard = screen.getByText("Quoted post").closest(".ui-input-strong"); 280 280 expect(quotedCard).not.toBeNull(); 281 - expect(within(quotedCard as HTMLElement).getByAltText("Preview image")).toBeInTheDocument(); 281 + expect(within(quotedCard as HTMLElement).queryByAltText("Preview image")).not.toBeInTheDocument(); 282 282 283 283 const quotedLink = screen.getByRole("link", { name: /quoted body/i }); 284 284 expect(quotedLink).toHaveAttribute("href", `#${buildPostRoute("at://did:plc:bob/app.bsky.feed.post/quoted")}`); ··· 287 287 288 288 expect(onOpenThread).toHaveBeenCalledTimes(1); 289 289 expect(onOpenThread).toHaveBeenCalledWith("at://did:plc:bob/app.bsky.feed.post/quoted"); 290 + }); 291 + 292 + it("uses outer post context for recordWithMedia media and keeps quoted embeds nested", async () => { 293 + downloadImageMock.mockResolvedValue({ bytes: 40, path: "/tmp/post-image.jpg" }); 294 + render(() => ( 295 + <PostCard 296 + post={{ 297 + ...createPost(), 298 + uri: "at://did:plc:alice/app.bsky.feed.post/outer-post", 299 + embed: { 300 + $type: "app.bsky.embed.recordWithMedia#view", 301 + media: { 302 + $type: "app.bsky.embed.images#view", 303 + images: [{ alt: "Outer media image", fullsize: "https://cdn.example.com/outer-image.jpg" }], 304 + }, 305 + record: { 306 + $type: "app.bsky.embed.record#view", 307 + record: { 308 + $type: "app.bsky.embed.record#viewRecord", 309 + author: { did: "did:plc:bob", handle: "bob.test", displayName: "Bob" }, 310 + embeds: [{ 311 + $type: "app.bsky.embed.images#view", 312 + images: [{ alt: "Quoted nested image", fullsize: "https://cdn.example.com/quoted-image.jpg" }], 313 + }], 314 + uri: "at://did:plc:bob/app.bsky.feed.post/quoted-post", 315 + value: { text: "Quoted body with nested media" }, 316 + }, 317 + }, 318 + }, 319 + }} /> 320 + )); 321 + 322 + const quotedCard = screen.getByText("Quoted post").closest(".ui-input-strong"); 323 + expect(quotedCard).not.toBeNull(); 324 + expect(within(quotedCard as HTMLElement).getByAltText("Quoted nested image")).toBeInTheDocument(); 325 + expect(within(quotedCard as HTMLElement).queryByAltText("Outer media image")).not.toBeInTheDocument(); 326 + 327 + fireEvent.contextMenu(screen.getByAltText("Outer media image")); 328 + fireEvent.click(screen.getByRole("menuitem", { name: "Save image" })); 329 + await waitFor(() => 330 + expect(downloadImageMock).toHaveBeenCalledWith("https://cdn.example.com/outer-image.jpg", "outer-post") 331 + ); 332 + 333 + fireEvent.contextMenu(screen.getByAltText("Quoted nested image")); 334 + fireEvent.click(screen.getByRole("menuitem", { name: "Save image" })); 335 + await waitFor(() => 336 + expect(downloadImageMock).toHaveBeenLastCalledWith("https://cdn.example.com/quoted-image.jpg", "quoted-post") 337 + ); 290 338 }); 291 339 292 340 it("renders quoted post image and video embeds from the quoted record", () => {
+5 -20
src/lib/feeds.ts
··· 844 844 type QuotedEmbedExtraction = { source: "value.embed" | "value.embeds" | "viewRecord.embeds"; values: unknown[] }; 845 845 846 846 function quotedEmbedExtraction(record: Record<string, unknown>): QuotedEmbedExtraction | null { 847 + // Prefer hydrated view fields first, then fall back to raw record payload fields. 847 848 if (Object.prototype.hasOwnProperty.call(record, "embeds")) { 848 849 const direct = asArray(record.embeds); 849 850 return { source: "viewRecord.embeds", values: direct ?? (record.embeds === undefined ? [] : [record.embeds]) }; 850 851 } 851 852 852 - if (Object.prototype.hasOwnProperty.call(record, "embed")) { 853 - if (record.embed === null || record.embed === undefined) { 853 + const postRecord = asRecord(record.record); 854 + if (postRecord && Object.prototype.hasOwnProperty.call(postRecord, "embed")) { 855 + if (postRecord.embed === null || postRecord.embed === undefined) { 854 856 return { source: "value.embed", values: [] }; 855 857 } 856 - return { source: "value.embed", values: [record.embed] }; 858 + return { source: "value.embed", values: [postRecord.embed] }; 857 859 } 858 860 859 861 const value = asRecord(record.value); ··· 869 871 const embeds = asArray(value.embeds); 870 872 return { source: "value.embeds", values: embeds ?? (value.embeds === undefined ? [] : [value.embeds]) }; 871 873 } 872 - } 873 - 874 - const postRecord = asRecord(record.record); 875 - if (!postRecord) { 876 - return null; 877 - } 878 - 879 - if (Object.prototype.hasOwnProperty.call(postRecord, "embed")) { 880 - if (postRecord.embed === null || postRecord.embed === undefined) { 881 - return { source: "value.embed", values: [] }; 882 - } 883 - return { source: "value.embed", values: [postRecord.embed] }; 884 - } 885 - 886 - if (Object.prototype.hasOwnProperty.call(postRecord, "embeds")) { 887 - const embeds = asArray(postRecord.embeds); 888 - return { source: "value.embeds", values: embeds ?? (postRecord.embeds === undefined ? [] : [postRecord.embeds]) }; 889 874 } 890 875 891 876 return null;
+48
src/lib/tests/feeds.test.ts
··· 509 509 expect(fromValueEmbeds.normalizedEmbeds[0]?.meta.source).toBe("value.embeds"); 510 510 }); 511 511 512 + it("prefers postView.record.embed before value embed fallbacks", () => { 513 + const fromPostViewEmbed = getQuotedPresentation({ 514 + $type: "app.bsky.embed.record#view", 515 + record: { 516 + $type: "app.bsky.feed.defs#postView", 517 + author: { did: "did:plc:bob", handle: "bob.test" }, 518 + record: { 519 + embed: { 520 + $type: "app.bsky.embed.images#view", 521 + images: [{ fullsize: "https://cdn.example.com/postview.png" }], 522 + }, 523 + text: "postview", 524 + }, 525 + uri: "at://did:plc:bob/app.bsky.feed.post/postview-priority", 526 + value: { 527 + embed: { $type: "app.bsky.embed.video#view", playlist: "https://cdn.example.com/fallback.m3u8" }, 528 + embeds: [{ 529 + $type: "app.bsky.embed.external#view", 530 + external: { title: "fallback", uri: "https://example.com/fallback" }, 531 + }], 532 + text: "fallback text", 533 + }, 534 + }, 535 + }); 536 + 537 + expect(fromPostViewEmbed.normalizedEmbeds).toHaveLength(1); 538 + expect(fromPostViewEmbed.normalizedEmbeds[0]?.kind).toBe("images"); 539 + expect(fromPostViewEmbed.normalizedEmbeds[0]?.meta.source).toBe("value.embed"); 540 + 541 + const fromFallbackValueEmbed = getQuotedPresentation({ 542 + $type: "app.bsky.embed.record#view", 543 + record: { 544 + $type: "app.bsky.feed.defs#postView", 545 + author: { did: "did:plc:bob", handle: "bob.test" }, 546 + record: { text: "postview without hydrated embed" }, 547 + uri: "at://did:plc:bob/app.bsky.feed.post/postview-fallback", 548 + value: { 549 + embed: { $type: "app.bsky.embed.video#view", playlist: "https://cdn.example.com/value-only.m3u8" }, 550 + text: "value-only", 551 + }, 552 + }, 553 + }); 554 + 555 + expect(fromFallbackValueEmbed.normalizedEmbeds).toHaveLength(1); 556 + expect(fromFallbackValueEmbed.normalizedEmbeds[0]?.kind).toBe("video"); 557 + expect(fromFallbackValueEmbed.normalizedEmbeds[0]?.meta.source).toBe("value.embed"); 558 + }); 559 + 512 560 it("keeps unknown custom embeds visible and aggregates telemetry by fingerprint", () => { 513 561 const custom = { $type: "dev.example.embed#view", payload: { nested: { key: "value" } } }; 514 562 const topUnknownA = normalizeEmbed(custom, { source: "top" });
src/lib/utils/typing.ts

This is a binary file and will not be displayed.