A canvas that several people open at once, as a shared design file is: each sees the others' changes as they happen, their cursors, the nodes they selected and those they drag, in their colours.

A shared canvas is a room of Issho, collaboration for Phoenix: Hakushi on its own is a canvas for one person, and Issho makes it a canvas for many, with the people's own permissions and no service besides your app.

<.canvas
  id="schema"
  graph={@schema.graph}
  on_change={&Schemas.change(&1, &2, @schema)}
  live={{MyApp.Issho, "schema:#{@schema.id}"}}
  user={%{id: @current_user.id, name: @current_user.name}}
>

Setting up

  1. Issho in your dependencies:

    {:issho, "~> 0.1"}
  2. An Issho instance in your supervision tree, after the PubSub, with your policy: who may open which canvas, and change it.

    {Issho,
     name: MyApp.Issho,
     pubsub: MyApp.PubSub,
     endpoint: MyAppWeb.Endpoint,
     policy: MyApp.Collaboration}
    defmodule MyApp.Collaboration do
      @behaviour Issho.Policy
    
      @impl true
      def authorize(%Issho.Identity{key: user_id}, "schema:" <> id, action) do
        if MyApp.Schemas.allowed?(user_id, id, action), do: :ok, else: {:error, :forbidden}
      end
    end

    action is :join (open the canvas, see who is here), :read (receive the changes), :write (change it) or :lock. The policy is asked as each person opens the canvas, and again for each change, in both directions: a person whose access you take away stops seeing the changes, and Issho.revoke(MyApp.Issho, topic, user_id) takes them out of the room at once.

  3. The hook, in the LiveView or its live_session:

    live_session :default, on_mount: [Hakushi.Live] do

    (Hakushi.Live is Issho's hook, Issho.Live: a LiveView with either needs no other.)

The room's document

The canvases of a topic share one document, the graph, with one process in your cluster that puts the changes of everyone in order. The first canvas to open it gives it its graph, rules and on_change; the others get the graph as it is then.

Each change of each person goes to that process, which checks the rules, applies it, asks on_change (once, whoever made it, with the graph before and after it), and only then sends it to every canvas of the topic. So on_change runs once for each change, and two people never save over each other: the second change applies to the graph the first one made. A change that no longer makes sense (an edge to a node someone deleted a moment before) is refused, with the reason.

The document lasts while a canvas has it open, and a few seconds more, so that a reload finds it. Your tables, which on_change writes, are the truth: the next canvas to open the topic starts from the graph you give it.

People

user is a map with an :id, and a :name, a :color (any CSS colour) and an :avatar (an image's URL) if you have them. A person with no colour gets one of eight, by their id, the same everywhere. A person with the canvas open in two tabs is one person, and does not see themselves. Without a user, a canvas joins as someone anonymous.

The avatars of the others show in the corner of the canvas. Draw them yourself with the :person slot, which gets a map with "id", "name", "color" and "avatar".

Cursors and drags

Each person's cursor, the nodes they selected and the nodes they drag go to the others as they move, in Issho's awareness frames: one frame of what changed, about twenty times a second, whatever the number of people. A node someone drags moves for everyone while they drag it, with their name; it lands where they drop it when the room has the move.

A cluster

The room's process is found from every node of a cluster (:global, unless your Issho instance names another registry), and the PubSub must reach every node, as Phoenix's does.

The app's own changes

In the LiveView of the canvas, Hakushi.Live.change/2. From anywhere (a job, another LiveView), Hakushi.Issho.change(MyApp.Issho, topic, change), while someone has the canvas open; it answers {:error, :not_running} when no one has, and then you change your own tables. Hakushi.Issho.graph/2 reads the graph a room has.