From e962c403a52d3f51576ca5dc7635b811c2725139 Mon Sep 17 00:00:00 2001 From: Jeremy Kahn Date: Sun, 10 Dec 2023 19:43:32 -0600 Subject: [PATCH] feat(profile) [closes #219] Import and export user profile (#220) * 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 --- package-lock.json | 59 +++++++++ package.json | 8 ++ src/pages/Settings/Settings.tsx | 135 +++++++++++++++++--- src/services/Serialization/Serialization.ts | 27 +++- src/services/Settings/Settings.ts | 80 ++++++++++++ src/services/Settings/index.ts | 1 + src/utils.ts | 9 ++ 7 files changed, 299 insertions(+), 20 deletions(-) create mode 100644 src/services/Settings/Settings.ts create mode 100644 src/services/Settings/index.ts diff --git a/package-lock.json b/package-lock.json index 1aed84d..76c7e2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "classnames": "^2.3.1", "detectincognitojs": "^1.1.2", "fast-memoize": "^2.5.2", + "file-saver": "^2.0.5", "fun-animal-names": "^0.1.1", "idb-chunk-store": "^1.0.1", "localforage": "^1.10.0", @@ -33,6 +34,7 @@ "querystring": "^0.2.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-file-reader-input": "^2.0.0", "react-git-info": "^2.0.1", "react-markdown": "^8.0.3", "react-qrcode-logo": "^2.8.0", @@ -56,6 +58,8 @@ }, "devDependencies": { "@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/streamsaver": "^2.0.1", "@types/uuid": "^8.3.4", @@ -7451,6 +7455,12 @@ "@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": { "version": "4.1.8", "license": "MIT", @@ -7637,6 +7647,15 @@ "@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": { "version": "15.5.9", "dev": true, @@ -13101,6 +13120,11 @@ "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": { "version": "1.0.4", "license": "Apache-2.0", @@ -22791,6 +22815,15 @@ "dev": true, "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": { "version": "2.0.1", "license": "MIT", @@ -31878,6 +31911,12 @@ "@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": { "version": "4.1.8", "requires": { @@ -32028,6 +32067,15 @@ "@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": { "version": "15.5.9", "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": { "version": "1.0.4", "requires": { @@ -41065,6 +41118,12 @@ "version": "6.0.9", "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": { "version": "2.0.1", "requires": { diff --git a/package.json b/package.json index 5c46657..0e82bf4 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "classnames": "^2.3.1", "detectincognitojs": "^1.1.2", "fast-memoize": "^2.5.2", + "file-saver": "^2.0.5", "fun-animal-names": "^0.1.1", "idb-chunk-store": "^1.0.1", "localforage": "^1.10.0", @@ -29,6 +30,7 @@ "querystring": "^0.2.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-file-reader-input": "^2.0.0", "react-git-info": "^2.0.1", "react-markdown": "^8.0.3", "react-qrcode-logo": "^2.8.0", @@ -89,6 +91,8 @@ }, "devDependencies": { "@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/streamsaver": "^2.0.1", "@types/uuid": "^8.3.4", @@ -123,6 +127,10 @@ "@svgr/plugin-svgo": { "nth-check": "2.0.1" }, + "react-file-reader-input": { + "react": "$react", + "react-dom": "$react-dom" + }, "resolve-url-loader": { "postcss": "8.4.31" } diff --git a/src/pages/Settings/Settings.tsx b/src/pages/Settings/Settings.tsx index a6ff906..cfee265 100644 --- a/src/pages/Settings/Settings.tsx +++ b/src/pages/Settings/Settings.tsx @@ -1,4 +1,5 @@ import { ChangeEvent, useContext, useEffect, useState } from 'react' +import FileReaderInput, { Result } from 'react-file-reader-input' import Box from '@mui/material/Box' import Button from '@mui/material/Button' import Typography from '@mui/material/Typography' @@ -7,21 +8,26 @@ import Switch from '@mui/material/Switch' import FormGroup from '@mui/material/FormGroup' import FormControlLabel from '@mui/material/FormControlLabel' import Paper from '@mui/material/Paper' +import useTheme from '@mui/material/styles/useTheme' +import { settingsService } from 'services/Settings' import { NotificationService } from 'services/Notification' import { ShellContext } from 'contexts/ShellContext' import { StorageContext } from 'contexts/StorageContext' +import { SettingsContext } from 'contexts/SettingsContext' import { PeerNameDisplay } from 'components/PeerNameDisplay' +import { ConfirmDialog } from 'components/ConfirmDialog' -import { ConfirmDialog } from '../../components/ConfirmDialog' -import { SettingsContext } from '../../contexts/SettingsContext' +import { isErrorWithMessage } from '../../utils' interface SettingsProps { userId: string } export const Settings = ({ userId }: SettingsProps) => { - const { setTitle } = useContext(ShellContext) + const theme = useTheme() + + const { setTitle, showAlert } = useContext(ShellContext) const { updateUserSettings, getUserSettings } = useContext(SettingsContext) const { getPersistedStorage } = useContext(StorageContext) const [ @@ -85,17 +91,41 @@ export const Settings = ({ userId }: SettingsProps) => { 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' return ( ({ + sx={{ fontSize: theme.typography.h3.fontSize, fontWeight: theme.typography.fontWeightMedium, mb: 2, - })} + }} > Chat @@ -137,44 +167,111 @@ export const Settings = ({ userId }: SettingsProps) => { label="Show active typing indicators" /> - ({})}> + Disabling this will also hide your active typing status from others. ({ + sx={{ fontSize: theme.typography.h3.fontSize, fontWeight: theme.typography.fontWeightMedium, mb: 2, - })} + }} > Data ({ + sx={{ fontSize: theme.typography.h5.fontSize, fontWeight: theme.typography.fontWeightMedium, mb: 1.5, - })} + }} > - Delete all settings data + Export profile data ({ + sx={{ mb: 2, - })} + }} + > + Export your Chitchatter profile data so that it can be moved to another + browser or device.{' '} + Be careful not to share the exported data with anyone. + It contains your unique verification keys. + + + + Import profile data + + + Import your Chitchatter profile that was previously exported from + another browser or device. + + { + handleImportSettingsClick(results) + }, + }} + > + + + + Delete all profile data + + Be careful with this. This will cause your user name to change from{' '} ({ + sx={{ fontWeight: theme.typography.fontWeightMedium, - })} + }} > {userId} @@ -185,9 +282,9 @@ export const Settings = ({ userId }: SettingsProps) => {