Import unfinished mute states refactor

This commit is contained in:
Robin
2025-08-28 17:40:35 +02:00
committed by Timo K
parent 02f4c73759
commit d46fe55a67
3 changed files with 190 additions and 7 deletions

163
src/state/MuteStates.ts Normal file
View File

@@ -0,0 +1,163 @@
/*
Copyright 2023-2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { type IWidgetApiRequest } from "matrix-widget-api";
import { logger } from "matrix-js-sdk/lib/logger";
import {
combineLatest,
distinctUntilChanged,
fromEvent,
map,
merge,
type Observable,
of,
Subject,
switchMap,
withLatestFrom,
} from "rxjs";
import { type MediaDevices, type MediaDevice } from "../state/MediaDevices";
import { ElementWidgetActions, widget } from "../widget";
import { Config } from "../config/Config";
import { getUrlParams } from "../UrlParams";
import { type ObservableScope } from "./ObservableScope";
import { accumulate } from "../utils/observable";
interface MuteStateData {
enabled$: Observable<boolean>;
set: ((enabled: boolean) => void) | null;
toggle: (() => void) | null;
}
class MuteState {
private readonly enabledByDefault$ =
this.enabledByConfig && !getUrlParams().skipLobby
? this.isJoined$.pipe(map((isJoined) => !isJoined))
: of(false);
private readonly data$: Observable<MuteStateData> =
this.device.available$.pipe(
map((available) => available.size > 0),
distinctUntilChanged(),
withLatestFrom(
this.enabledByDefault$,
(devicesConnected, enabledByDefault) => {
if (!devicesConnected)
return { enabled$: of(false), set: null, toggle: null };
const set$ = new Subject<boolean>();
const toggle$ = new Subject<void>();
return {
set: (enabled: boolean) => set$.next(enabled),
toggle: () => toggle$.next(),
// Assume the default value only once devices are actually connected
enabled$: merge(
set$,
toggle$.pipe(map(() => "toggle" as const)),
).pipe(
accumulate(enabledByDefault, (prev, update) =>
update === "toggle" ? !prev : update,
),
),
};
},
),
this.scope.state(),
);
public readonly enabled$: Observable<boolean> = this.data$.pipe(
switchMap(({ enabled$ }) => enabled$),
);
public readonly setEnabled$: Observable<((enabled: boolean) => void) | null> =
this.data$.pipe(map(({ set }) => set));
public readonly toggle$: Observable<(() => void) | null> = this.data$.pipe(
map(({ toggle }) => toggle),
);
public constructor(
private readonly scope: ObservableScope,
private readonly device: MediaDevice,
private readonly isJoined$: Observable<boolean>,
private readonly enabledByConfig: boolean,
) {}
}
export class MuteStates {
public readonly audio = new MuteState(
this.scope,
this.mediaDevices.audioInput,
this.isJoined$,
Config.get().media_devices.enable_video,
);
public readonly video = new MuteState(
this.scope,
this.mediaDevices.videoInput,
this.isJoined$,
Config.get().media_devices.enable_video,
);
public constructor(
private readonly scope: ObservableScope,
private readonly mediaDevices: MediaDevices,
private readonly isJoined$: Observable<boolean>,
) {
if (widget !== null) {
// Sync our mute states with the hosting client
const widgetApiState$ = combineLatest(
[this.audio.enabled$, this.video.enabled$],
(audio, video) => ({ audio_enabled: audio, video_enabled: video }),
);
widgetApiState$.pipe(this.scope.bind()).subscribe((state) => {
widget!.api.transport
.send(ElementWidgetActions.DeviceMute, state)
.catch((e) =>
logger.warn("Could not send DeviceMute action to widget", e),
);
});
// Also sync the hosting client's mute states back with ours
const muteActions$ = fromEvent(
widget.lazyActions,
ElementWidgetActions.DeviceMute,
) as Observable<CustomEvent<IWidgetApiRequest>>;
muteActions$
.pipe(
withLatestFrom(
widgetApiState$,
this.audio.setEnabled$,
this.video.setEnabled$,
),
this.scope.bind(),
)
.subscribe(([ev, state, setAudioEnabled, setVideoEnabled]) => {
// First copy the current state into our new state
const newState = { ...state };
// Update new state if there are any requested changes from the widget
// action in `ev.detail.data`.
if (
ev.detail.data.audio_enabled != null &&
typeof ev.detail.data.audio_enabled === "boolean" &&
setAudioEnabled !== null
) {
newState.audio_enabled = ev.detail.data.audio_enabled;
setAudioEnabled(newState.audio_enabled);
}
if (
ev.detail.data.video_enabled != null &&
typeof ev.detail.data.video_enabled === "boolean" &&
setVideoEnabled !== null
) {
newState.video_enabled = ev.detail.data.video_enabled;
setVideoEnabled(newState.video_enabled);
}
widget!.api.transport.reply(ev.detail, newState);
});
}
}
}