mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-13 21:59:30 +00:00
* Add AGENTS.md documenting the repo's conventions Codifies what is currently tribal knowledge or only discoverable by reading CI config: the view model / view contract, the rule that nothing reads the page, Clean Code ordering, the three test layers, and reuse-before-you-build. The root AGENTS.md is the always-loaded minimum and routes to docs/agents/, so an agent loads only the detail its task needs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Allow either hand at the commit, and measure continuous redraws Committing from the agent rules out running it where there is no git identity, a sandbox included. Either is now fine: commit on the user's word where the setup allows it, or write the message and hand it over. Redrawing continuously is the other addition. A level meter reported a fresh value every animation frame, so its whole menu reconciled sixty times a second, silence included, and nobody noticed until review. A PR that adds anything of the kind now says what it costs, or better, counts the redraws — commits rather than render calls, since React runs a component it then discards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
4.3 KiB
4.3 KiB
Architecture
Logic lives in view models; components render
- Call logic lives in
src/state/. A component that derives call state is misplaced logic. - Composing behaviors: a factory taking
(scope, ...deps$), returning named behaviors plus callbacks. Copysrc/state/LayoutSwitchViewModel.ts. Prefer this for anything new. - Owning a resource: a class taking the scope in its constructor —
MediaDevices,MuteStates,TileStore,Publisher,Connection. Both styles are current; don't convert one to the other in passing. foo$is an observable.Behavior<T>, an observable with a current value, is the default for anything a view reads.
The view model / view contract
- Components take
vm: ViewModel<Snapshot>and read state throughuseBehavior. SeeCallFooter,InCallView,LobbyView,SettingsModal. - A snapshot is
Actions & State; every field becomes afoo$behavior, and none is optional. - An unavailable action is
undefined, not a separatecanDoThingflag — the presence of the callback drives the rendering. - Subscribe in an effect only to drive a side effect off an event stream, as
ReactionAudioRendererdoes; never to read state a behavior already holds.
Scopes own lifetimes
ObservableScopebounds every subscription a view model creates.- Reference only the scope defined in the same function; one captured from an
enclosing scope outlives its owner, and
no-observablescope-leakrejects it.
Nothing reads the page
Element Call can be mounted several times inside a host's React tree, so it owns neither window, URL, document nor router. Each seam defaults to the old standalone and widget behaviour.
| Never | Use |
|---|---|
the widget global |
useHostBridge() — src/HostBridge.ts |
getUrlParams(), window.location |
useUrlParams() |
useNavigate("/"), <Link to="/"> |
useLeaveToHome(), LeaveToHomeLink |
document.body |
useRootElement() |
window.innerWidth/Height, useMediaQuery |
useRootSizeMatches() in views, windowSize$ in view models |
global i18next |
the instance in src/utils/i18n.ts |
| config or analytics reading their environment | Config.initWith(), PosthogAnalytics.configure() |
- View models take values as options, never
getUrlParams()(callViewModelOptionsFromParams,CallViewModelOptions.hostBridge). - The host bridge is the only channel to the host:
createWidgetHostBridge(widget),nullHostBridgestandalone,useComponentHostBridge. State a capability (supportsReactions,supportsProfileChanges); never infer it from being a widget. - Shortcuts and portals attach to the root element, so two instances don't fight.
- Known debt, not precedent:
Gridmeasureswindow.innerHeight;ErrorView's reload andgetAbsoluteRoomUrlusewindow.location; recaptcha appends todocument.bodyon the standalone login path.
Context differences are options, not checks
- Named options with per-intent defaults (
configurationForIntent), never a runtime check for who is hosting. controlledAudioDevices, not the platform, is what makesMediaDevicespickAndroidControlledAudioOutput/IOSControlledAudioOutputover the webAudioOutput. Intent presets set it on non-desktop; standalone leaves it off.- URL params are a published contract: change one, update
docs/url_params.md.
Build targets
Code that builds in only one is a bug.
build:full— standalone app, also widget mode.build:embedded—@element-hq/element-call-embedded.build:sdk— SDK library, entrysdk/main.ts.build:component—@element-hq/element-call-component, sources incomponent/(its own pnpm project; run pnpm from the repo root). Host API is in the README;pnpm lint:externalsrejects an import of areact/react-dom/matrix-js-sdk/livekit-clientsubpath the externals list omits.