- An add tile after the pictures opens the file picker, and the image
chosen joins the backgrounds on offer without being put on; it is in
force once chosen, like a shipped one. The tile is a command rather
than a choice, so a menu item beside the radios, and it isn't offered
where effects can't run.
- Kept in IndexedDB on this device and never sent anywhere, behind a
small interface, so the unit tests use an in-memory store; the
IndexedDB store itself is tested against fake-indexeddb, a new dev
dependency.
- The picker takes the focus, which the menu reads as a click elsewhere,
so the menu holds itself open until a file is chosen or the picker is
cancelled.
- A chosen image the device no longer keeps, as after its storage is
cleared, falls back to no effect once the device has said what it
keeps.
- Switching is checked between any two kinds of effect. End to end on
Chromium and Firefox: an added image shows in the preview once chosen;
once it is in force, no request carries the file or the copy kept,
which a deliberate upload fails; chosen, it is in force again after a
reload, gone once storage is cleared, and can be added again.
- Kept as given here; what can be added is checked in the commit after.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Brings design's blur icon, which the background effects grid uses.
- Of the icons the release redrew, guest and zoom-in, Element Call uses
neither, and none of the icons it removed are used here.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Compound 10.3.0 bounds a menu by the space it has and scrolls what
doesn't fit, within a boundary the app passes. The menus pass the call
area, 16px in from its sides, rather than measuring its width.
- The 296px width is the menu's own style. Compound gives that style only
to the dropdown, so menuIsDrawer, which copied its phone rule, goes.
- Compound draws the menu's frame over its content, so nothing that
scrolls can hide it in Safari: the list, headings and meter no longer
keep a border width clear of it, and the checks that they did go.
- The device list is Compound's scroll area, so it fades out at the
foot with Compound's chevron while there is more below.
- The microphone menu's meter sticks at that foot, with rows scrolling
under it as before, so the list has no fade above it: Compound's is a
mask fixed to the list's edge, while a bounce at the top carries the
meter with the rows. The band under the meter is filled, or rows would
show there. The edge fades from where the chevron starts, so a bounce
fades the meter out before it reaches the chevron. A row reached by
keyboard stays above the meter.
- The list's height is still its share of the call.
- The component harness leaves the host's stylesheets unscoped, Compound's
among them, as a host does: scoped, they never reached the menu, which is
portalled out of Element Call's root.
- Its specs check only Element Call's own document styles on the host
page, and find the device list within Compound's scroll area.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With this change, views now read every reactive value through `useBehavior`, as
`architecture.md` already prescribes. Where a view reads a plain stream, the view
model now exposes a `Behavior`.
`component/package.json` describes the built component as a package
(`@element-hq/element-call-component`: entry, stylesheet subpath, types,
peer dependencies) so that a host can depend on
`github:element-hq/element-call#<ref>&path:/component`. Its `prepare`
script builds on install, since nothing is published yet.
For that the component build now lands in `component/dist` instead of the
repository's `dist`, and `pnpm build:component` also emits the type
declarations (`component/tsconfig.build.json`, `build:component:types`),
which previously had to be produced by hand.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`pnpm dev:component` serves a page that stands in for a host application: it
signs in twice against the development backend and shows two calls side by
side, in resizable boxes, with furniture of its own around them. Two devices of
one account, so a real call happens between the two components and anything
Element Call keeps once per process rather than once per call shows itself.
The host bridge is driven by hand and reports both directions in a log along
the bottom, which is the first exercise the theme, hang-up and device-mute
requests have had outside widget mode. Each pane can be unmounted and remounted
to see what Element Call leaves behind, and there is a `position: fixed` dialog
belonging to the host to see whether it covers the calls. The page uses none of
Element Call's design tokens, so anything that looks styled outside a pane came
from Element Call reaching out of its container. It reaches Element Call only
through the component's public interface, which is how the exports missing from
that interface came to light.
Three things about the component build the harness turned up on the way, all
too small to be worth their own commits:
- It copied `public/` into `dist/`, including the developer's own gitignored
config.json, into output we would publish. `publicDir: false`, as the
embedded build already does. The sdk build has the same leak; untouched.
- `pnpm lint:externals` now exists, which the build config already claimed it
did. It reads the external list out of that config and fails if the source
imports React, the Matrix SDK or LiveKit by a path the list does not name.
Since the bundler silently ignores the pattern form of that option, an
unnamed subpath is bundled with no warning at all — which is how a host would
end up with a second React.
- `lint:oxlint` ran over `src playwright`, so nothing in `component/` had ever
been linted.
Serving a page also meant the shared plugin list could no longer inject the
app's HTML entry point unconditionally, so that is now optional — and off for
the library build too, which never had an HTML page to inject it into.
Adds component/index.tsx as a fourth build target: <ElementCall client
roomId /> and an initializeElementCall to await once beforehand. It gives
Element Call everything it would otherwise take from the page it is on —
the parameters, the host bridge, media devices, translations, a container
to confine itself to — and hands it the host's client rather than finding
one of its own.
React, the Matrix SDK and LiveKit stay external, since the host has them
and a second copy of any would not merely be wasteful: React would hold
two sets of hooks and the client would run two sync loops. Every subpath
has to be listed by name, because the pattern and callback forms of
rollupOptions.external are silently ignored here — a lesson worth the
comment that records it.
Element Call's own navigation runs in a MemoryRouter, so being embedded
cannot disturb the host's URL. ClientContext and GroupCallView both
navigate, so some router has to be present.
The bundle is not yet a reasonable size: library mode base64-inlines
assets referenced through import.meta.url, so MediaPipe's vision runtime
lands in it whole. Left for its own change, since the fix — loading the
background blur transformer lazily — is worth doing for the standalone app
too.
With the React Compiler, our component code is transformed at build time to automatically apply various forms of memoization. This changes the runtime semantics of our code a little bit and therefore could surface new bugs in the next release cycle in case any components fail to follow the rules of React. See https://react.dev/learn/react-compiler for more information.
This results in a small bundle size increase and modest performance gains, but I think it's worth it given the potential for larger performance gains once we tune the component structure a bit more.
The rule of thumb to avoid resource leaks is that you should never call ObservableScope methods in a callback unless the ObservableScope is directly passed to or created inside that callback. I had a go at codifying this as a lint rule.