fix(lotus): focus_participant works in grid/1:1, clears on leave, keeps PiP, pins by device
- A non-null pin forces layout "spotlight" and remembers the displaced mode; clearing restores it only if the user hasn't switched since; gridLayoutMedia$ surfaces the pinned item for narrow mode (#3). - Pin clears when the user is gone for 5 s or on leave$ (#16). - Screenshare branch keeps pip$ = auto speaker unless it IS the pinned user (#29). - Payload accepts an optional media id (userId:deviceId) and prefers it; userId-only picks the speaking device (#30). 18 unit tests. Fixes #3 Fixes #16 Fixes #29 Fixes #30 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
co-authored by
Claude Opus 5
parent
e504a31efd
commit
e5d5f13923
+26
-11
@@ -10,12 +10,33 @@ import { type IWidgetApiRequest } from "matrix-widget-api";
|
||||
import { type CallViewModel } from "../state/CallViewModel/CallViewModel";
|
||||
import { widget } from "../widget";
|
||||
import { LotusWidgetActions } from "./lotusActions";
|
||||
import { type ManualSpotlight } from "./lotusSpotlight";
|
||||
|
||||
/**
|
||||
* Parse a `focus_participant` payload into a pin, `null` to clear, or
|
||||
* `undefined` to leave the current pin alone (#30). Exported for tests.
|
||||
*
|
||||
* Mirror deafen's partial-payload semantics: a payload that OMITS `userId`
|
||||
* must keep the current spotlight, not clear it. Only act when the key is
|
||||
* actually present — an explicit `null` clears, a string pins that user. An
|
||||
* optional `id` (EC media id `${userId}:${deviceId}`, as sent to the host in
|
||||
* `io.lotus.call_state`) selects a specific device of that user.
|
||||
*/
|
||||
export function parseFocusPayload(
|
||||
data: unknown,
|
||||
): ManualSpotlight | null | undefined {
|
||||
if (typeof data !== "object" || data === null || !("userId" in data))
|
||||
return undefined;
|
||||
const { userId, id } = data as { userId?: unknown; id?: unknown };
|
||||
if (typeof userId !== "string") return null;
|
||||
return { userId, id: typeof id === "string" ? id : null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the host's `io.lotus.focus_participant` toWidget action (#4): pin a
|
||||
* participant to the spotlight by Matrix user id, or clear it with
|
||||
* `{ userId: null }`. This replaces cinny's old DOM `.click()` tile-selector
|
||||
* hack with a real, layout-aware spotlight override.
|
||||
* participant to the spotlight by Matrix user id (and optionally media id), or
|
||||
* clear it with `{ userId: null }`. This replaces cinny's old DOM `.click()`
|
||||
* tile-selector hack with a real, layout-aware spotlight override.
|
||||
*
|
||||
* No effect unless the host actually sends the action, so registering the
|
||||
* handler whenever we're a widget is safe. Returns a teardown function.
|
||||
@@ -27,14 +48,8 @@ export function startLotusFocus(vm: CallViewModel): () => void {
|
||||
const handler = (ev: CustomEvent<IWidgetApiRequest>): void => {
|
||||
// Always reply so the host transport doesn't time out.
|
||||
w.api.transport.reply(ev.detail, {});
|
||||
const data = ev.detail.data as { userId?: unknown } | undefined;
|
||||
// Mirror deafen's partial-payload semantics: a payload that OMITS `userId`
|
||||
// must keep the current spotlight, not clear it. Only act when the key is
|
||||
// actually present — an explicit `null` clears, a string pins that user.
|
||||
if (data && "userId" in data) {
|
||||
const userId = typeof data.userId === "string" ? data.userId : null;
|
||||
vm.setManualSpotlight(userId);
|
||||
}
|
||||
const target = parseFocusPayload(ev.detail.data);
|
||||
if (target !== undefined) vm.setManualSpotlight(target);
|
||||
};
|
||||
|
||||
w.lazyActions.on(LotusWidgetActions.FocusParticipant, handler);
|
||||
|
||||
Reference in New Issue
Block a user