Files
element-call/src/lotus/lotusControls.ts
T
Lotus CIandClaude Opus 5.5 6fa34911ef feat(lotus): io.lotus.set_screenshare for the host call bar (cinny #43)
toWidget `io.lotus.set_screenshare { on?: boolean }` (omit to toggle)
starts/stops sharing via the view model instead of the host clicking EC's
hidden button. The host sends it with Capability Delegation
(`delegate: "display-capture"`) where supported, so getDisplayMedia keeps
the user's click on engines that require it. `controls_state` now carries
`screenshareAction: true` so the host only uses it on a fork that has it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-09-26 12:49:58 -04:00

133 lines
4.9 KiB
TypeScript

/*
Copyright 2026 Lotus Guild
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { type IWidgetApiRequest } from "matrix-widget-api";
import { combineLatest, of, type Subscription } from "rxjs";
import { distinctUntilChanged, map, switchMap } from "rxjs/operators";
import { type CallViewModel } from "../state/CallViewModel/CallViewModel";
import { type LayoutMode } from "../state/LayoutSwitchViewModel";
import { widget } from "../widget";
import { LotusWidgetActions, lotusSendToHost } from "./lotusWidget";
/** Window event the reactions button listens for (see ReactionToggleButton). */
export const LOTUS_TOGGLE_REACTIONS_EVENT = "lotus:toggle-reactions";
export interface LotusControlsState {
screensharing: boolean;
/** Null while EC offers no layout switch (e.g. PiP or a 1:1 layout). */
layout: LayoutMode | null;
/** This fork handles `io.lotus.set_screenshare` (always true). */
screenshareAction: true;
}
/** `{ layout }` payload → a layout mode, or undefined if invalid. Exported for tests. */
export function parseLayoutPayload(data: unknown): LayoutMode | undefined {
if (typeof data !== "object" || data === null) return undefined;
const { layout } = data as { layout?: unknown };
return layout === "grid" || layout === "spotlight" ? layout : undefined;
}
/** `{ open? }` payload → the settings-open state to apply. Exported for tests. */
export function parseSettingsPayload(data: unknown, current: boolean): boolean {
if (typeof data === "object" && data !== null && "open" in data) {
const { open } = data as { open?: unknown };
if (typeof open === "boolean") return open;
}
return !current;
}
/**
* `{ on? }` payload + current sharing state → whether to call the toggle.
* Exported for tests.
*/
export function shouldToggleScreenshare(
data: unknown,
sharing: boolean,
): boolean {
if (typeof data === "object" && data !== null && "on" in data) {
const { on } = data as { on?: unknown };
if (typeof on === "boolean") return on !== sharing;
}
return true;
}
/**
* [cinny #43] Widget-API replacements for the host's DOM access to EC's
* controls: layout switch, settings modal and reactions menu, plus a
* `controls_state` report (screensharing + layout) so the host no longer reads
* EC's DOM for them. Screenshare start/stop too: `getDisplayMedia` needs the
* user's click in this frame, so the host sends `set_screenshare` with
* Capability Delegation (Chromium). Elsewhere it keeps clicking EC's button.
*
* No effect unless the host sends the actions; registering is safe whenever
* we're a widget. Returns a teardown function.
*/
export function startLotusControls(vm: CallViewModel): () => void {
const w = widget;
if (!w) return (): void => undefined;
const onSetLayout = (ev: CustomEvent<IWidgetApiRequest>): void => {
w.api.transport.reply(ev.detail, {});
const layout = parseLayoutPayload(ev.detail.data);
if (layout) vm.layoutSwitchVm$.value?.setLayout(layout);
};
const onOpenSettings = (ev: CustomEvent<IWidgetApiRequest>): void => {
w.api.transport.reply(ev.detail, {});
vm.setSettingsOpen$.value(
parseSettingsPayload(ev.detail.data, vm.settingsOpen$.value),
);
};
const onToggleReactions = (ev: CustomEvent<IWidgetApiRequest>): void => {
w.api.transport.reply(ev.detail, {});
window.dispatchEvent(new Event(LOTUS_TOGGLE_REACTIONS_EVENT));
};
const onSetScreenshare = (ev: CustomEvent<IWidgetApiRequest>): void => {
w.api.transport.reply(ev.detail, {});
// Call synchronously: the delegated activation is only good for a few
// seconds after the message arrived.
if (shouldToggleScreenshare(ev.detail.data, vm.sharingScreen$.value)) {
vm.toggleScreenSharing?.();
}
};
w.lazyActions.on(LotusWidgetActions.SetLayout, onSetLayout);
w.lazyActions.on(LotusWidgetActions.OpenSettings, onOpenSettings);
w.lazyActions.on(LotusWidgetActions.ToggleReactions, onToggleReactions);
w.lazyActions.on(LotusWidgetActions.SetScreenshare, onSetScreenshare);
const sub: Subscription = combineLatest([
vm.sharingScreen$,
vm.layoutSwitchVm$.pipe(
switchMap((l) => (l ? l.layout$ : of<LayoutMode | null>(null))),
),
])
.pipe(
map(
([screensharing, layout]): LotusControlsState => ({
screensharing,
layout,
screenshareAction: true,
}),
),
distinctUntilChanged(
(a, b) => a.screensharing === b.screensharing && a.layout === b.layout,
),
)
.subscribe((state) => {
lotusSendToHost(LotusWidgetActions.ControlsState, state);
});
return (): void => {
sub.unsubscribe();
w.lazyActions.off(LotusWidgetActions.SetLayout, onSetLayout);
w.lazyActions.off(LotusWidgetActions.OpenSettings, onOpenSettings);
w.lazyActions.off(LotusWidgetActions.ToggleReactions, onToggleReactions);
w.lazyActions.off(LotusWidgetActions.SetScreenshare, onSetScreenshare);
};
}