Skip to content

Commit 8732539

Browse files
committed
Listen to page changes and re-initialize button
1 parent aba4d6c commit 8732539

7 files changed

Lines changed: 74 additions & 69 deletions

File tree

package-lock.json

Lines changed: 10 additions & 55 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@
2626
"react-error-boundary": "^4.0.13",
2727
"swr": "^2.2.5",
2828
"tailwindcss": "^3.4.3",
29-
"ulidx": "^2.3.0",
30-
"yup": "^1.4.0"
29+
"zod": "^3.23.8"
3130
},
3231
"devDependencies": {
3332
"@types/react": "^18.3.3",

src/background.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
1+
import { isSupportedSite } from "@lib/utils/isSupportedSite";
2+
import { UpdateMessage } from "@lib/utils/messages";
13
import browser from "webextension-polyfill";
24

3-
console.log("Hello from the background!");
4-
55
browser.runtime.onInstalled.addListener((details) => {
66
console.log("Extension installed:", details);
77
});
8+
9+
browser.webNavigation.onHistoryStateUpdated.addListener((e) => {
10+
if (isSupportedSite(e.url)) {
11+
console.debug("Supported site:", e.url);
12+
try {
13+
browser.tabs.sendMessage(e.tabId, {
14+
type: "devpod-update",
15+
} satisfies UpdateMessage);
16+
} catch (error) {
17+
// This is expected if the tab is not ready to receive messages. It's not
18+
// a problem if that's the case, because it means the content script has
19+
// not initialized yet, and thus doesn't need to be updated.
20+
}
21+
}
22+
});

src/lib/utils/isSupportedSite.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function isSupportedSite(url: string) {
2+
return /^https?:[/][/]github.com[/][^/]+[/][^/]+/.test(url);
3+
}

src/lib/utils/messages.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { z } from "zod";
2+
3+
export const UpdateMessage = z.object({
4+
type: z.literal("devpod-update"),
5+
});
6+
export type UpdateMessage = z.infer<typeof UpdateMessage>;

src/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"{{chrome}}.service_worker": "src/background.ts",
1919
"{{firefox}}.scripts": ["src/background.ts"]
2020
},
21-
"permissions": ["activeTab", "storage"],
21+
"permissions": ["activeTab", "webNavigation"],
2222
"content_scripts": [
2323
{
2424
"matches": ["http://*/*", "https://*/*", "<all_urls>"],

src/pages/Content.tsx

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { EError, EShadowError } from "@lib/utils/error";
88
import { DevPodLogoIcon } from "@src/icons/devpod";
99
import { StrictMode } from "react";
1010
import { ButtonLink } from "@lib/components/Button";
11+
import { runtime } from "webextension-polyfill";
12+
import { UpdateMessage } from "@lib/utils/messages";
1113

1214
type PortalProps = { portal: HTMLElement | DocumentFragment };
1315

@@ -46,7 +48,13 @@ export function Control({ portal }: PortalProps) {
4648
<StrictMode>
4749
<div className="flex justify-center items-center">
4850
<ErrorBoundaryProvider>
49-
<ButtonLink href={getDevPodUrl()} color="primary" {...bindTarget}>
51+
<ButtonLink
52+
rel="noreferrer"
53+
target="_blank"
54+
href={getDevPodUrl()}
55+
color="primary"
56+
{...bindTarget}
57+
>
5058
<DevPodLogoIcon
5159
aria-label=""
5260
className="w-6 h-6 text-primary-contrast"
@@ -84,14 +92,11 @@ function attachShadow<E extends Element = Element>(target: E | null) {
8492
const shadowTarget = document.createElement("div");
8593
target.appendChild(shadowTarget);
8694
const shadow = shadowTarget.attachShadow({ mode: "closed" });
87-
const rootContainer = document.createElement("div");
88-
shadow.appendChild(rootContainer);
8995
attachStyles(shadow);
90-
return shadow;
96+
return { shadow, target: shadowTarget };
9197
}
9298

9399
const MAX_INIT_ATTEMPTS = 12;
94-
let isInitialized = false;
95100

96101
function findDOMNodeByContent(content: string) {
97102
for (const node of document.querySelectorAll("button")) {
@@ -107,19 +112,41 @@ function findDOMNodeByContent(content: string) {
107112
});
108113
}
109114

115+
let buttonContainer: HTMLDivElement | null = null;
116+
110117
function init(attempts: number = 0) {
111-
if (isInitialized || attempts > MAX_INIT_ATTEMPTS) return;
118+
if (attempts > MAX_INIT_ATTEMPTS) {
119+
// Too many attempts, aborting
120+
return;
121+
}
122+
if (document.contains(buttonContainer)) {
123+
// ALready initialized
124+
return;
125+
}
112126
try {
113127
const buttonTarget = findDOMNodeByContent("Code")?.parentElement;
114-
const rootContainer = attachShadow(buttonTarget);
128+
const { shadow: rootContainer, target: rootContainerTarget } =
129+
attachShadow(buttonTarget);
130+
buttonContainer = rootContainerTarget;
115131
const root = createRoot(rootContainer);
116-
const portalContainer = attachShadow(document.body);
132+
const { shadow: portalContainer } = attachShadow(document.body);
117133
root.render(<Control portal={portalContainer} />);
118-
isInitialized = true;
119134
} catch (error) {
135+
console.debug("Initialization failed", error);
120136
// 10ms, 20ms, 40ms, 80ms, 160ms, 320ms, 640ms, 1280ms, 2560ms, 5120ms, 10240ms, 20480ms
121137
setTimeout(() => init(attempts + 1), Math.pow(2, attempts) * 10);
122138
}
123139
}
124140

141+
console.debug("Initializing DevPod button");
125142
init();
143+
runtime.onMessage.addListener((message) => {
144+
UpdateMessage.safeParseAsync(message).then(async (result) => {
145+
if (!result.success) return;
146+
const message = result.data;
147+
console.debug(`Received message ${message.type}`);
148+
if (message.type === "devpod-update") {
149+
init();
150+
}
151+
});
152+
});

0 commit comments

Comments
 (0)