mirror of
https://github.com/insin/control-panel-for-twitter.git
synced 2025-06-18 14:45:31 -04:00
Run the page script in the MAIN world and communicate with it via BroadcastChannel instead of having it monitor a <script> tag
This commit is contained in:
parent
15b33a6d55
commit
ab85ad9bb5
162
content.js
162
content.js
@ -1,7 +1,7 @@
|
||||
//#region Constants
|
||||
const IS_SAFARI = navigator.userAgent.includes('Safari/') && !/Chrom(e|ium)\//.test(navigator.userAgent)
|
||||
/**
|
||||
* Config keys whose changes should be passed to the page script.
|
||||
* Config keys which should be passed to the page script.
|
||||
* @type {import("./types").StoredConfigKey[]}
|
||||
*/
|
||||
const PAGE_SCRIPT_CONFIG_KEYS = ['debug', 'debugLogTimelineStats', 'enabled', 'settings']
|
||||
@ -12,11 +12,17 @@ const TWITTER_LOGO_PATH = 'M23.643 4.937c-.835.37-1.732.62-2.675.733.962-.576 1.
|
||||
const X_LOGO_PATH = 'M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z'
|
||||
//#endregion
|
||||
|
||||
//#region Variables
|
||||
/** @type {BroadcastChannel} */
|
||||
let channel
|
||||
//#endregion
|
||||
|
||||
//#region Functions
|
||||
function error(...messages) {
|
||||
console.error('[content]', ...messages)
|
||||
console.error('❌ [content]', ...messages)
|
||||
}
|
||||
|
||||
// Can't import this from storage.js in a content script
|
||||
function get(keys) {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.storage.local.get(keys, (result) => {
|
||||
@ -29,6 +35,46 @@ function get(keys) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {chrome.storage.StorageChange} settingsChange
|
||||
* @param {import("./types").UserSettingsKey} key
|
||||
*/
|
||||
function hasSettingChanged(settingsChange, key) {
|
||||
return (
|
||||
settingsChange.newValue &&
|
||||
Object.hasOwn(settingsChange.newValue, key) && (
|
||||
!settingsChange.oldValue ||
|
||||
!Object.hasOwn(settingsChange.oldValue, key) ||
|
||||
settingsChange.oldValue[key] != settingsChange.newValue[key]
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass relevant storage changes to the page script.
|
||||
* @param {{[key: string]: chrome.storage.StorageChange}} storageChanges
|
||||
*/
|
||||
function onStorageChanged(storageChanges) {
|
||||
if (storageChanges.enabled) {
|
||||
localStorage.cpftEnabled = storageChanges.enabled.newValue
|
||||
}
|
||||
if (storageChanges.settings && hasSettingChanged(storageChanges.settings, 'revertXBranding')) {
|
||||
localStorage.cpftRevertXBranding = storageChanges.settings.newValue.revertXBranding
|
||||
}
|
||||
|
||||
/** @type {Partial<import("./types").StoredConfig>} */
|
||||
let config = Object.fromEntries(
|
||||
Object.entries(storageChanges)
|
||||
.filter(([key]) => PAGE_SCRIPT_CONFIG_KEYSET.has(key))
|
||||
.map(([key, {newValue}]) => [key, newValue])
|
||||
)
|
||||
|
||||
// Ignore storage changes which aren't relevant to the page script
|
||||
if (Object.keys(config).length == 0) return
|
||||
|
||||
channel.postMessage({type: 'change', config})
|
||||
}
|
||||
|
||||
// Can't import this from storage.js in a content script
|
||||
function set(keys) {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.storage.local.set(keys, () => {
|
||||
@ -40,8 +86,34 @@ function set(keys) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** @param {MessageEvent<Partial<import("./types").StoredConfig>>} message */
|
||||
async function storeConfigChangesFromPageScript({data: changes}) {
|
||||
let configToStore = {}
|
||||
if (changes.version) {
|
||||
configToStore.version = changes.version
|
||||
}
|
||||
try {
|
||||
if (changes.settings) {
|
||||
let {settings} = await get({settings: {}})
|
||||
configToStore.settings = {...settings, ...changes.settings}
|
||||
}
|
||||
} catch(e) {
|
||||
error('error merging settings change from page script', e)
|
||||
}
|
||||
|
||||
chrome.storage.local.onChanged.removeListener(onStorageChanged)
|
||||
try {
|
||||
await set(configToStore)
|
||||
} catch(e) {
|
||||
error('error storing settings change from page script', e)
|
||||
} finally {
|
||||
chrome.storage.local.onChanged.addListener(onStorageChanged)
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Main
|
||||
// Replace the X logo on initial load before the page script runs
|
||||
if (localStorage.cpftEnabled != 'false' && localStorage.cpftRevertXBranding != 'false') {
|
||||
if (!IS_SAFARI) {
|
||||
@ -71,77 +143,17 @@ if (localStorage.cpftEnabled != 'false' && localStorage.cpftRevertXBranding != '
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Replace with BroadcastChannel
|
||||
/** @type {HTMLScriptElement} */
|
||||
let $settings
|
||||
|
||||
// Get initial config and inject it and the page script
|
||||
get({
|
||||
debug: false,
|
||||
debugLogTimelineStats: false,
|
||||
settings: {},
|
||||
}).then((storedConfig) => {
|
||||
$settings = document.createElement('script')
|
||||
$settings.type = 'text/json'
|
||||
$settings.id = 'cpftSettings'
|
||||
document.documentElement.appendChild($settings)
|
||||
$settings.innerText = JSON.stringify(storedConfig)
|
||||
|
||||
let $pageScript = document.createElement('script')
|
||||
$pageScript.src = chrome.runtime.getURL('script.js')
|
||||
/** @this {HTMLScriptElement} */
|
||||
$pageScript.onload = function() {
|
||||
this.remove()
|
||||
}
|
||||
document.documentElement.appendChild($pageScript)
|
||||
chrome.storage.onChanged.addListener(onStorageChanged)
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* Pass relevant storage changes to the page script.
|
||||
* @param {{[key: string]: chrome.storage.StorageChange}} storageChanges
|
||||
*/
|
||||
function onStorageChanged(storageChanges) {
|
||||
if (storageChanges.enabled) localStorage.cpftEnabled = storageChanges.enabled.newValue
|
||||
if (storageChanges.settings && storageChanges.settings.newValue?.revertXBranding)
|
||||
localStorage.cpftRevertXBranding = storageChanges.settings.newValue.revertXBranding
|
||||
let changes = Object.fromEntries(
|
||||
Object.entries(storageChanges)
|
||||
.filter(([key]) => PAGE_SCRIPT_CONFIG_KEYSET.has(key))
|
||||
.map(([key, {newValue}]) => [key, newValue])
|
||||
)
|
||||
if (Object.keys(changes).length > 0) {
|
||||
$settings.innerText = JSON.stringify(changes)
|
||||
}
|
||||
}
|
||||
|
||||
// Store settings changes from the page script
|
||||
window.addEventListener('message', async (event) => {
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.source !== window) return
|
||||
if (event.data.type == 'cpft_config_change' && event.data.changes) {
|
||||
/** @type {Partial<import("./types").StoredConfig>} */
|
||||
let changes = event.data.changes
|
||||
let configToStore = {}
|
||||
if (changes.version) {
|
||||
configToStore.version = changes.version
|
||||
}
|
||||
try {
|
||||
if (changes.settings) {
|
||||
let {settings} = await get({settings: {}})
|
||||
configToStore.settings = {...settings, ...changes.settings}
|
||||
}
|
||||
} catch(e) {
|
||||
error('error merging settings change from page script', e)
|
||||
}
|
||||
|
||||
chrome.storage.onChanged.removeListener(onStorageChanged)
|
||||
try {
|
||||
await set(configToStore)
|
||||
} catch(e) {
|
||||
error('error storing settings change from page script', e)
|
||||
} finally {
|
||||
chrome.storage.onChanged.addListener(onStorageChanged)
|
||||
}
|
||||
}
|
||||
})
|
||||
if (event.data.type != 'init' || !event.data.channelName) return
|
||||
channel = new BroadcastChannel(event.data.channelName)
|
||||
channel.addEventListener('message', storeConfigChangesFromPageScript)
|
||||
chrome.storage.local.get((storedConfig) => {
|
||||
let config = Object.fromEntries(
|
||||
Object.entries(storedConfig).filter(([key]) => PAGE_SCRIPT_CONFIG_KEYSET.has(key))
|
||||
)
|
||||
chrome.storage.local.onChanged.addListener(onStorageChanged)
|
||||
channel.postMessage({type: 'initial', config})
|
||||
})
|
||||
})
|
||||
//#endregion
|
@ -32,11 +32,21 @@
|
||||
"content.js"
|
||||
],
|
||||
"run_at": "document_start"
|
||||
},
|
||||
{
|
||||
"world": "MAIN",
|
||||
"matches": [
|
||||
"https://twitter.com/*",
|
||||
"https://mobile.twitter.com/*",
|
||||
"https://x.com/*",
|
||||
"https://mobile.x.com/*"
|
||||
],
|
||||
"js": [
|
||||
"script.js"
|
||||
],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
"script.js"
|
||||
],
|
||||
"options_ui": {
|
||||
"browser_style": true,
|
||||
"page": "options.html"
|
||||
|
@ -29,19 +29,19 @@
|
||||
"content.js"
|
||||
],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
},
|
||||
{
|
||||
"world": "MAIN",
|
||||
"matches": [
|
||||
"https://twitter.com/*",
|
||||
"https://mobile.twitter.com/*",
|
||||
"https://x.com/*",
|
||||
"https://mobile.x.com/*"
|
||||
],
|
||||
"resources": [
|
||||
"js": [
|
||||
"script.js"
|
||||
]
|
||||
],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
"options_ui": {
|
||||
|
10
options.js
10
options.js
@ -360,14 +360,12 @@ function shouldDisplayMutedQuotes() {
|
||||
* @param {Partial<import("./types").StoredConfig>} changes
|
||||
*/
|
||||
async function storeConfigChanges(changes) {
|
||||
chrome.runtime.sendMessage({type: 'settings_change', changes})
|
||||
/** @type {Partial<import("./types").StoredConfig>} */
|
||||
let changesToStore = {}
|
||||
if (Object.hasOwn(changes, 'debug')) {
|
||||
changesToStore.debug = changes.debug
|
||||
}
|
||||
if (Object.hasOwn(changes, 'debugLogTimelineStats')) {
|
||||
changesToStore.debugLogTimelineStats = changes.debugLogTimelineStats
|
||||
for (let key of INTERNAL_CONFIG_OPTIONS) {
|
||||
if (Object.hasOwn(changes, key)) {
|
||||
changesToStore[key] = changes[key]
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (changes.settings) {
|
||||
|
@ -28,19 +28,19 @@
|
||||
"content.js"
|
||||
],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
},
|
||||
{
|
||||
"world": "MAIN",
|
||||
"matches": [
|
||||
"https://twitter.com/*",
|
||||
"https://mobile.twitter.com/*",
|
||||
"https://x.com/*",
|
||||
"https://mobile.x.com/*"
|
||||
],
|
||||
"resources": [
|
||||
"js": [
|
||||
"script.js"
|
||||
]
|
||||
],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
"options_ui": {
|
||||
|
160
script.js
160
script.js
@ -7049,6 +7049,9 @@ function tweakTweetEngagementPage() {
|
||||
//#endregion
|
||||
|
||||
//#region Main
|
||||
let channelName = crypto.randomUUID()
|
||||
let channel = new BroadcastChannel(channelName)
|
||||
|
||||
async function main() {
|
||||
// Don't run on URLs used for OAuth
|
||||
if (location.pathname.startsWith('/i/oauth2/authorize') ||
|
||||
@ -7096,7 +7099,7 @@ async function main() {
|
||||
storeConfigChanges({version})
|
||||
|
||||
if (lastFlexDirection == null) {
|
||||
log('initial config', {config: settings, lang, version})
|
||||
log('initial config', {enabled, config: settings, lang, version})
|
||||
|
||||
// One-time setup
|
||||
checkReactNativeStylesheet()
|
||||
@ -7202,97 +7205,90 @@ function onSettingsChanged(changedSettings = new Set()) {
|
||||
}
|
||||
}
|
||||
|
||||
// Initial config and config changes are injected into a <script> element
|
||||
let $settings = /** @type {HTMLScriptElement} */ (document.querySelector('script#cpftSettings'))
|
||||
/** @type {import("./types").StoredConfig} */
|
||||
let storedConfig = {}
|
||||
if ($settings) {
|
||||
$settings.removeAttribute('id')
|
||||
try {
|
||||
storedConfig = JSON.parse($settings.innerText)
|
||||
} catch(e) {
|
||||
error('error parsing initial settings', e)
|
||||
}
|
||||
|
||||
if (Object.hasOwn(storedConfig, 'enabled')) {
|
||||
enabled = storedConfig.enabled
|
||||
}
|
||||
if (Object.hasOwn(storedConfig, 'debug')) {
|
||||
debug = storedConfig.debug
|
||||
}
|
||||
if (Object.hasOwn(storedConfig, 'debugLogTimelineStats')) {
|
||||
debugLogTimelineStats = storedConfig.debugLogTimelineStats
|
||||
}
|
||||
settings = {...defaultSettings, ...storedConfig.settings}
|
||||
|
||||
let settingsChangeObserver = new MutationObserver(() => {
|
||||
/** @type {Partial<import("./types").StoredConfig>} */
|
||||
let configChanges
|
||||
try {
|
||||
configChanges = JSON.parse($settings.innerText)
|
||||
} catch(e) {
|
||||
error('error parsing incoming settings change', e)
|
||||
return
|
||||
/**
|
||||
* @param {MessageEvent<import("./types").StoredConfigMessage>} message
|
||||
*/
|
||||
function receiveConfigFromContentScript({data: {type, config}}) {
|
||||
if (type == 'initial') {
|
||||
if (Object.hasOwn(config, 'enabled')) {
|
||||
enabled = config.enabled
|
||||
}
|
||||
|
||||
if (Object.hasOwn(configChanges, 'enabled')) {
|
||||
enabled = configChanges.enabled
|
||||
log(`${enabled ? 'en' : 'dis'}abling extension functionality`)
|
||||
if (enabled) {
|
||||
// Process the current page if we've just been enabled on it
|
||||
observingPageChanges = true
|
||||
main()
|
||||
} else {
|
||||
// These functions have teardowns when disabled
|
||||
configureCss()
|
||||
configureFont()
|
||||
configureDynamicCss()
|
||||
configureThemeCss()
|
||||
// Manually remove custom UI elements which clone existing elements, as
|
||||
// adding a hidden attribute won't hide them by default.
|
||||
document.querySelector('#cpftSeparatedTweetsTab')?.remove()
|
||||
document.querySelectorAll('.cpft_menu_item').forEach(el => el.remove())
|
||||
disconnectObservers(modalObservers, 'modal')
|
||||
disconnectObservers(pageObservers, 'page')
|
||||
disconnectObservers(globalObservers, 'global')
|
||||
}
|
||||
return
|
||||
if (Object.hasOwn(config, 'debug')) {
|
||||
debug = config.debug
|
||||
}
|
||||
if (Object.hasOwn(config, 'debugLogTimelineStats')) {
|
||||
debugLogTimelineStats = config.debugLogTimelineStats
|
||||
}
|
||||
settings = {...defaultSettings, ...config.settings}
|
||||
|
||||
if (Object.hasOwn(configChanges, 'debug')) {
|
||||
log('disabling debug mode')
|
||||
debug = configChanges.debug
|
||||
log('enabled debug mode')
|
||||
main()
|
||||
return
|
||||
}
|
||||
|
||||
if (Object.hasOwn(config, 'enabled')) {
|
||||
enabled = config.enabled
|
||||
log(`${enabled ? 'en' : 'dis'}abling extension functionality`)
|
||||
if (enabled) {
|
||||
// Process the current page if we've just been enabled on it
|
||||
observingPageChanges = true
|
||||
main()
|
||||
} else {
|
||||
// These functions have teardowns when disabled
|
||||
configureCss()
|
||||
configureFont()
|
||||
configureDynamicCss()
|
||||
configureThemeCss()
|
||||
return
|
||||
configureCustomCss()
|
||||
// Manually remove custom UI elements which clone existing elements, as
|
||||
// adding a hidden attribute won't hide them by default.
|
||||
document.querySelector('#cpftSeparatedTweetsTab')?.remove()
|
||||
document.querySelectorAll('.cpft_menu_item').forEach(el => el.remove())
|
||||
disconnectObservers(modalObservers, 'modal')
|
||||
disconnectObservers(pageObservers, 'page')
|
||||
disconnectObservers(globalObservers, 'global')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (Object.hasOwn(configChanges, 'debugLogTimelineStats')) {
|
||||
debugLogTimelineStats = configChanges.debugLogTimelineStats
|
||||
return
|
||||
}
|
||||
if (Object.hasOwn(config, 'debug')) {
|
||||
log('disabling debug mode')
|
||||
debug = config.debug
|
||||
log('enabled debug mode')
|
||||
// Reconfigure CSS to display debug annotations
|
||||
configureThemeCss()
|
||||
return
|
||||
}
|
||||
|
||||
/** @type {Set<import("./types").UserSettingsKey>} */
|
||||
let changedSettings
|
||||
if (Object.hasOwn(configChanges, 'settings')) {
|
||||
/** @type {import("./types").UserSettingsKey[]} */
|
||||
let settingsWithSpecialHandling = [
|
||||
'hideNotifications',
|
||||
'redirectToTwitter',
|
||||
'revertXBranding',
|
||||
]
|
||||
changedSettings = new Set(settingsWithSpecialHandling.filter(
|
||||
(key) => Object.hasOwn(configChanges.settings, key) && configChanges.settings[key] != settings[key]
|
||||
))
|
||||
Object.assign(settings, configChanges.settings)
|
||||
}
|
||||
if (Object.hasOwn(config, 'debugLogTimelineStats')) {
|
||||
debugLogTimelineStats = config.debugLogTimelineStats
|
||||
log(`${debugLogTimelineStats ? 'en' : 'dis'}abled logging of timeline stats`)
|
||||
return
|
||||
}
|
||||
|
||||
onSettingsChanged(changedSettings)
|
||||
})
|
||||
settingsChangeObserver.observe($settings, {childList: true})
|
||||
/** @type {Set<import("./types").UserSettingsKey>} */
|
||||
let changedSettings
|
||||
if (Object.hasOwn(config, 'settings')) {
|
||||
/** @type {import("./types").UserSettingsKey[]} */
|
||||
let settingsWithSpecialHandling = [
|
||||
'hideNotifications',
|
||||
'redirectToTwitter',
|
||||
'revertXBranding',
|
||||
]
|
||||
changedSettings = new Set(settingsWithSpecialHandling.filter(
|
||||
(key) => Object.hasOwn(config.settings, key) && config.settings[key] != settings[key]
|
||||
))
|
||||
Object.assign(settings, config.settings)
|
||||
}
|
||||
onSettingsChanged(changedSettings)
|
||||
}
|
||||
|
||||
main()
|
||||
/** @param {Partial<import("./types").StoredConfig>} changes */
|
||||
function storeConfigChanges(changes) {
|
||||
channel.postMessage(changes)
|
||||
}
|
||||
|
||||
channel.addEventListener('message', receiveConfigFromContentScript)
|
||||
window.postMessage({type: 'init', channelName}, location.origin)
|
||||
//#endregion
|
||||
|
||||
}()
|
5
types.d.ts
vendored
5
types.d.ts
vendored
@ -16,6 +16,11 @@ export type StoredConfig = {
|
||||
|
||||
export type StoredConfigKey = keyof StoredConfig
|
||||
|
||||
export type StoredConfigMessage = {
|
||||
type: 'initial' | 'change'
|
||||
config: Partial<StoredConfig>
|
||||
}
|
||||
|
||||
export type UserSettings = {
|
||||
// Shared
|
||||
addAddMutedWordMenuItem: boolean
|
||||
|
Loading…
Reference in New Issue
Block a user