Skip to content

Commit a4b9ad0

Browse files
committed
[frontend] add page ipc and handle sending title and favicon
1 parent dc007be commit a4b9ad0

File tree

8 files changed

+252
-6
lines changed

8 files changed

+252
-6
lines changed

packages/chrome/src/IsolatedFrame.tsx

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ import { ElementType, type Handler, Parser } from "htmlparser2";
1515
import { type ChildNode, DomHandler, Element, Comment, Node } from "domhandler";
1616
import * as tldts from "tldts";
1717

18+
import type {
19+
Chromebound,
20+
ChromeboundMethods,
21+
Framebound,
22+
FrameboundMethods,
23+
} from "../../inject/src/types";
24+
import type { Tab } from "./Tab";
25+
import { browser } from "./Browser";
26+
1827
const ISOLATION_ORIGIN = import.meta.env.VITE_ISOLATION_ORIGIN;
1928

2029
const cfg = {
@@ -161,7 +170,7 @@ export class IsolatedFrame {
161170
}
162171
}
163172

164-
const inject_script = "/page_inject.js";
173+
const inject_script = "/inject.js";
165174

166175
const methods = {
167176
async fetch(
@@ -254,7 +263,6 @@ const methods = {
254263
}
255264

256265
const head = findhead(handler.root as Node as Element)!;
257-
console.log(head);
258266
head.children.unshift(new Element("script", { src: inject_script }));
259267
});
260268

@@ -359,3 +367,88 @@ async function makeAllResponse(): Promise<ScramjetFetchResponse> {
359367
statusText: "OK",
360368
};
361369
}
370+
371+
let synctoken = 0;
372+
let syncPool: { [token: number]: (val: any) => void } = {};
373+
export function sendFrame<T extends keyof Framebound>(
374+
controller: Controller,
375+
type: T,
376+
message: Framebound[T][0]
377+
): Promise<Framebound[T][1]> {
378+
let token = synctoken++;
379+
380+
controller.window.postMessage(
381+
{
382+
$ipc$type: "request",
383+
$ipc$token: token,
384+
$ipc$message: {
385+
type,
386+
message,
387+
},
388+
},
389+
"*"
390+
);
391+
392+
return new Promise((res) => {
393+
syncPool[token] = res;
394+
});
395+
}
396+
397+
window.addEventListener("message", (event) => {
398+
let data = event.data;
399+
if (!(data && data.$ipc$type)) return;
400+
401+
if (data.$ipc$type === "response") {
402+
let token = data.$ipc$token;
403+
if (typeof token !== "number") return;
404+
let cb = syncPool[token];
405+
if (cb) {
406+
cb(data.$ipc$message);
407+
delete syncPool[token];
408+
}
409+
} else if (data.$ipc$type === "request") {
410+
const { type, message } = data.$ipc$message;
411+
const token = data.$ipc$token;
412+
413+
const tab =
414+
browser.tabs.find((t) => t.frame.frame.contentWindow === event.source) ||
415+
null;
416+
417+
chromemethods[type as keyof ChromeboundMethods](tab, message).then(
418+
(response: any) => {
419+
(event.source as Window).postMessage(
420+
{
421+
$ipc$type: "response",
422+
$ipc$token: token,
423+
$ipc$message: response,
424+
},
425+
"*"
426+
);
427+
}
428+
);
429+
}
430+
});
431+
432+
type ChromeboundMethods = {
433+
[K in keyof Chromebound]: (
434+
tab: Tab | null,
435+
arg: Chromebound[K][0]
436+
) => Promise<Chromebound[K][1]>;
437+
};
438+
439+
const chromemethods: ChromeboundMethods = {
440+
titlechange: async (tab, { title, icon }) => {
441+
console.log("title changed...", tab, title, icon);
442+
if (tab) {
443+
if (title) {
444+
tab.title = title;
445+
tab.history.current().title = title;
446+
}
447+
if (icon) {
448+
tab.icon = icon;
449+
tab.history.current().favicon = icon;
450+
}
451+
}
452+
},
453+
contextmenu: async (controller, { x, y }) => {},
454+
};

packages/inject/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"license": "ISC",
1313
"packageManager": "[email protected]",
1414
"dependencies": {
15+
"@mercuryworkshop/scramjet": "link:../scramjet",
1516
"chobitsu": "workspace:*",
1617
"html-to-image": "^1.11.13"
1718
}

packages/inject/src/index.ts

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,73 @@
11
import chobitsu from "chobitsu";
22
import * as h2 from "html-to-image";
3-
window.h2 = h2;
4-
window.$sendToChobitsu = (message) => chobitsu.sendRawMessage(message);
5-
chobitsu.setOnMessage(window.$onChobitsuMessage);
6-
window.$onChobitsuInit();
3+
import { SCRAMJETCLIENT } from "@mercuryworkshop/scramjet/bundled";
4+
import { FrameboundMethods } from "./types";
5+
import { sendChrome } from "./ipc";
6+
7+
export const client = self[SCRAMJETCLIENT];
8+
export const chromeframe = top!;
9+
10+
export const methods: FrameboundMethods = {
11+
async navigate({ url }) {
12+
return "a";
13+
},
14+
};
15+
16+
let cachedfaviconurl: string | null = null;
17+
function setupTitleWatcher() {
18+
const observer = new MutationObserver(() => {
19+
const title = document.querySelector("title");
20+
if (title) {
21+
sendChrome("titlechange", { title: title.textContent });
22+
}
23+
const favicon = document.querySelector(
24+
"link[rel='icon'], link[rel='shortcut icon']"
25+
);
26+
27+
const loadAndSendData = async (href: string) => {
28+
let res = await fetch(href);
29+
let blob = await res.blob();
30+
const reader = new FileReader();
31+
reader.onload = () => {
32+
sendChrome("titlechange", { icon: reader.result as string });
33+
};
34+
reader.onabort = () => {
35+
console.warn("Failed to read favicon");
36+
cachedfaviconurl = null;
37+
};
38+
reader.readAsDataURL(blob);
39+
};
40+
41+
if (favicon) {
42+
const iconhref = favicon.getAttribute("href");
43+
if (iconhref) {
44+
if (iconhref !== cachedfaviconurl) {
45+
cachedfaviconurl = iconhref;
46+
loadAndSendData(iconhref);
47+
}
48+
}
49+
} else {
50+
if (cachedfaviconurl !== "/favicon.ico") {
51+
// check if there's a favicon.ico
52+
let img = new Image();
53+
img.src = "/favicon.ico";
54+
img.onload = () => {
55+
if (img.width > 0 && img.height > 0) {
56+
// it loads, send it
57+
cachedfaviconurl = img.src;
58+
loadAndSendData(img.src);
59+
}
60+
};
61+
img.onerror = () => {
62+
// nope...
63+
};
64+
}
65+
}
66+
});
67+
observer.observe(document, {
68+
childList: true,
69+
subtree: true,
70+
});
71+
}
72+
73+
setupTitleWatcher();

packages/inject/src/ipc.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { chromeframe, methods } from ".";
2+
import { Chromebound } from "./types";
3+
4+
let synctoken = 0;
5+
let syncPool: { [token: number]: (val: any) => void } = {};
6+
export function sendChrome<T extends keyof Chromebound>(
7+
type: T,
8+
message: Chromebound[T][0]
9+
): Promise<Chromebound[T][1]> {
10+
let token = synctoken++;
11+
12+
chromeframe.postMessage(
13+
{
14+
$ipc$type: "request",
15+
$ipc$token: token,
16+
$ipc$message: {
17+
type,
18+
message,
19+
},
20+
},
21+
"*"
22+
);
23+
24+
return new Promise((res) => {
25+
syncPool[token] = res;
26+
});
27+
}
28+
29+
window.addEventListener("message", (event) => {
30+
// TODO: this won't work in puter
31+
if (event.source !== chromeframe) return;
32+
let data = event.data;
33+
if (!(data && data.$ipc$type)) return;
34+
35+
if (data.$ipc$type === "response") {
36+
let token = data.$ipc$token;
37+
if (typeof token !== "number") return;
38+
let cb = syncPool[token];
39+
if (cb) {
40+
cb(data.$ipc$message);
41+
delete syncPool[token];
42+
}
43+
} else if (data.$ipc$type === "request") {
44+
const { type, message } = data.$ipc$message;
45+
const token = data.$ipc$token;
46+
47+
methods[type](message).then((response: any) => {
48+
chromeframe.postMessage(
49+
{
50+
$ipc$type: "response",
51+
$ipc$token: token,
52+
$ipc$message: response,
53+
},
54+
"*"
55+
);
56+
});
57+
}
58+
});

packages/inject/src/types.d.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export type FrameboundMethods = {
2+
[K in keyof Framebound]: (arg: Framebound[K][0]) => Promise<Framebound[K][1]>;
3+
};
4+
5+
export type Chromebound = {
6+
contextmenu: [
7+
{
8+
x: number;
9+
y: number;
10+
},
11+
];
12+
titlechange: [
13+
{
14+
title?: string;
15+
icon?: string;
16+
},
17+
];
18+
};
19+
20+
export type Framebound = {
21+
navigate: [
22+
{
23+
url: string;
24+
},
25+
string,
26+
];
27+
};
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)