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
@@ -158,7 +158,11 @@ import { type LocalUserMediaViewModel } from "../media/LocalUserMediaViewModel.t
|
||||
import { type RemoteUserMediaViewModel } from "../media/RemoteUserMediaViewModel.ts";
|
||||
// [lotus #4] Manual spotlight override, extracted so this file stays byte-close
|
||||
// to upstream (see the file for the behaviour contract).
|
||||
import { overrideSpotlight$ } from "../../lotus/lotusSpotlight";
|
||||
import {
|
||||
createManualSpotlight,
|
||||
type ManualSpotlight,
|
||||
overrideSpotlight$,
|
||||
} from "../../lotus/lotusSpotlight";
|
||||
import {
|
||||
createRingingMedia,
|
||||
type RingingMediaViewModel,
|
||||
@@ -267,10 +271,11 @@ export interface CallViewModel {
|
||||
*/
|
||||
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.
|
||||
* [lotus] Pin a participant to the spotlight (#4 focus-participant) by
|
||||
* Matrix user id and, optionally, EC media id (#30). Pass null to clear and
|
||||
* restore speaker-follows.
|
||||
*/
|
||||
setManualSpotlight: (userId: string | null) => void;
|
||||
setManualSpotlight: (target: ManualSpotlight | null) => void;
|
||||
/**
|
||||
* Whether we are sharing our screen.
|
||||
*/
|
||||
@@ -955,11 +960,6 @@ export function createCallViewModel$(
|
||||
),
|
||||
);
|
||||
|
||||
// [lotus #4] Host-pinned spotlight target (io.lotus.focus_participant). null =
|
||||
// follow the active speaker (upstream default), so this is inert unless the
|
||||
// host pins someone. Consumed by overrideSpotlight$ in spotlightAndPip$.
|
||||
const manualSpotlightUserId$ = new BehaviorSubject<string | null>(null);
|
||||
|
||||
const grid$ = scope.behavior<UserMediaViewModel[]>(
|
||||
userMedia$.pipe(
|
||||
switchMap((mediaItems) => {
|
||||
@@ -1000,6 +1000,12 @@ export function createCallViewModel$(
|
||||
),
|
||||
);
|
||||
|
||||
// [lotus #4] Host-pinned spotlight target (io.lotus.focus_participant). null =
|
||||
// follow the active speaker (upstream default), so this is inert unless the
|
||||
// host pins someone. Consumed by overrideSpotlight$ in spotlightAndPip$;
|
||||
// driven by createManualSpotlight below (after the layout switch exists).
|
||||
const manualSpotlight$ = new BehaviorSubject<ManualSpotlight | null>(null);
|
||||
|
||||
const spotlightAndPip$ = scope.behavior<{
|
||||
spotlight: MediaViewModel[];
|
||||
pip$: Observable<UserMediaViewModel | undefined>;
|
||||
@@ -1016,7 +1022,7 @@ export function createCallViewModel$(
|
||||
// stays close to upstream and rebases cleanly.
|
||||
return overrideSpotlight$(
|
||||
spotlightSpeaker$,
|
||||
manualSpotlightUserId$,
|
||||
manualSpotlight$,
|
||||
screenShares$,
|
||||
localUserMediaForPip$,
|
||||
userMedia$,
|
||||
@@ -1088,12 +1094,31 @@ export function createCallViewModel$(
|
||||
hasRemoteScreenShares$,
|
||||
);
|
||||
|
||||
// [lotus #3/#16] Pinning forces the layout switch to "spotlight" (grid and
|
||||
// 1:1 layouts would otherwise never render the pin) and the pin is dropped
|
||||
// when the participant leaves for good or we hang up; see
|
||||
// src/lotus/lotusSpotlight.ts.
|
||||
const manualSpotlight = createManualSpotlight(
|
||||
manualSpotlight$,
|
||||
userMedia$,
|
||||
leave$,
|
||||
layoutSwitchVm,
|
||||
);
|
||||
manualSpotlight.effects$.pipe(scope.bind()).subscribe();
|
||||
|
||||
const gridLayoutMedia$: Observable<GridLayoutMedia> = combineLatest(
|
||||
[grid$, spotlight$],
|
||||
(grid, spotlight) => ({
|
||||
[grid$, spotlight$, manualSpotlight$],
|
||||
(grid, spotlight, manual) => ({
|
||||
type: "grid",
|
||||
edgeToEdge: false,
|
||||
spotlight: spotlight.some((vm) => vm.type === "screen share")
|
||||
// [lotus #3] Also surface the spotlight when it holds a host-pinned
|
||||
// participant, so the pin is visible in window modes that ignore the
|
||||
// layout switch (narrow). Unchanged when manual is null.
|
||||
spotlight: spotlight.some(
|
||||
(vm) =>
|
||||
vm.type === "screen share" ||
|
||||
(manual !== null && vm.userId === manual.userId),
|
||||
)
|
||||
? spotlight
|
||||
: undefined,
|
||||
grid,
|
||||
@@ -1796,8 +1821,7 @@ export function createCallViewModel$(
|
||||
join: localMembership.requestJoinAndPublish,
|
||||
leave: localMembership.requestDisconnect,
|
||||
toggleScreenSharing: toggleScreenSharing,
|
||||
setManualSpotlight: (userId: string | null): void =>
|
||||
manualSpotlightUserId$.next(userId),
|
||||
setManualSpotlight: manualSpotlight.setManualSpotlight,
|
||||
sharingScreen$: sharingScreen$,
|
||||
|
||||
tapScreen: (): void => screenTap$.next(),
|
||||
|
||||
Reference in New Issue
Block a user