forked from Shiloh/remnantchat
* 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:
parent
e259196942
commit
492cfa58ce
BIN
public/sounds/new-message.aac
Normal file
BIN
public/sounds/new-message.aac
Normal file
Binary file not shown.
@ -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,
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -12,5 +12,6 @@ export const SettingsContext = createContext<SettingsContextProps>({
|
||||
getUserSettings: () => ({
|
||||
userId: '',
|
||||
colorMode: 'dark',
|
||||
playSoundOnNewMessage: true,
|
||||
}),
|
||||
})
|
||||
|
@ -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: () => {},
|
||||
|
@ -1,4 +1,5 @@
|
||||
export interface UserSettings {
|
||||
colorMode: 'dark' | 'light'
|
||||
userId: string
|
||||
playSoundOnNewMessage: boolean
|
||||
}
|
||||
|
@ -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 => ({
|
||||
|
Loading…
Reference in New Issue
Block a user