feat: [#7] Play a sound on new message (#25)

* feat: [#7] Play a sound on new message

* fix: [#7] Since this mock is a no-op, I think we can omit the argument to mockImplementation

Co-authored-by: Jeremy Kahn <jeremyckahn@gmail.com>

* fix: [#7] lazy initialization of this state

Co-authored-by: Jeremy Kahn <jeremyckahn@gmail.com>

* fix: [#7] More accurate error message

Co-authored-by: Jeremy Kahn <jeremyckahn@gmail.com>

* fix: [#7] Replace then with await

* [closes #24]  Settings UI (#26)

* feat: [#24] wire up settings page
* feat: [#24] stand up settings UI
* feat: [#24] implement storage deletion
* feat: [#24] confirm deletion of settings data

* feat: [#7] Add play sound switch in settings

* feat: [#7] avoid typescript warning

Co-authored-by: Jeremy Kahn <jeremyckahn@gmail.com>

* feat: [#7] more straighforward wording

Co-authored-by: Jeremy Kahn <jeremyckahn@gmail.com>

* feat: [#7] remove useless usestate

* feat: [#7] avoid new settings to be undefined in persisted storage

* feat: [#7] creating a chat section in settings

Co-authored-by: Jeremy Kahn <jeremyckahn@gmail.com>
This commit is contained in:
Flaykz 2022-09-27 00:10:31 +11:00 committed by GitHub
parent e259196942
commit 492cfa58ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 97 additions and 4 deletions

Binary file not shown.

View File

@ -54,6 +54,7 @@ test('persists user settings if none were already persisted', async () => {
expect(mockSetItem).toHaveBeenCalledWith(PersistedStorageKeys.USER_SETTINGS, {
colorMode: 'dark',
userId: 'abc123',
playSoundOnNewMessage: true,
})
})

View File

@ -33,6 +33,7 @@ function Bootstrap({
const [userSettings, setUserSettings] = useState<UserSettings>({
userId: getUuid(),
colorMode: 'dark',
playSoundOnNewMessage: true,
})
const { userId } = userSettings
@ -54,7 +55,7 @@ function Bootstrap({
)
if (persistedUserSettings) {
setUserSettings(persistedUserSettings)
setUserSettings({ ...userSettings, ...persistedUserSettings })
} else {
await persistedStorageProp.setItem(
PersistedStorageKeys.USER_SETTINGS,

View File

@ -8,6 +8,7 @@ import { Room } from './'
const mockUserId = 'user-id'
const mockRoomId = 'room-123'
window.AudioContext = jest.fn().mockImplementation()
const mockGetUuid = jest.fn()
const mockMessagedSender = jest
.fn()

View File

@ -1,4 +1,4 @@
import { useContext, useEffect, useState } from 'react'
import { useContext, useEffect, useRef, useState } from 'react'
import { v4 as uuid } from 'uuid'
import Box from '@mui/material/Box'
import Divider from '@mui/material/Divider'
@ -6,6 +6,7 @@ import Divider from '@mui/material/Divider'
import { rtcConfig } from 'config/rtcConfig'
import { trackerUrls } from 'config/trackerUrls'
import { ShellContext } from 'contexts/ShellContext'
import { SettingsContext } from 'contexts/SettingsContext'
import { usePeerRoom, usePeerRoomAction } from 'hooks/usePeerRoom'
import { PeerActions } from 'models/network'
import { UnsentMessage, ReceivedMessage } from 'models/chat'
@ -27,10 +28,13 @@ export function Room({
}: RoomProps) {
const [numberOfPeers, setNumberOfPeers] = useState(1) // Includes this peer
const shellContext = useContext(ShellContext)
const settingsContext = useContext(SettingsContext)
const [isMessageSending, setIsMessageSending] = useState(false)
const [messageLog, setMessageLog] = useState<
Array<ReceivedMessage | UnsentMessage>
>([])
const [audioContext] = useState(() => new AudioContext())
const audioBufferContainer = useRef<AudioBuffer | null>(null)
const peerRoom = usePeerRoom(
{
@ -41,6 +45,22 @@ export function Room({
roomId
)
useEffect(() => {
;(async () => {
try {
const response = await fetch(
process.env.PUBLIC_URL + '/sounds/new-message.aac'
)
const arrayBuffer = await response.arrayBuffer()
audioBufferContainer.current = await audioContext.decodeAudioData(
arrayBuffer
)
} catch (e) {
console.error(e)
}
})()
}, [audioBufferContainer, audioContext])
useEffect(() => {
shellContext.setDoShowPeers(true)
@ -96,9 +116,24 @@ export function Room({
}
receiveMessage(message => {
const userSettings = settingsContext.getUserSettings()
!shellContext.tabHasFocus &&
userSettings.playSoundOnNewMessage &&
playNewMessageSound()
setMessageLog([...messageLog, { ...message, timeReceived: Date.now() }])
})
const playNewMessageSound = () => {
if (!audioBufferContainer.current) {
console.error('Audio buffer not available')
return
}
const audioSource = audioContext.createBufferSource()
audioSource.buffer = audioBufferContainer.current
audioSource.connect(audioContext.destination)
audioSource.start()
}
const handleMessageSubmit = async (message: string) => {
await performMessageSend(message)
}

View File

@ -36,6 +36,7 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
const [title, setTitle] = useState('')
const [alertText, setAlertText] = useState('')
const [numberOfPeers, setNumberOfPeers] = useState(1)
const [tabHasFocus, setTabHasFocus] = useState(true)
const showAlert = useCallback<
(message: string, options?: AlertOptions) => void
@ -48,12 +49,20 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
const shellContextValue = useMemo(
() => ({
numberOfPeers,
tabHasFocus,
setDoShowPeers,
setNumberOfPeers,
setTitle,
showAlert,
}),
[numberOfPeers, setDoShowPeers, setNumberOfPeers, setTitle, showAlert]
[
numberOfPeers,
tabHasFocus,
setDoShowPeers,
setNumberOfPeers,
setTitle,
showAlert,
]
)
const colorMode = settingsContext.getUserSettings().colorMode
@ -83,6 +92,21 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
document.title = title
}, [title])
useEffect(() => {
const handleFocus = () => {
setTabHasFocus(true)
}
const handleBlur = () => {
setTabHasFocus(false)
}
window.addEventListener('focus', handleFocus)
window.addEventListener('blur', handleBlur)
return () => {
window.removeEventListener('focus', handleFocus)
window.removeEventListener('blur', handleBlur)
}
}, [])
const handleDrawerOpen = () => {
setIsDrawerOpen(true)
}

View File

@ -12,5 +12,6 @@ export const SettingsContext = createContext<SettingsContextProps>({
getUserSettings: () => ({
userId: '',
colorMode: 'dark',
playSoundOnNewMessage: true,
}),
})

View File

@ -4,6 +4,7 @@ import { AlertOptions } from 'models/shell'
interface ShellContextProps {
numberOfPeers: number
tabHasFocus: boolean
setDoShowPeers: Dispatch<SetStateAction<boolean>>
setNumberOfPeers: Dispatch<SetStateAction<number>>
setTitle: Dispatch<SetStateAction<string>>
@ -12,6 +13,7 @@ interface ShellContextProps {
export const ShellContext = createContext<ShellContextProps>({
numberOfPeers: 1,
tabHasFocus: true,
setDoShowPeers: () => {},
setNumberOfPeers: () => {},
setTitle: () => {},

View File

@ -1,4 +1,5 @@
export interface UserSettings {
colorMode: 'dark' | 'light'
userId: string
playSoundOnNewMessage: boolean
}

View File

@ -1,14 +1,16 @@
import { useContext, useEffect, useState } from 'react'
import { ChangeEvent, useContext, useEffect, useState } from 'react'
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import Typography from '@mui/material/Typography'
import Divider from '@mui/material/Divider'
import { Switch } from '@mui/material'
import { ShellContext } from 'contexts/ShellContext'
import { StorageContext } from 'contexts/StorageContext'
import { PeerNameDisplay } from 'components/PeerNameDisplay'
import { ConfirmDialog } from '../../components/ConfirmDialog'
import { SettingsContext } from '../../contexts/SettingsContext'
interface SettingsProps {
userId: string
@ -16,11 +18,13 @@ interface SettingsProps {
export const Settings = ({ userId }: SettingsProps) => {
const { setTitle } = useContext(ShellContext)
const { updateUserSettings, getUserSettings } = useContext(SettingsContext)
const { getPersistedStorage } = useContext(StorageContext)
const [
isDeleteSettingsConfirmDiaglogOpen,
setIsDeleteSettingsConfirmDiaglogOpen,
] = useState(false)
const { playSoundOnNewMessage } = getUserSettings()
const persistedStorage = getPersistedStorage()
@ -28,6 +32,13 @@ export const Settings = ({ userId }: SettingsProps) => {
setTitle('Settings')
}, [setTitle])
const handlePlaySoundOnNewMessageChange = (
_event: ChangeEvent,
value: boolean
) => {
updateUserSettings({ playSoundOnNewMessage: value })
}
const handleDeleteSettingsClick = () => {
setIsDeleteSettingsConfirmDiaglogOpen(true)
}
@ -43,6 +54,22 @@ export const Settings = ({ userId }: SettingsProps) => {
return (
<Box className="max-w-3xl mx-auto p-4">
<Typography
variant="h2"
sx={theme => ({
fontSize: theme.typography.h3.fontSize,
fontWeight: theme.typography.fontWeightMedium,
mb: 2,
})}
>
Chat
</Typography>
<Switch
checked={playSoundOnNewMessage}
onChange={handlePlaySoundOnNewMessageChange}
/>{' '}
Play a sound when a new message is received
<Divider sx={{ my: 2 }} />
<Typography
variant="h2"
sx={theme => ({