diff --git a/apps/mocksi-lite-next/src/pages/background/index.ts b/apps/mocksi-lite-next/src/pages/background/index.ts index 484536c..da3affe 100644 --- a/apps/mocksi-lite-next/src/pages/background/index.ts +++ b/apps/mocksi-lite-next/src/pages/background/index.ts @@ -1,12 +1,12 @@ +import { AppEvents, AuthEvents, LayoutEvents } from "@pages/events"; import { jwtDecode } from "jwt-decode"; console.log("background script loaded"); const MOCKSI_AUTH = "mocksi-auth"; -let prevRequest = { - data: {}, - message: "INIT", -}; + +let fallbackTab: null | chrome.tabs.Tab = null; +let prevLayoutEvent = ""; const getAuth = async (): Promise => { }; async function getCurrentTab() { - const queryOptions = { active: true, lastFocusedWindow: true }; + const queryOptions: chrome.tabs.QueryInfo = { + active: true, + lastFocusedWindow: true, + windowType: "normal", + }; + // `tab` will either be a `tabs.Tab` instance or `undefined`. - const [tab] = await chrome.tabs.query(queryOptions); - return tab; + const tabs = await chrome.tabs.query(queryOptions); + if (!tabs[0] && !fallbackTab) { + console.error("tab is undefined"); + return null; + } + if (tabs[0]) { + fallbackTab = tabs[0]; + return tabs[0]; + } else { + return fallbackTab; + } } async function showAuthTab(force?: boolean) { @@ -74,6 +88,20 @@ async function showAuthTab(force?: boolean) { }); } +async function showDefaultIcon(tabId: number) { + await chrome.action.setIcon({ + path: "mocksi-icon.png", + tabId: tabId, + }); +} + +async function showPlayIcon(tabId: number) { + await chrome.action.setIcon({ + path: "play-icon.png", + tabId: tabId, + }); +} + addEventListener("install", () => { // TODO test if this works on other browsers chrome.tabs.create({ @@ -84,24 +112,22 @@ addEventListener("install", () => { // when user clicks toolbar mount extension chrome.action.onClicked.addListener((tab) => { if (!tab?.id) { - console.log("No tab found, could not mount extension"); + console.log("No tab exits click, could not mount extension"); return; } + // store the tab they clicked on to open the extension + // so we can use it as a fallback + fallbackTab = tab; - chrome.tabs.sendMessage(tab.id, { message: "mount-extension" }); + chrome.tabs.sendMessage(tab.id, { + message: LayoutEvents.MOUNT, + }); - if (prevRequest.message) { + if (prevLayoutEvent === LayoutEvents.HIDE) { chrome.tabs.sendMessage(tab.id, { - data: prevRequest.data, - message: prevRequest.message, - }); - } - - if (prevRequest.message === "PLAY") { - chrome.action.setIcon({ - path: "play-icon.png", - tabId: tab.id, + message: LayoutEvents.SHOW, }); + prevLayoutEvent = LayoutEvents.HIDE; } }); @@ -118,57 +144,61 @@ chrome.runtime.onMessage.addListener( chrome.runtime.onMessageExternal.addListener( (request, _sender, sendResponse) => { - // This logging is useful and only shows up in the service worker - console.log(" "); - console.log("Previous message from external:", prevRequest); - console.log("Received new message from external:", request); + console.log("on message external: ", request); // execute in async block so that we return true // synchronously, telling chrome to wait for the response (async () => { - if (request.message === "AUTH_ERROR") { + if (request.message === AuthEvents.AUTH_ERROR) { await clearAuth(); sendResponse({ - message: "retry", + message: AuthEvents.RETRY, status: "ok", }); - } else if (request.message === "UNAUTHORIZED") { + } else if (request.message === AuthEvents.UNAUTHORIZED) { const auth = await getAuth(); if (auth) { const { accessToken, email } = auth; const tab = await getCurrentTab(); sendResponse({ - message: { accessToken, email, url: tab.url }, + message: { accessToken, email, url: tab?.url }, status: "ok", }); } else { await showAuthTab(true); sendResponse({ - message: "authenticating", + message: AuthEvents.AUTHENTICATING, status: "ok", }); } } else { const tab = await getCurrentTab(); if (!tab?.id) { - sendResponse({ message: request.message, status: "no-tab" }); - console.log("No active tab found, could not send message"); - return true; + sendResponse({ + message: LayoutEvents.NO_TAB, + status: "ok", + }); + return; } - // handle icon changes triggered by messaging - switch (request.message) { - case "MINIMIZED": // No action needed for "MINIMIZED" - break; - case "PLAY": - await chrome.action.setIcon({ - path: "play-icon.png", - tabId: tab.id, - }); - break; - default: - chrome.action.setIcon({ path: "mocksi-icon.png", tabId: tab.id }); - break; + if ( + request.message === LayoutEvents.HIDE || + request.message === LayoutEvents.RESIZE || + request.message === LayoutEvents.SHOW + ) { + prevLayoutEvent = request.message; + } + + if (request.message === AppEvents.PLAY_DEMO_START) { + showPlayIcon(tab?.id); + } + + if ( + request.message === AppEvents.PLAY_DEMO_STOP || + request.message === AppEvents.EDIT_DEMO_START || + request.message === AppEvents.EDIT_DEMO_STOP + ) { + showDefaultIcon(tab.id); } chrome.tabs.sendMessage( @@ -184,11 +214,6 @@ chrome.runtime.onMessageExternal.addListener( } })(); - // Store last app state so we can return to the correct state when the - // menu is reopened - if (request.message !== "MINIMIZED") { - prevRequest = request; - } return true; }, ); diff --git a/apps/mocksi-lite-next/src/pages/content/mocksi-extension.tsx b/apps/mocksi-lite-next/src/pages/content/mocksi-extension.tsx index 8bce402..2b3b5b6 100644 --- a/apps/mocksi-lite-next/src/pages/content/mocksi-extension.tsx +++ b/apps/mocksi-lite-next/src/pages/content/mocksi-extension.tsx @@ -3,15 +3,33 @@ import { Reactor } from "@repo/reactor"; import React from "react"; import ReactDOM from "react-dom"; import { createRoot } from "react-dom/client"; +import { DemoEditEvents, LayoutEvents } from "../events"; import { getHighlighter } from "./highlighter"; +export enum IframePosition { + BOTTOM_CENTER = "BOTTOM_CENTER", + TOP_RIGHT = "TOP_RIGHT", + BOTTOM_RIGHT = "BOTTOM_RIGHT", +} +export interface IframeResizeArgs { + height: number; + id?: string; + position: + | IframePosition.BOTTOM_CENTER + | IframePosition.BOTTOM_RIGHT + | IframePosition.TOP_RIGHT; + width: number; +} + const STORAGE_CHANGE_EVENT = "MOCKSI_STORAGE_CHANGE"; const div = document.createElement("div"); div.id = "__mocksi__root"; document.body.appendChild(div); + let mounted = false; const reactor = new Reactor(); +const highlighter = getHighlighter(); window.addEventListener("message", (event: MessageEvent) => { const eventData = event.data; @@ -27,15 +45,7 @@ window.addEventListener("message", (event: MessageEvent) => { } }); -function getIframeSizePosition({ - height, - position, - width, -}: { - height: number; - position: string; - width: number; -}) { +function getIframeStyles({ height, position, width }: IframeResizeArgs) { if (!height || !width || !position) { console.error( "Cannot update iframe size / position, make sure 'request.data.iframe' has 'height', 'width', and 'position' set correctly", @@ -45,86 +55,68 @@ function getIframeSizePosition({ const bounds = document.body.getBoundingClientRect(); - const styles = { - bottom: "auto", - display: "block", - height: `${height}px`, - left: "auto", - right: "auto", - top: "auto", - width: `${width}px`, - }; - + let styles = {}; switch (position) { - case "BOTTOM_CENTER": - styles.bottom = "0px"; - styles.right = `${bounds.width / 2 - width / 2}px`; - break; - case "BOTTOM_RIGHT": - styles.bottom = "10px"; - styles.right = "10px"; + case IframePosition.BOTTOM_CENTER: + styles = { + bottom: "0px", + right: `${bounds.width / 2 - width / 2}px`, + top: "auto", + }; break; - case "NONE": - styles.display = "none"; + case IframePosition.BOTTOM_RIGHT: + styles = { + bottom: "10px", + right: "10px", + top: "auto", + }; break; - case "TOP_RIGHT": - styles.top = "10px"; - styles.right = "10px"; + case IframePosition.TOP_RIGHT: + styles = { + bottom: "auto", + display: "block", + right: "10px", + top: "10px", + }; break; } - return styles; + + return Object.assign( + { + bottom: "auto", + display: "block", + height: `${height}px`, + left: "auto", + right: "auto", + top: "auto", + width: `${width}px`, + }, + styles, + ); } -// Function to get styles based on the message, -function getIframeStyles(message: string): Partial { - switch (message) { - case "ANALYZING": - case "PLAY": - case "RECORDING": - return { - display: "block", - height: "150px", - inset: "0px 10px auto auto", - width: "300px", - }; - case "EDITING": - case "INIT": - case "LIST": - case "NEW_EDIT": - case "READYTORECORD": - case "SETTINGS": - case "STOP_EDITING": - case "STOP_PLAYING": - case "UNAUTHORIZED": - return { - display: "block", - height: "600px", - inset: "auto 10px 10px auto", - width: "500px", - }; - case "MINIMIZED": - return { - display: "none", - inset: "0px 0px auto auto", - }; - default: - return {}; - } +interface AppMessageRequest { + data: { + edits?: ModificationRequest[]; + uuid: string; + }; + message: string; } chrome.runtime.onMessage.addListener((request) => { - if (request.message === "mount-extension") { + if (request.message === LayoutEvents.MOUNT) { const rootContainer = document.querySelector("#__mocksi__root"); if (!rootContainer) throw new Error("Can't find Content root element"); + const root = createRoot(rootContainer); const Iframe = () => { + const prevAppEvent = React.useRef({ data: { uuid: "" }, message: "" }); const iframeRef = React.useRef(null); async function findReplaceAll( find: string, replace: string, flags: string, - highlight: boolean, ) { const modification: ModificationRequest = { description: `Change ${find} to ${replace}`, @@ -138,15 +130,20 @@ chrome.runtime.onMessage.addListener((request) => { }; const modifications = await reactor.pushModification(modification); - if (highlight) { - for (const mod of modifications) { - mod.setHighlight(true); - } - } - console.log("mods in find and replace fn: ", modifications); return modifications; } + async function startDemo(request: AppMessageRequest) { + if (!request.data.edits) { + console.debug("request did not contain edits"); + return; + } + for (const mod of request.data.edits) { + await reactor.pushModification(mod); + } + return await reactor.attach(document, highlighter); + } + React.useEffect(() => { chrome.runtime.onMessage.addListener( (request, _sender, sendResponse) => { @@ -155,56 +152,88 @@ chrome.runtime.onMessage.addListener((request) => { (async () => { let data = null; - // reactor - if (request.message === "EDITING" || request.message === "PLAY") { - for (const mod of request.data.edits) { - await reactor.pushModification(mod); + // check if app is asking to start or stop PLAY or EDIT + const startRequestRegExp = new RegExp(/_DEMO_START/); + const stopRequestRegExp = new RegExp(/_DEMO_STOP/); + + const requestingStopDemo = stopRequestRegExp.test( + request.message, + ); + const requestingStartDemo = startRequestRegExp.test( + request.message, + ); + + // if a demo is running already we want to avoid mounting the same + // modifications more than once, this is more performant, and edits + // persist in the dom if transitioning between EDIT and PLAY states + if (requestingStartDemo) { + prevAppEvent.current = request; + const prevDemoUUID = prevAppEvent.current?.data?.uuid || null; + + const demoRunning = + prevDemoUUID && + startRequestRegExp.test(prevAppEvent.current.message); + + if (!demoRunning) { + await startDemo(request); + } else { + const isDupeEvent = + prevAppEvent.current.message === request.message; + + const isNewDemo = prevDemoUUID !== request.data.uuid; + + const hasMods = request.data.edits.length > 0; + + if (!isDupeEvent && isNewDemo && hasMods) { + if (reactor.isAttached()) { + await reactor.detach(true); + } + await startDemo(request); + } } - reactor.attach(document, getHighlighter()); } - if (request.message === "NEW_EDIT") { + + if (requestingStopDemo) { + prevAppEvent.current = request; + await reactor.detach(true); + } + + if (request.message === DemoEditEvents.NEW_EDIT) { if (request.data) { - const { find, flags, highlightEdits, replace } = request.data; - await findReplaceAll(find, replace, flags, highlightEdits); + const { find, flags, replace } = request.data; + await findReplaceAll(find, replace, flags); data = Array.from(reactor.getAppliedModifications()).map( (mod) => mod.modificationRequest, ); } } - if ( - request.message === "STOP_EDITING" || - request.message === "STOP_PLAYING" || - request.message === "STOP_CHAT" - ) { - reactor.detach(); - } - // chat actions - if (request.message === "CHAT") { - reactor.attach(document, getHighlighter()); - } - if (request.message === "CHAT_MESSAGE") { + // chat events + if (request.message === DemoEditEvents.CHAT_MESSAGE) { data = reactor.exportDOM(); } - if (request.message === "CHAT_RESPONSE") { + if (request.message === DemoEditEvents.CHAT_RESPONSE) { await reactor.pushModification(request.data); data = Array.from(reactor.getAppliedModifications()).map( (mod) => mod.modificationRequest, ); } - // Resize iframe with the new styles + // Resize iframe, how or hide it if (iframeRef.current) { - if (request.data?.iframe) { - // v1 iframe size / position pattern - const styles = getIframeSizePosition(request.data.iframe); - Object.assign(iframeRef.current.style, styles); - } else { - // v0+ - const styles = getIframeStyles(request.message); - Object.assign(iframeRef.current.style, styles); + switch (request.message) { + case LayoutEvents.HIDE: + iframeRef.current.style.display = "none"; + break; + case LayoutEvents.SHOW: + iframeRef.current.style.display = "block"; + break; + case LayoutEvents.RESIZE: + const styles = getIframeStyles(request.data.iframe); + Object.assign(iframeRef.current.style, styles); } } + sendResponse({ data, message: request.message, @@ -248,7 +277,6 @@ chrome.runtime.onMessage.addListener((request) => { }; // avoid remounting react tree - try { if (!mounted) { root.render(