feat: [closes #6] Show notifications for messages recieved in the background (#31)

* feat: [#6] show notification when message is received
* feat: [#6] add setting for enabling/disabling notifications
* refactor: [#6] decouple PeerNameDisplay from funAnimalName
* feat: [#6] disable notifications setting when notifications are unavailable
This commit is contained in:
Jeremy Kahn 2022-09-29 21:56:28 -05:00 committed by GitHub
parent 492cfa58ce
commit b4decae69c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 105 additions and 14 deletions

View File

@ -55,6 +55,7 @@ test('persists user settings if none were already persisted', async () => {
colorMode: 'dark', colorMode: 'dark',
userId: 'abc123', userId: 'abc123',
playSoundOnNewMessage: true, playSoundOnNewMessage: true,
showNotificationOnNewMessage: true,
}) })
}) })

View File

@ -34,6 +34,7 @@ function Bootstrap({
userId: getUuid(), userId: getUuid(),
colorMode: 'dark', colorMode: 'dark',
playSoundOnNewMessage: true, playSoundOnNewMessage: true,
showNotificationOnNewMessage: true,
}) })
const { userId } = userSettings const { userId } = userSettings

View File

@ -1,5 +1,6 @@
import Typography, { TypographyProps } from '@mui/material/Typography' import Typography, { TypographyProps } from '@mui/material/Typography'
import { funAnimalName } from 'fun-animal-names'
import { getPeerName } from './getPeerName'
interface PeerNameDisplayProps extends TypographyProps { interface PeerNameDisplayProps extends TypographyProps {
children: string children: string
@ -11,7 +12,7 @@ export const PeerNameDisplay = ({
}: PeerNameDisplayProps) => { }: PeerNameDisplayProps) => {
return ( return (
<Typography component="span" {...rest}> <Typography component="span" {...rest}>
{funAnimalName(children)} {getPeerName(children)}
</Typography> </Typography>
) )
} }

View File

@ -0,0 +1,5 @@
import { funAnimalName } from 'fun-animal-names'
export const getPeerName = (peerId: string) => {
return funAnimalName(peerId)
}

View File

@ -1 +1,2 @@
export * from './PeerNameDisplay' export * from './PeerNameDisplay'
export * from './getPeerName'

View File

@ -12,6 +12,8 @@ import { PeerActions } from 'models/network'
import { UnsentMessage, ReceivedMessage } from 'models/chat' import { UnsentMessage, ReceivedMessage } from 'models/chat'
import { MessageForm } from 'components/MessageForm' import { MessageForm } from 'components/MessageForm'
import { ChatTranscript } from 'components/ChatTranscript' import { ChatTranscript } from 'components/ChatTranscript'
import { getPeerName } from 'components/PeerNameDisplay'
import { NotificationService } from 'services/Notification'
export interface RoomProps { export interface RoomProps {
appId?: string appId?: string
@ -45,6 +47,7 @@ export function Room({
roomId roomId
) )
// TODO: Move audio logic to a service
useEffect(() => { useEffect(() => {
;(async () => { ;(async () => {
try { try {
@ -117,9 +120,19 @@ export function Room({
receiveMessage(message => { receiveMessage(message => {
const userSettings = settingsContext.getUserSettings() const userSettings = settingsContext.getUserSettings()
!shellContext.tabHasFocus &&
userSettings.playSoundOnNewMessage && if (!shellContext.tabHasFocus) {
if (userSettings.playSoundOnNewMessage) {
playNewMessageSound() playNewMessageSound()
}
if (userSettings.showNotificationOnNewMessage) {
NotificationService.showNotification(
`${getPeerName(message.authorId)}: ${message.text}`
)
}
}
setMessageLog([...messageLog, { ...message, timeReceived: Date.now() }]) setMessageLog([...messageLog, { ...message, timeReceived: Date.now() }])
}) })

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import { Room } from 'components/Room'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
import { NotificationService } from 'services/Notification'
interface PublicRoomProps { interface PublicRoomProps {
userId: string userId: string
@ -12,6 +13,10 @@ export function PublicRoom({ userId }: PublicRoomProps) {
const { roomId = '' } = useParams() const { roomId = '' } = useParams()
const { setTitle } = useContext(ShellContext) const { setTitle } = useContext(ShellContext)
useEffect(() => {
NotificationService.requestPermission()
}, [])
useEffect(() => { useEffect(() => {
setTitle(`Room: ${roomId}`) setTitle(`Room: ${roomId}`)
}, [roomId, setTitle]) }, [roomId, setTitle])

View File

@ -3,8 +3,11 @@ import Box from '@mui/material/Box'
import Button from '@mui/material/Button' import Button from '@mui/material/Button'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import Divider from '@mui/material/Divider' 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 { ShellContext } from 'contexts/ShellContext'
import { StorageContext } from 'contexts/StorageContext' import { StorageContext } from 'contexts/StorageContext'
import { PeerNameDisplay } from 'components/PeerNameDisplay' import { PeerNameDisplay } from 'components/PeerNameDisplay'
@ -24,19 +27,38 @@ export const Settings = ({ userId }: SettingsProps) => {
isDeleteSettingsConfirmDiaglogOpen, isDeleteSettingsConfirmDiaglogOpen,
setIsDeleteSettingsConfirmDiaglogOpen, setIsDeleteSettingsConfirmDiaglogOpen,
] = useState(false) ] = useState(false)
const { playSoundOnNewMessage } = getUserSettings() const [, setIsNotificationPermissionDetermined] = useState(false)
const { playSoundOnNewMessage, showNotificationOnNewMessage } =
getUserSettings()
const persistedStorage = getPersistedStorage() 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(() => { useEffect(() => {
setTitle('Settings') setTitle('Settings')
}, [setTitle]) }, [setTitle])
const handlePlaySoundOnNewMessageChange = ( const handlePlaySoundOnNewMessageChange = (
_event: ChangeEvent, _event: ChangeEvent,
value: boolean playSoundOnNewMessage: boolean
) => { ) => {
updateUserSettings({ playSoundOnNewMessage: value }) updateUserSettings({ playSoundOnNewMessage })
}
const handleShowNotificationOnNewMessageChange = (
_event: ChangeEvent,
showNotificationOnNewMessage: boolean
) => {
updateUserSettings({ showNotificationOnNewMessage })
} }
const handleDeleteSettingsClick = () => { const handleDeleteSettingsClick = () => {
@ -52,6 +74,8 @@ export const Settings = ({ userId }: SettingsProps) => {
window.location.reload() window.location.reload()
} }
const areNotificationsAvailable = NotificationService.permission === 'granted'
return ( return (
<Box className="max-w-3xl mx-auto p-4"> <Box className="max-w-3xl mx-auto p-4">
<Typography <Typography
@ -64,11 +88,30 @@ export const Settings = ({ userId }: SettingsProps) => {
> >
Chat Chat
</Typography> </Typography>
<Typography>When a message is received in the background:</Typography>
<FormGroup>
<FormControlLabel
control={
<Switch <Switch
checked={playSoundOnNewMessage} checked={playSoundOnNewMessage}
onChange={handlePlaySoundOnNewMessageChange} onChange={handlePlaySoundOnNewMessageChange}
/>{' '} />
Play a sound when a new message is received }
label="Play a sound"
/>
<FormControlLabel
control={
<Switch
checked={
areNotificationsAvailable && showNotificationOnNewMessage
}
onChange={handleShowNotificationOnNewMessageChange}
disabled={!areNotificationsAvailable}
/>
}
label="Show a notification"
/>
</FormGroup>
<Divider sx={{ my: 2 }} /> <Divider sx={{ my: 2 }} />
<Typography <Typography
variant="h2" variant="h2"

View File

@ -0,0 +1,18 @@
export class NotificationService {
static permission: NotificationPermission
static requestPermission = async () => {
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)
}
}

View File

@ -0,0 +1 @@
export * from './Notification'