Skip to content

feature request: in-app web browser #35

@matildepark

Description

@matildepark

Current external link functionality

Right now we relay external links (links inside our windows that have target="_blank") to the host operating system's default browser:

scene/public/electron.js

Lines 46 to 57 in b02a623

mainWindow.webContents.setWindowOpenHandler(({ url }) => {
if (url.includes(("/apps/talk")) || url.includes(("/apps/groups"))) {
mainWindow.webContents.executeJavaScript(`window.scene.linkToWindow("${url}")`);
return {
action: "deny"
}
}
shell.openExternal(url)
return {
action: "deny"
}
})

But not always! You can see that in this code snippet that we already check for specific apps first so that we can relay them into the app itself. If it exists, we throw it into a function that sets our React state appropriately.[1]

scene/src/App.js

Lines 89 to 104 in cfe0636

useEffect(() => {
window.scene.linkToWindow = (link) => incomingLinkToWindow(link, {
apps: {
value: apps
},
windows: {
set: setWindows,
value: windows
},
selectedWindow: {
set: setSelectedWindow,
value: selectedWindow
}
})
}, [apps, windows, setWindows, selectedWindow, setSelectedWindow])

scene/src/lib/window.js

Lines 1 to 16 in cfe0636

export function incomingLinkToWindow(link, { apps, windows, selectedWindow }) {
const splitUrl = link.split("/");
const desk = splitUrl[splitUrl.indexOf("apps") + 1];
const remainderUrl = splitUrl.slice(splitUrl.indexOf(desk)).join("/");
if (desk in apps?.value?.charges) {
const charge = { ...apps.value.charges[desk], ...{ href: { glob: { base: remainderUrl } } } };
if (windows.value.some((e) => e.desk === desk)) {
windows.set([charge, ...windows.value.filter((e) => e.desk !== desk)]);
selectedWindow.set([charge, ...selectedWindow.value.filter((e) => e.desk !== desk)])
} else {
windows.set([...windows.value, charge])
selectedWindow.set([charge, ...selectedWindow.value])
}
}
return false
}

These functions iterate through our windows, selected windows and hidden windows and manipulate charges (which are metadata objects that correspond to registered desks and their front-end endpoints on an Urbit ship) to then set a window's origin to a direct link specified in another app. Say that you see a permalink to a blog inside a Talk session. This would open a Groups window and redirect. Vice versa, hitting 'message' in Groups should change Talk's contents. Since React keys off the desk name, windows stay stable as long as we don't reshuffle the array order.

In-app web browser

What an in-app web browser would do is handle every other link. If it isn't associated with a desk (eg. it isn't relative, doesn't start with /apps/, etc.), we expect it to open in the same context as Urbit apps — within Scene.

There's a few ways of doing a webview inside a webview in Electron. They generally recommend BrowserViews, but those are essentially overlaid on top of the window, and have zero relation to the DOM. As we are working on a window manager here, the amount of overhead — the work required between the host process and our main window process to pretend it's a part of the DOM layout the other windows and elements are — is high enough that I'd generally instead gesture at its non-recommended solution, <webview/>.

Webview tag

Webviews are essentially sandboxed by default, out-of-process iFrames. They don't add memory and CPU load on the rest of the app's renderer thread, and actions in them won't crash the app generally if, say, the page that we load in is extremely heavy.

What I would suggest is to set the webviewTag boolean to true here and inject a pseudo-charge called 'Browser' when we load our list of apps. When we open a link, we take that charge, copy and place another one in our list, incrementing its desk name, and set its href to the href we are opening.

In the Window component, if the charge is Browser (or, say, 'browser-[x]' if we want to allow multiple windows for the browser) then don't use iframe, we use webview here:

return (
<iframe
className={cn("w-full h-full bg-white min-h-0 select-none", {
"pointer-events-none":
selectedWindow.value?.[0]?.title !== win.title ||
launchOpen.value ||
dragged,
})}
src={href}
title={title}
key={keyName}
/>

Then we start experimenting with it, basically.

Bookmarks

One baseline feature is going to be bookmarking links. This is doable as just a JSON file — "name" and "url" objects in an array of objects. But we probably want to store it portably — settings-store? Our own back-end? — instead of relying on localstorage. You would expect something like this to carry across sessions if you have two computers.

If settings-store, you can use the Urbit API to .putEntry() into settings-store. However, it isn't clear what the lifecycle of settings-store is, as it is a Tlon userspace API.

Browsing

You should be able to just change the charge's glob.base and have it propagate to the webview's src, refreshing the page when necessary. However, when hitting Back or Forward, you may want to hook into the webview so that injecting the cookies (see below 'current questions' section) doesn't cause strange behaviour.

Thus, BrowserFrame as a separate component may be necessary, splitting off from Frame to add these methods and render a specialised browser UI (address bar, etc.)

Current questions

  • Obviously, do we store cookies?

If you sign into facebook.com from the browser for whatever reason, you expect to stay signed in. If our webviews are new, out-of-process browsers inside our app, I'd expect they would have no access to host cookies. Appropriately sandboxing our child webviews while having the parent store all cookies seems like a problem we're going to have to solve. If it doesn't "just work", we may have to write a catch to take all cookies and store them in mainWindow, passing them to child windows when opened. Chromium should sandbox cookie access for us.

  • Ensuring nothing feels janky in practice.

Probably we can't do tabs that easily. But we don't want infinite app icons to show up in the Launchpad, and we want browser desks to clean themselves up when done.

Likewise, we don't want weird cookie injection behaviour causing web browsing to feel poor if refreshing a page, going back or forward, or opening a new tab.

[1]: You may note that we constantly redefine this whenever apps change. There is likely a performance optimisation here by being smarter with React.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions