forked from Shiloh/remnantchat
* feat: [#24] wire up settings page * feat: [#24] stand up settings UI * feat: [#24] implement storage deletion * feat: [#24] confirm deletion of settings data
This commit is contained in:
parent
20d8337082
commit
e259196942
@ -4,10 +4,12 @@ import { v4 as uuid } from 'uuid'
|
||||
import localforage from 'localforage'
|
||||
|
||||
import * as serviceWorkerRegistration from 'serviceWorkerRegistration'
|
||||
import { StorageContext } from 'contexts/StorageContext'
|
||||
import { SettingsContext } from 'contexts/SettingsContext'
|
||||
import { routes } from 'config/routes'
|
||||
import { Home } from 'pages/Home'
|
||||
import { About } from 'pages/About'
|
||||
import { Settings } from 'pages/Settings'
|
||||
import { PublicRoom } from 'pages/PublicRoom'
|
||||
import { UserSettings } from 'models/settings'
|
||||
import { PersistedStorageKeys } from 'models/storage'
|
||||
@ -19,12 +21,13 @@ export interface BootstrapProps {
|
||||
}
|
||||
|
||||
function Bootstrap({
|
||||
persistedStorage = localforage.createInstance({
|
||||
persistedStorage: persistedStorageProp = localforage.createInstance({
|
||||
name: 'chitchatter',
|
||||
description: 'Persisted settings data for chitchatter',
|
||||
}),
|
||||
getUuid = uuid,
|
||||
}: BootstrapProps) {
|
||||
const [persistedStorage] = useState(persistedStorageProp)
|
||||
const [appNeedsUpdate, setAppNeedsUpdate] = useState(false)
|
||||
const [hasLoadedSettings, setHasLoadedSettings] = useState(false)
|
||||
const [userSettings, setUserSettings] = useState<UserSettings>({
|
||||
@ -46,14 +49,14 @@ function Bootstrap({
|
||||
if (hasLoadedSettings) return
|
||||
|
||||
const persistedUserSettings =
|
||||
await persistedStorage.getItem<UserSettings>(
|
||||
await persistedStorageProp.getItem<UserSettings>(
|
||||
PersistedStorageKeys.USER_SETTINGS
|
||||
)
|
||||
|
||||
if (persistedUserSettings) {
|
||||
setUserSettings(persistedUserSettings)
|
||||
} else {
|
||||
await persistedStorage.setItem(
|
||||
await persistedStorageProp.setItem(
|
||||
PersistedStorageKeys.USER_SETTINGS,
|
||||
userSettings
|
||||
)
|
||||
@ -61,7 +64,7 @@ function Bootstrap({
|
||||
|
||||
setHasLoadedSettings(true)
|
||||
})()
|
||||
}, [hasLoadedSettings, persistedStorage, userSettings, userId])
|
||||
}, [hasLoadedSettings, persistedStorageProp, userSettings, userId])
|
||||
|
||||
const settingsContextValue = {
|
||||
updateUserSettings: async (changedSettings: Partial<UserSettings>) => {
|
||||
@ -70,7 +73,7 @@ function Bootstrap({
|
||||
...changedSettings,
|
||||
}
|
||||
|
||||
await persistedStorage.setItem(
|
||||
await persistedStorageProp.setItem(
|
||||
PersistedStorageKeys.USER_SETTINGS,
|
||||
newSettings
|
||||
)
|
||||
@ -80,30 +83,40 @@ function Bootstrap({
|
||||
getUserSettings: () => ({ ...userSettings }),
|
||||
}
|
||||
|
||||
const storageContextValue = {
|
||||
getPersistedStorage: () => persistedStorage,
|
||||
}
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<SettingsContext.Provider value={settingsContextValue}>
|
||||
<Shell appNeedsUpdate={appNeedsUpdate} userPeerId={userId}>
|
||||
{hasLoadedSettings ? (
|
||||
<Routes>
|
||||
{[routes.ROOT, routes.INDEX_HTML].map(path => (
|
||||
<StorageContext.Provider value={storageContextValue}>
|
||||
<SettingsContext.Provider value={settingsContextValue}>
|
||||
<Shell appNeedsUpdate={appNeedsUpdate} userPeerId={userId}>
|
||||
{hasLoadedSettings ? (
|
||||
<Routes>
|
||||
{[routes.ROOT, routes.INDEX_HTML].map(path => (
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
element={<Home userId={userId} />}
|
||||
/>
|
||||
))}
|
||||
<Route path={routes.ABOUT} element={<About />} />
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
element={<Home userId={userId} />}
|
||||
path={routes.SETTINGS}
|
||||
element={<Settings userId={userId} />}
|
||||
/>
|
||||
))}
|
||||
<Route path={routes.ABOUT} element={<About />} />
|
||||
<Route
|
||||
path={routes.PUBLIC_ROOM}
|
||||
element={<PublicRoom userId={userId} />}
|
||||
/>
|
||||
</Routes>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Shell>
|
||||
</SettingsContext.Provider>
|
||||
<Route
|
||||
path={routes.PUBLIC_ROOM}
|
||||
element={<PublicRoom userId={userId} />}
|
||||
/>
|
||||
</Routes>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Shell>
|
||||
</SettingsContext.Provider>
|
||||
</StorageContext.Provider>
|
||||
</Router>
|
||||
)
|
||||
}
|
||||
|
59
src/components/ConfirmDialog/ConfirmDialog.tsx
Normal file
59
src/components/ConfirmDialog/ConfirmDialog.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import Box from '@mui/material/Box'
|
||||
import Button from '@mui/material/Button'
|
||||
import Dialog from '@mui/material/Dialog'
|
||||
import DialogActions from '@mui/material/DialogActions'
|
||||
import DialogContent from '@mui/material/DialogContent'
|
||||
import DialogContentText from '@mui/material/DialogContentText'
|
||||
import DialogTitle from '@mui/material/DialogTitle'
|
||||
import WarningIcon from '@mui/icons-material/Warning'
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
isOpen: boolean
|
||||
onCancel: () => void
|
||||
onConfirm: () => void
|
||||
}
|
||||
|
||||
export const ConfirmDialog = ({
|
||||
isOpen,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: ConfirmDialogProps) => {
|
||||
const handleDialogClose = () => {
|
||||
onCancel()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
onClose={handleDialogClose}
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<WarningIcon
|
||||
fontSize="medium"
|
||||
sx={theme => ({
|
||||
color: theme.palette.warning.main,
|
||||
mr: theme.spacing(1),
|
||||
})}
|
||||
/>
|
||||
Are you sure?
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
This action cannot be undone.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel} autoFocus>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={onConfirm} color="error">
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
1
src/components/ConfirmDialog/index.ts
Normal file
1
src/components/ConfirmDialog/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './ConfirmDialog'
|
@ -13,6 +13,7 @@ import ListItemButton from '@mui/material/ListItemButton'
|
||||
import ListItemIcon from '@mui/material/ListItemIcon'
|
||||
import ListItemText from '@mui/material/ListItemText'
|
||||
import Home from '@mui/icons-material/Home'
|
||||
import SettingsApplications from '@mui/icons-material/SettingsRounded'
|
||||
import QuestionMark from '@mui/icons-material/QuestionMark'
|
||||
import Brightness4Icon from '@mui/icons-material/Brightness4'
|
||||
import Brightness7Icon from '@mui/icons-material/Brightness7'
|
||||
@ -27,18 +28,20 @@ export const drawerWidth = 240
|
||||
|
||||
export interface DrawerProps extends PropsWithChildren {
|
||||
isDrawerOpen: boolean
|
||||
onAboutLinkClick: () => void
|
||||
onDrawerClose: () => void
|
||||
onHomeLinkClick: () => void
|
||||
onAboutLinkClick: () => void
|
||||
onSettingsLinkClick: () => void
|
||||
theme: Theme
|
||||
userPeerId: string
|
||||
}
|
||||
|
||||
export const Drawer = ({
|
||||
isDrawerOpen,
|
||||
onAboutLinkClick,
|
||||
onDrawerClose,
|
||||
onHomeLinkClick,
|
||||
onAboutLinkClick,
|
||||
onSettingsLinkClick,
|
||||
theme,
|
||||
userPeerId,
|
||||
}: DrawerProps) => {
|
||||
@ -101,6 +104,16 @@ export const Drawer = ({
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</Link>
|
||||
<Link to={routes.SETTINGS} onClick={onSettingsLinkClick}>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton>
|
||||
<ListItemIcon>
|
||||
<SettingsApplications />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Settings" />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</Link>
|
||||
<Link to={routes.ABOUT} onClick={onAboutLinkClick}>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton>
|
||||
|
@ -107,6 +107,10 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
||||
setIsDrawerOpen(false)
|
||||
}
|
||||
|
||||
const handleSettingsLinkClick = () => {
|
||||
setIsDrawerOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<ShellContext.Provider value={shellContextValue}>
|
||||
<ThemeProvider theme={theme}>
|
||||
@ -135,9 +139,10 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
||||
/>
|
||||
<Drawer
|
||||
isDrawerOpen={isDrawerOpen}
|
||||
onAboutLinkClick={handleAboutLinkClick}
|
||||
onDrawerClose={handleDrawerClose}
|
||||
onHomeLinkClick={handleHomeLinkClick}
|
||||
onAboutLinkClick={handleAboutLinkClick}
|
||||
onSettingsLinkClick={handleSettingsLinkClick}
|
||||
theme={theme}
|
||||
userPeerId={userPeerId}
|
||||
/>
|
||||
|
@ -4,4 +4,5 @@ export enum routes {
|
||||
INDEX_HTML = '/index.html',
|
||||
PUBLIC_ROOM = '/public/:roomId',
|
||||
ROOT = '/',
|
||||
SETTINGS = '/settings',
|
||||
}
|
||||
|
10
src/contexts/StorageContext.ts
Normal file
10
src/contexts/StorageContext.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { createContext } from 'react'
|
||||
import localforage from 'localforage'
|
||||
|
||||
interface StorageContextProps {
|
||||
getPersistedStorage: () => typeof localforage
|
||||
}
|
||||
|
||||
export const StorageContext = createContext<StorageContextProps>({
|
||||
getPersistedStorage: () => localforage,
|
||||
})
|
114
src/pages/Settings/Settings.tsx
Normal file
114
src/pages/Settings/Settings.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import { 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 { ShellContext } from 'contexts/ShellContext'
|
||||
import { StorageContext } from 'contexts/StorageContext'
|
||||
import { PeerNameDisplay } from 'components/PeerNameDisplay'
|
||||
|
||||
import { ConfirmDialog } from '../../components/ConfirmDialog'
|
||||
|
||||
interface SettingsProps {
|
||||
userId: string
|
||||
}
|
||||
|
||||
export const Settings = ({ userId }: SettingsProps) => {
|
||||
const { setTitle } = useContext(ShellContext)
|
||||
const { getPersistedStorage } = useContext(StorageContext)
|
||||
const [
|
||||
isDeleteSettingsConfirmDiaglogOpen,
|
||||
setIsDeleteSettingsConfirmDiaglogOpen,
|
||||
] = useState(false)
|
||||
|
||||
const persistedStorage = getPersistedStorage()
|
||||
|
||||
useEffect(() => {
|
||||
setTitle('Settings')
|
||||
}, [setTitle])
|
||||
|
||||
const handleDeleteSettingsClick = () => {
|
||||
setIsDeleteSettingsConfirmDiaglogOpen(true)
|
||||
}
|
||||
|
||||
const handleDeleteSettingsCancel = () => {
|
||||
setIsDeleteSettingsConfirmDiaglogOpen(false)
|
||||
}
|
||||
|
||||
const handleDeleteSettingsConfirm = async () => {
|
||||
await persistedStorage.clear()
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
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,
|
||||
})}
|
||||
>
|
||||
Data
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h2"
|
||||
sx={theme => ({
|
||||
fontSize: theme.typography.h5.fontSize,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
mb: 1.5,
|
||||
})}
|
||||
>
|
||||
Delete all settings data
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={_theme => ({
|
||||
mb: 2,
|
||||
})}
|
||||
>
|
||||
<strong>Be careful with this</strong>. This will cause your user name to
|
||||
change from{' '}
|
||||
<strong>
|
||||
<PeerNameDisplay
|
||||
sx={theme => ({
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
})}
|
||||
>
|
||||
{userId}
|
||||
</PeerNameDisplay>
|
||||
</strong>{' '}
|
||||
to a new, randomly-assigned name. It will also reset all of your saved
|
||||
Chitchatter application preferences.
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
sx={_theme => ({
|
||||
mb: 2,
|
||||
})}
|
||||
onClick={handleDeleteSettingsClick}
|
||||
>
|
||||
Delete all data and restart
|
||||
</Button>
|
||||
<ConfirmDialog
|
||||
isOpen={isDeleteSettingsConfirmDiaglogOpen}
|
||||
onCancel={handleDeleteSettingsCancel}
|
||||
onConfirm={handleDeleteSettingsConfirm}
|
||||
/>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={_theme => ({
|
||||
mb: 2,
|
||||
})}
|
||||
>
|
||||
Chitchatter only stores user preferences and never message content of
|
||||
any kind. This preference data is only stored locally on your device and
|
||||
not a server.
|
||||
</Typography>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
</Box>
|
||||
)
|
||||
}
|
1
src/pages/Settings/index.ts
Normal file
1
src/pages/Settings/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './Settings'
|
Loading…
Reference in New Issue
Block a user