From dce88acc042e32613a17b815eb91be16841bbec7 Mon Sep 17 00:00:00 2001 From: vineyardbovines Date: Tue, 9 Jun 2026 12:53:13 -0400 Subject: [PATCH] [bskyembed] Add gallery embed support Renders app.bsky.embed.gallery#view in the embed iframe using the same 1/2/3/4 layouts as legacy images, plus a 2x2 grid with a +N overlay on the 4th tile for 5+ photo posts. Embed remains non-interactive (whole embed navigates to the post), so no carousel. Bumps @atproto/api from ^0.15.25 to 0.20.11 to match the main app and pick up AppBskyEmbedGallery types. APP-2326 --- bskyembed/package.json | 2 +- bskyembed/pnpm-lock.yaml | 123 +++++++++++++---------------- bskyembed/src/components/embed.tsx | 84 +++++++++++++++----- 3 files changed, 123 insertions(+), 86 deletions(-) diff --git a/bskyembed/package.json b/bskyembed/package.json index 7d6b62cf6c..5f8ab4e871 100644 --- a/bskyembed/package.json +++ b/bskyembed/package.json @@ -13,7 +13,7 @@ "format": "prettier -w src" }, "dependencies": { - "@atproto/api": "^0.15.25", + "@atproto/api": "0.20.11", "preact": "^10.4.8" }, "devDependencies": { diff --git a/bskyembed/pnpm-lock.yaml b/bskyembed/pnpm-lock.yaml index fe0c80db65..eb57826b1d 100644 --- a/bskyembed/pnpm-lock.yaml +++ b/bskyembed/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@atproto/api': - specifier: ^0.15.25 - version: 0.15.27 + specifier: 0.20.11 + version: 0.20.11 preact: specifier: ^10.4.8 version: 10.29.1 @@ -73,32 +73,33 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@atproto/api@0.15.27': - resolution: {integrity: sha512-ok/WGafh1nz4t8pEQGtAF/32x2E2VDWU4af6BajkO5Gky2jp2q6cv6aB2A5yuvNNcc3XkYMYipsqVHVwLPMF9g==} + '@atproto/api@0.20.11': + resolution: {integrity: sha512-1NoVJpBDAdotxo1iMZdMd75JstpdKWgBYOnxfVD4m+52bRjgU4cFg3EOGNUognZntFgL/bIHyEgyN7SJWVf6Ig==} + engines: {node: '>=22'} - '@atproto/common-web@0.4.21': - resolution: {integrity: sha512-Odq+wdk3YNasGCjjlpl3bCIPvqYHige5DLfMkIffNv/2PI/iIj5ZvAvMvJlJ59OhReKSxtpI0invx5UQPc3+fw==} + '@atproto/common-web@0.5.0': + resolution: {integrity: sha512-ReWnkuZdDU/74/I47gaI26uxQjHmpq4edp41NnZZQ5vIIKGb7Ei6pZHzDTUD9JURo109SKrPx9RMP2IQm0fOKA==} + engines: {node: '>=22'} - '@atproto/lex-data@0.0.15': - resolution: {integrity: sha512-ZsbGiaM5S3CnGrcTMbDGON3bLZzCi/Mx9UvcMREKSRujnF68eHgMiXxJqvykP7+QpOX6tYCK93axZkuJVhtSEw==} + '@atproto/lex-data@0.1.1': + resolution: {integrity: sha512-/xza8nU/YhtzhETnHL3QKKofaJ28/0NCzhT7LaYoUkm8EgypWp5ykEtmW52yLhQM2JF6fVa25g1soQmNTGqtSg==} + engines: {node: '>=22'} - '@atproto/lex-json@0.0.16': - resolution: {integrity: sha512-IgLgQ0krshVlrIYZ+heTBDbCnM3LmAgWvsaYn5MxvKA3LcBot3PG3ptdO8VOweVZ+WgCLuo39cz9EbUmIbqdtg==} + '@atproto/lex-json@0.1.0': + resolution: {integrity: sha512-oWUrRMwFyWpmi/5k1Se3xBTbP06XdxBS5iFuUz9LmqItaPXwrWRD87a9ldPvINQ/A2/mn7J6/qug8sDVlhD+vQ==} + engines: {node: '>=22'} - '@atproto/lexicon@0.4.14': - resolution: {integrity: sha512-jiKpmH1QER3Gvc7JVY5brwrfo+etFoe57tKPQX/SmPwjvUsFnJAow5xLIryuBaJgFAhnTZViXKs41t//pahGHQ==} + '@atproto/lexicon@0.7.1': + resolution: {integrity: sha512-voNfNED5KUxn3vpo7N5DMRblBDfWf7kSfdKhJFC1RrLCxg38YbBzzURNVQJ32bp13Oot8kYfyXBWxTgtKLvw8w==} + engines: {node: '>=22'} - '@atproto/lexicon@0.6.2': - resolution: {integrity: sha512-p3Ly6hinVZW0ETuAXZMeUGwuMm3g8HvQMQ41yyEE6AL0hAkfeKFaZKos6BdBrr6CjkpbrDZqE8M+5+QOceysMw==} + '@atproto/syntax@0.6.1': + resolution: {integrity: sha512-kA4dQDoMPpWCH8N0Q4KoSq024u5MkVfDVa8DdhyLjGA72z/khbOf1jXKPv7NIL2oEc9aj7geKELdvqyf4ogopA==} + engines: {node: '>=22'} - '@atproto/syntax@0.4.3': - resolution: {integrity: sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==} - - '@atproto/syntax@0.5.4': - resolution: {integrity: sha512-9XJOpMAgsGFxMEIp8nJ8AIWv+krrY1xQMj+wULbbXhQztQV+9aZ0TbG9Jtn3Op2or8Kr6OqyWR4ga9Z189kKDw==} - - '@atproto/xrpc@0.7.7': - resolution: {integrity: sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==} + '@atproto/xrpc@0.8.0': + resolution: {integrity: sha512-NJy02bIKrWlE2NQkRV1kT0Cj0ixbuxlF/MejBdo4cPWAa9v3oZexvAcjjb0zaOYeABkaU14iyIhvn2G4e/oLpw==} + engines: {node: '>=22'} '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} @@ -1101,8 +1102,8 @@ packages: peerDependencies: postcss: ^8.1.0 - await-lock@2.2.2: - resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} + await-lock@3.0.0: + resolution: {integrity: sha512-eO6fLiSnrJrMdjWMNK8zbVRXPs2TKJg78iKZd9wDpN3na5tcoV6EoeiOlMgk2QaAQ1gIrK1YuMsJHXWqz89tSA==} babel-plugin-polyfill-corejs2@0.4.17: resolution: {integrity: sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==} @@ -1787,8 +1788,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - multiformats@9.9.0: - resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} + multiformats@13.4.2: + resolution: {integrity: sha512-eh6eHCrRi1+POZ3dA+Dq1C6jhP1GNtr9CRINMb67OKzqW9I5DUuZM/3jLPlzhgpGeiNUlEGEbkCYChXMCc/8DQ==} mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -2194,8 +2195,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - uint8arrays@3.0.0: - resolution: {integrity: sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==} + uint8arrays@5.1.1: + resolution: {integrity: sha512-9muQwa4wZG4dKi9gMAIBtnk2Pw87SRpvWTH6lOGm19V2Uqxr4uomUf2PGqPnWc+qs06sN8owUU4jfcoWOcfwVQ==} unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} @@ -2324,63 +2325,51 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@atproto/api@0.15.27': + '@atproto/api@0.20.11': dependencies: - '@atproto/common-web': 0.4.21 - '@atproto/lexicon': 0.4.14 - '@atproto/syntax': 0.4.3 - '@atproto/xrpc': 0.7.7 - await-lock: 2.2.2 - multiformats: 9.9.0 + '@atproto/common-web': 0.5.0 + '@atproto/lexicon': 0.7.1 + '@atproto/syntax': 0.6.1 + '@atproto/xrpc': 0.8.0 + await-lock: 3.0.0 + multiformats: 13.4.2 tlds: 1.261.0 zod: 3.25.76 - '@atproto/common-web@0.4.21': + '@atproto/common-web@0.5.0': dependencies: - '@atproto/lex-data': 0.0.15 - '@atproto/lex-json': 0.0.16 - '@atproto/syntax': 0.5.4 + '@atproto/lex-data': 0.1.1 + '@atproto/lex-json': 0.1.0 + '@atproto/syntax': 0.6.1 zod: 3.25.76 - '@atproto/lex-data@0.0.15': + '@atproto/lex-data@0.1.1': dependencies: - multiformats: 9.9.0 + multiformats: 13.4.2 tslib: 2.8.1 - uint8arrays: 3.0.0 + uint8arrays: 5.1.1 unicode-segmenter: 0.14.5 - '@atproto/lex-json@0.0.16': + '@atproto/lex-json@0.1.0': dependencies: - '@atproto/lex-data': 0.0.15 + '@atproto/lex-data': 0.1.1 tslib: 2.8.1 - '@atproto/lexicon@0.4.14': + '@atproto/lexicon@0.7.1': dependencies: - '@atproto/common-web': 0.4.21 - '@atproto/syntax': 0.4.3 - iso-datestring-validator: 2.2.2 - multiformats: 9.9.0 + '@atproto/common-web': 0.5.0 + '@atproto/syntax': 0.6.1 + multiformats: 13.4.2 zod: 3.25.76 - '@atproto/lexicon@0.6.2': + '@atproto/syntax@0.6.1': dependencies: - '@atproto/common-web': 0.4.21 - '@atproto/syntax': 0.5.4 iso-datestring-validator: 2.2.2 - multiformats: 9.9.0 - zod: 3.25.76 - - '@atproto/syntax@0.4.3': - dependencies: tslib: 2.8.1 - '@atproto/syntax@0.5.4': + '@atproto/xrpc@0.8.0': dependencies: - tslib: 2.8.1 - - '@atproto/xrpc@0.7.7': - dependencies: - '@atproto/lexicon': 0.6.2 + '@atproto/lexicon': 0.7.1 zod: 3.25.76 '@babel/code-frame@7.29.0': @@ -3536,7 +3525,7 @@ snapshots: postcss: 8.5.14 postcss-value-parser: 4.2.0 - await-lock@2.2.2: {} + await-lock@3.0.0: {} babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.0): dependencies: @@ -4142,7 +4131,7 @@ snapshots: ms@2.1.3: {} - multiformats@9.9.0: {} + multiformats@13.4.2: {} mz@2.7.0: dependencies: @@ -4546,9 +4535,9 @@ snapshots: typescript@6.0.3: {} - uint8arrays@3.0.0: + uint8arrays@5.1.1: dependencies: - multiformats: 9.9.0 + multiformats: 13.4.2 unicode-canonical-property-names-ecmascript@2.0.1: {} diff --git a/bskyembed/src/components/embed.tsx b/bskyembed/src/components/embed.tsx index 3afa7a611f..af7a97b8fe 100644 --- a/bskyembed/src/components/embed.tsx +++ b/bskyembed/src/components/embed.tsx @@ -1,5 +1,6 @@ import { AppBskyEmbedExternal, + AppBskyEmbedGallery, AppBskyEmbedImages, AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, @@ -43,6 +44,11 @@ export function Embed({ return } + // Case 1b: Gallery (Photos v2) + if (AppBskyEmbedGallery.isView(content)) { + return + } + // Case 2: External link if (AppBskyEmbedExternal.isView(content)) { return @@ -230,6 +236,8 @@ function Info({children}: {children: ComponentChildren}) { ) } +type GridImage = {thumb: string; alt: string} + function ImageEmbed({ content, labelInfo, @@ -240,20 +248,45 @@ function ImageEmbed({ if (labelInfo) { return {labelInfo} } + return ( + ({thumb: i.thumb, alt: i.alt}))} + /> + ) +} - switch (content.images.length) { +function GalleryEmbed({ + content, + labelInfo, +}: { + content: AppBskyEmbedGallery.View + labelInfo?: string +}) { + if (labelInfo) { + return {labelInfo} + } + const images = content.items + .filter(AppBskyEmbedGallery.isViewImage) + .map(i => ({thumb: i.thumbnail, alt: i.alt})) + return +} + +function ImageGrid({images}: {images: GridImage[]}) { + switch (images.length) { + case 0: + return null case 1: return ( {content.images[0].alt} ) case 2: return (
- {content.images.map((image, i) => ( + {images.map((image, i) => (
{content.images[0].alt}
- {content.images.slice(1).map((image, i) => ( + {images.slice(1).map((image, i) => (
) - case 4: + default: { + const remaining = images.length - 4 return (
- {content.images.map((image, i) => ( - {image.alt} - ))} + {images.slice(0, 4).map((image, i) => { + const isOverflowCell = i === 3 && remaining > 0 + return ( +
+ {image.alt} + {isOverflowCell && ( +
+ + +{remaining} + +
+ )} +
+ ) + })}
) - default: - return null + } } }