diff --git a/src/SEQTA.ts b/src/SEQTA.ts index c70d9220..b5e5df46 100644 --- a/src/SEQTA.ts +++ b/src/SEQTA.ts @@ -92,25 +92,21 @@ async function init() { console.info('[BetterSEQTA+] Successfully initalised BetterSEQTA+, starting to load assets.') main() - waitForElm('.Viewer__Viewer___32BH-', true).then(() => { + /* waitForElm('.Viewer__Viewer___32BH-', true).then(async () => { console.log('Element exists') - const button = document.createElement('button') - button.innerHTML = 'Click me' - button.onclick = () => { - // function call to run onclick - ReactFiber.find(".Viewer__Viewer___32BH-", { debug: true }) - .setState(prev => ({ ...prev, selected: new Set([999999]) })) - } - button.style.position = 'fixed' - button.style.top = '0' - button.style.right = '0' - button.style.padding = '10px' - button.style.zIndex = '9999' - button.style.backgroundColor = 'red' - button.style.color = 'white' - document.body.appendChild(button) - }) + await browser.runtime.sendMessage({ type: 'injectMainScript' }) + + const nice = ReactFiber.find(".Viewer__Viewer___32BH-", { debug: true }) + + + + console.log(nice.getState()) + nice.setState({ selected: new Set([999431]) }) + + + //console.log(nice) + }) */ } catch (error: any) { console.error(error) } diff --git a/src/background.ts b/src/background.ts index 3038b12d..18b445a4 100644 --- a/src/background.ts +++ b/src/background.ts @@ -13,13 +13,7 @@ function reloadSeqtaPages() { result.then(open, console.error) } -const injectedTabs = new Set(); // Keep track of injected tabs - async function injectPageState(tabId: number) { - if (injectedTabs.has(tabId)) { - return; // Already injected - } - try { await browser.scripting.executeScript({ target: { tabId }, @@ -27,18 +21,12 @@ async function injectPageState(tabId: number) { // @ts-ignore world: "MAIN", }); - injectedTabs.add(tabId); - console.log(`[background] Injected pageState.js into tab ${tabId}`); + console.info(`[background] Injected pageState.js into tab ${tabId}`); } catch (err) { console.error(`[background] Failed to inject pageState.js into tab ${tabId}:`, err); } } -// Remove the script when a tab is closed -browser.tabs.onRemoved.addListener((tabId) => { - injectedTabs.delete(tabId); -}); - // Main message listener browser.runtime.onMessage.addListener((request: any, sender: any, sendResponse: (response?: any) => void) => { @@ -88,26 +76,13 @@ browser.runtime.onMessage.addListener((request: any, sender: any, sendResponse: GetNews(sendResponse, url); return true; - case 'reactFiberRequest': { - //const { tabId, /* selector, action, payload, debug */ } = request; - const tabId = sender.tab.id; - // Ensure pageState.js is injected - injectPageState(tabId).then(() => { - // Forward the request to the injected script - browser.tabs.sendMessage(tabId, { ...request, type: "reactFiberAction" }, { frameId: 0 }) // Target the main world - .then(response => { - sendResponse(response); // Send the response back to the content script - }) - .catch(err => { - console.error(`[background] Error communicating with injected script in tab ${tabId}:`, err); - }); - }).catch(err => { - console.error("[background] Failed to inject", err); - }); - return true; // Keep the message channel open for the async response + case 'injectMainScript': { + const senderTab = sender.tab ? sender.tab.id : undefined; + + injectPageState(senderTab!) + return; } - default: console.log('Unknown request type'); diff --git a/src/pageState.js b/src/pageState.js index 6cdcf0fd..beb4e1f2 100644 --- a/src/pageState.js +++ b/src/pageState.js @@ -1,3 +1,4 @@ +// pageState.ts class ReactFiber { constructor(selector, options = {}) { this.selector = selector; @@ -36,37 +37,62 @@ class ReactFiber { return null; } - getState(key = null) { - if (!this.components.length) return null; - const state = this.components[0]?.state || null; - return key ? state?.[key] : state; + getState(key) { + if (!this.components.length) return null; + const state = this.components[0]?.state || null; + + if (key === undefined) { + return state; // Return entire state + } else if (typeof key === 'string') { + return state?.[key]; // Return single key + } else if (Array.isArray(key)) { + // Return object with only specified keys + const filteredState = {}; + for (const k of key) { + if (state && Object.hasOwn(state, k)) { // Use Object.hasOwn for safety + filteredState[k] = state[k]; + } + } + return filteredState; + } + return null; // Invalid key type } - setState(updateFn) { - this.components.forEach(component => { - if (component?.setState) { - component.setState(prevState => { - const newState = updateFn(prevState); - if (this.debug) console.log("✅ Updated State:", newState); - return newState; - }); - } - }); - return this; // Enable chaining + setState(update) { + this.components.forEach(component => { + if (component?.setState) { + if (typeof update === 'function') { + // Functional update + component.setState(prevState => { + const newState = update(prevState); + if (this.debug) console.log("✅ Updated State (Functional):", newState); + return newState; + }); + } else { + // Object update (merge with existing state) + component.setState(prevState => { + const newState = { ...prevState, ...update }; // Merge here! + if (this.debug) console.log("✅ Updated State (Object Merge):", newState); + return newState; + }); + } + } + }); + return this; } getProp(propName) { - if (!this.fibers.length) return null; - return this.fibers[0]?.memoizedProps?.[propName] || null; + if (!this.fibers.length) return null; + return this.fibers[0]?.memoizedProps?.[propName]; } - setProp(propName, value) { - this.fibers.forEach(fiber => { - if (fiber?.memoizedProps) { - fiber.memoizedProps[propName] = value; - } - }); - return this.forceUpdate(); // Apply the change and return this for chaining + setProp(propName) { + this.fibers.forEach(fiber => { + if (fiber?.memoizedProps) { + fiber.memoizedProps[propName] = value; + } + }); + return this; // Enable chaining } forceUpdate() { @@ -80,37 +106,53 @@ class ReactFiber { } } -// Message listener for communication with the background script -browser.runtime.onMessage.addListener((request, sender, sendResponse) => { - if (request.type === "reactFiberAction") { - const { selector, action, payload, debug } = request; - const fiberInstance = ReactFiber.find(selector, {debug}); // Use the class +console.log("Window cat: ", window.cat); +// Listen for messages from the background script (via window.postMessage) +window.addEventListener('message', (event) => { + console.log(event) + + if (event.data.type === "reactFiberRequest") { + const { selector, action, payload, debug, messageId } = event.data; + const fiberInstance = ReactFiber.find(selector, { debug }); + + let response; switch (action) { case "getState": - sendResponse(fiberInstance.getState(payload.key)); + response = fiberInstance.getState(payload.key); break; case "setState": - // Very important: Eval the function string in the context of the page - const updateFn = eval(`(${payload.updateFn})`); - fiberInstance.setState(updateFn); - sendResponse({}); // Send acknowledgement + // Handle both function and object updates + if (payload.updateFn) { + const updateFn = eval(`(${payload.updateFn})`); + fiberInstance.setState(updateFn); + } else { + fiberInstance.setState(payload.updateObject); + } + response = {}; // Acknowledge break; + case "getProp": - sendResponse(fiberInstance.getProp(payload.propName)); + response = fiberInstance.getProp(payload.propName); break; case "setProp": fiberInstance.setProp(payload.propName, payload.value); - sendResponse({}); // Send acknowledgement + response = {}; // Acknowledge break; case "forceUpdate": fiberInstance.forceUpdate(); - sendResponse({}); // Send acknowledgement + response = {}; // Acknowledge break; default: console.warn(`[pageState] Unknown action: ${action}`); - sendResponse(null); + response = null; } - return true; // Keep message channel open (for consistency, even if not always needed) + + // Send the response back to the background script using window.postMessage + window.postMessage({ + type: "reactFiberResponse", + response, + messageId, + }, "*"); } }); \ No newline at end of file diff --git a/src/seqta/utils/ReactFiber.ts b/src/seqta/utils/ReactFiber.ts index 0eab49a5..f7ca51d7 100644 --- a/src/seqta/utils/ReactFiber.ts +++ b/src/seqta/utils/ReactFiber.ts @@ -1,8 +1,9 @@ import browser from 'webextension-polyfill'; class ReactFiber { -private selector: string; + private selector: string; private debug: boolean; + private messageIdCounter: number = 0; // Counter for unique message IDs constructor(selector: string, options: { debug?: boolean } = {}) { this.selector = selector; @@ -14,54 +15,61 @@ private selector: string; } private async sendMessage(action: string, payload: any = {}): Promise { - const message = { - type: "reactFiberRequest", - selector: this.selector, - action, - payload, - debug: this.debug - }; + return new Promise((resolve, _) => { + const messageId = this.messageIdCounter++; + const message = { + type: "reactFiberRequest", + selector: this.selector, + action, + payload, + debug: this.debug, + messageId, // Include the unique message ID + }; - try { - const response = await browser.runtime.sendMessage(message); - - if (this.debug) { - console.log("Content Received Response:", response); - } - return response; - } catch (error) { - console.error("Error sending message:", error); - return null; // or throw error, depending on desired behavior - } - } + const listener = (response: any) => { + if (response.data?.type === 'reactFiberResponse' && response.data?.messageId === messageId) { + if (this.debug) { + console.log("Content Received Response:", response.data.response); + } + resolve(response.data.response); + window.removeEventListener("message", listener) + } + }; + window.addEventListener('message', listener); + window.postMessage(message, "*"); + }); + } - async getState(key?: string): Promise { + async getState(key?: string | string[]): Promise { // Type change: allow string or string[] return this.sendMessage("getState", { key }); } - async setState(updateFn: (prevState: any) => any): Promise { - // Serialize the update function to a string. This is the tricky part. - // We can only send strings/JSON-serializable data through messages. - const updateFnString = updateFn.toString(); - await this.sendMessage("setState", { updateFn: updateFnString }); + async setState(update: any | ((prevState: any) => any)): Promise { + // Now async again. + const updateFnString = typeof update === 'function' ? update.toString() : null; + const updateObject = typeof update !== 'function' ? update : null; + + await this.sendMessage("setState", { updateFn: updateFnString, updateObject }); return this; } + async getProp(propName: string): Promise { return this.sendMessage("getProp", { propName }); } async setProp(propName: string, value: any): Promise { + // Now async again await this.sendMessage("setProp", { propName, value }); return this; } async forceUpdate(): Promise { + // Now async again await this.sendMessage("forceUpdate"); return this; } } - -export default ReactFiber \ No newline at end of file +export default ReactFiber; \ No newline at end of file