diff --git a/content.js b/content.js index 3bcfe00..990aec6 100644 --- a/content.js +++ b/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} */ + 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>} 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} */ - 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) - } - } -}) \ No newline at end of file + 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 \ No newline at end of file diff --git a/manifest.mv2.json b/manifest.mv2.json index 0e8a0b1..10e6e2c 100644 --- a/manifest.mv2.json +++ b/manifest.mv2.json @@ -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" diff --git a/manifest.mv3.json b/manifest.mv3.json index a730d87..9e18cc3 100644 --- a/manifest.mv3.json +++ b/manifest.mv3.json @@ -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": { diff --git a/options.js b/options.js index 15b16e3..c4421c1 100644 --- a/options.js +++ b/options.js @@ -360,14 +360,12 @@ function shouldDisplayMutedQuotes() { * @param {Partial} changes */ async function storeConfigChanges(changes) { - chrome.runtime.sendMessage({type: 'settings_change', changes}) /** @type {Partial} */ 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) { diff --git a/safari/Shared (Extension)/Resources/manifest.json b/safari/Shared (Extension)/Resources/manifest.json index 01caa8b..b942f18 100644 --- a/safari/Shared (Extension)/Resources/manifest.json +++ b/safari/Shared (Extension)/Resources/manifest.json @@ -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": { diff --git a/script.js b/script.js index de228c3..ed17e6c 100644 --- a/script.js +++ b/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