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
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
35bc7a1e98
commit
6fa34911ef
@@ -30,6 +30,7 @@ describe("LotusWidgetActions", () => {
|
|||||||
LotusWidgetActions.SetLayout,
|
LotusWidgetActions.SetLayout,
|
||||||
LotusWidgetActions.OpenSettings,
|
LotusWidgetActions.OpenSettings,
|
||||||
LotusWidgetActions.ToggleReactions,
|
LotusWidgetActions.ToggleReactions,
|
||||||
|
LotusWidgetActions.SetScreenshare,
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(new Set(LOTUS_TO_WIDGET_ACTIONS)).toEqual(new Set(expectedToWidget));
|
expect(new Set(LOTUS_TO_WIDGET_ACTIONS)).toEqual(new Set(expectedToWidget));
|
||||||
|
|||||||
@@ -75,7 +75,15 @@ export enum LotusWidgetActions {
|
|||||||
/** toWidget: toggle the reactions / raise-hand menu (cinny #43). */
|
/** toWidget: toggle the reactions / raise-hand menu (cinny #43). */
|
||||||
ToggleReactions = "io.lotus.toggle_reactions",
|
ToggleReactions = "io.lotus.toggle_reactions",
|
||||||
/**
|
/**
|
||||||
* fromWidget: `{ screensharing: boolean, layout: "grid" | "spotlight" | null }`
|
* toWidget: start/stop sharing `{ on?: boolean }` (omit to toggle; cinny #43).
|
||||||
|
* Starting calls getDisplayMedia, which needs the user's click: the host
|
||||||
|
* sends this with Capability Delegation (`delegate: "display-capture"`), and
|
||||||
|
* without it the browser rejects the share.
|
||||||
|
*/
|
||||||
|
SetScreenshare = "io.lotus.set_screenshare",
|
||||||
|
/**
|
||||||
|
* fromWidget: `{ screensharing: boolean, layout: "grid" | "spotlight" | null,
|
||||||
|
* screenshareAction: true }`
|
||||||
* whenever either changes (cinny #43), so the host stops reading EC's DOM for
|
* whenever either changes (cinny #43), so the host stops reading EC's DOM for
|
||||||
* them. Its arrival also tells the host this fork supports the actions above.
|
* them. Its arrival also tells the host this fork supports the actions above.
|
||||||
*/
|
*/
|
||||||
@@ -98,4 +106,5 @@ export const LOTUS_TO_WIDGET_ACTIONS: LotusWidgetActions[] = [
|
|||||||
LotusWidgetActions.SetLayout,
|
LotusWidgetActions.SetLayout,
|
||||||
LotusWidgetActions.OpenSettings,
|
LotusWidgetActions.OpenSettings,
|
||||||
LotusWidgetActions.ToggleReactions,
|
LotusWidgetActions.ToggleReactions,
|
||||||
|
LotusWidgetActions.SetScreenshare,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { parseLayoutPayload, parseSettingsPayload } from "./lotusControls";
|
import {
|
||||||
|
parseLayoutPayload,
|
||||||
|
parseSettingsPayload,
|
||||||
|
shouldToggleScreenshare,
|
||||||
|
} from "./lotusControls";
|
||||||
|
|
||||||
describe("parseLayoutPayload", () => {
|
describe("parseLayoutPayload", () => {
|
||||||
it("accepts grid and spotlight", () => {
|
it("accepts grid and spotlight", () => {
|
||||||
@@ -33,3 +37,17 @@ describe("parseSettingsPayload", () => {
|
|||||||
expect(parseSettingsPayload({ open: "yes" }, true)).toBe(false);
|
expect(parseSettingsPayload({ open: "yes" }, true)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("shouldToggleScreenshare", () => {
|
||||||
|
it("toggles only when an explicit on differs from the current state", () => {
|
||||||
|
expect(shouldToggleScreenshare({ on: true }, false)).toBe(true);
|
||||||
|
expect(shouldToggleScreenshare({ on: true }, true)).toBe(false);
|
||||||
|
expect(shouldToggleScreenshare({ on: false }, true)).toBe(true);
|
||||||
|
expect(shouldToggleScreenshare({ on: false }, false)).toBe(false);
|
||||||
|
});
|
||||||
|
it("toggles when on is missing or not a boolean", () => {
|
||||||
|
expect(shouldToggleScreenshare({}, false)).toBe(true);
|
||||||
|
expect(shouldToggleScreenshare({ on: "yes" }, true)).toBe(true);
|
||||||
|
expect(shouldToggleScreenshare(null, true)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export interface LotusControlsState {
|
|||||||
screensharing: boolean;
|
screensharing: boolean;
|
||||||
/** Null while EC offers no layout switch (e.g. PiP or a 1:1 layout). */
|
/** Null while EC offers no layout switch (e.g. PiP or a 1:1 layout). */
|
||||||
layout: LayoutMode | null;
|
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. */
|
/** `{ layout }` payload → a layout mode, or undefined if invalid. Exported for tests. */
|
||||||
@@ -39,13 +41,28 @@ export function parseSettingsPayload(data: unknown, current: boolean): boolean {
|
|||||||
return !current;
|
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
|
* [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: layout switch, settings modal and reactions menu, plus a
|
||||||
* `controls_state` report (screensharing + layout) so the host no longer reads
|
* `controls_state` report (screensharing + layout) so the host no longer reads
|
||||||
* EC's DOM for them. Screensharing itself stays host-DOM driven for now:
|
* EC's DOM for them. Screenshare start/stop too: `getDisplayMedia` needs the
|
||||||
* `getDisplayMedia` needs the user's click to reach this frame (Capability
|
* user's click in this frame, so the host sends `set_screenshare` with
|
||||||
* Delegation), which a plain widget message doesn't carry.
|
* Capability Delegation (Chromium). Elsewhere it keeps clicking EC's button.
|
||||||
*
|
*
|
||||||
* No effect unless the host sends the actions; registering is safe whenever
|
* No effect unless the host sends the actions; registering is safe whenever
|
||||||
* we're a widget. Returns a teardown function.
|
* we're a widget. Returns a teardown function.
|
||||||
@@ -69,10 +86,19 @@ export function startLotusControls(vm: CallViewModel): () => void {
|
|||||||
w.api.transport.reply(ev.detail, {});
|
w.api.transport.reply(ev.detail, {});
|
||||||
window.dispatchEvent(new Event(LOTUS_TOGGLE_REACTIONS_EVENT));
|
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.SetLayout, onSetLayout);
|
||||||
w.lazyActions.on(LotusWidgetActions.OpenSettings, onOpenSettings);
|
w.lazyActions.on(LotusWidgetActions.OpenSettings, onOpenSettings);
|
||||||
w.lazyActions.on(LotusWidgetActions.ToggleReactions, onToggleReactions);
|
w.lazyActions.on(LotusWidgetActions.ToggleReactions, onToggleReactions);
|
||||||
|
w.lazyActions.on(LotusWidgetActions.SetScreenshare, onSetScreenshare);
|
||||||
|
|
||||||
const sub: Subscription = combineLatest([
|
const sub: Subscription = combineLatest([
|
||||||
vm.sharingScreen$,
|
vm.sharingScreen$,
|
||||||
@@ -85,6 +111,7 @@ export function startLotusControls(vm: CallViewModel): () => void {
|
|||||||
([screensharing, layout]): LotusControlsState => ({
|
([screensharing, layout]): LotusControlsState => ({
|
||||||
screensharing,
|
screensharing,
|
||||||
layout,
|
layout,
|
||||||
|
screenshareAction: true,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
distinctUntilChanged(
|
distinctUntilChanged(
|
||||||
@@ -100,5 +127,6 @@ export function startLotusControls(vm: CallViewModel): () => void {
|
|||||||
w.lazyActions.off(LotusWidgetActions.SetLayout, onSetLayout);
|
w.lazyActions.off(LotusWidgetActions.SetLayout, onSetLayout);
|
||||||
w.lazyActions.off(LotusWidgetActions.OpenSettings, onOpenSettings);
|
w.lazyActions.off(LotusWidgetActions.OpenSettings, onOpenSettings);
|
||||||
w.lazyActions.off(LotusWidgetActions.ToggleReactions, onToggleReactions);
|
w.lazyActions.off(LotusWidgetActions.ToggleReactions, onToggleReactions);
|
||||||
|
w.lazyActions.off(LotusWidgetActions.SetScreenshare, onSetScreenshare);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user