Skip to content

Commit 56e736b

Browse files
authored
Merge pull request element-hq#2732 from element-hq/hs/add-volume-effect-level
Add sound effect volume slider
2 parents 1df2e0c + 3e1e08c commit 56e736b

File tree

8 files changed

+94
-8
lines changed

8 files changed

+94
-8
lines changed

public/locales/en-GB/app.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@
144144
"room_auth_view_eula_caption": "By clicking \"Continue\", you agree to our <2>End User Licensing Agreement (EULA)</2>",
145145
"screenshare_button_label": "Share screen",
146146
"settings": {
147+
"audio_tab": {
148+
"effect_volume_description": "Adjust the volume at which reactions and hand raised effects play",
149+
"effect_volume_label": "Sound effect volume"
150+
},
147151
"developer_settings_label": "Developer Settings",
148152
"developer_settings_label_description": "Expose developer settings in the settings window.",
149153
"developer_tab_title": "Developer",

src/room/InCallView.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,11 @@ import handSoundOgg from "../sound/raise_hand.ogg?url";
8585
import handSoundMp3 from "../sound/raise_hand.mp3?url";
8686
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
8787
import { useSwitchCamera } from "./useSwitchCamera";
88-
import { showReactions, useSetting } from "../settings/settings";
88+
import {
89+
soundEffectVolumeSetting,
90+
showReactions,
91+
useSetting,
92+
} from "../settings/settings";
8993

9094
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
9195

@@ -182,6 +186,7 @@ export const InCallView: FC<InCallViewProps> = ({
182186
onShareClick,
183187
}) => {
184188
const [shouldShowReactions] = useSetting(showReactions);
189+
const [soundEffectVolume] = useSetting(soundEffectVolumeSetting);
185190
const { supportsReactions, raisedHands, reactions } = useReactions();
186191
const raisedHandCount = useMemo(
187192
() => Object.keys(raisedHands).length,
@@ -344,11 +349,17 @@ export const InCallView: FC<InCallViewProps> = ({
344349
return;
345350
}
346351
if (previousRaisedHandCount < raisedHandCount) {
352+
handRaisePlayer.current.volume = soundEffectVolume;
347353
handRaisePlayer.current.play().catch((ex) => {
348354
logger.warn("Failed to play raise hand sound", ex);
349355
});
350356
}
351-
}, [raisedHandCount, handRaisePlayer, previousRaisedHandCount]);
357+
}, [
358+
raisedHandCount,
359+
handRaisePlayer,
360+
previousRaisedHandCount,
361+
soundEffectVolume,
362+
]);
352363

353364
useEffect(() => {
354365
widget?.api.transport

src/room/ReactionAudioRenderer.test.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import {
1717
} from "../utils/testReactions";
1818
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
1919
import { GenericReaction, ReactionSet } from "../reactions";
20-
import { playReactionsSound } from "../settings/settings";
20+
import {
21+
playReactionsSound,
22+
soundEffectVolumeSetting,
23+
} from "../settings/settings";
2124

2225
const memberUserIdAlice = "@alice:example.org";
2326
const memberUserIdBob = "@bob:example.org";
@@ -49,6 +52,7 @@ function TestComponent({
4952
const originalPlayFn = window.HTMLMediaElement.prototype.play;
5053
afterAll(() => {
5154
playReactionsSound.setValue(playReactionsSound.defaultValue);
55+
soundEffectVolumeSetting.setValue(soundEffectVolumeSetting.defaultValue);
5256
window.HTMLMediaElement.prototype.play = originalPlayFn;
5357
});
5458

@@ -125,6 +129,28 @@ test("will play the generic audio sound when there is soundless reaction", () =>
125129
expect(audioIsPlaying[0]).toContain(GenericReaction.sound?.ogg);
126130
});
127131

132+
test("will play an audio sound with the correct volume", () => {
133+
playReactionsSound.setValue(true);
134+
soundEffectVolumeSetting.setValue(0.5);
135+
const room = new MockRoom(memberUserIdAlice);
136+
const rtcSession = new MockRTCSession(room, membership);
137+
const { getByTestId } = render(<TestComponent rtcSession={rtcSession} />);
138+
139+
// Find the first reaction with a sound effect
140+
const chosenReaction = ReactionSet.find((r) => !!r.sound);
141+
if (!chosenReaction) {
142+
throw Error(
143+
"No reactions have sounds configured, this test cannot succeed",
144+
);
145+
}
146+
act(() => {
147+
room.testSendReaction(memberEventAlice, chosenReaction, membership);
148+
});
149+
expect((getByTestId(chosenReaction.name) as HTMLAudioElement).volume).toEqual(
150+
0.5,
151+
);
152+
});
153+
128154
test("will play multiple audio sounds when there are multiple different reactions", () => {
129155
const audioIsPlaying: string[] = [];
130156
window.HTMLMediaElement.prototype.play = async function (): Promise<void> {

src/room/ReactionAudioRenderer.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@ Please see LICENSE in the repository root for full details.
88
import { ReactNode, useEffect, useRef } from "react";
99

1010
import { useReactions } from "../useReactions";
11-
import { playReactionsSound, useSetting } from "../settings/settings";
11+
import {
12+
playReactionsSound,
13+
soundEffectVolumeSetting as effectSoundVolumeSetting,
14+
useSetting,
15+
} from "../settings/settings";
1216
import { GenericReaction, ReactionSet } from "../reactions";
1317

1418
export function ReactionsAudioRenderer(): ReactNode {
1519
const { reactions } = useReactions();
1620
const [shouldPlay] = useSetting(playReactionsSound);
21+
const [effectSoundVolume] = useSetting(effectSoundVolumeSetting);
1722
const audioElements = useRef<Record<string, HTMLAudioElement | null>>({});
1823

1924
useEffect(() => {
@@ -30,10 +35,11 @@ export function ReactionsAudioRenderer(): ReactNode {
3035
const audioElement =
3136
audioElements.current[reactionName] ?? audioElements.current.generic;
3237
if (audioElement?.paused) {
38+
audioElement.volume = effectSoundVolume;
3339
void audioElement.play();
3440
}
3541
}
36-
}, [audioElements, shouldPlay, reactions]);
42+
}, [audioElements, shouldPlay, reactions, effectSoundVolume]);
3743

3844
// Do not render any audio elements if playback is disabled. Will save
3945
// audio file fetches.

src/settings/SettingsModal.module.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,20 @@ Please see LICENSE in the repository root for full details.
1616
.fieldRowText {
1717
margin-bottom: 0;
1818
}
19+
20+
.volumeSlider {
21+
margin-top: var(--cpd-space-2x);
22+
}
23+
24+
.volumeSlider > label {
25+
margin-bottom: var(--cpd-space-1x);
26+
display: block;
27+
}
28+
29+
.volumeSlider > span {
30+
max-width: 20em;
31+
}
32+
33+
.volumeSlider > p {
34+
color: var(--cpd-color-text-secondary);
35+
}

src/settings/SettingsModal.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details.
88
import { ChangeEvent, FC, ReactNode, useCallback } from "react";
99
import { Trans, useTranslation } from "react-i18next";
1010
import { MatrixClient } from "matrix-js-sdk/src/matrix";
11-
import { Dropdown, Text } from "@vector-im/compound-web";
11+
import { Dropdown, Separator, Text } from "@vector-im/compound-web";
1212

1313
import { Modal } from "../Modal";
1414
import styles from "./SettingsModal.module.css";
@@ -28,9 +28,11 @@ import {
2828
developerSettingsTab as developerSettingsTabSetting,
2929
duplicateTiles as duplicateTilesSetting,
3030
useOptInAnalytics,
31+
soundEffectVolumeSetting,
3132
} from "./settings";
3233
import { isFirefox } from "../Platform";
3334
import { PreferencesSettingsTab } from "./PreferencesSettingsTab";
35+
import { Slider } from "../Slider";
3436

3537
type SettingsTab =
3638
| "audio"
@@ -116,6 +118,8 @@ export const SettingsModal: FC<Props> = ({
116118
const devices = useMediaDevices();
117119
useMediaDeviceNames(devices, open);
118120

121+
const [soundVolume, setSoundVolume] = useSetting(soundEffectVolumeSetting);
122+
119123
const audioTab: Tab<SettingsTab> = {
120124
key: "audio",
121125
name: t("common.audio"),
@@ -127,6 +131,19 @@ export const SettingsModal: FC<Props> = ({
127131
devices.audioOutput,
128132
t("settings.speaker_device_selection_label"),
129133
)}
134+
<Separator />
135+
<div className={styles.volumeSlider}>
136+
<label>{t("settings.audio_tab.effect_volume_label")}</label>
137+
<p>{t("settings.audio_tab.effect_volume_description")}</p>
138+
<Slider
139+
label={t("video_tile.volume")}
140+
value={soundVolume}
141+
onValueChange={setSoundVolume}
142+
min={0}
143+
max={1}
144+
step={0.01}
145+
/>
146+
</div>
130147
</>
131148
),
132149
};

src/settings/settings.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,9 @@ export const playReactionsSound = new Setting<boolean>(
100100
true,
101101
);
102102

103+
export const soundEffectVolumeSetting = new Setting<number>(
104+
"sound-effect-volume",
105+
1,
106+
);
107+
103108
export const alwaysShowSelf = new Setting<boolean>("always-show-self", true);

src/useReactions.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export const ReactionsProvider = ({
182182

183183
// This effect handles any *live* reaction/redactions in the room.
184184
useEffect(() => {
185-
const reactionTimeouts = new Set<NodeJS.Timeout>();
185+
const reactionTimeouts = new Set<number>();
186186
const handleReactionEvent = (event: MatrixEvent): void => {
187187
if (event.isSending()) {
188188
// Skip any events that are still sending.
@@ -245,7 +245,7 @@ export const ReactionsProvider = ({
245245
// We've still got a reaction from this user, ignore it to prevent spamming
246246
return reactions;
247247
}
248-
const timeout = setTimeout(() => {
248+
const timeout = window.setTimeout(() => {
249249
// Clear the reaction after some time.
250250
setReactions(({ [sender]: _unused, ...remaining }) => remaining);
251251
reactionTimeouts.delete(timeout);

0 commit comments

Comments
 (0)