From 134157bc71efe0955e65a4bff4cb3ec5f6b5a197 Mon Sep 17 00:00:00 2001 From: wagesj45 Date: Tue, 25 Aug 2026 01:48:30 -0500 Subject: [PATCH] Fix Threads dynamic post discovery --- src/content/definition-engine.ts | 8 ++++++-- src/content/definitions/default.json | 8 ++++---- src/options/main.ts | 1 + tests/definition-engine.test.ts | 17 ++++++++++++++++- tests/fixtures/threads-feed.html | 2 +- 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/content/definition-engine.ts b/src/content/definition-engine.ts index 7f725ff..95aef2f 100644 --- a/src/content/definition-engine.ts +++ b/src/content/definition-engine.ts @@ -75,8 +75,12 @@ export class DefinitionEngine { private extractText(post: Element): string { for (const selector of this.definition.post.textSelectors) { - const content = post.matches(selector) ? post : post.querySelector(selector); - if (content) return visibleText(content, this.definition.post.excludedSelectors ?? []); + const matches = Array.from(post.querySelectorAll(selector)); + const contents = post.matches(selector) ? [post, ...matches] : matches; + for (const content of contents) { + const text = visibleText(content, this.definition.post.excludedSelectors ?? []); + if (text) return text; + } } return ""; } diff --git a/src/content/definitions/default.json b/src/content/definitions/default.json index f5af089..6a235ab 100644 --- a/src/content/definitions/default.json +++ b/src/content/definitions/default.json @@ -26,11 +26,11 @@ "https://threads.net/*", "https://www.threads.net/*" ], - "requiredSelectors": ["[data-pressable-container=\"true\"], [role=\"article\"], article"], + "requiredSelectors": ["[role=\"article\"], article"], "post": { - "rootSelectors": ["[data-pressable-container=\"true\"]", "[role=\"article\"]", "article"], - "textSelectors": ["span[dir=\"auto\"]:not(a span):not(time span):not([role=\"button\"] span)"], - "excludedSelectors": ["button", "[role=\"button\"]", "time", "svg"], + "rootSelectors": ["[role=\"article\"]", "article"], + "textSelectors": ["span[dir=\"auto\"]"], + "excludedSelectors": ["button", "[role=\"button\"]", "time", "svg", "a span", "time span", "[role=\"button\"] span"], "permalinkSelectors": ["a[href*=\"/post/\"]"] } } diff --git a/src/options/main.ts b/src/options/main.ts index a07e4e5..f290e42 100644 --- a/src/options/main.ts +++ b/src/options/main.ts @@ -46,6 +46,7 @@ addSubscriptionButton?.addEventListener("click", async () => { refreshSubscriptionsButton?.addEventListener("click", async () => { try { const response = await chrome.runtime.sendMessage({ type: "REFRESH_SUBSCRIPTIONS" }); + if (!response) throw new Error("The background page did not respond. Reload the extension and try again."); if (response?.error) throw new Error(response.error); currentSettings = response.settings; renderSubscriptions(currentSettings); diff --git a/tests/definition-engine.test.ts b/tests/definition-engine.test.ts index b096446..128e966 100644 --- a/tests/definition-engine.test.ts +++ b/tests/definition-engine.test.ts @@ -61,6 +61,21 @@ describe("DefinitionEngine", () => { expect(observed).toContain("content warning revealed text"); }); + it("discovers Threads posts appended after observation starts", async () => { + document.body.innerHTML = '
Initial post
'; + const threads = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "threads")!; + const engine = new DefinitionEngine(threads); + const observed: string[] = []; + const stop = engine.observe((posts) => observed.push(...posts.map((post) => post.text))); + const post = document.createElement("div"); + post.setAttribute("role", "article"); + post.innerHTML = 'Scrolled-in post'; + document.querySelector("main")?.append(post); + await new Promise((resolve) => setTimeout(resolve, 0)); + stop(); + expect(observed).toContain("scrolled-in post"); + }); + it("discovers Threads posts from stable semantic/data selectors and permalink IDs", () => { document.body.innerHTML = readFileSync("tests/fixtures/threads-feed.html", "utf8"); const threads = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "threads")!; @@ -71,7 +86,7 @@ describe("DefinitionEngine", () => { it("selects Threads on current and legacy domains", () => { const threads = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "threads")!; - document.body.innerHTML = '
'; + document.body.innerHTML = '
'; for (const url of ["https://threads.com/home", "https://www.threads.com/@alice/post/ABC123", "https://threads.net/home", "https://www.threads.net/@alice/post/ABC123"]) { expect(selectDefinition([threads], url)).toBe(threads); } diff --git a/tests/fixtures/threads-feed.html b/tests/fixtures/threads-feed.html index c40e2b6..b452519 100644 --- a/tests/fixtures/threads-feed.html +++ b/tests/fixtures/threads-feed.html @@ -1,5 +1,5 @@
-
+
alice First Threads post with visible text