lotus(#4): focus-participant spotlight via widget action
Adds io.lotus.focus_participant (toWidget): the host can pin a participant to the spotlight by Matrix user id (or clear with userId:null), via a manual override injected into CallViewModel.spotlightSpeaker$. Replaces cinny's fragile DOM .click() tile-selector focus hack. Extracts the action enum into lotusActions.ts (no circular import) and allow-lists Lotus toWidget actions in initializeWidget. Additive: no-op unless the host sends the action. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
444af5f7ac
commit
9d7784b9bc
@@ -255,6 +255,11 @@ export interface CallViewModel {
|
||||
* Callback to toggle screen sharing. If null, screen sharing is not possible.
|
||||
*/
|
||||
toggleScreenSharing: (() => void) | null;
|
||||
/**
|
||||
* [lotus] Pin a participant to the spotlight by Matrix user id (#4
|
||||
* focus-participant). Pass null to clear and restore speaker-follows.
|
||||
*/
|
||||
setManualSpotlight: (userId: string | null) => void;
|
||||
/**
|
||||
* Whether we are sharing our screen.
|
||||
*/
|
||||
@@ -897,7 +902,7 @@ export function createCallViewModel$(
|
||||
merge(userHangup$, widgetHangup$).pipe(map(() => "user" as const)),
|
||||
).pipe(scope.share);
|
||||
|
||||
const spotlightSpeaker$ = scope.behavior<UserMediaViewModel | undefined>(
|
||||
const autoSpotlightSpeaker$ = scope.behavior<UserMediaViewModel | undefined>(
|
||||
userMedia$.pipe(
|
||||
switchMap((mediaItems) =>
|
||||
mediaItems.length === 0
|
||||
@@ -933,6 +938,29 @@ export function createCallViewModel$(
|
||||
),
|
||||
);
|
||||
|
||||
// [lotus] Manual spotlight override (#4 focus-participant widget action).
|
||||
// When set to a userId still present in the call, that participant is
|
||||
// spotlighted instead of the auto-selected active speaker; null restores the
|
||||
// default speaker-follows behaviour. Defaults to null, so without the host
|
||||
// using io.lotus.focus_participant this is a no-op.
|
||||
const manualSpotlightUserId$ = new BehaviorSubject<string | null>(null);
|
||||
const spotlightSpeaker$ = scope.behavior<UserMediaViewModel | undefined>(
|
||||
combineLatest([
|
||||
autoSpotlightSpeaker$,
|
||||
manualSpotlightUserId$,
|
||||
userMedia$,
|
||||
]).pipe(
|
||||
map(([auto, manualUserId, mediaItems]) => {
|
||||
if (manualUserId !== null) {
|
||||
const pinned = mediaItems.find((m) => m.userId === manualUserId);
|
||||
if (pinned) return pinned;
|
||||
}
|
||||
return auto;
|
||||
}),
|
||||
),
|
||||
undefined,
|
||||
);
|
||||
|
||||
const grid$ = scope.behavior<UserMediaViewModel[]>(
|
||||
userMedia$.pipe(
|
||||
switchMap((mediaItems) => {
|
||||
@@ -1699,6 +1727,8 @@ export function createCallViewModel$(
|
||||
join: localMembership.requestJoinAndPublish,
|
||||
leave: localMembership.requestDisconnect,
|
||||
toggleScreenSharing: toggleScreenSharing,
|
||||
setManualSpotlight: (userId: string | null): void =>
|
||||
manualSpotlightUserId$.next(userId),
|
||||
sharingScreen$: sharingScreen$,
|
||||
|
||||
tapScreen: (): void => screenTap$.next(),
|
||||
|
||||
Reference in New Issue
Block a user