forked from Shiloh/remnantchat
* feat(profile): implement profile export * feat(profile): load user-selected file * feat(profile): import loaded profile * feat(profile): validate public/private keys * refactor(settings): remove unnecessary theme functions * refactor(settings): use theme hook * feat(settings): show error alert if profile export fails
This commit is contained in:
parent
15142f9328
commit
e962c403a5
59
package-lock.json
generated
59
package-lock.json
generated
@ -26,6 +26,7 @@
|
|||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"detectincognitojs": "^1.1.2",
|
"detectincognitojs": "^1.1.2",
|
||||||
"fast-memoize": "^2.5.2",
|
"fast-memoize": "^2.5.2",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"fun-animal-names": "^0.1.1",
|
"fun-animal-names": "^0.1.1",
|
||||||
"idb-chunk-store": "^1.0.1",
|
"idb-chunk-store": "^1.0.1",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
@ -33,6 +34,7 @@
|
|||||||
"querystring": "^0.2.1",
|
"querystring": "^0.2.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-file-reader-input": "^2.0.0",
|
||||||
"react-git-info": "^2.0.1",
|
"react-git-info": "^2.0.1",
|
||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
"react-qrcode-logo": "^2.8.0",
|
"react-qrcode-logo": "^2.8.0",
|
||||||
@ -56,6 +58,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
|
"@types/file-saver": "^2.0.7",
|
||||||
|
"@types/react-file-reader-input": "^2.0.4",
|
||||||
"@types/react-syntax-highlighter": "^15.5.5",
|
"@types/react-syntax-highlighter": "^15.5.5",
|
||||||
"@types/streamsaver": "^2.0.1",
|
"@types/streamsaver": "^2.0.1",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
@ -7451,6 +7455,12 @@
|
|||||||
"@types/send": "*"
|
"@types/send": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/file-saver": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/graceful-fs": {
|
"node_modules/@types/graceful-fs": {
|
||||||
"version": "4.1.8",
|
"version": "4.1.8",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -7637,6 +7647,15 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-file-reader-input": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-file-reader-input/-/react-file-reader-input-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-Jqrfn+w42j8t8Q3npMXXKPdk+reIM0UHLKVc3ykrA7q7bN3Z62SGhsClZX0+Edlqm66lcKwmDQl+WMm+Xor7Xg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react-syntax-highlighter": {
|
"node_modules/@types/react-syntax-highlighter": {
|
||||||
"version": "15.5.9",
|
"version": "15.5.9",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@ -13101,6 +13120,11 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/file-saver": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
|
||||||
|
},
|
||||||
"node_modules/filelist": {
|
"node_modules/filelist": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
@ -22791,6 +22815,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-file-reader-input": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-file-reader-input/-/react-file-reader-input-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-1XgkCpwMnNQsuOIy938UCntz8Xzwt9ECwHaH3cCfIQK1SPpH+y7gCYtqEcb6Rm0hAUq7Lp9+Ljoti9zGMswYrQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^15.0.0 || ^16.0.0",
|
||||||
|
"react-dom": "^15.0.0 || ^16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-git-info": {
|
"node_modules/react-git-info": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -31878,6 +31911,12 @@
|
|||||||
"@types/send": "*"
|
"@types/send": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/file-saver": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/graceful-fs": {
|
"@types/graceful-fs": {
|
||||||
"version": "4.1.8",
|
"version": "4.1.8",
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -32028,6 +32067,15 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/react-file-reader-input": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-file-reader-input/-/react-file-reader-input-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-Jqrfn+w42j8t8Q3npMXXKPdk+reIM0UHLKVc3ykrA7q7bN3Z62SGhsClZX0+Edlqm66lcKwmDQl+WMm+Xor7Xg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/react-syntax-highlighter": {
|
"@types/react-syntax-highlighter": {
|
||||||
"version": "15.5.9",
|
"version": "15.5.9",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@ -35366,6 +35414,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"file-saver": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
|
||||||
|
},
|
||||||
"filelist": {
|
"filelist": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -41065,6 +41118,12 @@
|
|||||||
"version": "6.0.9",
|
"version": "6.0.9",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"react-file-reader-input": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-file-reader-input/-/react-file-reader-input-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-1XgkCpwMnNQsuOIy938UCntz8Xzwt9ECwHaH3cCfIQK1SPpH+y7gCYtqEcb6Rm0hAUq7Lp9+Ljoti9zGMswYrQ==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"react-git-info": {
|
"react-git-info": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"requires": {
|
"requires": {
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"detectincognitojs": "^1.1.2",
|
"detectincognitojs": "^1.1.2",
|
||||||
"fast-memoize": "^2.5.2",
|
"fast-memoize": "^2.5.2",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"fun-animal-names": "^0.1.1",
|
"fun-animal-names": "^0.1.1",
|
||||||
"idb-chunk-store": "^1.0.1",
|
"idb-chunk-store": "^1.0.1",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
@ -29,6 +30,7 @@
|
|||||||
"querystring": "^0.2.1",
|
"querystring": "^0.2.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-file-reader-input": "^2.0.0",
|
||||||
"react-git-info": "^2.0.1",
|
"react-git-info": "^2.0.1",
|
||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
"react-qrcode-logo": "^2.8.0",
|
"react-qrcode-logo": "^2.8.0",
|
||||||
@ -89,6 +91,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
|
"@types/file-saver": "^2.0.7",
|
||||||
|
"@types/react-file-reader-input": "^2.0.4",
|
||||||
"@types/react-syntax-highlighter": "^15.5.5",
|
"@types/react-syntax-highlighter": "^15.5.5",
|
||||||
"@types/streamsaver": "^2.0.1",
|
"@types/streamsaver": "^2.0.1",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
@ -123,6 +127,10 @@
|
|||||||
"@svgr/plugin-svgo": {
|
"@svgr/plugin-svgo": {
|
||||||
"nth-check": "2.0.1"
|
"nth-check": "2.0.1"
|
||||||
},
|
},
|
||||||
|
"react-file-reader-input": {
|
||||||
|
"react": "$react",
|
||||||
|
"react-dom": "$react-dom"
|
||||||
|
},
|
||||||
"resolve-url-loader": {
|
"resolve-url-loader": {
|
||||||
"postcss": "8.4.31"
|
"postcss": "8.4.31"
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ChangeEvent, useContext, useEffect, useState } from 'react'
|
import { ChangeEvent, useContext, useEffect, useState } from 'react'
|
||||||
|
import FileReaderInput, { Result } from 'react-file-reader-input'
|
||||||
import Box from '@mui/material/Box'
|
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'
|
||||||
@ -7,21 +8,26 @@ import Switch from '@mui/material/Switch'
|
|||||||
import FormGroup from '@mui/material/FormGroup'
|
import FormGroup from '@mui/material/FormGroup'
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
import Paper from '@mui/material/Paper'
|
import Paper from '@mui/material/Paper'
|
||||||
|
import useTheme from '@mui/material/styles/useTheme'
|
||||||
|
|
||||||
|
import { settingsService } from 'services/Settings'
|
||||||
import { NotificationService } from 'services/Notification'
|
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 { SettingsContext } from 'contexts/SettingsContext'
|
||||||
import { PeerNameDisplay } from 'components/PeerNameDisplay'
|
import { PeerNameDisplay } from 'components/PeerNameDisplay'
|
||||||
|
import { ConfirmDialog } from 'components/ConfirmDialog'
|
||||||
|
|
||||||
import { ConfirmDialog } from '../../components/ConfirmDialog'
|
import { isErrorWithMessage } from '../../utils'
|
||||||
import { SettingsContext } from '../../contexts/SettingsContext'
|
|
||||||
|
|
||||||
interface SettingsProps {
|
interface SettingsProps {
|
||||||
userId: string
|
userId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Settings = ({ userId }: SettingsProps) => {
|
export const Settings = ({ userId }: SettingsProps) => {
|
||||||
const { setTitle } = useContext(ShellContext)
|
const theme = useTheme()
|
||||||
|
|
||||||
|
const { setTitle, showAlert } = useContext(ShellContext)
|
||||||
const { updateUserSettings, getUserSettings } = useContext(SettingsContext)
|
const { updateUserSettings, getUserSettings } = useContext(SettingsContext)
|
||||||
const { getPersistedStorage } = useContext(StorageContext)
|
const { getPersistedStorage } = useContext(StorageContext)
|
||||||
const [
|
const [
|
||||||
@ -85,17 +91,41 @@ export const Settings = ({ userId }: SettingsProps) => {
|
|||||||
window.location.reload()
|
window.location.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleExportSettingsClick = async () => {
|
||||||
|
try {
|
||||||
|
await settingsService.exportSettings(getUserSettings())
|
||||||
|
} catch (e) {
|
||||||
|
if (isErrorWithMessage(e)) {
|
||||||
|
showAlert(e.message, { severity: 'error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImportSettingsClick = async ([[, file]]: Result[]) => {
|
||||||
|
try {
|
||||||
|
const userSettings = await settingsService.importSettings(file)
|
||||||
|
|
||||||
|
updateUserSettings(userSettings)
|
||||||
|
|
||||||
|
showAlert('Profile successfully imported', { severity: 'success' })
|
||||||
|
} catch (e) {
|
||||||
|
if (isErrorWithMessage(e)) {
|
||||||
|
showAlert(e.message, { severity: 'error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const areNotificationsAvailable = NotificationService.permission === 'granted'
|
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
|
||||||
variant="h2"
|
variant="h2"
|
||||||
sx={theme => ({
|
sx={{
|
||||||
fontSize: theme.typography.h3.fontSize,
|
fontSize: theme.typography.h3.fontSize,
|
||||||
fontWeight: theme.typography.fontWeightMedium,
|
fontWeight: theme.typography.fontWeightMedium,
|
||||||
mb: 2,
|
mb: 2,
|
||||||
})}
|
}}
|
||||||
>
|
>
|
||||||
Chat
|
Chat
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -137,44 +167,111 @@ export const Settings = ({ userId }: SettingsProps) => {
|
|||||||
label="Show active typing indicators"
|
label="Show active typing indicators"
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<Typography variant="subtitle2" sx={_theme => ({})}>
|
<Typography variant="subtitle2">
|
||||||
Disabling this will also hide your active typing status from others.
|
Disabling this will also hide your active typing status from others.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Paper>
|
</Paper>
|
||||||
<Divider sx={{ my: 2 }} />
|
<Divider sx={{ my: 2 }} />
|
||||||
<Typography
|
<Typography
|
||||||
variant="h2"
|
variant="h2"
|
||||||
sx={theme => ({
|
sx={{
|
||||||
fontSize: theme.typography.h3.fontSize,
|
fontSize: theme.typography.h3.fontSize,
|
||||||
fontWeight: theme.typography.fontWeightMedium,
|
fontWeight: theme.typography.fontWeightMedium,
|
||||||
mb: 2,
|
mb: 2,
|
||||||
})}
|
}}
|
||||||
>
|
>
|
||||||
Data
|
Data
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="h2"
|
variant="h2"
|
||||||
sx={theme => ({
|
sx={{
|
||||||
fontSize: theme.typography.h5.fontSize,
|
fontSize: theme.typography.h5.fontSize,
|
||||||
fontWeight: theme.typography.fontWeightMedium,
|
fontWeight: theme.typography.fontWeightMedium,
|
||||||
mb: 1.5,
|
mb: 1.5,
|
||||||
})}
|
}}
|
||||||
>
|
>
|
||||||
Delete all settings data
|
Export profile data
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body1"
|
variant="body1"
|
||||||
sx={_theme => ({
|
sx={{
|
||||||
mb: 2,
|
mb: 2,
|
||||||
})}
|
}}
|
||||||
|
>
|
||||||
|
Export your Chitchatter profile data so that it can be moved to another
|
||||||
|
browser or device.{' '}
|
||||||
|
<strong>Be careful not to share the exported data with anyone</strong>.
|
||||||
|
It contains your unique verification keys.
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
|
onClick={handleExportSettingsClick}
|
||||||
|
>
|
||||||
|
Export profile data
|
||||||
|
</Button>
|
||||||
|
<Typography
|
||||||
|
variant="h2"
|
||||||
|
sx={{
|
||||||
|
fontSize: theme.typography.h5.fontSize,
|
||||||
|
fontWeight: theme.typography.fontWeightMedium,
|
||||||
|
mb: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Import profile data
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Import your Chitchatter profile that was previously exported from
|
||||||
|
another browser or device.
|
||||||
|
</Typography>
|
||||||
|
<FileReaderInput
|
||||||
|
{...{
|
||||||
|
as: 'text',
|
||||||
|
onChange: (_e, results) => {
|
||||||
|
handleImportSettingsClick(results)
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
color="warning"
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Import profile data
|
||||||
|
</Button>
|
||||||
|
</FileReaderInput>
|
||||||
|
<Typography
|
||||||
|
variant="h2"
|
||||||
|
sx={{
|
||||||
|
fontSize: theme.typography.h5.fontSize,
|
||||||
|
fontWeight: theme.typography.fontWeightMedium,
|
||||||
|
mb: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete all profile data
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<strong>Be careful with this</strong>. This will cause your user name to
|
<strong>Be careful with this</strong>. This will cause your user name to
|
||||||
change from{' '}
|
change from{' '}
|
||||||
<strong>
|
<strong>
|
||||||
<PeerNameDisplay
|
<PeerNameDisplay
|
||||||
sx={theme => ({
|
sx={{
|
||||||
fontWeight: theme.typography.fontWeightMedium,
|
fontWeight: theme.typography.fontWeightMedium,
|
||||||
})}
|
}}
|
||||||
>
|
>
|
||||||
{userId}
|
{userId}
|
||||||
</PeerNameDisplay>
|
</PeerNameDisplay>
|
||||||
@ -185,9 +282,9 @@ export const Settings = ({ userId }: SettingsProps) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="error"
|
color="error"
|
||||||
sx={_theme => ({
|
sx={{
|
||||||
mb: 2,
|
mb: 2,
|
||||||
})}
|
}}
|
||||||
onClick={handleDeleteSettingsClick}
|
onClick={handleDeleteSettingsClick}
|
||||||
>
|
>
|
||||||
Delete all data and restart
|
Delete all data and restart
|
||||||
@ -199,9 +296,9 @@ export const Settings = ({ userId }: SettingsProps) => {
|
|||||||
/>
|
/>
|
||||||
<Typography
|
<Typography
|
||||||
variant="subtitle2"
|
variant="subtitle2"
|
||||||
sx={_theme => ({
|
sx={{
|
||||||
mb: 2,
|
mb: 2,
|
||||||
})}
|
}}
|
||||||
>
|
>
|
||||||
Chitchatter only stores user preferences and never message content of
|
Chitchatter only stores user preferences and never message content of
|
||||||
any kind. This preference data is only stored locally on your device and
|
any kind. This preference data is only stored locally on your device and
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { UserSettings } from 'models/settings'
|
import { ColorMode, UserSettings } from 'models/settings'
|
||||||
import { AllowedKeyType, encryptionService } from 'services/Encryption'
|
import { AllowedKeyType, encryptionService } from 'services/Encryption'
|
||||||
|
|
||||||
export interface SerializedUserSettings
|
export interface SerializedUserSettings
|
||||||
@ -7,6 +7,31 @@ export interface SerializedUserSettings
|
|||||||
privateKey: string
|
privateKey: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isSerializedUserSettings = (
|
||||||
|
data: any
|
||||||
|
): data is SerializedUserSettings => {
|
||||||
|
return (
|
||||||
|
typeof data === 'object' &&
|
||||||
|
data !== null &&
|
||||||
|
'colorMode' in data &&
|
||||||
|
Object.values(ColorMode).includes(data.colorMode) &&
|
||||||
|
'userId' in data &&
|
||||||
|
typeof data.userId === 'string' &&
|
||||||
|
'customUsername' in data &&
|
||||||
|
typeof data.customUsername === 'string' &&
|
||||||
|
'playSoundOnNewMessage' in data &&
|
||||||
|
typeof data.playSoundOnNewMessage === 'boolean' &&
|
||||||
|
'showNotificationOnNewMessage' in data &&
|
||||||
|
typeof data.showNotificationOnNewMessage === 'boolean' &&
|
||||||
|
'showActiveTypingStatus' in data &&
|
||||||
|
typeof data.showActiveTypingStatus === 'boolean' &&
|
||||||
|
'publicKey' in data &&
|
||||||
|
typeof data.publicKey === 'string' &&
|
||||||
|
'privateKey' in data &&
|
||||||
|
typeof data.privateKey === 'string'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export class SerializationService {
|
export class SerializationService {
|
||||||
serializeUserSettings = async (
|
serializeUserSettings = async (
|
||||||
userSettings: UserSettings
|
userSettings: UserSettings
|
||||||
|
80
src/services/Settings/Settings.ts
Normal file
80
src/services/Settings/Settings.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { saveAs } from 'file-saver'
|
||||||
|
|
||||||
|
import { UserSettings } from 'models/settings'
|
||||||
|
import { encryptionService } from 'services/Encryption'
|
||||||
|
import {
|
||||||
|
isSerializedUserSettings,
|
||||||
|
serializationService,
|
||||||
|
} from 'services/Serialization/Serialization'
|
||||||
|
|
||||||
|
class InvalidFileError extends Error {
|
||||||
|
message = 'InvalidFileError: File could not be imported'
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptionTestTarget = 'chitchatter'
|
||||||
|
|
||||||
|
export class SettingsService {
|
||||||
|
exportSettings = async (userSettings: UserSettings) => {
|
||||||
|
const serializedUserSettings =
|
||||||
|
await serializationService.serializeUserSettings(userSettings)
|
||||||
|
|
||||||
|
const blob = new Blob([JSON.stringify(serializedUserSettings)], {
|
||||||
|
type: 'application/json;charset=utf-8',
|
||||||
|
})
|
||||||
|
|
||||||
|
saveAs(blob, `chitchatter-profile-${userSettings.userId}.json`)
|
||||||
|
}
|
||||||
|
|
||||||
|
importSettings = async (file: File) => {
|
||||||
|
const fileReader = new FileReader()
|
||||||
|
|
||||||
|
const promise = new Promise<UserSettings>((resolve, reject) => {
|
||||||
|
fileReader.addEventListener('loadend', async evt => {
|
||||||
|
try {
|
||||||
|
const fileReaderResult = evt.target?.result
|
||||||
|
|
||||||
|
if (typeof fileReaderResult !== 'string') {
|
||||||
|
throw new Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedFileResult = JSON.parse(fileReaderResult)
|
||||||
|
|
||||||
|
if (!isSerializedUserSettings(parsedFileResult)) {
|
||||||
|
throw new Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
const deserializedUserSettings =
|
||||||
|
await serializationService.deserializeUserSettings(parsedFileResult)
|
||||||
|
|
||||||
|
const encryptedString = await encryptionService.encryptString(
|
||||||
|
deserializedUserSettings.publicKey,
|
||||||
|
encryptionTestTarget
|
||||||
|
)
|
||||||
|
|
||||||
|
const decryptedString = await encryptionService.decryptString(
|
||||||
|
deserializedUserSettings.privateKey,
|
||||||
|
encryptedString
|
||||||
|
)
|
||||||
|
|
||||||
|
// NOTE: This determines whether the public and private keys match
|
||||||
|
// and are compatible with Chitchatter.
|
||||||
|
if (decryptedString !== encryptionTestTarget) {
|
||||||
|
throw new Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(deserializedUserSettings)
|
||||||
|
} catch (e) {
|
||||||
|
const err = new InvalidFileError()
|
||||||
|
console.error(err)
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fileReader.readAsText(file.slice())
|
||||||
|
})
|
||||||
|
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const settingsService = new SettingsService()
|
1
src/services/Settings/index.ts
Normal file
1
src/services/Settings/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './Settings'
|
@ -14,3 +14,12 @@ export const isRecord = (variable: any): variable is Record<string, any> => {
|
|||||||
export const isError = (e: any): e is Error => {
|
export const isError = (e: any): e is Error => {
|
||||||
return e instanceof Error
|
return e instanceof Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isErrorWithMessage = (e: unknown): e is { message: string } => {
|
||||||
|
return (
|
||||||
|
typeof e === 'object' &&
|
||||||
|
e !== null &&
|
||||||
|
'message' in e &&
|
||||||
|
typeof e.message === 'string'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user