fix: when decrypting file, have better error messages #17

Merged
b merged 11 commits from issue-11 into main 2024-05-14 15:09:01 +00:00
3 changed files with 26 additions and 152 deletions
Showing only changes of commit 8e7620201e - Show all commits

View File

@ -1,80 +0,0 @@
import axios, { AxiosError, AxiosInstance } from "axios"
import { PostMediaResponse } from "../types/media"
import sha256 from 'crypto-js/sha256'
import Hex from 'crypto-js/enc-hex'
import { NostrController } from "."
export class ApiController {
private mediaUrl: string = 'https://media.nquiz.io'
private api: AxiosInstance
private baseUrl: string = ''
private nostrController = NostrController.getInstance()
constructor(baseUrl?: string) {
this.baseUrl = baseUrl ?? this.baseUrl
this.api = axios.create({
withCredentials: true,
baseURL: this.baseUrl,
headers: {
'Content-Type': 'application/json'
}
})
}
uploadMediaImage = async (
file: Buffer,
): Promise<PostMediaResponse> => {
const mediaUrl = this.mediaUrl
const formData = new FormData()
const imageBlob = new Blob([file])
formData.append('mediafile', imageBlob)
const endpointUrl = 'v1/media'
const tags = [
['u', `${mediaUrl}/${endpointUrl}`],
['method', 'POST'],
['payload', this.hashString('{}')]
]
const authToken = await this.nostrController.createNostrHttpAuthToken(tags)
console.info('Auth token created for media server.', authToken)
return axios
.post<PostMediaResponse>(`${mediaUrl}/api/v1/media`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Nostr ${authToken}`
}
})
.then((res) => {
console.info(
`POST ${mediaUrl}/api/v1/media | res.data:`,
res.data
)
// Create correct api url, because res.url is not including 'api/v1/'
if (!res || !res.data) {
console.warn('Image upload: no data returned after upload')
}
const data = res.data
data.directLink = `${data.url.replace(mediaUrl, `${mediaUrl}/api/v1`)}`
return data
})
.catch((err: AxiosError) => {
console.info(`POST ${mediaUrl}/api/v1/media | err:`, err)
throw {
code: 500,
message: err.response?.data || err.response || err.message || err
}
})
}
hashString = (payload: any): string => {
return sha256(payload).toString(Hex)
}
}

View File

@ -1,10 +1,13 @@
import ContentCopyIcon from '@mui/icons-material/ContentCopy' import ContentCopyIcon from '@mui/icons-material/ContentCopy'
import { import {
CircularProgress, CircularProgress,
IconButton,
InputProps,
List, List,
ListItem, ListItem,
ListSubheader, ListSubheader,
TextField, TextField,
Tooltip,
Typography, Typography,
useTheme useTheme
} from '@mui/material' } from '@mui/material'
@ -23,8 +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 { ApiController } from '../../controllers/ApiController' import { SmartToy } from '@mui/icons-material'
import { PostMediaResponse } from '../../types/media'
export const ProfilePage = () => { export const ProfilePage = () => {
const theme = useTheme() const theme = useTheme()
@ -45,7 +47,6 @@ export const ProfilePage = () => {
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)
const [uploadingImage, setUploadingImage] = useState<boolean>()
const [isUsersOwnProfile, setIsUsersOwnProfile] = useState(false) const [isUsersOwnProfile, setIsUsersOwnProfile] = useState(false)
@ -118,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
@ -130,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
@ -208,41 +211,6 @@ export const ProfilePage = () => {
setSavingProfileMetadata(false) setSavingProfileMetadata(false)
} }
const onProfileImageChange = (event: any) => {
const file = event?.target?.files[0]
if (file) {
setUploadingImage(true)
const apiController = new ApiController()
apiController.uploadMediaImage(file).then((imageResponse: PostMediaResponse) => {
if (imageResponse && imageResponse.directLink) {
setTimeout(() => {
setProfileMetadata((prev) => ({
...prev,
picture: imageResponse.directLink
}))
}, 200)
} else {
toast.error(`Uploading image failed. Please try again.`)
}
})
.catch((err) => {
console.error('Error uploading image to media server.', err)
})
.finally(() => {
setUploadingImage(false)
})
}
}
const progressSpinner = (show = false) => {
if (!show) return undefined
return <CircularProgress size={20} />
}
const generateRobotAvatar = () => { const generateRobotAvatar = () => {
setAvatarLoading(true) setAvatarLoading(true)
@ -261,6 +229,21 @@ export const ProfilePage = () => {
}) })
} }
/**
*
* @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} />}
@ -304,20 +287,6 @@ export const ProfilePage = () => {
alt='Profile Image' alt='Profile Image'
/> />
<LoadingButton onClick={generateRobotAvatar} loading={avatarLoading}>Generate Robot Avatar</LoadingButton>
<TextField
onChange={onProfileImageChange}
type="file"
size="small"
label="Profile Image"
className={styles.textField}
InputProps={{
endAdornment: progressSpinner(uploadingImage)
}}
focused
/>
{nostrJoiningBlock && ( {nostrJoiningBlock && (
<Typography <Typography
sx={{ sx={{
@ -334,6 +303,10 @@ 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)')}

View File

@ -1,19 +0,0 @@
export interface PostMediaResponse {
result: boolean
description: string
status: string
id: number
pubkey: string
url: string
directLink: string
hash: string
magnet: string
tags: any[]
}
export interface GetMediaResponse extends PostMediaResponse {}
export interface MediaErrorResponse {
description: string
result: boolean
}