forked from Shiloh/remnantchat
* 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:
parent
492cfa58ce
commit
b4decae69c
@ -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,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
5
src/components/PeerNameDisplay/getPeerName.ts
Normal file
5
src/components/PeerNameDisplay/getPeerName.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { funAnimalName } from 'fun-animal-names'
|
||||||
|
|
||||||
|
export const getPeerName = (peerId: string) => {
|
||||||
|
return funAnimalName(peerId)
|
||||||
|
}
|
@ -1 +1,2 @@
|
|||||||
export * from './PeerNameDisplay'
|
export * from './PeerNameDisplay'
|
||||||
|
export * from './getPeerName'
|
||||||
|
@ -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() }])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -13,5 +13,6 @@ export const SettingsContext = createContext<SettingsContextProps>({
|
|||||||
userId: '',
|
userId: '',
|
||||||
colorMode: 'dark',
|
colorMode: 'dark',
|
||||||
playSoundOnNewMessage: true,
|
playSoundOnNewMessage: true,
|
||||||
|
showNotificationOnNewMessage: true,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
@ -2,4 +2,5 @@ export interface UserSettings {
|
|||||||
colorMode: 'dark' | 'light'
|
colorMode: 'dark' | 'light'
|
||||||
userId: string
|
userId: string
|
||||||
playSoundOnNewMessage: boolean
|
playSoundOnNewMessage: boolean
|
||||||
|
showNotificationOnNewMessage: boolean
|
||||||
}
|
}
|
||||||
|
@ -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])
|
||||||
|
@ -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"
|
||||||
|
18
src/services/Notification/Notification.tsx
Normal file
18
src/services/Notification/Notification.tsx
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
1
src/services/Notification/index.ts
Normal file
1
src/services/Notification/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './Notification'
|
Loading…
Reference in New Issue
Block a user