From 25c56b05ce6b9275c7e035ec9c8cb245bdc7b625 Mon Sep 17 00:00:00 2001 From: Grace Kind Date: Sun, 16 Aug 2026 16:01:46 -0500 Subject: [PATCH] Enable CDN configuration --- package.json | 2 +- src/js/components/add-to-lists-dialog.js | 4 +- src/js/components/edit-list-details-dialog.js | 3 +- src/js/components/edit-profile-dialog.js | 6 +-- src/js/components/image-carousel.js | 7 +++- src/js/config.js | 2 +- src/js/dataHelpers.js | 22 +++++++++- src/js/templates/avatar.template.js | 5 ++- .../feedGeneratorListItem.template.js | 3 +- src/js/templates/labelBadges.template.js | 5 ++- src/js/templates/listFeed.template.js | 4 +- src/js/templates/postEmbed.template.js | 14 ++++--- src/js/templates/profileCard.template.js | 4 +- src/js/views/feeds.view.js | 8 ++-- src/js/views/listDetail.view.js | 4 +- src/js/views/notifications.view.js | 8 +++- src/js/views/search.view.js | 8 +++- tests/e2e/mockServer.js | 21 ++++++---- .../components/plugin-blob-image.test.js | 3 +- tests/unit/specs/dataHelpers.test.js | 42 ++++++++++++++++++- tests/unit/specs/dataLayer/mutations.test.js | 15 +++++-- .../specs/templates/avatar.template.test.js | 6 ++- 22 files changed, 144 insertions(+), 52 deletions(-) diff --git a/package.json b/package.json index 7c6f1f9b..1e511aa1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impro", - "version": "0.18.212", + "version": "0.18.213", "type": "module", "scripts": { "start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve", diff --git a/src/js/components/add-to-lists-dialog.js b/src/js/components/add-to-lists-dialog.js index da22d340..4506b999 100644 --- a/src/js/components/add-to-lists-dialog.js +++ b/src/js/components/add-to-lists-dialog.js @@ -4,7 +4,7 @@ import { scrollLocks } from "/js/scrollLocks.js"; import { closeWithAnimation } from "/js/dialogHelpers.js"; import { enableDragToDismiss } from "/js/dragHelpers.js"; import { Signal, SignalSet, ReactiveStore, effect } from "/js/signals.js"; -import { isModerationList } from "/js/dataHelpers.js"; +import { cdnImageUrl, isModerationList } from "/js/dataHelpers.js"; import { closeIconTemplate } from "/js/templates/icons/closeIcon.template.js"; import "/js/components/infinite-scroll-container.js"; @@ -204,7 +204,7 @@ class AddToListsDialog extends Component { >
${list.name} diff --git a/src/js/components/edit-list-details-dialog.js b/src/js/components/edit-list-details-dialog.js index 6121a5cb..e1436f4d 100644 --- a/src/js/components/edit-list-details-dialog.js +++ b/src/js/components/edit-list-details-dialog.js @@ -5,6 +5,7 @@ import { closeWithAnimation, resetScrollOnBlur } from "/js/dialogHelpers.js"; import { enableDragToDismiss } from "/js/dragHelpers.js"; import { classnames, graphemeCount, readFileAsDataUrl } from "/js/utils.js"; import { ImageCompressor } from "/js/imageCompressor.js"; +import { cdnImageUrl } from "/js/dataHelpers.js"; import "/js/components/image-cropper.js"; import "/js/components/context-menu.js"; import "/js/components/context-menu-item.js"; @@ -41,7 +42,7 @@ class EditListDetailsDialog extends Component { this._list = list; this._name = list.name || ""; this._description = list.description || ""; - this._currentAvatar = list.avatar || null; + this._currentAvatar = cdnImageUrl(list.avatar) || null; this._newAvatarDataUrl = null; this._removeAvatar = false; this._saving = false; diff --git a/src/js/components/edit-profile-dialog.js b/src/js/components/edit-profile-dialog.js index b2ff5534..f605d2ee 100644 --- a/src/js/components/edit-profile-dialog.js +++ b/src/js/components/edit-profile-dialog.js @@ -3,7 +3,7 @@ import { Component } from "/js/components/component.js"; import { scrollLocks } from "/js/scrollLocks.js"; import { closeWithAnimation, resetScrollOnBlur } from "/js/dialogHelpers.js"; import { enableDragToDismiss } from "/js/dragHelpers.js"; -import { avatarThumbnailUrl } from "/js/dataHelpers.js"; +import { avatarThumbnailUrl, cdnImageUrl } from "/js/dataHelpers.js"; import { classnames, graphemeCount, readFileAsDataUrl } from "/js/utils.js"; import { ImageCompressor } from "/js/imageCompressor.js"; import "/js/components/image-cropper.js"; @@ -47,9 +47,9 @@ class EditProfileDialog extends Component { this._displayName = profile.displayName || ""; this._description = profile.description || ""; this._currentAvatar = profile.avatar - ? avatarThumbnailUrl(profile.avatar) + ? cdnImageUrl(avatarThumbnailUrl(profile.avatar)) : null; - this._currentBanner = profile.banner || null; + this._currentBanner = cdnImageUrl(profile.banner) || null; this._newAvatarDataUrl = null; this._newBannerDataUrl = null; this._removeAvatar = false; diff --git a/src/js/components/image-carousel.js b/src/js/components/image-carousel.js index 32d36abb..0c9ff555 100644 --- a/src/js/components/image-carousel.js +++ b/src/js/components/image-carousel.js @@ -1,6 +1,7 @@ import { Component } from "/js/components/component.js"; import { html, render } from "/js/lib/lit-html.js"; import { isSafari } from "/js/utils.js"; +import { cdnImageUrl } from "/js/dataHelpers.js"; import "/js/components/lightbox-image-group.js"; import { cropIndicatorIconTemplate } from "/js/templates/icons/cropIndicatorIcon.template.js"; @@ -99,8 +100,10 @@ export class ImageCarousel extends Component { > ${image.alt ${feedGenerator.avatar ? html`${feedGenerator.displayName}` diff --git a/src/js/templates/labelBadges.template.js b/src/js/templates/labelBadges.template.js index fb68eb82..807cf445 100644 --- a/src/js/templates/labelBadges.template.js +++ b/src/js/templates/labelBadges.template.js @@ -1,5 +1,5 @@ import { html } from "/js/lib/lit-html.js"; -import { getLabelNameAndDescription } from "/js/dataHelpers.js"; +import { cdnImageUrl, getLabelNameAndDescription } from "/js/dataHelpers.js"; import "/js/components/plugin-slot.js"; export function authorBadgesTemplate({ badgeLabels, did, pluginService }) { @@ -27,7 +27,8 @@ function labelBadgesTemplate({ badgeLabels }) { ${labeler.creator?.handle ??
${list.name} diff --git a/src/js/templates/postEmbed.template.js b/src/js/templates/postEmbed.template.js index 62920c9c..6ed4bbce 100644 --- a/src/js/templates/postEmbed.template.js +++ b/src/js/templates/postEmbed.template.js @@ -1,5 +1,6 @@ import { html, render } from "/js/lib/lit-html.js"; import { + cdnImageUrl, getRKey, doHideAuthorOnUnauthenticated, getLabelNameAndDescription, @@ -124,7 +125,7 @@ function condensedMediaTemplate({ embed, lazyLoadImages }) { (image) => html`${image.alt || `, @@ -136,7 +137,7 @@ function condensedMediaTemplate({ embed, lazyLoadImages }) {
${embed.alt || @@ -278,8 +279,8 @@ function imageContainerTemplate({ image, lazyLoad, doCalculateAspectRatio }) { return html`
${image.alt}
@@ -596,7 +598,7 @@ function feedGeneratorTemplate({ feedGenerator }) { } function listTemplate({ list }) { - const avatarUrl = list.avatar ?? "/img/list-avatar-fallback.svg"; + const avatarUrl = cdnImageUrl(list.avatar) ?? "/img/list-avatar-fallback.svg"; return html`
${getDisplayName(profile)} banner
${item.data.name} @@ -161,7 +162,8 @@ export default async function feedsView({ >
${feedGenerator.displayName} diff --git a/src/js/views/listDetail.view.js b/src/js/views/listDetail.view.js index daf6f419..5e074cd0 100644 --- a/src/js/views/listDetail.view.js +++ b/src/js/views/listDetail.view.js @@ -1,7 +1,7 @@ import { html, render } from "/js/lib/lit-html.js"; import { Signal, ReactiveStore } from "/js/signals.js"; import { classnames } from "/js/utils.js"; -import { isModerationList } from "/js/dataHelpers.js"; +import { cdnImageUrl, isModerationList } from "/js/dataHelpers.js"; import { postFeedTemplate } from "/js/templates/postFeed.template.js"; import { profileFeedTemplate } from "/js/templates/profileFeed.template.js"; import { auth } from "/js/auth.js"; @@ -232,7 +232,7 @@ export default async function listDetailView({ ${list.avatar ? html`${list.name}` : html` html` ${image.alt || @@ -110,7 +111,10 @@ export default async function notificationsView({ ${video ? html`
- ${video.alt || + ${video.alt ||
` diff --git a/src/js/views/search.view.js b/src/js/views/search.view.js index 5523695f..b7be4ed7 100644 --- a/src/js/views/search.view.js +++ b/src/js/views/search.view.js @@ -4,7 +4,11 @@ import { closeIconTemplate } from "/js/templates/icons/closeIcon.template.js"; import { headerTemplate } from "/js/templates/header.template.js"; import { avatarTemplate } from "/js/templates/avatar.template.js"; import { classnames } from "/js/utils.js"; -import { getDisplayName, MISSING_HANDLE } from "/js/dataHelpers.js"; +import { + cdnImageUrl, + getDisplayName, + MISSING_HANDLE, +} from "/js/dataHelpers.js"; import { Signal, ReactiveStore } from "/js/signals.js"; import { linkToFeed, @@ -523,7 +527,7 @@ export default async function searchView({
${feedGenerator.avatar ? html`${feedGenerator.displayName}` diff --git a/tests/e2e/mockServer.js b/tests/e2e/mockServer.js index 7d2d904b..55c45dd8 100644 --- a/tests/e2e/mockServer.js +++ b/tests/e2e/mockServer.js @@ -1,3 +1,4 @@ +import { CDN_URL } from "../../src/js/config.js"; import { createPost } from "../shared/factories.js"; import { bskyLabeler, notificationService, userProfile } from "./testData.js"; import { @@ -1888,16 +1889,18 @@ export class MockServer { }); }); - await page.route("https://cdn.bsky.app/img/**", (route) => { - return route.fulfill({ - status: 200, - contentType: "image/png", - body: Buffer.from( - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", - "base64", - ), + for (const cdnUrl of new Set(["https://cdn.bsky.app", CDN_URL])) { + await page.route(`${cdnUrl}/img/**`, (route) => { + return route.fulfill({ + status: 200, + contentType: "image/png", + body: Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + "base64", + ), + }); }); - }); + } await page.route("https://ogcard.cdn.bsky.app/**", (route) => { return route.fulfill({ diff --git a/tests/unit/specs/components/plugin-blob-image.test.js b/tests/unit/specs/components/plugin-blob-image.test.js index 67ebd7d5..9eee5ab2 100644 --- a/tests/unit/specs/components/plugin-blob-image.test.js +++ b/tests/unit/specs/components/plugin-blob-image.test.js @@ -1,6 +1,7 @@ import { describe, it, beforeEach } from "node:test"; import assert from "node:assert/strict"; import "/js/components/plugin-blob-image.js"; +import { CDN_URL } from "/js/config.js"; describe("plugin-blob-image", () => { const VALID_CID = @@ -106,7 +107,7 @@ describe("plugin-blob-image", () => { assert(img !== null); assert.deepEqual( img.getAttribute("src"), - `https://cdn.bsky.app/img/avatar_thumbnail/plain/${did}/${VALID_CID}@jpeg`, + `${CDN_URL}/img/avatar_thumbnail/plain/${did}/${VALID_CID}@jpeg`, ); assert.deepEqual(img.getAttribute("alt"), "avatar"); }); diff --git a/tests/unit/specs/dataHelpers.test.js b/tests/unit/specs/dataHelpers.test.js index 9637bf8f..15b6156c 100644 --- a/tests/unit/specs/dataHelpers.test.js +++ b/tests/unit/specs/dataHelpers.test.js @@ -1,8 +1,10 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; +import { CDN_URL } from "/js/config.js"; import { avatarThumbnailUrl, buildProfileFromRecord, + cdnImageUrl, getRKey, getIsLiked, isListFeed, @@ -83,8 +85,8 @@ describe("buildProfileFromRecord", () => { handle: "me.test", displayName: "Me", description: "hello", - avatar: `https://cdn.bsky.app/img/avatar/plain/${did}/avatarcid@jpeg`, - banner: `https://cdn.bsky.app/img/banner/plain/${did}/bannercid@jpeg`, + avatar: `${CDN_URL}/img/avatar/plain/${did}/avatarcid@jpeg`, + banner: `${CDN_URL}/img/banner/plain/${did}/bannercid@jpeg`, pinnedPost: { uri: `at://${did}/app.bsky.feed.post/1`, cid: "abc" }, createdAt: "2024-01-01T00:00:00.000Z", labels: [], @@ -128,6 +130,42 @@ describe("avatarThumbnailUrl", () => { }); }); +describe("cdnImageUrl", () => { + it("should rewrite the bsky CDN origin to the configured CDN", () => { + assert.deepEqual( + cdnImageUrl( + "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:123/imagecid@jpeg", + ), + `${CDN_URL}/img/feed_thumbnail/plain/did:plc:123/imagecid@jpeg`, + ); + }); + + it("should leave other hosts alone", () => { + const videoThumb = + "https://video.bsky.app/watch/did:plc:123/videocid/thumbnail.jpg"; + assert.deepEqual(cdnImageUrl(videoThumb), videoThumb); + const ogCard = "https://ogcard.cdn.bsky.app/start/did:plc:123/rkey"; + assert.deepEqual(cdnImageUrl(ogCard), ogCard); + }); + + it("should leave non-absolute and non-URL values alone", () => { + assert.deepEqual( + cdnImageUrl("/img/avatar-fallback.svg"), + "/img/avatar-fallback.svg", + ); + assert.deepEqual( + cdnImageUrl("data:image/png;base64,abc"), + "data:image/png;base64,abc", + ); + }); + + it("should pass through empty values", () => { + assert.deepEqual(cdnImageUrl(""), ""); + assert.deepEqual(cdnImageUrl(null), null); + assert.deepEqual(cdnImageUrl(undefined), undefined); + }); +}); + describe("getRKey", () => { it("should extract rkey from post URI", () => { const post = { uri: "at://did:plc:123/app.bsky.feed.post/3l7q2wm5ws22k" }; diff --git a/tests/unit/specs/dataLayer/mutations.test.js b/tests/unit/specs/dataLayer/mutations.test.js index 572b25bc..1d2be3a8 100644 --- a/tests/unit/specs/dataLayer/mutations.test.js +++ b/tests/unit/specs/dataLayer/mutations.test.js @@ -8,6 +8,11 @@ import { Derived } from "/js/dataLayer/derived.js"; import { Preferences } from "/js/preferences.js"; import { Signal } from "/js/signals.js"; import { HiddenFeedItemsStore } from "/js/dataLayer/hiddenFeedItemsStore.js"; +import { CDN_URL } from "/js/config.js"; + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} const mockIdentityResolver = { resolveHandle: async () => null, @@ -1007,11 +1012,15 @@ describe("updateProfile", () => { const updatedProfile = dataStore.$profiles.get(testProfile.did); assert.match( updatedProfile.avatar, - /^https:\/\/cdn\.bsky\.app\/img\/avatar\/plain\/did:plc:test123\/bafkreiblob\d+@jpeg$/, + new RegExp( + `^${escapeRegExp(CDN_URL)}/img/avatar/plain/did:plc:test123/bafkreiblob\\d+@jpeg$`, + ), ); assert.match( updatedProfile.banner, - /^https:\/\/cdn\.bsky\.app\/img\/banner\/plain\/did:plc:test123\/bafkreiblob\d+@jpeg$/, + new RegExp( + `^${escapeRegExp(CDN_URL)}/img/banner/plain/did:plc:test123/bafkreiblob\\d+@jpeg$`, + ), ); }); @@ -4301,7 +4310,7 @@ describe("updateList", () => { assert.equal( dataStore.$lists.get(listUri).avatar, - "https://cdn.bsky.app/img/avatar/plain/did:plc:test123/bafkreiavatarcid@jpeg", + `${CDN_URL}/img/avatar/plain/did:plc:test123/bafkreiavatarcid@jpeg`, ); }); diff --git a/tests/unit/specs/templates/avatar.template.test.js b/tests/unit/specs/templates/avatar.template.test.js index e54b1e0f..2b8df71e 100644 --- a/tests/unit/specs/templates/avatar.template.test.js +++ b/tests/unit/specs/templates/avatar.template.test.js @@ -1,6 +1,7 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { avatarTemplate } from "/js/templates/avatar.template.js"; +import { cdnImageUrl } from "/js/dataHelpers.js"; import { post } from "../../testData.js"; import { render } from "/js/lib/lit-html.js"; @@ -96,7 +97,10 @@ describe("avatarTemplate", () => { const container = document.createElement("div"); render(result, container); const img = container.querySelector("[data-testid='avatar-image']"); - assert.deepEqual(img.getAttribute("data-lightbox-src"), post.author.avatar); + assert.deepEqual( + img.getAttribute("data-lightbox-src"), + cdnImageUrl(post.author.avatar), + ); }); it("should use thumbnail URL for src and full-size URL for data-lightbox-src", () => { -- 2.51.2