* 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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user