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>",
|
"room_auth_view_eula_caption": "By clicking \"Continue\", you agree to our <2>End User Licensing Agreement (EULA)</2>",
|
||||||
"screenshare_button_label": "Share screen",
|
"screenshare_button_label": "Share screen",
|
||||||
"settings": {
|
"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": "Developer Settings",
|
||||||
"developer_settings_label_description": "Expose developer settings in the settings window.",
|
"developer_settings_label_description": "Expose developer settings in the settings window.",
|
||||||
"developer_tab_title": "Developer",
|
"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 handSoundMp3 from "../sound/raise_hand.mp3?url";
|
||||||
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
|
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
|
||||||
import { useSwitchCamera } from "./useSwitchCamera";
|
import { useSwitchCamera } from "./useSwitchCamera";
|
||||||
import { showReactions, useSetting } from "../settings/settings";
|
import {
|
||||||
|
soundEffectVolumeSetting,
|
||||||
|
showReactions,
|
||||||
|
useSetting,
|
||||||
|
} from "../settings/settings";
|
||||||
|
|
||||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||||
|
|
||||||
@@ -182,6 +186,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
onShareClick,
|
onShareClick,
|
||||||
}) => {
|
}) => {
|
||||||
const [shouldShowReactions] = useSetting(showReactions);
|
const [shouldShowReactions] = useSetting(showReactions);
|
||||||
|
const [soundEffectVolume] = useSetting(soundEffectVolumeSetting);
|
||||||
const { supportsReactions, raisedHands, reactions } = useReactions();
|
const { supportsReactions, raisedHands, reactions } = useReactions();
|
||||||
const raisedHandCount = useMemo(
|
const raisedHandCount = useMemo(
|
||||||
() => Object.keys(raisedHands).length,
|
() => Object.keys(raisedHands).length,
|
||||||
@@ -344,11 +349,17 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (previousRaisedHandCount < raisedHandCount) {
|
if (previousRaisedHandCount < raisedHandCount) {
|
||||||
|
handRaisePlayer.current.volume = soundEffectVolume;
|
||||||
handRaisePlayer.current.play().catch((ex) => {
|
handRaisePlayer.current.play().catch((ex) => {
|
||||||
logger.warn("Failed to play raise hand sound", ex);
|
logger.warn("Failed to play raise hand sound", ex);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [raisedHandCount, handRaisePlayer, previousRaisedHandCount]);
|
}, [
|
||||||
|
raisedHandCount,
|
||||||
|
handRaisePlayer,
|
||||||
|
previousRaisedHandCount,
|
||||||
|
soundEffectVolume,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
widget?.api.transport
|
widget?.api.transport
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ import {
|
|||||||
} from "../utils/testReactions";
|
} from "../utils/testReactions";
|
||||||
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
|
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
|
||||||
import { GenericReaction, ReactionSet } from "../reactions";
|
import { GenericReaction, ReactionSet } from "../reactions";
|
||||||
import { playReactionsSound } from "../settings/settings";
|
import {
|
||||||
|
playReactionsSound,
|
||||||
|
soundEffectVolumeSetting,
|
||||||
|
} from "../settings/settings";
|
||||||
|
|
||||||
const memberUserIdAlice = "@alice:example.org";
|
const memberUserIdAlice = "@alice:example.org";
|
||||||
const memberUserIdBob = "@bob:example.org";
|
const memberUserIdBob = "@bob:example.org";
|
||||||
@@ -49,6 +52,7 @@ function TestComponent({
|
|||||||
const originalPlayFn = window.HTMLMediaElement.prototype.play;
|
const originalPlayFn = window.HTMLMediaElement.prototype.play;
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
playReactionsSound.setValue(playReactionsSound.defaultValue);
|
playReactionsSound.setValue(playReactionsSound.defaultValue);
|
||||||
|
soundEffectVolumeSetting.setValue(soundEffectVolumeSetting.defaultValue);
|
||||||
window.HTMLMediaElement.prototype.play = originalPlayFn;
|
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);
|
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", () => {
|
test("will play multiple audio sounds when there are multiple different reactions", () => {
|
||||||
const audioIsPlaying: string[] = [];
|
const audioIsPlaying: string[] = [];
|
||||||
window.HTMLMediaElement.prototype.play = async function (): Promise<void> {
|
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 { ReactNode, useEffect, useRef } from "react";
|
||||||
|
|
||||||
import { useReactions } from "../useReactions";
|
import { useReactions } from "../useReactions";
|
||||||
import { playReactionsSound, useSetting } from "../settings/settings";
|
import {
|
||||||
|
playReactionsSound,
|
||||||
|
soundEffectVolumeSetting as effectSoundVolumeSetting,
|
||||||
|
useSetting,
|
||||||
|
} from "../settings/settings";
|
||||||
import { GenericReaction, ReactionSet } from "../reactions";
|
import { GenericReaction, ReactionSet } from "../reactions";
|
||||||
|
|
||||||
export function ReactionsAudioRenderer(): ReactNode {
|
export function ReactionsAudioRenderer(): ReactNode {
|
||||||
const { reactions } = useReactions();
|
const { reactions } = useReactions();
|
||||||
const [shouldPlay] = useSetting(playReactionsSound);
|
const [shouldPlay] = useSetting(playReactionsSound);
|
||||||
|
const [effectSoundVolume] = useSetting(effectSoundVolumeSetting);
|
||||||
const audioElements = useRef<Record<string, HTMLAudioElement | null>>({});
|
const audioElements = useRef<Record<string, HTMLAudioElement | null>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -30,10 +35,11 @@ export function ReactionsAudioRenderer(): ReactNode {
|
|||||||
const audioElement =
|
const audioElement =
|
||||||
audioElements.current[reactionName] ?? audioElements.current.generic;
|
audioElements.current[reactionName] ?? audioElements.current.generic;
|
||||||
if (audioElement?.paused) {
|
if (audioElement?.paused) {
|
||||||
|
audioElement.volume = effectSoundVolume;
|
||||||
void audioElement.play();
|
void audioElement.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [audioElements, shouldPlay, reactions]);
|
}, [audioElements, shouldPlay, reactions, effectSoundVolume]);
|
||||||
|
|
||||||
// Do not render any audio elements if playback is disabled. Will save
|
// Do not render any audio elements if playback is disabled. Will save
|
||||||
// audio file fetches.
|
// audio file fetches.
|
||||||
|
|||||||
@@ -16,3 +16,20 @@ Please see LICENSE in the repository root for full details.
|
|||||||
.fieldRowText {
|
.fieldRowText {
|
||||||
margin-bottom: 0;
|
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 { ChangeEvent, FC, ReactNode, useCallback } from "react";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
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 { Modal } from "../Modal";
|
||||||
import styles from "./SettingsModal.module.css";
|
import styles from "./SettingsModal.module.css";
|
||||||
@@ -28,9 +28,11 @@ import {
|
|||||||
developerSettingsTab as developerSettingsTabSetting,
|
developerSettingsTab as developerSettingsTabSetting,
|
||||||
duplicateTiles as duplicateTilesSetting,
|
duplicateTiles as duplicateTilesSetting,
|
||||||
useOptInAnalytics,
|
useOptInAnalytics,
|
||||||
|
soundEffectVolumeSetting,
|
||||||
} from "./settings";
|
} from "./settings";
|
||||||
import { isFirefox } from "../Platform";
|
import { isFirefox } from "../Platform";
|
||||||
import { PreferencesSettingsTab } from "./PreferencesSettingsTab";
|
import { PreferencesSettingsTab } from "./PreferencesSettingsTab";
|
||||||
|
import { Slider } from "../Slider";
|
||||||
|
|
||||||
type SettingsTab =
|
type SettingsTab =
|
||||||
| "audio"
|
| "audio"
|
||||||
@@ -116,6 +118,8 @@ export const SettingsModal: FC<Props> = ({
|
|||||||
const devices = useMediaDevices();
|
const devices = useMediaDevices();
|
||||||
useMediaDeviceNames(devices, open);
|
useMediaDeviceNames(devices, open);
|
||||||
|
|
||||||
|
const [soundVolume, setSoundVolume] = useSetting(soundEffectVolumeSetting);
|
||||||
|
|
||||||
const audioTab: Tab<SettingsTab> = {
|
const audioTab: Tab<SettingsTab> = {
|
||||||
key: "audio",
|
key: "audio",
|
||||||
name: t("common.audio"),
|
name: t("common.audio"),
|
||||||
@@ -127,6 +131,19 @@ export const SettingsModal: FC<Props> = ({
|
|||||||
devices.audioOutput,
|
devices.audioOutput,
|
||||||
t("settings.speaker_device_selection_label"),
|
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,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const soundEffectVolumeSetting = new Setting<number>(
|
||||||
|
"sound-effect-volume",
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
export const alwaysShowSelf = new Setting<boolean>("always-show-self", true);
|
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.
|
// This effect handles any *live* reaction/redactions in the room.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const reactionTimeouts = new Set<NodeJS.Timeout>();
|
const reactionTimeouts = new Set<number>();
|
||||||
const handleReactionEvent = (event: MatrixEvent): void => {
|
const handleReactionEvent = (event: MatrixEvent): void => {
|
||||||
if (event.isSending()) {
|
if (event.isSending()) {
|
||||||
// Skip any events that are still sending.
|
// 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
|
// We've still got a reaction from this user, ignore it to prevent spamming
|
||||||
return reactions;
|
return reactions;
|
||||||
}
|
}
|
||||||
const timeout = setTimeout(() => {
|
const timeout = window.setTimeout(() => {
|
||||||
// Clear the reaction after some time.
|
// Clear the reaction after some time.
|
||||||
setReactions(({ [sender]: _unused, ...remaining }) => remaining);
|
setReactions(({ [sender]: _unused, ...remaining }) => remaining);
|
||||||
reactionTimeouts.delete(timeout);
|
reactionTimeouts.delete(timeout);
|
||||||
|
|||||||
Reference in New Issue
Block a user