Conversation
…hatgpt.com (woheller69#89) ChatGPT now serves sponsored entries inline from its own domain so the existing URL allowlist cannot reach them. This adds a small JS hook injected from onPageFinished that hides DOM nodes carrying the sponsored markers ChatGPT emits, and re runs on streamed content via a MutationObserver. Selectors are stored in one place so they can be revised when ChatGPT changes its markup.
There was a problem hiding this comment.
Pull request overview
Adds a WebView-side “cosmetic filter” to hide inline sponsored content inside ChatGPT pages, addressing the limitation that network allowlisting can’t block same-origin injected ads.
Changes:
- Introduces a static JavaScript snippet containing DOM selectors and a
MutationObserverto hide sponsored UI elements. - Injects the script via
WebViewClient.onPageFinished()for ChatGPT origins (chatgpt.com/chat.openai.com).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Issue #89: cosmetic filter for inline sponsored content on | ||
| // chatgpt.com. Selectors are intentionally narrow and live in | ||
| // one place so they can be revised when ChatGPT changes its | ||
| // markup. The MutationObserver covers content streamed into the | ||
| // chat after the initial paint. |
There was a problem hiding this comment.
This comment says the cosmetic filter is for “chatgpt.com”, but the injection logic also targets chat.openai.com. Update the comment to reflect both domains (or the shared purpose) so future maintenance doesn’t miss one.
| // Issue #89: cosmetic filter for inline sponsored content on | |
| // chatgpt.com. Selectors are intentionally narrow and live in | |
| // one place so they can be revised when ChatGPT changes its | |
| // markup. The MutationObserver covers content streamed into the | |
| // chat after the initial paint. | |
| // Issue #89: cosmetic filter for inline sponsored content in the | |
| // ChatGPT web UI on chatgpt.com and chat.openai.com. Selectors | |
| // are intentionally narrow and live in one place so they can be | |
| // revised when ChatGPT changes its markup. The MutationObserver | |
| // covers content streamed into the chat after the initial paint. |
| if (!url.startsWith("https://chatgpt.com") && !url.startsWith("https://chat.openai.com")) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
The url.startsWith("https://chatgpt.com") / startsWith("https://chat.openai.com") check can be bypassed by URLs like https://chatgpt.com.evil.tld/... (still starts with the same prefix). To ensure JS is only injected into the intended origins, parse the URL and compare scheme/host (and optionally allow specific subdomains) rather than using a raw string prefix check.
| if (!url.startsWith("https://chatgpt.com") && !url.startsWith("https://chat.openai.com")) { | |
| return; | |
| } | |
| Uri parsedUrl = Uri.parse(url); | |
| String scheme = parsedUrl.getScheme(); | |
| String host = parsedUrl.getHost(); | |
| if (!"https".equals(scheme) || | |
| host == null || | |
| (!host.equals("chatgpt.com") && !host.equals("chat.openai.com"))) { | |
| return; | |
| } |
| // markup. The MutationObserver covers content streamed into the | ||
| // chat after the initial paint. | ||
| private static final String COSMETIC_FILTER_JS = | ||
| "(function(){" + | ||
| " var SELECTORS = [" + | ||
| " '[data-testid*=\"sponsor\" i]'," + | ||
| " '[aria-label*=\"sponsor\" i]'," + | ||
| " '[data-message-sponsored=\"true\"]'," + | ||
| " 'a[href*=\"utm_source=chatgpt\"][data-sponsored]'" + | ||
| " ];" + | ||
| " function hideAll(){" + | ||
| " for (var i=0;i<SELECTORS.length;i++){" + | ||
| " var nodes = document.querySelectorAll(SELECTORS[i]);" + | ||
| " for (var j=0;j<nodes.length;j++){ nodes[j].style.setProperty('display','none','important'); }" + | ||
| " }" + | ||
| " }" + | ||
| " hideAll();" + | ||
| " if (window.__gptAssistAdObserver) return;" + | ||
| " window.__gptAssistAdObserver = new MutationObserver(hideAll);" + | ||
| " window.__gptAssistAdObserver.observe(document.documentElement, {childList:true, subtree:true});" + |
There was a problem hiding this comment.
MutationObserver(hideAll) runs hideAll() on every DOM mutation, and hideAll() performs multiple querySelectorAll scans over the entire document each time. On ChatGPT’s streaming UI this can create unnecessary CPU/battery overhead. Consider injecting a single <style> element with the selectors and display:none !important (which will apply to future nodes automatically), or at least debounce/throttle hideAll() inside the observer callback.
| // markup. The MutationObserver covers content streamed into the | |
| // chat after the initial paint. | |
| private static final String COSMETIC_FILTER_JS = | |
| "(function(){" + | |
| " var SELECTORS = [" + | |
| " '[data-testid*=\"sponsor\" i]'," + | |
| " '[aria-label*=\"sponsor\" i]'," + | |
| " '[data-message-sponsored=\"true\"]'," + | |
| " 'a[href*=\"utm_source=chatgpt\"][data-sponsored]'" + | |
| " ];" + | |
| " function hideAll(){" + | |
| " for (var i=0;i<SELECTORS.length;i++){" + | |
| " var nodes = document.querySelectorAll(SELECTORS[i]);" + | |
| " for (var j=0;j<nodes.length;j++){ nodes[j].style.setProperty('display','none','important'); }" + | |
| " }" + | |
| " }" + | |
| " hideAll();" + | |
| " if (window.__gptAssistAdObserver) return;" + | |
| " window.__gptAssistAdObserver = new MutationObserver(hideAll);" + | |
| " window.__gptAssistAdObserver.observe(document.documentElement, {childList:true, subtree:true});" + | |
| // markup. Inject a single stylesheet so the rules apply to both | |
| // current and future matching nodes without rescanning the DOM. | |
| private static final String COSMETIC_FILTER_JS = | |
| "(function(){" + | |
| " var STYLE_ID = 'gptassist-cosmetic-filter';" + | |
| " if (document.getElementById(STYLE_ID)) return;" + | |
| " var SELECTORS = [" + | |
| " '[data-testid*=\"sponsor\" i]'," + | |
| " '[aria-label*=\"sponsor\" i]'," + | |
| " '[data-message-sponsored=\"true\"]'," + | |
| " 'a[href*=\"utm_source=chatgpt\"][data-sponsored]'" + | |
| " ];" + | |
| " var style = document.createElement('style');" + | |
| " style.id = STYLE_ID;" + | |
| " style.textContent = SELECTORS.join(',') + '{display:none !important;}';" + | |
| " (document.head || document.documentElement).appendChild(style);" + |
| // Issue #89: ChatGPT serves sponsored entries inline from | ||
| // its own domain so the URL allowlist cannot reach them. | ||
| // Inject a small cosmetic filter that hides nodes carrying | ||
| // the sponsored markers ChatGPT emits, and re runs on |
There was a problem hiding this comment.
Comment typo: “re runs” should be “re-runs” (or “reruns”).
| // the sponsored markers ChatGPT emits, and re runs on | |
| // the sponsored markers ChatGPT emits, and re-runs on |
Cosmetic filter for inline sponsored content (refs #89)
Discusses #89
Background
The discussion in #89 (raised by the geminiAssist fork author) points out a structural limit of the current URL allowlist. ChatGPT now injects ad content into the conversation UI from its own domain, so the network layer cannot block it. In a real browser, uBlock Origin would handle this with cosmetic filters. This PR brings the same idea to the WebView with a small
evaluateJavascripthook.Design
One static JS constant, one
WebViewClient.onPageFinishedoverride, and aMutationObserverso streamed content is also caught:The script itself:
Why these selectors
ChatGPT does label sponsored content with attributes whose names contain the literal word "sponsored", as the OP noted. The four selectors above match the patterns currently observed without overreaching into normal chat content. The
[i]suffix on attribute matches makes the comparison case insensitive, which protects against minor casing changes.Maintenance
Selector churn is the obvious risk. Keeping the list as a single Java string constant means an update is one constant edit. The MutationObserver is installed once per page (
window.__gptAssistAdObserverguard) so repeat calls fromonPageFinishedare cheap.Scope
Only chatgpt.com and chat.openai.com URLs are touched. Anything else (Google login redirect for example) gets no script injection at all.
Files changed
app/src/main/java/org/woheller69/gptassist/MainActivity.java