Skip to content

Commit

Permalink
Add sounds to the dashboard (#206)
Browse files Browse the repository at this point in the history
* Add sound setting

* Add sounds on status change

* Add new query parameter for sound to the docs

* Fix sound enabled via url and reset sounds before playing
  • Loading branch information
rick-nu authored Mar 18, 2024
1 parent a287ada commit bcfa479
Show file tree
Hide file tree
Showing 18 changed files with 139 additions and 22 deletions.
3 changes: 2 additions & 1 deletion .parcelrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"extends": "@parcel/config-default",
"transformers": {
"*.{ts,tsx}": ["@futureportal/parcel-transformer-package-version", "@parcel/transformer-typescript-tsc"],
"*.svg": ["@parcel/transformer-svg-react"]
"*.svg": ["@parcel/transformer-svg-react"],
"*.mp3": ["@parcel/transformer-raw"]
}
}
1 change: 1 addition & 0 deletions docs/config/query-parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ query parameters are available for you.
| --------- | ------------------------------------------- | ------------------------ |
| completed | Show completed CI steps (hidden by default) | `1` to show, `0` to hide |
| avatars | Show user avatars | `1` to show, `0` to hide |
| sound | Enable warning/error/success sounds | `1` for on, `0` for off |

## How to use them

Expand Down
9 changes: 4 additions & 5 deletions frontend/App/AppContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@ import { createContext } from 'react';
type AppContext = {
showCompleted: boolean | null;
showAvatars: boolean | null;
};

const defaultContext: AppContext = {
showCompleted: null,
showAvatars: null,
sound: boolean | null;
};

export const getQueryContext = (): AppContext => {
Expand All @@ -28,9 +24,12 @@ export const getQueryContext = (): AppContext => {
return {
showCompleted: isEnabled('completed'),
showAvatars: isEnabled('avatars'),
sound: isEnabled('sound'),
};
};

const defaultContext: AppContext = getQueryContext();

const appContext = createContext<AppContext | null>(defaultContext);

export default appContext;
8 changes: 4 additions & 4 deletions frontend/App/Favicon/Favicon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ import WarningIcon from './icon/warning.png';

import { State } from '/types/status';

const getIcon = (state: State) => {
const getIcon = (state: State): string => {
if (state === 'error') {
return ErrorIcon;
return ErrorIcon as string;
}

if (state === 'warning') {
return WarningIcon;
return WarningIcon as string;
}

return SuccessIcon;
return SuccessIcon as string;
};

const Favicon = (): ReactElement => {
Expand Down
35 changes: 33 additions & 2 deletions frontend/App/SettingsPanel/Customization/Customization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,37 @@ import { Content } from '/frontend/App/SettingsPanel/SettingsPanel.style';
import Icon from '/frontend/components/Icon';
import Modifier from '/frontend/components/Modifier';
import Toggle from '/frontend/components/Toggle';
import { setSizeModifier, toggleShowCompleted, toggleShowUserAvatars } from '/frontend/store/settings/actions';
import { getSizeModifier, isHidingUserAvatars, isShowingCompleted } from '/frontend/store/settings/selectors';
import Sounds from '/frontend/sounds/Sounds';
import {
setSizeModifier,
toggleShowCompleted,
toggleShowUserAvatars,
toggleSound,
} from '/frontend/store/settings/actions';
import {
getSizeModifier,
isHidingUserAvatars,
isShowingCompleted,
isSoundEnabled,
} from '/frontend/store/settings/selectors';

const Customization = (): ReactElement => {
const showCompleted = useSelector(isShowingCompleted);
const sizeModifier = useSelector(getSizeModifier);
const soundEnabled = useSelector(isSoundEnabled);
const isHidingAvatars = useSelector(isHidingUserAvatars);
const dispatch = useDispatch();

const server = <Icon icon="warning" state="warning" title="Server setting" />;

const handleSoundToggle = () => {
if (!soundEnabled) {
Sounds.playSuccess();
}

dispatch(toggleSound());
};

return (
<Content>
<p>
Expand All @@ -35,6 +55,17 @@ const Customization = (): ReactElement => {
<Toggle onToggle={() => dispatch(toggleShowCompleted())} enabled={showCompleted} />
</Tool>
</Setting>
<Setting>
<About>
<Title>Enable sounds</Title>
<Description>
Do you want to hear a status sound when a status starts, finishes or fails?
</Description>
</About>
<Tool>
<Toggle onToggle={handleSoundToggle} enabled={soundEnabled} />
</Tool>
</Setting>
<Setting>
<About>
<Title>Hide user avatars</Title>
Expand Down
4 changes: 2 additions & 2 deletions frontend/components/Toggle/Toggle.style.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { stateColor, textMutedColor } from '/frontend/style/colors';

export const Switch = styled.div`
position: absolute;
top: 0.2rem;
right: 2.1rem;
top: 0.25rem;
right: 2.2rem;
width: 1.25rem;
height: 1.25rem;
border-radius: 50%;
Expand Down
6 changes: 4 additions & 2 deletions frontend/hooks/useSetting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { useContext } from 'react';
import { useSelector } from 'react-redux';

import appContext from '../App/AppContext';
import { isHidingUserAvatars, isShowingCompleted } from '../store/settings/selectors';
import { isHidingUserAvatars, isShowingCompleted, isSoundEnabled } from '../store/settings/selectors';

type Setting = 'showCompleted' | 'showAvatars';
type Setting = 'showCompleted' | 'showAvatars' | 'sound';

const useSetting = (setting: Setting): boolean => {
const context = useContext(appContext);
Expand All @@ -18,6 +18,8 @@ const useSetting = (setting: Setting): boolean => {
return !useSelector(isHidingUserAvatars);
case 'showCompleted':
return useSelector(isShowingCompleted);
case 'sound':
return useSelector(isSoundEnabled);
}
};

Expand Down
31 changes: 26 additions & 5 deletions frontend/hooks/useSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { io } from 'socket.io-client';

import useSetting from '/frontend/hooks/useSetting';
import Sounds from '/frontend/sounds/Sounds';
import { addStatus, deleteStatus, patchStatus, setAllStatus } from '/frontend/store/status/actions';

import { socketEvent } from '/types/cimonitor';
import Status from '/types/status';

type UseSocketOutput = {
socketConnected: boolean;
Expand All @@ -13,16 +16,34 @@ type UseSocketOutput = {
const useSocket = (): UseSocketOutput => {
const [socketConnected, setSocketConnected] = useState(false);
const dispatch = useDispatch();
const soundEnabled = useSetting('sound');

useEffect(() => {
const socket = io();

socket.on(socketEvent.connect, () => setSocketConnected(true));
socket.on(socketEvent.disconnect, () => setSocketConnected(false));
socket.on(socketEvent.allStatuses, (statuses) => dispatch(setAllStatus(statuses)));
socket.on(socketEvent.patchStatus, (status) => dispatch(patchStatus(status)));
socket.on(socketEvent.newStatus, (status) => dispatch(addStatus(status)));
socket.on(socketEvent.deleteStatus, (statusId) => dispatch(deleteStatus(statusId)));
socket.on(socketEvent.allStatuses, (statuses: Status[]) => dispatch(setAllStatus(statuses)));
socket.on(socketEvent.patchStatus, (status: Status) => dispatch(patchStatus(status)));
socket.on(socketEvent.newStatus, (status: Status) => dispatch(addStatus(status)));
socket.on(socketEvent.statusStateChange, (status: Status) => {
if (!soundEnabled) {
return;
}

switch (status.state) {
case 'warning':
Sounds.playInfo();
return;
case 'success':
Sounds.playSuccess();
return;
case 'error':
Sounds.playError();
return;
}
});
socket.on(socketEvent.deleteStatus, (statusId: string) => dispatch(deleteStatus(statusId)));

// Refresh all statuses once a day
const requestStatusesInterval = setInterval(() => socket.emit(socketEvent.requestAllStatuses), 60000 * 60 * 24);
Expand All @@ -31,7 +52,7 @@ const useSocket = (): UseSocketOutput => {
socket.disconnect();
clearInterval(requestStatusesInterval);
};
}, []);
}, [soundEnabled]);

return {
socketConnected,
Expand Down
6 changes: 6 additions & 0 deletions frontend/sounds/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Sounds

Included sounds are created by "FoolBoyMedia" and are released under the CC BY 4.0 DEED.
https://creativecommons.org/licenses/by/4.0/

The sounds can be found and downloaded from: https://freesound.org/people/FoolBoyMedia/packs/20086/
32 changes: 32 additions & 0 deletions frontend/sounds/Sounds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import ErrorSound from './error.mp3';
import InfoSound from './info.mp3';
import SuccessSound from './success.mp3';

class Sounds {
info: HTMLAudioElement;
error: HTMLAudioElement;
success: HTMLAudioElement;

constructor() {
this.info = new Audio(InfoSound as string);
this.error = new Audio(ErrorSound as string);
this.success = new Audio(SuccessSound as string);
}

playInfo() {
this.info.currentTime = 0;
this.info.play();
}

playSuccess() {
this.success.currentTime = 0;
this.success.play();
}

playError() {
this.error.currentTime = 0;
this.error.play();
}
}

export default new Sounds();
Binary file added frontend/sounds/error.mp3
Binary file not shown.
Binary file added frontend/sounds/info.mp3
Binary file not shown.
Binary file added frontend/sounds/success.mp3
Binary file not shown.
5 changes: 5 additions & 0 deletions frontend/store/settings/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ToggleSettingsPanelAction,
ToggleShowCompletedAction,
ToggleShowUserAvatarsAction,
ToggleSoundAction,
} from './types';

export const toggleShowCompleted = (): ToggleShowCompletedAction => ({
Expand All @@ -14,6 +15,10 @@ export const toggleShowUserAvatars = (): ToggleShowUserAvatarsAction => ({
type: 'settings-show-user-avatars-toggle',
});

export const toggleSound = (): ToggleSoundAction => ({
type: 'settings-sound-toggle',
});

export const toggleSettingsPanel = (): ToggleSettingsPanelAction => ({
type: 'settings-panel-toggle',
});
Expand Down
6 changes: 6 additions & 0 deletions frontend/store/settings/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const defaultState: StateType = {
showCompleted: false,
sizeModifier: 1,
showUserAvatars: true,
soundEnabled: false,
};

const reducer = (state = defaultState, action: ActionTypes): StateType => {
Expand All @@ -19,6 +20,11 @@ const reducer = (state = defaultState, action: ActionTypes): StateType => {
...state,
open: false,
};
case 'settings-sound-toggle':
return {
...state,
soundEnabled: !state.soundEnabled,
};
case 'settings-show-completed-toggle':
return {
...state,
Expand Down
2 changes: 2 additions & 0 deletions frontend/store/settings/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export const isSettingsPanelOpen = (state: RootState): boolean => state.setting.

export const isShowingCompleted = (state: RootState): boolean => state.setting.showCompleted;

export const isSoundEnabled = (state: RootState): boolean => state.setting.soundEnabled;

export const isHidingUserAvatars = (state: RootState): boolean => !state.setting.showUserAvatars;

export const getSizeModifier = (state: RootState): number =>
Expand Down
8 changes: 7 additions & 1 deletion frontend/store/settings/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export type StateType = {
showCompleted: boolean;
sizeModifier: number;
showUserAvatars: boolean;
soundEnabled: boolean;
};

export type SetSizeModifierAction = {
Expand All @@ -14,6 +15,10 @@ export type ToggleShowCompletedAction = {
type: 'settings-show-completed-toggle';
};

export type ToggleSoundAction = {
type: 'settings-sound-toggle';
};

export type ToggleSettingsPanelAction = {
type: 'settings-panel-toggle';
};
Expand All @@ -31,4 +36,5 @@ export type ActionTypes =
| CloseSettingsPanelAction
| ToggleShowCompletedAction
| SetSizeModifierAction
| ToggleShowUserAvatarsAction;
| ToggleShowUserAvatarsAction
| ToggleSoundAction;
5 changes: 5 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ declare module '*.png' {
const content: string;
export default content;
}

declare module '*.mp3' {
const content: string;
export default content;
}

0 comments on commit bcfa479

Please sign in to comment.