/* 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): void => { w.api.transport.reply(ev.detail, {}); const layout = parseLayoutPayload(ev.detail.data); if (layout) vm.layoutSwitchVm$.value?.setLayout(layout); }; const onOpenSettings = (ev: CustomEvent): void => { w.api.transport.reply(ev.detail, {}); vm.setSettingsOpen$.value( parseSettingsPayload(ev.detail.data, vm.settingsOpen$.value), ); }; const onToggleReactions = (ev: CustomEvent): void => { w.api.transport.reply(ev.detail, {}); window.dispatchEvent(new Event(LOTUS_TOGGLE_REACTIONS_EVENT)); }; const onSetScreenshare = (ev: CustomEvent): 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(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); }; }