[READ-ONLY] Mirror of https://github.com/improsocial/impro An extensible Bluesky client for web impro.social
6

Configure Feed

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

Update video aspect ratio logic

Grace Kind (May 19, 2026, 4:25 PM -0500) 40de8d94 a53b21ef

+118 -70
+1 -1
package.json
··· 1 1 { 2 2 "name": "impro", 3 - "version": "0.14.66", 3 + "version": "0.14.67", 4 4 "type": "module", 5 5 "scripts": { 6 6 "start": "rm -rf build && NODE_ENV=development eleventy --serve",
+8
src/css/style.css
··· 2229 2229 transform: translateZ(0); /* Prevents video jiggle on iPadOS scroll */ 2230 2230 } 2231 2231 2232 + .post-video streaming-video, 2233 + .post-video > video { 2234 + display: block; 2235 + width: 100%; 2236 + height: 100%; 2237 + object-fit: contain; 2238 + } 2239 + 2232 2240 .alt-indicator { 2233 2241 pointer-events: none; 2234 2242 position: absolute;
+1 -8
src/js/components/streaming-video.js
··· 26 26 this.controls = this.getAttribute("controls") !== null; 27 27 this.autoplay = this.getAttribute("autoplay") !== null; 28 28 this.muted = this.getAttribute("muted") !== null; 29 - this.height = this.getAttribute("height") ?? ""; 30 - this.width = this.getAttribute("width") ?? ""; 31 29 this._streamingEnabled = false; 32 30 this.render(); 33 31 this.initialized = true; ··· 39 37 40 38 render() { 41 39 render( 42 - html`<video 43 - controls=${this.controls} 44 - muted=${this.muted} 45 - height=${this.height} 46 - width=${this.width} 47 - ></video>`, 40 + html`<video controls=${this.controls} muted=${this.muted}></video>`, 48 41 this, 49 42 ); 50 43 const video = this.querySelector("video");
+5 -1
src/js/postCreator.js
··· 142 142 if (video.alt) { 143 143 embed.alt = video.alt; 144 144 } 145 - if (video.aspectRatio) { 145 + if ( 146 + video.aspectRatio && 147 + video.aspectRatio.width > 0 && 148 + video.aspectRatio.height > 0 149 + ) { 146 150 embed.aspectRatio = { 147 151 $type: "app.bsky.embed.defs#aspectRatio", 148 152 width: video.aspectRatio.width,
+12 -7
src/js/templates/postEmbed.template.js
··· 242 242 </lightbox-image-group>`; 243 243 } 244 244 245 + const MIN_VIDEO_ASPECT_RATIO = 1 / 2; 246 + 247 + function getVideoAspectRatio(video) { 248 + if (!video.aspectRatio) return 1; 249 + const aspectRatio = video.aspectRatio.width / video.aspectRatio.height; 250 + if (!Number.isFinite(aspectRatio) || aspectRatio <= 0) return 1; 251 + return Math.max(aspectRatio, MIN_VIDEO_ASPECT_RATIO); 252 + } 253 + 245 254 function videoTemplate({ video }) { 255 + const aspectRatio = getVideoAspectRatio(video); 246 256 return html`<div 247 257 class="post-video" 258 + style="aspect-ratio: ${aspectRatio};" 248 259 @click=${(e) => { 249 260 e.stopPropagation(); 250 261 e.preventDefault(); 251 262 }} 252 263 > 253 - <streaming-video 254 - src="${video.playlist}" 255 - controls 256 - muted 257 - height=${video.aspectRatio?.height ?? ""} 258 - width=${video.aspectRatio?.width ?? ""} 259 - ></streaming-video> 264 + <streaming-video src="${video.playlist}" controls muted></streaming-video> 260 265 </div>`; 261 266 } 262 267
+7 -23
src/js/videoUtils.js
··· 40 40 video.src = url; 41 41 video.onloadedmetadata = () => { 42 42 const duration = video.duration; 43 - const width = video.videoWidth || 1; 44 - const height = video.videoHeight || 1; 43 + const width = video.videoWidth; 44 + const height = video.videoHeight; 45 45 URL.revokeObjectURL(url); 46 46 if (duration > VIDEO_MAX_DURATION_S) { 47 47 reject( ··· 52 52 ); 53 53 return; 54 54 } 55 - resolve({ 56 - duration, 57 - width, 58 - height, 59 - aspectRatio: clampAspectRatio(width, height), 60 - }); 55 + // Leave aspectRatio unset when dimensions are missing - matches 56 + // social-app, which prefers an absent field to a 0 value that would 57 + // fail lexicon validation. 58 + const aspectRatio = width > 0 && height > 0 ? { width, height } : null; 59 + resolve({ duration, width, height, aspectRatio }); 61 60 }; 62 61 video.onerror = () => { 63 62 URL.revokeObjectURL(url); ··· 66 65 ); 67 66 }; 68 67 }); 69 - } 70 - 71 - // Clamp aspect ratio between 1:1 and 3:1 to match social-app 72 - function clampAspectRatio(width, height) { 73 - if (width <= 0 || height <= 0) { 74 - return { width: 1, height: 1 }; 75 - } 76 - const ratio = width / height; 77 - if (ratio > 3) { 78 - return { width: 3, height: 1 }; 79 - } 80 - if (ratio < 1 / 3) { 81 - return { width: 1, height: 3 }; 82 - } 83 - return { width, height }; 84 68 } 85 69 86 70 export class VideoUploader {
-30
tests/unit/specs/components/streaming-video.test.js
··· 49 49 document.body.appendChild(element); 50 50 assertEquals(element.muted, true); 51 51 }); 52 - 53 - it("should read height attribute", () => { 54 - const element = document.createElement("streaming-video"); 55 - element.setAttribute("src", "test.m3u8"); 56 - element.setAttribute("height", "480"); 57 - document.body.appendChild(element); 58 - assertEquals(element.height, "480"); 59 - }); 60 - 61 - it("should read width attribute", () => { 62 - const element = document.createElement("streaming-video"); 63 - element.setAttribute("src", "test.m3u8"); 64 - element.setAttribute("width", "640"); 65 - document.body.appendChild(element); 66 - assertEquals(element.width, "640"); 67 - }); 68 - 69 - it("should default height to empty string", () => { 70 - const element = document.createElement("streaming-video"); 71 - element.setAttribute("src", "test.m3u8"); 72 - document.body.appendChild(element); 73 - assertEquals(element.height, ""); 74 - }); 75 - 76 - it("should default width to empty string", () => { 77 - const element = document.createElement("streaming-video"); 78 - element.setAttribute("src", "test.m3u8"); 79 - document.body.appendChild(element); 80 - assertEquals(element.width, ""); 81 - }); 82 52 }); 83 53 84 54 t.describe("StreamingVideo - muted state", (it) => {
+37
tests/unit/specs/postCreator.test.js
··· 81 81 assertEquals(embed.aspectRatio.height, 9); 82 82 }); 83 83 84 + it("omits aspectRatio when missing", async () => { 85 + const api = makeApi(); 86 + const pc = new PostCreator(api); 87 + await pc.createPost({ 88 + postText: "hi", 89 + video: { ...videoFixture(), aspectRatio: null }, 90 + }); 91 + assert(!("aspectRatio" in api.lastEmbed)); 92 + }); 93 + 94 + it("omits aspectRatio when width or height is zero", async () => { 95 + const api = makeApi(); 96 + const pc = new PostCreator(api); 97 + await pc.createPost({ 98 + postText: "hi", 99 + video: { ...videoFixture(), aspectRatio: { width: 0, height: 9 } }, 100 + }); 101 + assert(!("aspectRatio" in api.lastEmbed)); 102 + 103 + await pc.createPost({ 104 + postText: "hi", 105 + video: { ...videoFixture(), aspectRatio: { width: 16, height: 0 } }, 106 + }); 107 + assert(!("aspectRatio" in api.lastEmbed)); 108 + }); 109 + 110 + it("preserves raw (unclamped) dimensions", async () => { 111 + const api = makeApi(); 112 + const pc = new PostCreator(api); 113 + await pc.createPost({ 114 + postText: "hi", 115 + video: { ...videoFixture(), aspectRatio: { width: 1080, height: 100 } }, 116 + }); 117 + assertEquals(api.lastEmbed.aspectRatio.width, 1080); 118 + assertEquals(api.lastEmbed.aspectRatio.height, 100); 119 + }); 120 + 84 121 it("omits alt when empty", async () => { 85 122 const api = makeApi(); 86 123 const pc = new PostCreator(api);
+47
tests/unit/specs/templates/postEmbed.template.test.js
··· 77 77 }); 78 78 }); 79 79 80 + t.describe("postEmbedTemplate - video", (it) => { 81 + function videoEmbed(aspectRatio) { 82 + return { 83 + $type: "app.bsky.embed.video#view", 84 + playlist: "https://example.com/video.m3u8", 85 + aspectRatio, 86 + }; 87 + } 88 + 89 + function renderVideo(aspectRatio) { 90 + const result = postEmbedTemplate({ 91 + embed: videoEmbed(aspectRatio), 92 + labels: [], 93 + isAuthenticated: true, 94 + }); 95 + const container = document.createElement("div"); 96 + render(result, container); 97 + return container.querySelector(".post-video"); 98 + } 99 + 100 + it("renders the aspect ratio inline on .post-video", () => { 101 + const el = renderVideo({ width: 16, height: 9 }); 102 + assert(el !== null); 103 + assertEquals(el.style.aspectRatio, String(16 / 9)); 104 + }); 105 + 106 + it("caps tall videos at a 1:2 ratio", () => { 107 + const el = renderVideo({ width: 1, height: 4 }); 108 + assertEquals(el.style.aspectRatio, String(1 / 2)); 109 + }); 110 + 111 + it("passes through wide videos without clamping", () => { 112 + const el = renderVideo({ width: 5, height: 1 }); 113 + assertEquals(el.style.aspectRatio, "5"); 114 + }); 115 + 116 + it("falls back to a square when aspectRatio is missing", () => { 117 + const el = renderVideo(undefined); 118 + assertEquals(el.style.aspectRatio, "1"); 119 + }); 120 + 121 + it("falls back to a square when aspectRatio is invalid", () => { 122 + const el = renderVideo({ width: 0, height: 0 }); 123 + assertEquals(el.style.aspectRatio, "1"); 124 + }); 125 + }); 126 + 80 127 t.describe("postEmbedTemplate - external links", (it) => { 81 128 it("should render external link embed", () => { 82 129 const embed = {