mirror of
https://github.com/vector-im/element-call.git
synced 2026-01-30 03:15:55 +00:00
Merge pull request #2732 from element-hq/hs/add-volume-effect-level
Add sound effect volume slider
This commit is contained in:
@@ -144,6 +144,10 @@
|
||||
"room_auth_view_eula_caption": "By clicking \"Continue\", you agree to our <2>End User Licensing Agreement (EULA)</2>",
|
||||
"screenshare_button_label": "Share screen",
|
||||
"settings": {
|
||||
"audio_tab": {
|
||||
"effect_volume_description": "Adjust the volume at which reactions and hand raised effects play",
|
||||
"effect_volume_label": "Sound effect volume"
|
||||
},
|
||||
"developer_settings_label": "Developer Settings",
|
||||
"developer_settings_label_description": "Expose developer settings in the settings window.",
|
||||
"developer_tab_title": "Developer",
|
||||
|
||||
@@ -85,7 +85,11 @@ import handSoundOgg from "../sound/raise_hand.ogg?url";
|
||||
import handSoundMp3 from "../sound/raise_hand.mp3?url";
|
||||
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
|
||||
import { useSwitchCamera } from "./useSwitchCamera";
|
||||
import { showReactions, useSetting } from "../settings/settings";
|
||||
import {
|
||||
soundEffectVolumeSetting,
|
||||
showReactions,
|
||||
useSetting,
|
||||
} from "../settings/settings";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||
|
||||
@@ -182,6 +186,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
onShareClick,
|
||||
}) => {
|
||||
const [shouldShowReactions] = useSetting(showReactions);
|
||||
const [soundEffectVolume] = useSetting(soundEffectVolumeSetting);
|
||||
const { supportsReactions, raisedHands, reactions } = useReactions();
|
||||
const raisedHandCount = useMemo(
|
||||
() => Object.keys(raisedHands).length,
|
||||
@@ -344,11 +349,17 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
return;
|
||||
}
|
||||
if (previousRaisedHandCount < raisedHandCount) {
|
||||
handRaisePlayer.current.volume = soundEffectVolume;
|
||||
handRaisePlayer.current.play().catch((ex) => {
|
||||
logger.warn("Failed to play raise hand sound", ex);
|
||||
});
|
||||
}
|
||||
}, [raisedHandCount, handRaisePlayer, previousRaisedHandCount]);
|
||||
}, [
|
||||
raisedHandCount,
|
||||
handRaisePlayer,
|
||||
previousRaisedHandCount,
|
||||
soundEffectVolume,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
widget?.api.transport
|
||||
|
||||
@@ -17,7 +17,10 @@ import {
|
||||
} from "../utils/testReactions";
|
||||
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
|
||||
import { GenericReaction, ReactionSet } from "../reactions";
|
||||
import { playReactionsSound } from "../settings/settings";
|
||||
import {
|
||||
playReactionsSound,
|
||||
soundEffectVolumeSetting,
|
||||
} from "../settings/settings";
|
||||
|
||||
const memberUserIdAlice = "@alice:example.org";
|
||||
const memberUserIdBob = "@bob:example.org";
|
||||
@@ -49,6 +52,7 @@ function TestComponent({
|
||||
const originalPlayFn = window.HTMLMediaElement.prototype.play;
|
||||
afterAll(() => {
|
||||
playReactionsSound.setValue(playReactionsSound.defaultValue);
|
||||
soundEffectVolumeSetting.setValue(soundEffectVolumeSetting.defaultValue);
|
||||
window.HTMLMediaElement.prototype.play = originalPlayFn;
|
||||
});
|
||||
|
||||
@@ -125,6 +129,28 @@ test("will play the generic audio sound when there is soundless reaction", () =>
|
||||
expect(audioIsPlaying[0]).toContain(GenericReaction.sound?.ogg);
|
||||
});
|
||||
|
||||
test("will play an audio sound with the correct volume", () => {
|
||||
playReactionsSound.setValue(true);
|
||||
soundEffectVolumeSetting.setValue(0.5);
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const { getByTestId } = render(<TestComponent rtcSession={rtcSession} />);
|
||||
|
||||
// Find the first reaction with a sound effect
|
||||
const chosenReaction = ReactionSet.find((r) => !!r.sound);
|
||||
if (!chosenReaction) {
|
||||
throw Error(
|
||||
"No reactions have sounds configured, this test cannot succeed",
|
||||
);
|
||||
}
|
||||
act(() => {
|
||||
room.testSendReaction(memberEventAlice, chosenReaction, membership);
|
||||
});
|
||||
expect((getByTestId(chosenReaction.name) as HTMLAudioElement).volume).toEqual(
|
||||
0.5,
|
||||
);
|
||||
});
|
||||
|
||||
test("will play multiple audio sounds when there are multiple different reactions", () => {
|
||||
const audioIsPlaying: string[] = [];
|
||||
window.HTMLMediaElement.prototype.play = async function (): Promise<void> {
|
||||
|
||||
@@ -8,12 +8,17 @@ Please see LICENSE in the repository root for full details.
|
||||
import { ReactNode, useEffect, useRef } from "react";
|
||||
|
||||
import { useReactions } from "../useReactions";
|
||||
import { playReactionsSound, useSetting } from "../settings/settings";
|
||||
import {
|
||||
playReactionsSound,
|
||||
soundEffectVolumeSetting as effectSoundVolumeSetting,
|
||||
useSetting,
|
||||
} from "../settings/settings";
|
||||
import { GenericReaction, ReactionSet } from "../reactions";
|
||||
|
||||
export function ReactionsAudioRenderer(): ReactNode {
|
||||
const { reactions } = useReactions();
|
||||
const [shouldPlay] = useSetting(playReactionsSound);
|
||||
const [effectSoundVolume] = useSetting(effectSoundVolumeSetting);
|
||||
const audioElements = useRef<Record<string, HTMLAudioElement | null>>({});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -30,10 +35,11 @@ export function ReactionsAudioRenderer(): ReactNode {
|
||||
const audioElement =
|
||||
audioElements.current[reactionName] ?? audioElements.current.generic;
|
||||
if (audioElement?.paused) {
|
||||
audioElement.volume = effectSoundVolume;
|
||||
void audioElement.play();
|
||||
}
|
||||
}
|
||||
}, [audioElements, shouldPlay, reactions]);
|
||||
}, [audioElements, shouldPlay, reactions, effectSoundVolume]);
|
||||
|
||||
// Do not render any audio elements if playback is disabled. Will save
|
||||
// audio file fetches.
|
||||
|
||||
@@ -16,3 +16,20 @@ Please see LICENSE in the repository root for full details.
|
||||
.fieldRowText {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.volumeSlider {
|
||||
margin-top: var(--cpd-space-2x);
|
||||
}
|
||||
|
||||
.volumeSlider > label {
|
||||
margin-bottom: var(--cpd-space-1x);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.volumeSlider > span {
|
||||
max-width: 20em;
|
||||
}
|
||||
|
||||
.volumeSlider > p {
|
||||
color: var(--cpd-color-text-secondary);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details.
|
||||
import { ChangeEvent, FC, ReactNode, useCallback } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { Dropdown, Text } from "@vector-im/compound-web";
|
||||
import { Dropdown, Separator, Text } from "@vector-im/compound-web";
|
||||
|
||||
import { Modal } from "../Modal";
|
||||
import styles from "./SettingsModal.module.css";
|
||||
@@ -28,9 +28,11 @@ import {
|
||||
developerSettingsTab as developerSettingsTabSetting,
|
||||
duplicateTiles as duplicateTilesSetting,
|
||||
useOptInAnalytics,
|
||||
soundEffectVolumeSetting,
|
||||
} from "./settings";
|
||||
import { isFirefox } from "../Platform";
|
||||
import { PreferencesSettingsTab } from "./PreferencesSettingsTab";
|
||||
import { Slider } from "../Slider";
|
||||
|
||||
type SettingsTab =
|
||||
| "audio"
|
||||
@@ -116,6 +118,8 @@ export const SettingsModal: FC<Props> = ({
|
||||
const devices = useMediaDevices();
|
||||
useMediaDeviceNames(devices, open);
|
||||
|
||||
const [soundVolume, setSoundVolume] = useSetting(soundEffectVolumeSetting);
|
||||
|
||||
const audioTab: Tab<SettingsTab> = {
|
||||
key: "audio",
|
||||
name: t("common.audio"),
|
||||
@@ -127,6 +131,19 @@ export const SettingsModal: FC<Props> = ({
|
||||
devices.audioOutput,
|
||||
t("settings.speaker_device_selection_label"),
|
||||
)}
|
||||
<Separator />
|
||||
<div className={styles.volumeSlider}>
|
||||
<label>{t("settings.audio_tab.effect_volume_label")}</label>
|
||||
<p>{t("settings.audio_tab.effect_volume_description")}</p>
|
||||
<Slider
|
||||
label={t("video_tile.volume")}
|
||||
value={soundVolume}
|
||||
onValueChange={setSoundVolume}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -100,4 +100,9 @@ export const playReactionsSound = new Setting<boolean>(
|
||||
true,
|
||||
);
|
||||
|
||||
export const soundEffectVolumeSetting = new Setting<number>(
|
||||
"sound-effect-volume",
|
||||
1,
|
||||
);
|
||||
|
||||
export const alwaysShowSelf = new Setting<boolean>("always-show-self", true);
|
||||
|
||||
@@ -182,7 +182,7 @@ export const ReactionsProvider = ({
|
||||
|
||||
// This effect handles any *live* reaction/redactions in the room.
|
||||
useEffect(() => {
|
||||
const reactionTimeouts = new Set<NodeJS.Timeout>();
|
||||
const reactionTimeouts = new Set<number>();
|
||||
const handleReactionEvent = (event: MatrixEvent): void => {
|
||||
if (event.isSending()) {
|
||||
// Skip any events that are still sending.
|
||||
@@ -245,7 +245,7 @@ export const ReactionsProvider = ({
|
||||
// We've still got a reaction from this user, ignore it to prevent spamming
|
||||
return reactions;
|
||||
}
|
||||
const timeout = setTimeout(() => {
|
||||
const timeout = window.setTimeout(() => {
|
||||
// Clear the reaction after some time.
|
||||
setReactions(({ [sender]: _unused, ...remaining }) => remaining);
|
||||
reactionTimeouts.delete(timeout);
|
||||
|
||||
Reference in New Issue
Block a user