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',
userId: 'abc123',
playSoundOnNewMessage: true,
showNotificationOnNewMessage: true,
})
})

View File

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

View File

@ -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 (
<Typography component="span" {...rest}>
{funAnimalName(children)}
{getPeerName(children)}
</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 './getPeerName'

View File

@ -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() }])
})

View File

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

View File

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

View File

@ -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])

View File

@ -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 (
<Box className="max-w-3xl mx-auto p-4">
<Typography
@ -64,11 +88,30 @@ export const Settings = ({ userId }: SettingsProps) => {
>
Chat
</Typography>
<Switch
checked={playSoundOnNewMessage}
onChange={handlePlaySoundOnNewMessageChange}
/>{' '}
Play a sound when a new message is received
<Typography>When a message is received in the background:</Typography>
<FormGroup>
<FormControlLabel
control={
<Switch
checked={playSoundOnNewMessage}
onChange={handlePlaySoundOnNewMessageChange}
/>
}
label="Play a sound"
/>
<FormControlLabel
control={
<Switch
checked={
areNotificationsAvailable && showNotificationOnNewMessage
}
onChange={handleShowNotificationOnNewMessageChange}
disabled={!areNotificationsAvailable}
/>
}
label="Show a notification"
/>
</FormGroup>
<Divider sx={{ my: 2 }} />
<Typography
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'