Webnative team hackathon projects (Early 2023 edition)

The Webnative team recently completed a hackathon where we experimented with some app ideas. In this post, we share our projects with the hopes that they provide inspiration for your apps.

The projects included:

  • @icidasset added public playlists to Diffuse, a web-based music player, and implemented a code snippet that loads public playlists and populates them with YouTube videos
  • @avivash and @bgins implemented a VST delay plugin and companion app that demonstrate progressive storage enhancement and lightweight public sharing of presets.

Diffuse playlists

Diffuse is a music player that stores its user data using Webnative among others (and hopefully music soon too). It has playlists which are normally private, but Webnative allows us to make these public.

We can make a playlist public by first syncing with Webnative and then switching the toggle on the playlists screen:

Switching the playlist to public stores a playlists.json file in public/Apps/icidasset/Diffuse/ on our file system which contains the playlists that we made public.

Steven (@icidasset) made a code sandbox that retrieves this file, allows you to choose a playlist and renders Youtube videos for each track in the playlist. It’s something we can refer to if people need a demo of viewing public data of another user. People can fork it and try it out immediately.

Example of a rendered playlist:

Ditto

Ditto is a VST delay plugin written using Elementary Audio. Ditto Companion is a web app where users can share and collect Ditto presets.

Elementary Audio

We are huge fans of the Elementary Audio project, which makes it easy to develop audio plugins using web technologies. Our project uses the @elemaudio/plugin-renderer, which runs a web app in WebView inside of a VST binary. The WebView gives us a browser environment where we can run Webnative.

Here is the Ditto plugin loaded into Bitwig Studio:

Preset manager

Audio plugins typically have a system for saving presets, which captures plugin state and metadata about the preset. We implemented presets in Ditto that store plugin state, notes, tags, and favorite status:

The presets interface is organized by categories, including All, Favorites, and categories automatically generated from preset tags.

Progressive filesystem enhancement

Plugin users expect plugins to store presets without signing into an account. Our first experiment with Ditto was to implement a local-only filesystem that does not require a sign-in, and an optional, synced filesystem that requires an account. When a user decides to create an account, their presets are copied from the local-only filesystem to a filesystem associated with a username and persisted to Fission IPFS nodes.

Each filesystem is associated with an app namespace to keep them separate in browser storage. We determine which filesystem to use by checking whether the user has created an account or not:

	  // Program associated with a user account
	  const program = await webnative.program({
	    ...configuration,
	    auth: await CustomAuth.implementation(configuration)
	  })
	  programStore.set(program)
	  
	  const { session } = program
	  
	  // Session exists when a user has an account
	  if (session && session.fs) {
	    // Filesystem is local and persisted to Fission IPFS nodes
	    const fs = session.fs
	    fileSystemStore.set(fs)
		  
	    const connectedStatus = await checkConnectedStatus(fs)
	    sessionStore.set({
	      connectedStatus,
	      session
	    })
	  
	  } else {
	    // Local-only filesystem
	    const localOnlyFs = await getLocalOnlyFs()
	    fileSystemStore.set(localOnlyFs)
	  
	  }

See the Local Filesystem implementation to examine how getLocalOnlyFs works. We also have a Webnative Temp Fs glitch that demonstrates this technique.

Connect to Ditto Companion

Account creation is cast in the context of connecting to the Ditto Companion app.

The user is given a QRCode and connection link on entering a username and selecting Connect:

The QRCode and connection link open Ditto Companion and start the device linking using AWAKE:

The user enters the code shown in Ditto Companion into Ditto:

After entering the code, Ditto shows the user a message confirming they have connected with Ditto Companion:

The user can connect again later if they would like to connect to another browser or device.

The Ditto plugin and Ditto Companion use distinct app namespaces:

// Ditto plugin
const configuration = {
  namespace: { creator: 'fission', name: 'ditto' },
  debug: true,
}
// Ditto Companion app
export const webnativeNamespace = { creator: 'fission', name: 'ditto-companion' }
	  
const program: webnative.Program = await webnative.program({
  namespace: webnativeNamespace,
  debug: dev
})

Device linking works across app namespaces because it only concerns UCAN authorization and transmitting a user’s WNFS encryption key. Namespaces exist to isolate apps in browser storage. As we saw above, this can be useful when using multiple filesystems.

Sharing and collecting presets

Ditto Companion is a web app for sharing and collecting presets.

We started Ditto Companion from the Webnative App Template. Our connect flow is simplified when compared to the complete feature set provided in the template. We only link from the plugin to the companion app. This simple model meant we could remove quite a bit from the template for our use case.

Our second experiment was to implement a lightweight share and collect system in the companion app. Shared presets are stored in the public WNFS tree. Presets that are not shared are in the private tree. Users share presets in the companion app:

Users collect shared presets from other users. After selecting the plus button, they enter the username they want to collect from:

We check whether a filesystem exists for the user:

  /**
   * Check filesystem exists for a username
   *
   * @param username Display name
   * @param reference Reference.Implementation
   * @returns CID or null if not found
   */
  export async function lookupFileSystem(username: string, reference: Reference.Implementation): Promise<CID | null> {
    return await reference.dataRoot.lookup(`ditto-${username}`)
  }

If a filesystem exists, we get a link for the public tree and check for a presets directory:

  /**
   * Get the presets directory from a filesystem
   *
   * @param cid Data root CID for the filesystem
   * @param depot Depot.Implementation
   * @param reference Reference.Implementation
   * @returns Public tree for presets directory or null if not found
   */
  export async function getPresetsDirectory(
    cid: CID,
    depot: Depot.Implementation,
    reference: Reference.Implementation
  ): Promise<PublicTree | null> {
    const links = await getSimpleLinks(depot, cid)
		  
    if (links.public) {
      const publicCid = links.public.cid as CID
      const publicTree = await PublicTree.fromCID(depot, reference, publicCid)
  
      const presetsDirectory = (await publicTree.get(
        path.unwrap(path.directory('presets'))
      )) as PublicTree
  
      return presetsDirectory
    }
  
    return null
  }

Lastly, we load presets from the presets directory if one exists:

  /**
   * Gets presets froma preset directory
   *
   * @param presetsDirectory Public tree for presets directory
   * @returns Patch[]
   */
  export async function getPresets(presetsDirectory: PublicTree): Promise<Patch[]> {
    const presetsLinks = Object.values(await presetsDirectory.ls([]))
  
    return await Promise.all(
      presetsLinks.map(async presetLink => {
        const file = (await presetsDirectory.get([
          presetLink.name
        ])) as PublicFile
  
        const preset = JSON.parse(new TextDecoder().decode(file.content))
  
        return preset
      })
    )
  }

See the Collect implementation for more context on how this works in the app. We also have a Webnative public data viewer glitch that demonstrates this technique.

While we are searching for presets, the user sees a spinner:

If presets were found, we add them to the Collect tab:

Users collect presets by checking the Collect checkbox for the preset. Collected presets are copied into the user’s filesystem, and the user can load them in the plugin after a refresh.

User namespacing

Astute readers will have noticed that we are namspacing usernames by prepending a ditto- string. Prefixing creates a tacit namespace. It’s a bit imperfect because another app could have also used the ditto- prefix, but it’s unlikely, so this is good enough for hackathon work. :smile:

Custom auth component

The Ditto plugin uses a custom auth component to change the behavior of the isUsernameAvailable function. We wanted to debounce the function so we only check the username after the user has stopped typing.

The custom auth implementation looks like this:

  import * as webnative from 'webnative'
  import type { Auth, Components } from 'webnative/components'
  import type { Configuration } from 'webnative'
  
  import { asyncDebounce } from '$lib/utils'
  
  // Custom Implementation
  
  export async function implementation(configuration: Configuration): Promise<Auth.Implementation<Components>> {
    const base = await webnative.auth.fissionWebCrypto(configuration)
  
    return {
      ...base,
      isUsernameAvailable: asyncDebounce(base.isUsernameAvailable, 300)
    }
  }

The asyncDebounce function delays checking the username until the user has stopped typing for 300 milliseconds.

We use the custom implementation when instantiating the program:

  import * as webnative from 'webnative'
  
  import * as CustomAuth from '$lib/auth'
  
  const configuration = {
    namespace: { creator: 'fission', name: 'ditto' },
    debug: true,
  }
      
  const program = await webnative.program({
    ...configuration,
    auth: await CustomAuth.implementation(configuration)
  })

The custom implementation rewires the behavior of isUsernameAvailable so that it debounced when we call it later.

3 Likes

:star_struck:

These UIs look so clean. Love the design.

1 Like

@bgins That “find presets” functionality is great :ok_hand: I think I might add something like that to Diffuse in the future, where you could import a playlist from another Fission user.

1 Like

Maybe minimally expand what Diffuse is here? “to Diffuse, a web-based music player”

It’s confusing who “I” is, maybe:

“Steven ( @icidasset ) made a …”

I think this section might be better placed after the following section, right before “Connect to Ditto Companion” - the idea being that we explain the technical approach to “Progressive filesystem enhancement” first, then talk about the UI aspects of the project, starting with the plugin and then moving to the companion app. Does that make sense?