/* 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 { BehaviorSubject, 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, lotusFlag, 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; /** This fork handles `io.lotus.set_hotkeys` (always true). */ hotkeys: true; /** * EC's own screenshare button is shown inside the frame, so the host should * hide its own. Always false since the prompt replaced that button; kept so * a host that still reads it doesn't hide its button. */ frameScreenshare: false; /** * The host should start a share with `io.lotus.prompt_screenshare` (the * in-frame prompt) instead of `set_screenshare`: see * `lotusFrameScreenshareEnabled`. */ screensharePrompt: boolean; /** * The prompt is showing. The host must let clicks through to this frame * meanwhile (e.g. its picture-in-picture overlay). */ screensharePromptOpen: boolean; } /** * [cinny #43] `lotusFrameScreenshare=1` (with `lotusHostControls`): the host * can't hand the user's click into this frame (no Capability Delegation: * Firefox, Safari, WebKitGTK), and `getDisplayMedia` needs that click here. * The host's call-bar button then asks for `io.lotus.prompt_screenshare`, and * the "Share your screen?" prompt is drawn inside this frame, where the Share * click is native. */ export function lotusFrameScreenshareEnabled(): boolean { return lotusFlag("lotusHostControls") && lotusFlag("lotusFrameScreenshare"); } /** Whether the in-frame "Share your screen?" prompt is showing. */ export const lotusScreensharePrompt$ = new BehaviorSubject(false); /** * Host-controlled visibility of the in-frame screenshare button * (`io.lotus.set_frame_screenshare`): the host hides it while the room's * call policy forbids screensharing, as it does its own call-bar button. */ export const lotusFrameScreenshareVisible$ = new BehaviorSubject(true); /** * Whether `io.lotus.prompt_screenshare` should show the prompt: only in * prompt mode, where the room's policy allows sharing, EC can share, and no * share is running (stopping goes through `set_screenshare`, which needs no * click). Exported for tests. */ export function shouldPromptScreenshare(s: { enabled: boolean; allowed: boolean; canShare: boolean; sharing: boolean; }): boolean { return s.enabled && s.allowed && s.canShare && !s.sharing; } /** `{ visible }` payload → the visibility to apply, or undefined. Exported for tests. */ export function parseFrameScreensharePayload( data: unknown, ): boolean | undefined { if (typeof data !== "object" || data === null) return undefined; const { visible } = data as { visible?: unknown }; return typeof visible === "boolean" ? visible : undefined; } /** `{ 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); const onSetFrameScreenshare = (ev: CustomEvent): void => { w.api.transport.reply(ev.detail, {}); const visible = parseFrameScreensharePayload(ev.detail.data); if (visible !== undefined) lotusFrameScreenshareVisible$.next(visible); }; const onPromptScreenshare = (ev: CustomEvent): void => { w.api.transport.reply(ev.detail, {}); if ( shouldPromptScreenshare({ enabled: lotusFrameScreenshareEnabled(), allowed: lotusFrameScreenshareVisible$.value, canShare: !!vm.toggleScreenSharing, sharing: vm.sharingScreen$.value, }) ) { lotusScreensharePrompt$.next(true); } }; w.lazyActions.on(LotusWidgetActions.SetScreenshare, onSetScreenshare); w.lazyActions.on( LotusWidgetActions.SetFrameScreenshare, onSetFrameScreenshare, ); w.lazyActions.on(LotusWidgetActions.PromptScreenshare, onPromptScreenshare); const sub: Subscription = combineLatest([ vm.sharingScreen$, vm.layoutSwitchVm$.pipe( switchMap((l) => (l ? l.layout$ : of(null))), ), lotusScreensharePrompt$, ]) .pipe( map( ([screensharing, layout, promptOpen]): LotusControlsState => ({ screensharing, layout, screenshareAction: true, hotkeys: true, frameScreenshare: false, screensharePrompt: lotusFrameScreenshareEnabled(), screensharePromptOpen: promptOpen, }), ), distinctUntilChanged( (a, b) => a.screensharing === b.screensharing && a.layout === b.layout && a.screensharePromptOpen === b.screensharePromptOpen, ), ) .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); w.lazyActions.off( LotusWidgetActions.SetFrameScreenshare, onSetFrameScreenshare, ); w.lazyActions.off( LotusWidgetActions.PromptScreenshare, onPromptScreenshare, ); lotusScreensharePrompt$.next(false); }; }