Patchfox reborn as a desktop app

Posted  

As many people know Patchfox has been in a hard situation for the past several months. It is a combination of three challenges, three sphinxes looking funny at our fox crew.

Challenge 1: Bundle Splitting

I was never able to transpile the SSB NPM modules successfully with any tool except browserify. WebPack, Vite, Rollup, they all barfed and ended up with non-working JS. It all boils down to node built-in module polyfills and how that transformations are handled.

There is a ceiling to the size of a JS bundle when you’re trying to ship a WebExtension to Firefox Add-on Store, it is 5mb. If you try to ship a larger file, the portal will just reject it. In theory splitting bundles is easy, right?

Not if you’re using browserify. Anyway, Patchfox was divided into two 4.4mb bundles. One covered the SSB low-level and high-level libraries, the other was all the UI (aka the Patchfox Packages). All that decoupling was done manually. Both pats of the add-on were compiled separately and glue code connected them using the most awkward shortcuts. It served me well for many years, but now I can’t add more features to Patchfox without exploding the the limit. Patchfox is stuck.

Challenge 2: No control over the server

Patchfox has always been built as a companion, a lightweight client that piggybacks onto whatever server you’re running. This worked well for many years but the signs of trouble have been knocking on my door for some months already.

The new metafeeds and per-application feed are a game changer for SSB and Patchfox can’t participate on that without control over a server.

When Manyverse shipped without a WebSocket server endpoint, all Patchfox users who migrated to Manyverse lost the ability to use Patchfox. As the userbase of Manyverse grows, the userbase of classic Patchfox becomes more limited.

Challenge 3: WebExtensions exist in a hostile environment

Lots of engineers inside the companies responsible for building browsers love WebExtensions, but the truth is that Web companies and Browser vendors as entities treat them as hostile. They don’t really want to make WebExtensions a prime citizen of the Web. They want just enough features.

The changes to manifest v3 proved to me that Google has complete control over the WebExtension ecosystem and will force their hand whenever they feel they should. Mozilla will do a mea culpa and follow suite because they are not strong enough to resist that pressure at the moment. Mozilla can’t steer the WebExtension ecosystem in any direction that Google doesn’t want it to go. It might be able to steer it in a direction that Google feels it is OK to go even if they don’t really care, but try to ship a feature that makes a dent on Google business and you’ll see how quickly this ecosystem fragments.


All that is mentioned above shows the lack of agency Patchfox WebExtension has in its own future: it can’t get more features without being rejected, it can’t adopt cool new SSB stuff because it has no server, it can’t trust it’s own ecosystem because it is a tug-of-war between browser vendors.

SOMETHING HAD TO CHANGE!

I struggled with a long time with this decision, but the only way forward for Patchfox is to escape the browser. Patchfox needs to become a desktop application with a server. That will solve all the challenges mentioned in the previous message.

What you’re seeing that video is a work-in-progress sneak peek into the new Patchfox. It is an Electron-based application because trying to use anything else would just lead to a lot of extra friction which I can’t handle right now.

It is early days, I’ve been working on it for three or four days, but it is shaping up really nicely.

The objective is to have all Patchfox features available. This is the future of Patchfox.


Oh, you are still reading? Ok, let me tell you some of the development details.

I’m reusing as many code from the WebExtension as I can, but I hit a wall getting Svelte to play nicely with true nodejs built-in modules. Actually from the three or four days I’ve been working on it, I lost half of them actually trying to get the build system working.

See Svelte is a compiler, not a library, and it really wants to be on the web. The combination of WebPack/Rollup/Vite and Svelte was not working. Svelte wanted new import calls and would convert them to the wrong require calls. If I passed requires on my own, it would complain even if the compiler was set to accept them.

The answers I found online for those challenges has been: “Svelte exposes its own compiler as svelte/compiler, you can fiddle with it at runtime to guarantee the transpilation works.” to which my answer was “fuck you.”.

I’ve always hated fantasy js and build systems anyway, so I’m converting everything to Mithril. Fuck fantasy js, Patchfox is coded in real JS, the bugs I write are the ones that end up in the webview.

Patchfox is now developed without a build system. Mithril is used to power all that Svelte was doing before.

I’m slowly converting all the packages from Svelte to Mithril. It is a tedious and error-prone process.

I was going to develop all this in secret and surprise everyone, but that is not really how I roll, so you’re all getting a sneak peak. The code is being worked in a branch called escape-the-browser which I’ll push later tonight.

If you want to see more work happening on Patchfox, want it to be a kickass desktop client, and would like to support my work, you can do a one-time or recurring donation at my ko-fi.

Let a thousand clients bloom.

As can be seen in the video, it is quite early. I’ve implemented just some of the views. It is very fast, way faster than the WebExtension version of Patchfox.

The current code is being worked on as a direct port of the Svelte codebase. I’m just reworking the Svelte templates into Mithril components. It is being done as idiomatic Mithril, at the moment the main objective is getting it all to work, then I’ll polish.

In the video, you’ve seen some single-line messages of type vote (aka likes) flying by, let me quote the full source-code responsible for rendering them so that you can get a feel for the new codebase:

const m = require("mithril")
const AvatarChip = require("../../core/components/AvatarChip.js")
const VoteView = {
  oninit: (vnode) => {
    vnode.state.loadingBlurb = true
    vnode.state.loadingAvatar = true
    vnode.state.label = vnode.attrs.msg.value.content.vote.link
    vnode.state.person = vnode.attrs.msg.value.author
  },
  view: (vnode) => {
    let msg = vnode.attrs.msg

    let expression =
      msg.value.content.vote.expression === "Like"
        ? ":heart:"
        : msg.value.content.vote.expression
    let msgid = msg.value.content.vote.link
    let encodedid = encodeURIComponent(msgid)

    if (vnode.state.loadingBlurb) {
      ssb
        .blurbFromMsg(msgid, 50)
        .then((blurb) => {
          vnode.state.label = blurb
          vnode.state.loadingBlurb = false
          m.redraw()
        })
        .catch((n) => {
          console.log("error retrieving blurb for", msgid)
          console.error(n)
        })
    }

    if (vnode.state.loadingAvatar) {
      ssb.avatar(msg.value.author).then((data) => {
        if (data?.name) {
          vnode.state.person = data.name
          vnode.state.loadingAvatar = false
          m.redraw()
        } else {
          console.log("odd", data)
        }
      })
    }

    const goThread = (ev) => {
      ev.stopPropagation()
      ev.preventDefault()
      if (typeof msgid === "undefined") {
        throw "Can't go to undefined message id"
      }
      if (ev.ctrlKey) {
        window.open(`?pkg=hub&view=thread&thread=${encodeURIComponent(msgid)}`)
      } else {
        patchfox.go("hub", "thread", { thread: msgid })
      }
    }

    const avatarClick = (ev) => {
      let feed = ev.detail.feed
      patchfox.go("contacts", "profile", { feed })
    }

    return m("p.m-2", [
      m(AvatarChip, {
        inline: true,
        glyph: expression,
        feed: msg.value.author,
        onclick: avatarClick,
      }),
      m(
        "a",
        {
          href: `?pkg=hub&view=thread&thread=${encodedid}`,
          onclick: goThread,
        },
        vnode.state.label
      ),
    ])
  },
}

module.exports = VoteView

It is a very old-school way of doing JS. Just plain-old objects keeping their own state. There is no TEA, no reducers, no actions. It is boring old technology. I like it that way.

Also, don’t you love having real menus? Like a proper desktop app? :D :D :D :D

When you have real menus on macOS, you get integrated help:

I hope you folks enjoy this path moving forward. I’m really excited to ship the new Patchfox as soon as possible.

Did you enjoyed reading this content? Want to support me?

You can buy me a coffee at ko-fi.

Comments? Questions? Feedback?

You can reach out to me on Twitter, or Mastodon, Secure Scuttlebutt, or through WebMentions.

Mentions