Activities ​
An activity is a web page that people in a call open together: a game, a whiteboard, a watch-together player, a quiz. Refract runs it in a sandboxed frame in the call and passes messages between everyone's copies, so an activity needs no server of its own to stay in step.
Installing activities ​
Instance admins manage them under Manage instance → Activities:
- Packaged with Refract: activities that ship with the server. Install adds one in a click. Two are included: Basic, a shared button with a debug sidebar that shows every message (the place to start building), and Watch together, a video played in step for everyone.
- Add an activity: one hosted anywhere else, by its name, address and a short description. The address must:
- use
https://. Pages onlocalhostare allowed on development instances, or with developer mode on, for building one; - not be on the instance's own site;
- be the page's final address, after any redirects, since only messages from that origin are accepted.
- use
Refract adds each activity's origin to its Content-Security-Policy, so people see a new one the next time they load the app. If you set CSP_DIRECTIVES yourself, add the origins to its frame-src.
After an activity's page changes, Refresh reloads it in every call that has it open.
Using one ​
Anyone in a call opens an activity from the Activities button in the call panel. The others in the call are invited to join. One activity runs per call at a time, as a tile on the call's stage, like a screen share. It closes for each person when they leave it, and for everyone when the last person leaves or the call ends.
Building one ​
An activity is a static page that loads refract-activity.js, a small module every instance serves at /activities/refract-activity.js. Copy it (and refract-activity.css, for Refract's look) into your page's folder:
js
import { connectToRefract } from "./refract-activity.js";
const refract = connectToRefract();
const { self, participants, theme } = await refract.ready;
refract.onMessage((from, data) => {
if (data.type === "move") applyMove(from, data.square);
});
refract.onParticipants((participants, { joined, left }) => showPeople(participants));
await refract.broadcast({ type: "move", square: "e4" });| Member | What it is |
|---|---|
ready | Resolves when Refract answers, with self, participants, theme ("dark" or "light") and IDs |
self | The person using this copy: { id, name, avatarUrl } |
participants | Everyone with it open, in the order they joined |
leader | Whoever has had it open longest. Every copy agrees, so it's the one to do work only one should do |
broadcast(data, to?) | Sends JSON to everyone else, or only the user IDs in to |
onMessage((from, data) => …) | Messages from the other copies |
onParticipants(…) | When someone opens or closes it |
setFullscreen(on) | Asks Refract to show the page full screen. Call it from a click or key press |
close() | Leaves the activity |
- Messages are JSON of up to 16 KiB, about 20 a second per person (bursts of up to 40). They aren't stored, and your own aren't echoed back.
- Late joiners have missed what came before. Have each new page send a
helloonce it's ready, and have theleaderanswer it with the current state. - What others send is untrusted. Check its shape, and show names and messages as text, never HTML.
- The page holds no credentials. Everything goes through the person's own signed-in connection, so a page can only ever act as the person using it.
Hosting it ​
Any static host over HTTPS works, such as GitHub Pages, GitLab Pages or Netlify. To start, copy the Basic activity's folder with refract-activity.js and refract-activity.css beside it, and change its imports from ../ to ./.
To host one beside your instance, docker-compose.override.example.yaml has an activity-chess service: a plain nginx container on a host name of its own, serving the activity's folder.
To build one locally, serve the page on http://localhost, add it on a development instance, and open it in a call from two browsers signed in as different people.