Compare commits
12 Commits
323c06c909
...
a99c16a3a7
Author | SHA1 | Date | |
---|---|---|---|
a99c16a3a7 | |||
c5ad828dca | |||
|
a1f096018c | ||
|
e7a57c9aa1 | ||
|
8e7620201e | ||
|
2221b22285 | ||
|
1fbc2414fe | ||
|
0cdb5c2136 | ||
|
413c9cc575 | ||
|
041bd0daff | ||
|
2c2eeba83f | ||
|
5d6a3580a6 |
13
package-lock.json
generated
13
package-lock.json
generated
@ -17,6 +17,7 @@
|
|||||||
"@reduxjs/toolkit": "2.2.1",
|
"@reduxjs/toolkit": "2.2.1",
|
||||||
"axios": "1.6.7",
|
"axios": "1.6.7",
|
||||||
"crypto-hash": "3.0.0",
|
"crypto-hash": "3.0.0",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
@ -31,6 +32,7 @@
|
|||||||
"tseep": "1.2.1"
|
"tseep": "1.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/file-saver": "2.0.7",
|
"@types/file-saver": "2.0.7",
|
||||||
"@types/lodash": "4.14.202",
|
"@types/lodash": "4.14.202",
|
||||||
"@types/react": "^18.2.56",
|
"@types/react": "^18.2.56",
|
||||||
@ -1938,6 +1940,12 @@
|
|||||||
"@babel/types": "^7.20.7"
|
"@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": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
||||||
@ -2630,6 +2638,11 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"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"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -20,6 +21,7 @@
|
|||||||
"@reduxjs/toolkit": "2.2.1",
|
"@reduxjs/toolkit": "2.2.1",
|
||||||
"axios": "1.6.7",
|
"axios": "1.6.7",
|
||||||
"crypto-hash": "3.0.0",
|
"crypto-hash": "3.0.0",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
@ -34,6 +36,7 @@
|
|||||||
"tseep": "1.2.1"
|
"tseep": "1.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/file-saver": "2.0.7",
|
"@types/file-saver": "2.0.7",
|
||||||
"@types/lodash": "4.14.202",
|
"@types/lodash": "4.14.202",
|
||||||
"@types/react": "^18.2.56",
|
"@types/react": "^18.2.56",
|
||||||
|
@ -8,6 +8,7 @@ import styles from './style.module.scss'
|
|||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { useSearchParams } from 'react-router-dom'
|
import { useSearchParams } from 'react-router-dom'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { DecryptionError } from '../../types/errors/DecryptionError'
|
||||||
|
|
||||||
export const DecryptZip = () => {
|
export const DecryptZip = () => {
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
@ -60,9 +61,10 @@ export const DecryptZip = () => {
|
|||||||
const arrayBuffer = await decryptArrayBuffer(
|
const arrayBuffer = await decryptArrayBuffer(
|
||||||
encryptedArrayBuffer,
|
encryptedArrayBuffer,
|
||||||
encryptionKey
|
encryptionKey
|
||||||
).catch((err) => {
|
).catch((err: DecryptionError) => {
|
||||||
console.log('err in decryption:>> ', err)
|
console.log('err in decryption:>> ', err)
|
||||||
toast.error(err.message || 'An error occurred while decrypting file.')
|
|
||||||
|
toast.error(err.message)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
||||||
import {
|
import {
|
||||||
|
CircularProgress,
|
||||||
|
IconButton,
|
||||||
|
InputProps,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListSubheader,
|
ListSubheader,
|
||||||
TextField,
|
TextField,
|
||||||
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
useTheme
|
useTheme
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
@ -22,6 +26,7 @@ import { Dispatch } from '../../store/store'
|
|||||||
import { setMetadataEvent } from '../../store/actions'
|
import { setMetadataEvent } from '../../store/actions'
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
import { LoginMethods } from '../../store/auth/types'
|
import { LoginMethods } from '../../store/auth/types'
|
||||||
|
import { SmartToy } from '@mui/icons-material'
|
||||||
|
|
||||||
export const ProfilePage = () => {
|
export const ProfilePage = () => {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
@ -38,6 +43,7 @@ export const ProfilePage = () => {
|
|||||||
useState<NostrJoiningBlock | null>(null)
|
useState<NostrJoiningBlock | null>(null)
|
||||||
const [profileMetadata, setProfileMetadata] = useState<ProfileMetadata>()
|
const [profileMetadata, setProfileMetadata] = useState<ProfileMetadata>()
|
||||||
const [savingProfileMetadata, setSavingProfileMetadata] = useState(false)
|
const [savingProfileMetadata, setSavingProfileMetadata] = useState(false)
|
||||||
|
const [avatarLoading, setAvatarLoading] = useState(false)
|
||||||
const metadataState = useSelector((state: State) => state.metadata)
|
const metadataState = useSelector((state: State) => state.metadata)
|
||||||
const keys = useSelector((state: State) => state.auth?.keyPair)
|
const keys = useSelector((state: State) => state.auth?.keyPair)
|
||||||
const { usersPubkey, loginMethod } = useSelector((state: State) => state.auth)
|
const { usersPubkey, loginMethod } = useSelector((state: State) => state.auth)
|
||||||
@ -97,7 +103,9 @@ export const ProfilePage = () => {
|
|||||||
if (metadataEvent) {
|
if (metadataEvent) {
|
||||||
const metadataContent =
|
const metadataContent =
|
||||||
metadataController.extractProfileMetadataContent(metadataEvent)
|
metadataController.extractProfileMetadataContent(metadataEvent)
|
||||||
if (metadataContent) setProfileMetadata(metadataContent)
|
if (metadataContent) {
|
||||||
|
setProfileMetadata(metadataContent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
@ -111,7 +119,8 @@ export const ProfilePage = () => {
|
|||||||
key: keyof ProfileMetadata,
|
key: keyof ProfileMetadata,
|
||||||
label: string,
|
label: string,
|
||||||
multiline = false,
|
multiline = false,
|
||||||
rows = 1
|
rows = 1,
|
||||||
|
inputProps?: InputProps
|
||||||
) => (
|
) => (
|
||||||
<ListItem sx={{ marginTop: 1 }}>
|
<ListItem sx={{ marginTop: 1 }}>
|
||||||
<TextField
|
<TextField
|
||||||
@ -123,6 +132,7 @@ export const ProfilePage = () => {
|
|||||||
rows={rows}
|
rows={rows}
|
||||||
className={styles.textField}
|
className={styles.textField}
|
||||||
disabled={!isUsersOwnProfile}
|
disabled={!isUsersOwnProfile}
|
||||||
|
InputProps={inputProps}
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { value } = event.target
|
const { value } = event.target
|
||||||
|
|
||||||
@ -201,6 +211,39 @@ export const ProfilePage = () => {
|
|||||||
setSavingProfileMetadata(false)
|
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 <Tooltip title="Generate a robohash avatar">
|
||||||
|
{avatarLoading ? <CircularProgress style={{padding: 8}} size={22}/>
|
||||||
|
: <IconButton onClick={generateRobotAvatar}>
|
||||||
|
<SmartToy/>
|
||||||
|
</IconButton>}
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||||
@ -236,10 +279,14 @@ export const ProfilePage = () => {
|
|||||||
onError={(event: any) => {
|
onError={(event: any) => {
|
||||||
event.target.src = placeholderAvatar
|
event.target.src = placeholderAvatar
|
||||||
}}
|
}}
|
||||||
|
onLoad={() => {
|
||||||
|
setAvatarLoading(false)
|
||||||
|
}}
|
||||||
className={styles.img}
|
className={styles.img}
|
||||||
src={profileMetadata.picture || placeholderAvatar}
|
src={profileMetadata.picture || placeholderAvatar}
|
||||||
alt='Profile Image'
|
alt='Profile Image'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{nostrJoiningBlock && (
|
{nostrJoiningBlock && (
|
||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
@ -256,11 +303,16 @@ export const ProfilePage = () => {
|
|||||||
)}
|
)}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
{editItem('picture', 'Picture URL', undefined, undefined, {
|
||||||
|
endAdornment: robohashButton()
|
||||||
|
})}
|
||||||
|
|
||||||
{editItem('name', 'Username')}
|
{editItem('name', 'Username')}
|
||||||
{editItem('display_name', 'Display Name')}
|
{editItem('display_name', 'Display Name')}
|
||||||
{editItem('nip05', 'Nostr Address (nip05)')}
|
{editItem('nip05', 'Nostr Address (nip05)')}
|
||||||
{editItem('lud16', 'Lightning Address (lud16)')}
|
{editItem('lud16', 'Lightning Address (lud16)')}
|
||||||
{editItem('about', 'About', true, 4)}
|
{editItem('about', 'About', true, 4)}
|
||||||
|
{editItem('website', 'Website')}
|
||||||
{isUsersOwnProfile && (
|
{isUsersOwnProfile && (
|
||||||
<>
|
<>
|
||||||
{usersPubkey &&
|
{usersPubkey &&
|
||||||
|
20
src/types/errors/DecryptionError.ts
Normal file
20
src/types/errors/DecryptionError.ts
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ export interface ProfileMetadata {
|
|||||||
display_name?: string
|
display_name?: string
|
||||||
picture?: string
|
picture?: string
|
||||||
about?: string
|
about?: string
|
||||||
|
website?: string
|
||||||
nip05?: string
|
nip05?: string
|
||||||
lud16?: string
|
lud16?: string
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
stringToHex,
|
stringToHex,
|
||||||
uint8ArrayToHexString
|
uint8ArrayToHexString
|
||||||
} from '.'
|
} from '.'
|
||||||
|
import { DecryptionError } from '../types/errors/DecryptionError'
|
||||||
|
|
||||||
const ENCRYPTION_ALGO_NAME = 'AES-GCM'
|
const ENCRYPTION_ALGO_NAME = 'AES-GCM'
|
||||||
|
|
||||||
@ -63,6 +64,7 @@ export const decryptArrayBuffer = async (
|
|||||||
encryptedData: ArrayBuffer,
|
encryptedData: ArrayBuffer,
|
||||||
key: string
|
key: string
|
||||||
) => {
|
) => {
|
||||||
|
try {
|
||||||
const { cryptoKey, iv } = await importKey(key)
|
const { cryptoKey, iv } = await importKey(key)
|
||||||
|
|
||||||
// Decrypt the data
|
// Decrypt the data
|
||||||
@ -73,4 +75,7 @@ export const decryptArrayBuffer = async (
|
|||||||
)
|
)
|
||||||
|
|
||||||
return decryptedData
|
return decryptedData
|
||||||
|
} catch (err) {
|
||||||
|
throw new DecryptionError(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,7 @@ export const sendDM = async (
|
|||||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
reject(new Error('Timeout occurred'))
|
reject(new Error('Timeout occurred'))
|
||||||
}, 15000) // Timeout duration = 15 seconds
|
}, 60000) // Timeout duration = 60 seconds
|
||||||
})
|
})
|
||||||
|
|
||||||
// Encrypt the DM content, with timeout
|
// Encrypt the DM content, with timeout
|
||||||
|
Loading…
Reference in New Issue
Block a user