[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.

Clean up event listeners

Grace Kind (Jul 10, 2026, 8:10 PM -0500) 9db15cbf 103e9565

+136 -24
+11 -4
src/js/components/streaming-video.js
··· 19 19 ); 20 20 21 21 class StreamingVideo extends Component { 22 + handlePageTransition = () => { 23 + const video = this.querySelector("video"); 24 + if (!video) { 25 + return; 26 + } 27 + video.muted = true; 28 + video.pause(); 29 + }; 30 + 22 31 connectedCallback() { 23 32 // We always want to observe / unobserve the video to ensure it's streaming when it should be 24 33 streamingVideoObserver.observe(this); 34 + window.addEventListener("page-transition", this.handlePageTransition); 25 35 if (this.initialized) { 26 36 return; 27 37 } ··· 39 49 40 50 disconnectedCallback() { 41 51 streamingVideoObserver.unobserve(this); 52 + window.removeEventListener("page-transition", this.handlePageTransition); 42 53 } 43 54 44 55 render() { ··· 57 68 if (this.muted) { 58 69 video.muted = true; 59 70 } 60 - window.addEventListener("page-transition", () => { 61 - video.muted = true; 62 - video.pause(); 63 - }); 64 71 } 65 72 66 73 resumeAutoplay() {
+20 -6
src/js/postSeenObserver.js
··· 74 74 this.observedElements = []; // { postUri, el, feedContext } 75 75 this.seenPosts = new Set(); 76 76 this.interactionsDispatch = new InteractionsDispatch(api, feedProxyUrl); 77 + this.throttledHandleScroll = throttle(() => { 78 + this.handleScroll(); 79 + }, 100); 80 + this.connected = false; 81 + this.connect(); 82 + } 77 83 78 - window.addEventListener( 79 - "scroll", 80 - throttle(() => { 81 - this.handleScroll(); 82 - }, 100), 83 - ); 84 + connect() { 85 + if (this.connected) { 86 + return; 87 + } 88 + window.addEventListener("scroll", this.throttledHandleScroll); 89 + this.connected = true; 90 + } 91 + 92 + disconnect() { 93 + if (!this.connected) { 94 + return; 95 + } 96 + window.removeEventListener("scroll", this.throttledHandleScroll); 97 + this.connected = false; 84 98 } 85 99 86 100 async checkIntersection(el, postUri, feedContext) {
+5 -14
src/js/utils.js
··· 375 375 isDragging: false, 376 376 initialHeight: 0, 377 377 canDismiss: true, 378 - keyboardOpen: false, 379 378 }; 380 379 381 380 // Detect keyboard open on mobile 382 381 const KEYBOARD_THRESHOLD = 150; 383 382 const viewport = window.visualViewport; 384 - let handleViewportResize = null; 385 - if (disableWhenKeyboardOpen && viewport) { 386 - handleViewportResize = () => { 387 - dragState.keyboardOpen = 388 - window.innerHeight - viewport.height > KEYBOARD_THRESHOLD; 389 - }; 390 - viewport.addEventListener("resize", handleViewportResize); 391 - handleViewportResize(); 392 - } 383 + const isKeyboardOpen = () => 384 + disableWhenKeyboardOpen && 385 + viewport && 386 + window.innerHeight - viewport.height > KEYBOARD_THRESHOLD; 393 387 394 388 const handleTouchStart = (e) => { 395 - if (dragState.keyboardOpen) return; 389 + if (isKeyboardOpen()) return; 396 390 if (ignoreTouchTarget(e.target)) return; 397 391 398 392 dragState.startY = e.touches[0].clientY; ··· 460 454 eventSource.removeEventListener("touchstart", handleTouchStart); 461 455 eventSource.removeEventListener("touchmove", handleTouchMove); 462 456 eventSource.removeEventListener("touchend", handleTouchEnd); 463 - if (handleViewportResize) { 464 - viewport.removeEventListener("resize", handleViewportResize); 465 - } 466 457 target.style.transform = ""; 467 458 target.style.transition = ""; 468 459 target.style.height = "";
+13
src/js/views/home.view.js
··· 73 73 const interactableItems = pinnedItems.filter( 74 74 (item) => item.acceptsInteractions || item.uri === DISCOVER_FEED_URI, 75 75 ); 76 + for (const observer of postSeenObservers.values()) { 77 + observer.disconnect(); 78 + } 79 + postSeenObservers.clear(); 76 80 for (const item of interactableItems) { 77 81 const proxyUrl = getProxyUrl(item); 78 82 if (proxyUrl) { ··· 305 309 root.addEventListener("page-restore", (e) => { 306 310 const scrollY = e.detail?.scrollY ?? 0; 307 311 window.scrollTo(0, scrollY); 312 + for (const observer of postSeenObservers.values()) { 313 + observer.connect(); 314 + } 315 + }); 316 + 317 + root.addEventListener("page-exit", () => { 318 + for (const observer of postSeenObservers.values()) { 319 + observer.disconnect(); 320 + } 308 321 }); 309 322 } 310 323 }
+33
tests/unit/specs/components/streaming-video.test.js
··· 286 286 window.dispatchEvent(new Event("page-transition")); 287 287 assert(video.muted); 288 288 }); 289 + 290 + it("should stop listening for page-transition after disconnect", () => { 291 + const element = document.createElement("streaming-video"); 292 + element.setAttribute("src", "test.m3u8"); 293 + document.body.appendChild(element); 294 + 295 + const video = element.querySelector("video"); 296 + let pauseCalled = false; 297 + video.pause = () => { 298 + pauseCalled = true; 299 + }; 300 + 301 + element.remove(); 302 + window.dispatchEvent(new Event("page-transition")); 303 + assert(!pauseCalled); 304 + }); 305 + 306 + it("should listen for page-transition again after reconnect", () => { 307 + const element = document.createElement("streaming-video"); 308 + element.setAttribute("src", "test.m3u8"); 309 + document.body.appendChild(element); 310 + element.remove(); 311 + document.body.appendChild(element); 312 + 313 + const video = element.querySelector("video"); 314 + let pauseCalled = false; 315 + video.pause = () => { 316 + pauseCalled = true; 317 + }; 318 + 319 + window.dispatchEvent(new Event("page-transition")); 320 + assert(pauseCalled); 321 + }); 289 322 }); 290 323 291 324 t.describe("StreamingVideo - reinitialization protection", (it) => {
+54
tests/unit/specs/postSeenObserver.test.js
··· 1 + import { TestSuite } from "../testSuite.js"; 2 + import { assert, assertEquals } from "../testHelpers.js"; 3 + import { PostSeenObserver } from "/js/postSeenObserver.js"; 4 + 5 + const t = new TestSuite("PostSeenObserver"); 6 + 7 + function createObserver() { 8 + const api = { sendInteractions: async () => {} }; 9 + const observer = new PostSeenObserver(api, "did:web:example.com#bsky_fg"); 10 + let scrollHandlerCalls = 0; 11 + observer.handleScroll = () => { 12 + scrollHandlerCalls++; 13 + }; 14 + return { observer, getScrollHandlerCalls: () => scrollHandlerCalls }; 15 + } 16 + 17 + t.describe("PostSeenObserver - scroll listener lifecycle", (it) => { 18 + it("should handle scroll events after construction", () => { 19 + const { observer, getScrollHandlerCalls } = createObserver(); 20 + window.dispatchEvent(new window.Event("scroll")); 21 + assertEquals(getScrollHandlerCalls(), 1); 22 + observer.disconnect(); 23 + }); 24 + 25 + it("should stop handling scroll events after disconnect", () => { 26 + const { observer, getScrollHandlerCalls } = createObserver(); 27 + observer.disconnect(); 28 + window.dispatchEvent(new window.Event("scroll")); 29 + assertEquals(getScrollHandlerCalls(), 0); 30 + }); 31 + 32 + it("should resume handling scroll events after reconnect", () => { 33 + const { observer, getScrollHandlerCalls } = createObserver(); 34 + observer.disconnect(); 35 + observer.connect(); 36 + window.dispatchEvent(new window.Event("scroll")); 37 + assertEquals(getScrollHandlerCalls(), 1); 38 + observer.disconnect(); 39 + }); 40 + 41 + it("should tolerate repeated connect and disconnect calls", () => { 42 + const { observer } = createObserver(); 43 + observer.connect(); 44 + observer.connect(); 45 + assert(observer.connected); 46 + observer.disconnect(); 47 + observer.disconnect(); 48 + assert(!observer.connected); 49 + window.dispatchEvent(new window.Event("scroll")); 50 + assert(!observer.connected); 51 + }); 52 + }); 53 + 54 + await t.run();