lotus(#2): stream per-participant call state over widget API
CI / Build embedded bundle (push) Successful in 1m45s
CI / Publish to Gitea npm registry (push) Has been skipped

Opt-in (lotusCallState=1) bridge that emits io.lotus.call_state with each
participant's speaking/audio/video state, so the Lotus host can drive
speaking rings / mute badges / PiP from real events instead of scraping
EC's rendered DOM. Exposes vm.userMedia$ on the public CallViewModel.
Additive: no-op without the flag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Lotus CI
2026-06-29 23:05:20 -04:00
co-authored by Claude Opus 4.8
parent 39377fb6e1
commit 444af5f7ac
4 changed files with 146 additions and 0 deletions
+68
View File
@@ -0,0 +1,68 @@
/*
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.
*/
/**
* Shared helpers for the Lotus fork's widget extensions.
*
* Everything here is **opt-in**: each feature is gated behind a `lotus*` URL
* param that the Lotus host (cinny) appends to the widget iframe URL. With no
* param present, none of this code changes Element Call's behaviour — which
* keeps the fork a minimal, additive, easy-to-rebase diff over upstream.
*/
import { logger } from "matrix-js-sdk/lib/logger";
import { widget } from "../widget";
/** Custom widget actions used between the Lotus host and this fork. */
export enum LotusWidgetActions {
/** fromWidget: in-call per-participant speaking / mute state. */
CallState = "io.lotus.call_state",
/** toWidget: pin/spotlight (or clear) a participant. */
FocusParticipant = "io.lotus.focus_participant",
/** toWidget: mix an audio clip into the local published mic track. */
InjectAudio = "io.lotus.inject_audio",
}
let cachedParams: URLSearchParams | undefined;
/**
* Read a URL param from either the query string or the hash fragment (Element
* Call passes widget params via both depending on host), without depending on
* EC's own `getUrlParams` parser (keeps the rebase surface small).
*/
export function lotusParam(name: string): string | null {
if (!cachedParams) {
cachedParams = new URLSearchParams(window.location.search);
const hash = window.location.hash.replace(/^#\/?/, "");
const hashQuery = hash.includes("?") ? hash.slice(hash.indexOf("?") + 1) : "";
for (const [k, v] of new URLSearchParams(hashQuery)) {
if (!cachedParams.has(k)) cachedParams.append(k, v);
}
}
return cachedParams.get(name);
}
/** Whether a boolean-ish Lotus feature flag is enabled. */
export function lotusFlag(name: string): boolean {
const v = lotusParam(name);
return v === "1" || v === "true";
}
/**
* Send a fromWidget message to the Lotus host, swallowing the inevitable
* rejection when the host hasn't (yet) registered a handler for it. Returns
* true if the widget transport was available to attempt the send.
*/
export function lotusSendToHost(action: LotusWidgetActions, data: unknown): boolean {
const api = widget?.api;
if (!api) return false;
void api.transport.send(action, data as Record<string, unknown>).catch((e) => {
logger.debug(`[lotus] host did not ack ${action}`, e);
});
return true;
}