diff --git a/src/Bootstrap.test.tsx b/src/Bootstrap.test.tsx index effc0dc..99a9f74 100644 --- a/src/Bootstrap.test.tsx +++ b/src/Bootstrap.test.tsx @@ -55,6 +55,7 @@ test('persists user settings if none were already persisted', async () => { colorMode: 'dark', userId: 'abc123', playSoundOnNewMessage: true, + showNotificationOnNewMessage: true, }) }) diff --git a/src/Bootstrap.tsx b/src/Bootstrap.tsx index 1cfba9c..91c3fcd 100644 --- a/src/Bootstrap.tsx +++ b/src/Bootstrap.tsx @@ -34,6 +34,7 @@ function Bootstrap({ userId: getUuid(), colorMode: 'dark', playSoundOnNewMessage: true, + showNotificationOnNewMessage: true, }) const { userId } = userSettings diff --git a/src/components/PeerNameDisplay/PeerNameDisplay.tsx b/src/components/PeerNameDisplay/PeerNameDisplay.tsx index 10d5186..34b6128 100644 --- a/src/components/PeerNameDisplay/PeerNameDisplay.tsx +++ b/src/components/PeerNameDisplay/PeerNameDisplay.tsx @@ -1,5 +1,6 @@ import Typography, { TypographyProps } from '@mui/material/Typography' -import { funAnimalName } from 'fun-animal-names' + +import { getPeerName } from './getPeerName' interface PeerNameDisplayProps extends TypographyProps { children: string @@ -11,7 +12,7 @@ export const PeerNameDisplay = ({ }: PeerNameDisplayProps) => { return ( - {funAnimalName(children)} + {getPeerName(children)} ) } diff --git a/src/components/PeerNameDisplay/getPeerName.ts b/src/components/PeerNameDisplay/getPeerName.ts new file mode 100644 index 0000000..97cc8de --- /dev/null +++ b/src/components/PeerNameDisplay/getPeerName.ts @@ -0,0 +1,5 @@ +import { funAnimalName } from 'fun-animal-names' + +export const getPeerName = (peerId: string) => { + return funAnimalName(peerId) +} diff --git a/src/components/PeerNameDisplay/index.ts b/src/components/PeerNameDisplay/index.ts index 77f427c..593dfcf 100644 --- a/src/components/PeerNameDisplay/index.ts +++ b/src/components/PeerNameDisplay/index.ts @@ -1 +1,2 @@ export * from './PeerNameDisplay' +export * from './getPeerName' diff --git a/src/components/Room/Room.tsx b/src/components/Room/Room.tsx index 0253378..265d779 100644 --- a/src/components/Room/Room.tsx +++ b/src/components/Room/Room.tsx @@ -12,6 +12,8 @@ import { PeerActions } from 'models/network' import { UnsentMessage, ReceivedMessage } from 'models/chat' import { MessageForm } from 'components/MessageForm' import { ChatTranscript } from 'components/ChatTranscript' +import { getPeerName } from 'components/PeerNameDisplay' +import { NotificationService } from 'services/Notification' export interface RoomProps { appId?: string @@ -45,6 +47,7 @@ export function Room({ roomId ) + // TODO: Move audio logic to a service useEffect(() => { ;(async () => { try { @@ -117,9 +120,19 @@ export function Room({ receiveMessage(message => { const userSettings = settingsContext.getUserSettings() - !shellContext.tabHasFocus && - userSettings.playSoundOnNewMessage && - playNewMessageSound() + + if (!shellContext.tabHasFocus) { + if (userSettings.playSoundOnNewMessage) { + playNewMessageSound() + } + + if (userSettings.showNotificationOnNewMessage) { + NotificationService.showNotification( + `${getPeerName(message.authorId)}: ${message.text}` + ) + } + } + setMessageLog([...messageLog, { ...message, timeReceived: Date.now() }]) }) diff --git a/src/contexts/SettingsContext.ts b/src/contexts/SettingsContext.ts index 0cda8a0..25467d9 100644 --- a/src/contexts/SettingsContext.ts +++ b/src/contexts/SettingsContext.ts @@ -13,5 +13,6 @@ export const SettingsContext = createContext({ userId: '', colorMode: 'dark', playSoundOnNewMessage: true, + showNotificationOnNewMessage: true, }), }) diff --git a/src/models/settings.ts b/src/models/settings.ts index ae32eb6..00b7a57 100644 --- a/src/models/settings.ts +++ b/src/models/settings.ts @@ -2,4 +2,5 @@ export interface UserSettings { colorMode: 'dark' | 'light' userId: string playSoundOnNewMessage: boolean + showNotificationOnNewMessage: boolean } diff --git a/src/pages/PublicRoom/PublicRoom.tsx b/src/pages/PublicRoom/PublicRoom.tsx index 24833c3..34da391 100644 --- a/src/pages/PublicRoom/PublicRoom.tsx +++ b/src/pages/PublicRoom/PublicRoom.tsx @@ -3,6 +3,7 @@ import { Room } from 'components/Room' import { useParams } from 'react-router-dom' import { ShellContext } from 'contexts/ShellContext' +import { NotificationService } from 'services/Notification' interface PublicRoomProps { userId: string @@ -12,6 +13,10 @@ export function PublicRoom({ userId }: PublicRoomProps) { const { roomId = '' } = useParams() const { setTitle } = useContext(ShellContext) + useEffect(() => { + NotificationService.requestPermission() + }, []) + useEffect(() => { setTitle(`Room: ${roomId}`) }, [roomId, setTitle]) diff --git a/src/pages/Settings/Settings.tsx b/src/pages/Settings/Settings.tsx index 588fa38..7257b9a 100644 --- a/src/pages/Settings/Settings.tsx +++ b/src/pages/Settings/Settings.tsx @@ -3,8 +3,11 @@ 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 Switch from '@mui/material/Switch' +import FormGroup from '@mui/material/FormGroup' +import FormControlLabel from '@mui/material/FormControlLabel' +import { NotificationService } from 'services/Notification' import { ShellContext } from 'contexts/ShellContext' import { StorageContext } from 'contexts/StorageContext' import { PeerNameDisplay } from 'components/PeerNameDisplay' @@ -24,19 +27,38 @@ export const Settings = ({ userId }: SettingsProps) => { isDeleteSettingsConfirmDiaglogOpen, setIsDeleteSettingsConfirmDiaglogOpen, ] = useState(false) - const { playSoundOnNewMessage } = getUserSettings() + const [, setIsNotificationPermissionDetermined] = useState(false) + const { playSoundOnNewMessage, showNotificationOnNewMessage } = + getUserSettings() const persistedStorage = getPersistedStorage() + useEffect(() => { + ;(async () => { + await NotificationService.requestPermission() + + // This state needs to be set to cause a rerender so that + // areNotificationsAvailable is up-to-date. + setIsNotificationPermissionDetermined(true) + })() + }, []) + useEffect(() => { setTitle('Settings') }, [setTitle]) const handlePlaySoundOnNewMessageChange = ( _event: ChangeEvent, - value: boolean + playSoundOnNewMessage: boolean ) => { - updateUserSettings({ playSoundOnNewMessage: value }) + updateUserSettings({ playSoundOnNewMessage }) + } + + const handleShowNotificationOnNewMessageChange = ( + _event: ChangeEvent, + showNotificationOnNewMessage: boolean + ) => { + updateUserSettings({ showNotificationOnNewMessage }) } const handleDeleteSettingsClick = () => { @@ -52,6 +74,8 @@ export const Settings = ({ userId }: SettingsProps) => { window.location.reload() } + const areNotificationsAvailable = NotificationService.permission === 'granted' + return ( { > Chat - {' '} - Play a sound when a new message is received + When a message is received in the background: + + + } + label="Play a sound" + /> + + } + label="Show a notification" + /> + { + if (NotificationService.permission === 'granted') return + + NotificationService.permission = await Notification.requestPermission() + } + + static showNotification = ( + message: string, + options?: NotificationOptions + ) => { + if (NotificationService.permission !== 'granted') return + + new Notification(message, options) + } +} diff --git a/src/services/Notification/index.ts b/src/services/Notification/index.ts new file mode 100644 index 0000000..934dbb2 --- /dev/null +++ b/src/services/Notification/index.ts @@ -0,0 +1 @@ +export * from './Notification'