Files
element-call/src/state/VolumeControls.ts
T
Lotus CIandClaude Opus 5.5 44023a4c84
CI / Build embedded bundle (push) Successful in 1m51s
CI / Publish to Gitea npm registry (push) Successful in 1m5s
style(lotus): oxfmt the volume memory change (#36)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-09-25 10:03:49 -04:00

146 lines
4.2 KiB
TypeScript

/*
Copyright 2026 Element Software Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import {
combineLatest,
distinctUntilChanged,
map,
merge,
of,
skip,
Subject,
switchMap,
} from "rxjs";
import { type Behavior } from "./Behavior";
import { type ObservableScope } from "./ObservableScope";
import { accumulate } from "../utils/observable";
/**
* Controls for audio playback volume.
*/
export interface VolumeControls {
/**
* The volume to which the audio is set, as a scalar multiplier.
*/
playbackVolume$: Behavior<number>;
/**
* Whether playback of this audio is disabled.
*/
playbackMuted$: Behavior<boolean>;
togglePlaybackMuted: () => void;
adjustPlaybackVolume: (value: number) => void;
/**
* Commit the volume. [lotus #36] Pass the slider's committed value: with the
* keyboard the slider commits before its last change reaches us, which left
* the committed (and remembered) volume one step behind.
*/
commitPlaybackVolume: (value?: number) => void;
}
interface VolumeControlsInputs {
pretendToBeDisconnected$: Behavior<boolean>;
/**
* The callback to run to notify the module performing audio playback of the
* requested volume.
*/
sink$: Behavior<(volume: number) => void>;
/** [lotus #36] Starting volume (a remembered one); defaults to 1. */
initialVolume?: number;
/** [lotus #36] Called with each newly committed volume. */
onCommit?: (volume: number) => void;
}
/**
* Creates a set of controls for audio playback volume and syncs this with the
* audio playback module for the duration of the scope.
*/
export function createVolumeControls(
scope: ObservableScope,
{
pretendToBeDisconnected$,
sink$,
initialVolume = 1,
onCommit,
}: VolumeControlsInputs,
): VolumeControls {
const toggleMuted$ = new Subject<"toggle mute">();
const adjustVolume$ = new Subject<number>();
const commitVolume$ = new Subject<"commit">();
const state$ = scope.behavior(
merge(toggleMuted$, adjustVolume$, commitVolume$).pipe(
accumulate(
{ volume: initialVolume, committedVolume: initialVolume },
(state, event) => {
switch (event) {
case "toggle mute":
return {
...state,
volume: state.volume === 0 ? state.committedVolume : 0,
};
case "commit":
// Dragging the slider to zero should have the same effect as
// muting: keep the original committed volume, as if it were never
// dragged
return {
...state,
committedVolume:
state.volume === 0 ? state.committedVolume : state.volume,
};
default:
// Volume adjustment
return { ...state, volume: event };
}
},
),
),
);
const playbackVolume$ = scope.behavior<number>(
state$.pipe(map(({ volume }) => volume)),
);
// [lotus #36] Report committed changes (not the starting value) so the
// caller can remember them.
if (onCommit) {
state$
.pipe(
map(({ committedVolume }) => committedVolume),
distinctUntilChanged(),
skip(1),
scope.bind(),
)
.subscribe(onCommit);
}
// Sync the requested volume with the audio playback module
combineLatest([
sink$,
// The playback volume, taking into account whether we're supposed to
// pretend that the audio stream is disconnected (since we don't necessarily
// want that to modify the UI state).
pretendToBeDisconnected$.pipe(
switchMap((disconnected) => (disconnected ? of(0) : playbackVolume$)),
),
])
.pipe(scope.bind())
.subscribe(([sink, volume]) => sink(volume));
return {
playbackVolume$,
playbackMuted$: scope.behavior<boolean>(
playbackVolume$.pipe(map((volume) => volume === 0)),
),
togglePlaybackMuted: () => toggleMuted$.next("toggle mute"),
adjustPlaybackVolume: (value: number) => adjustVolume$.next(value),
commitPlaybackVolume: (value?: number) => {
if (value !== undefined) adjustVolume$.next(value);
commitVolume$.next("commit");
},
};
}