mirror of
https://github.com/element-hq/element-call.git
synced 2026-10-09 20:37:38 +00:00
Set up `sdk/` as a package shaped exactly like `component/`: its own package.json with peer dependencies, the pnpm workspace marker so a host can install it as a git dependency, a build tsconfig for type emission, and a vite lib config with an externals list that `pnpm lint:externals` now checks for both library builds. The package holds only the interface for now. `createRtcSession` returns an empty object; `RtcSession`, `RtcMember`, `MemberMedia` and `TransportMetadata` are the contract Element Call's `CallViewModel` will be rebuilt on (see sdk-plan.md). The shared primitives are re-exported from `src` until the first implementation slice moves them. `sdk/dev` is the smallest consumer of the SDK: a plain page that logs in, joins a room and renders one tile per member, served by `pnpm dev:sdk` on port 3002. Playwright starts it as a third web server, and `playwright/sdk/smoke.spec.ts` has two browsers join one session. It fails on the status line with the SDK's own error until there is an implementation behind the interface. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
5.2 KiB
5.2 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—@element-hq/matrixrtc-sdk, sources insdk/, entrysdk/index.ts. Packaged exactly like the component (own pnpm project, run pnpm from the repo root, types fromsdk/tsconfig.build.json, externals checked bypnpm lint:externals). Interface only so far; the design issdk-plan.md.build:sdk-target-based-on-call-view-model— the SDK demo that predates the package, one bundle of theCallViewModel; entrysdk-target-based-on-call-view-model/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-client/@vector-im/compound-web/@vector-im/compound-design-tokenssubpath the externals list omits. Compound's stylesheets are loaded bysrc/index.cssfor the page and by the host for the component, never bysrc/base.css. Two entry points:index.tsx(the component) andapi.ts(types, enums andconfigurationForIntent, for a host that must not load the component).api.tsmay only reach types and constants —src/UrlConfiguration.ts, notUrlParams.