diff --git a/package-lock.json b/package-lock.json index 7bc19cd..f1556fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@reduxjs/toolkit": "2.2.1", "axios": "1.6.7", "crypto-hash": "3.0.0", + "crypto-js": "^4.2.0", "file-saver": "2.0.5", "jszip": "3.10.1", "lodash": "4.17.21", @@ -31,6 +32,7 @@ "tseep": "1.2.1" }, "devDependencies": { + "@types/crypto-js": "^4.2.2", "@types/file-saver": "2.0.7", "@types/lodash": "4.14.202", "@types/react": "^18.2.56", @@ -1938,6 +1940,12 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/crypto-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", + "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", + "dev": true + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -2630,6 +2638,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", diff --git a/package.json b/package.json index 16480a5..6d1ea04 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dev": "vite", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint:fix": "eslint . --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, "dependencies": { @@ -20,6 +21,7 @@ "@reduxjs/toolkit": "2.2.1", "axios": "1.6.7", "crypto-hash": "3.0.0", + "crypto-js": "^4.2.0", "file-saver": "2.0.5", "jszip": "3.10.1", "lodash": "4.17.21", @@ -34,6 +36,7 @@ "tseep": "1.2.1" }, "devDependencies": { + "@types/crypto-js": "^4.2.2", "@types/file-saver": "2.0.7", "@types/lodash": "4.14.202", "@types/react": "^18.2.56", diff --git a/src/pages/decrypt/index.tsx b/src/pages/decrypt/index.tsx index 6a25cab..6b20491 100644 --- a/src/pages/decrypt/index.tsx +++ b/src/pages/decrypt/index.tsx @@ -8,6 +8,7 @@ import styles from './style.module.scss' import { toast } from 'react-toastify' import { useSearchParams } from 'react-router-dom' import axios from 'axios' +import { DecryptionError } from '../../types/errors/DecryptionError' export const DecryptZip = () => { const [searchParams] = useSearchParams() @@ -60,9 +61,10 @@ export const DecryptZip = () => { const arrayBuffer = await decryptArrayBuffer( encryptedArrayBuffer, encryptionKey - ).catch((err) => { + ).catch((err: DecryptionError) => { console.log('err in decryption:>> ', err) - toast.error(err.message || 'An error occurred while decrypting file.') + + toast.error(err.message) setIsLoading(false) return null }) diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index 6ad9c2f..4f64858 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -1,9 +1,13 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy' import { + CircularProgress, + IconButton, + InputProps, List, ListItem, ListSubheader, TextField, + Tooltip, Typography, useTheme } from '@mui/material' @@ -22,6 +26,7 @@ import { Dispatch } from '../../store/store' import { setMetadataEvent } from '../../store/actions' import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoginMethods } from '../../store/auth/types' +import { SmartToy } from '@mui/icons-material' export const ProfilePage = () => { const theme = useTheme() @@ -38,6 +43,7 @@ export const ProfilePage = () => { useState(null) const [profileMetadata, setProfileMetadata] = useState() const [savingProfileMetadata, setSavingProfileMetadata] = useState(false) + const [avatarLoading, setAvatarLoading] = useState(false) const metadataState = useSelector((state: State) => state.metadata) const keys = useSelector((state: State) => state.auth?.keyPair) const { usersPubkey, loginMethod } = useSelector((state: State) => state.auth) @@ -97,7 +103,9 @@ export const ProfilePage = () => { if (metadataEvent) { const metadataContent = metadataController.extractProfileMetadataContent(metadataEvent) - if (metadataContent) setProfileMetadata(metadataContent) + if (metadataContent) { + setProfileMetadata(metadataContent) + } } setIsLoading(false) @@ -111,7 +119,8 @@ export const ProfilePage = () => { key: keyof ProfileMetadata, label: string, multiline = false, - rows = 1 + rows = 1, + inputProps?: InputProps ) => ( { rows={rows} className={styles.textField} disabled={!isUsersOwnProfile} + InputProps={inputProps} onChange={(event: React.ChangeEvent) => { const { value } = event.target @@ -201,6 +211,39 @@ export const ProfilePage = () => { setSavingProfileMetadata(false) } + const generateRobotAvatar = () => { + setAvatarLoading(true) + + const robotAvatarLink = `https://robohash.org/${npub}.png?set=set3` + + setProfileMetadata((prev) => ({ + ...prev, + picture: '' + })) + + setTimeout(() => { + setProfileMetadata((prev) => ({ + ...prev, + picture: robotAvatarLink + })) + }) + } + + /** + * + * @returns robohash generate button, loading spinner or no button + */ + const robohashButton = () => { + if (profileMetadata?.picture?.includes('robohash')) return null + + return + {avatarLoading ? + : + + } + + } + return ( <> {isLoading && } @@ -236,10 +279,14 @@ export const ProfilePage = () => { onError={(event: any) => { event.target.src = placeholderAvatar }} + onLoad={() => { + setAvatarLoading(false) + }} className={styles.img} src={profileMetadata.picture || placeholderAvatar} alt='Profile Image' /> + {nostrJoiningBlock && ( { )} + {editItem('picture', 'Picture URL', undefined, undefined, { + endAdornment: robohashButton() + })} + {editItem('name', 'Username')} {editItem('display_name', 'Display Name')} {editItem('nip05', 'Nostr Address (nip05)')} {editItem('lud16', 'Lightning Address (lud16)')} {editItem('about', 'About', true, 4)} + {editItem('website', 'Website')} {isUsersOwnProfile && ( <> {usersPubkey && diff --git a/src/types/errors/DecryptionError.ts b/src/types/errors/DecryptionError.ts new file mode 100644 index 0000000..05ee071 --- /dev/null +++ b/src/types/errors/DecryptionError.ts @@ -0,0 +1,20 @@ +export class DecryptionError extends Error { + public message: string = '' + + constructor( + public inputError: any + ) { + super() + + if (inputError.message.toLowerCase().includes('expected')) { + this.message = `The decryption key length or format is invalid.` + } else if (inputError.message.includes('The JWK "alg" member was inconsistent')) { + this.message = `The decryption key is invalid.` + } else { + this.message = inputError.message || 'An error occurred while decrypting file.' + } + + this.name = 'DecryptionError' + Object.setPrototypeOf(this, DecryptionError.prototype) + } +} \ No newline at end of file diff --git a/src/types/profile.ts b/src/types/profile.ts index 46f120f..e65d602 100644 --- a/src/types/profile.ts +++ b/src/types/profile.ts @@ -3,6 +3,7 @@ export interface ProfileMetadata { display_name?: string picture?: string about?: string + website?: string nip05?: string lud16?: string } diff --git a/src/utils/crypto.ts b/src/utils/crypto.ts index dc55c47..dd344b3 100644 --- a/src/utils/crypto.ts +++ b/src/utils/crypto.ts @@ -4,6 +4,7 @@ import { stringToHex, uint8ArrayToHexString } from '.' +import { DecryptionError } from '../types/errors/DecryptionError' const ENCRYPTION_ALGO_NAME = 'AES-GCM' @@ -63,14 +64,18 @@ export const decryptArrayBuffer = async ( encryptedData: ArrayBuffer, key: string ) => { - const { cryptoKey, iv } = await importKey(key) - - // Decrypt the data - const decryptedData = await window.crypto.subtle.decrypt( - { name: ENCRYPTION_ALGO_NAME, iv }, - cryptoKey, - encryptedData - ) - - return decryptedData + try { + const { cryptoKey, iv } = await importKey(key) + + // Decrypt the data + const decryptedData = await window.crypto.subtle.decrypt( + { name: ENCRYPTION_ALGO_NAME, iv }, + cryptoKey, + encryptedData + ) + + return decryptedData + } catch (err) { + throw new DecryptionError(err) + } } diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 69290a1..dc84493 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -92,7 +92,7 @@ export const sendDM = async ( const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error('Timeout occurred')) - }, 15000) // Timeout duration = 15 seconds + }, 60000) // Timeout duration = 60 seconds }) // Encrypt the DM content, with timeout