mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-04-23 19:37:52 +00:00
add avatar
This commit is contained in:
parent
5e2778d475
commit
ac5a6b251b
@ -90,6 +90,7 @@ import { requestQueueGroupJoinRequests } from "./components/Group/GroupJoinReque
|
|||||||
import { DrawerComponent } from "./components/Drawer/Drawer";
|
import { DrawerComponent } from "./components/Drawer/Drawer";
|
||||||
import { LitecoinQRCode } from "./components/LitecoinQRCode";
|
import { LitecoinQRCode } from "./components/LitecoinQRCode";
|
||||||
import { Settings } from "./components/Group/Settings";
|
import { Settings } from "./components/Group/Settings";
|
||||||
|
import { MainAvatar } from "./components/MainAvatar";
|
||||||
|
|
||||||
type extStates =
|
type extStates =
|
||||||
| "not-authenticated"
|
| "not-authenticated"
|
||||||
@ -1263,7 +1264,7 @@ function App() {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<img src={Logo2} />
|
<MainAvatar myName={userInfo?.name} />
|
||||||
<Spacer height="32px" />
|
<Spacer height="32px" />
|
||||||
<TextP
|
<TextP
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
decryptGroupEncryption,
|
decryptGroupEncryption,
|
||||||
encryptAndPublishSymmetricKeyGroupChat,
|
encryptAndPublishSymmetricKeyGroupChat,
|
||||||
publishGroupEncryptedResource,
|
publishGroupEncryptedResource,
|
||||||
|
publishOnQDN,
|
||||||
uint8ArrayToObject,
|
uint8ArrayToObject,
|
||||||
} from "./backgroundFunctions/encryption";
|
} from "./backgroundFunctions/encryption";
|
||||||
import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from "./constants/codes";
|
import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from "./constants/codes";
|
||||||
@ -3841,6 +3842,24 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => {
|
|||||||
return true;
|
return true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "publishOnQDN": {
|
||||||
|
const { data, identifier, service } = request.payload;
|
||||||
|
|
||||||
|
publishOnQDN({
|
||||||
|
data,
|
||||||
|
identifier,
|
||||||
|
service
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
sendResponse(data);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error.message);
|
||||||
|
sendResponse({ error: error.message });
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "handleActiveGroupDataFromSocket": {
|
case "handleActiveGroupDataFromSocket": {
|
||||||
const { groups, directs } = request.payload;
|
const { groups, directs } = request.payload;
|
||||||
handleActiveGroupDataFromSocket({
|
handleActiveGroupDataFromSocket({
|
||||||
|
@ -152,6 +152,24 @@ export const publishGroupEncryptedResource = async ({encryptedData, identifier})
|
|||||||
throw new Error(error.message);
|
throw new Error(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export const publishOnQDN = async ({data, identifier, service}) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
if(data && identifier && service){
|
||||||
|
const registeredName = await getNameInfo()
|
||||||
|
if(!registeredName) throw new Error('You need a name to publish')
|
||||||
|
const res = await publishData({
|
||||||
|
registeredName, file: data, service, identifier, uploadType: 'file', isBase64: true, withFee: true
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error('Cannot encrypt content')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function uint8ArrayToBase64(uint8Array: any) {
|
export function uint8ArrayToBase64(uint8Array: any) {
|
||||||
const length = uint8Array.length
|
const length = uint8Array.length
|
||||||
|
102
src/common/ImageUploader.tsx
Normal file
102
src/common/ImageUploader.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import React, { useCallback } from 'react'
|
||||||
|
import { Box } from '@mui/material'
|
||||||
|
import { useDropzone, DropzoneRootProps, DropzoneInputProps } from 'react-dropzone'
|
||||||
|
import Compressor from 'compressorjs'
|
||||||
|
|
||||||
|
const toBase64 = (file: File): Promise<string | ArrayBuffer | null> =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
reader.onload = () => resolve(reader.result)
|
||||||
|
reader.onerror = (error) => {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
interface ImageUploaderProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
onPick: (file: File) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImageUploader: React.FC<ImageUploaderProps> = ({ children, onPick }) => {
|
||||||
|
const onDrop = useCallback(
|
||||||
|
async (acceptedFiles: File[]) => {
|
||||||
|
if (acceptedFiles.length > 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = acceptedFiles[0]
|
||||||
|
let compressedFile: File | undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if the file is a GIF
|
||||||
|
if (image.type === 'image/gif') {
|
||||||
|
// Check if the GIF is larger than 500 KB
|
||||||
|
if (image.size > 500 * 1024) {
|
||||||
|
console.error('GIF file size exceeds 500KB limit.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// No compression for GIF, pass the original file
|
||||||
|
compressedFile = image
|
||||||
|
} else {
|
||||||
|
// For non-GIF files, compress them
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
new Compressor(image, {
|
||||||
|
quality: 0.6,
|
||||||
|
maxWidth: 1200,
|
||||||
|
mimeType: 'image/webp',
|
||||||
|
success(result) {
|
||||||
|
const file = new File([result], image.name, {
|
||||||
|
type: 'image/webp'
|
||||||
|
})
|
||||||
|
compressedFile = file
|
||||||
|
resolve()
|
||||||
|
},
|
||||||
|
error(err) {
|
||||||
|
console.error('Compression error:', err)
|
||||||
|
resolve() // Proceed even if there's an error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!compressedFile) return
|
||||||
|
|
||||||
|
onPick(compressedFile)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('File processing error:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onPick]
|
||||||
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
getRootProps,
|
||||||
|
getInputProps,
|
||||||
|
isDragActive
|
||||||
|
}: {
|
||||||
|
getRootProps: () => DropzoneRootProps
|
||||||
|
getInputProps: () => DropzoneInputProps
|
||||||
|
isDragActive: boolean
|
||||||
|
} = useDropzone({
|
||||||
|
onDrop,
|
||||||
|
accept: {
|
||||||
|
'image/*': []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
{...getRootProps()}
|
||||||
|
sx={{
|
||||||
|
display: 'flex'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImageUploader
|
206
src/components/MainAvatar.tsx
Normal file
206
src/components/MainAvatar.tsx
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
|
import Logo2 from "../assets/svgs/Logo2.svg";
|
||||||
|
import { MyContext, getArbitraryEndpointReact, getBaseApiReact } from "../App";
|
||||||
|
import { Avatar, Box, Button, ButtonBase, Popover, Typography } from "@mui/material";
|
||||||
|
import { Spacer } from "../common/Spacer";
|
||||||
|
import ImageUploader from "../common/ImageUploader";
|
||||||
|
import { getFee } from "../background";
|
||||||
|
import { fileToBase64 } from "../utils/fileReading";
|
||||||
|
import { LoadingButton } from "@mui/lab";
|
||||||
|
|
||||||
|
export const MainAvatar = ({ myName }) => {
|
||||||
|
const [hasAvatar, setHasAvatar] = useState(false);
|
||||||
|
const [avatarFile, setAvatarFile] = useState(null);
|
||||||
|
const [tempAvatar, setTempAvatar] = useState(null)
|
||||||
|
const { show } = useContext(MyContext);
|
||||||
|
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
// Handle child element click to open Popover
|
||||||
|
const handleChildClick = (event) => {
|
||||||
|
event.stopPropagation(); // Prevent parent onClick from firing
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle closing the Popover
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine if the popover is open
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
const id = open ? 'avatar-img' : undefined;
|
||||||
|
|
||||||
|
const checkIfAvatarExists = async () => {
|
||||||
|
try {
|
||||||
|
const identifier = `qortal_avatar`;
|
||||||
|
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=THUMBNAIL&identifier=${identifier}&limit=1&name=${myName}&includemetadata=false&prefix=true`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const responseData = await response.json();
|
||||||
|
if (responseData?.length > 0) {
|
||||||
|
setHasAvatar(true);
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (!myName) return;
|
||||||
|
checkIfAvatarExists();
|
||||||
|
}, [myName]);
|
||||||
|
|
||||||
|
const publishAvatar = async ()=> {
|
||||||
|
try {
|
||||||
|
const fee = await getFee('ARBITRARY')
|
||||||
|
|
||||||
|
await show({
|
||||||
|
message: "Would you like to publish an avatar?" ,
|
||||||
|
publishFee: fee.fee + ' QORT'
|
||||||
|
})
|
||||||
|
setIsLoading(true);
|
||||||
|
const avatarBase64 = await fileToBase64(avatarFile)
|
||||||
|
await new Promise((res, rej) => {
|
||||||
|
chrome?.runtime?.sendMessage(
|
||||||
|
{
|
||||||
|
action: "publishOnQDN",
|
||||||
|
payload: {
|
||||||
|
data: avatarBase64,
|
||||||
|
identifier: "qortal_avatar",
|
||||||
|
service: 'THUMBNAIL'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
(response) => {
|
||||||
|
|
||||||
|
if (!response?.error) {
|
||||||
|
res(response);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rej(response.error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
setAvatarFile(null);
|
||||||
|
setTempAvatar(`data:image/webp;base64,${avatarBase64}`)
|
||||||
|
handleClose()
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(tempAvatar){
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
height: "138px",
|
||||||
|
width: "138px",
|
||||||
|
}}
|
||||||
|
src={tempAvatar}
|
||||||
|
alt={myName}
|
||||||
|
>
|
||||||
|
{myName?.charAt(0)}
|
||||||
|
</Avatar>
|
||||||
|
<ButtonBase onClick={handleChildClick}>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
opacity: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
change avatar
|
||||||
|
</Typography>
|
||||||
|
</ButtonBase>
|
||||||
|
<PopoverComp avatarFile={avatarFile} setAvatarFile={setAvatarFile} id={id} open={open} anchorEl={anchorEl} handleClose={handleClose} publishAvatar={publishAvatar} isLoading={isLoading} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAvatar) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
height: "138px",
|
||||||
|
width: "138px",
|
||||||
|
}}
|
||||||
|
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${myName}/qortal_avatar?async=true`}
|
||||||
|
alt={myName}
|
||||||
|
>
|
||||||
|
{myName?.charAt(0)}
|
||||||
|
</Avatar>
|
||||||
|
<ButtonBase onClick={handleChildClick}>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
opacity: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
change avatar
|
||||||
|
</Typography>
|
||||||
|
</ButtonBase>
|
||||||
|
<PopoverComp avatarFile={avatarFile} setAvatarFile={setAvatarFile} id={id} open={open} anchorEl={anchorEl} handleClose={handleClose} publishAvatar={publishAvatar} isLoading={isLoading} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<img src={Logo2} />
|
||||||
|
<ButtonBase onClick={handleChildClick}>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
opacity: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
set avatar
|
||||||
|
</Typography>
|
||||||
|
</ButtonBase>
|
||||||
|
<PopoverComp avatarFile={avatarFile} setAvatarFile={setAvatarFile} id={id} open={open} anchorEl={anchorEl} handleClose={handleClose} publishAvatar={publishAvatar} isLoading={isLoading} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const PopoverComp = ({avatarFile, setAvatarFile, id, open, anchorEl, handleClose, publishAvatar, isLoading}) => {
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
id={id}
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={handleClose} // Close popover on click outside
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: "bottom",
|
||||||
|
horizontal: "center",
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: "top",
|
||||||
|
horizontal: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ p: 2, display: "flex", flexDirection: "column", gap: 1 }}>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
(500 KB max. for GIFS){" "}
|
||||||
|
</Typography>
|
||||||
|
<ImageUploader onPick={(file) => setAvatarFile(file)}>
|
||||||
|
<Button variant="contained">Choose Image</Button>
|
||||||
|
</ImageUploader>
|
||||||
|
{avatarFile?.name}
|
||||||
|
<Spacer height="25px" />
|
||||||
|
|
||||||
|
<LoadingButton loading={isLoading} disabled={!avatarFile} onClick={publishAvatar} variant="contained">
|
||||||
|
Publish avatar
|
||||||
|
</LoadingButton>
|
||||||
|
</Box>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
};
|
@ -321,7 +321,7 @@ const Header = ({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
zIndex: 500,
|
zIndex: 6,
|
||||||
width: "30px", // Adjust as needed
|
width: "30px", // Adjust as needed
|
||||||
height: "30px", // Adjust as needed
|
height: "30px", // Adjust as needed
|
||||||
backgroundColor: "#232428", // Circle background
|
backgroundColor: "#232428", // Circle background
|
||||||
|
57
src/utils/fileReading/index.ts
Normal file
57
src/utils/fileReading/index.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
class Semaphore {
|
||||||
|
constructor(count) {
|
||||||
|
this.count = count
|
||||||
|
this.waiting = []
|
||||||
|
}
|
||||||
|
acquire() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if (this.count > 0) {
|
||||||
|
this.count--
|
||||||
|
resolve()
|
||||||
|
} else {
|
||||||
|
this.waiting.push(resolve)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
release() {
|
||||||
|
if (this.waiting.length > 0) {
|
||||||
|
const resolve = this.waiting.shift()
|
||||||
|
resolve()
|
||||||
|
} else {
|
||||||
|
this.count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let semaphore = new Semaphore(1)
|
||||||
|
let reader = new FileReader()
|
||||||
|
|
||||||
|
export const fileToBase64 = (file) => new Promise(async (resolve, reject) => {
|
||||||
|
if (!reader) {
|
||||||
|
reader = new FileReader()
|
||||||
|
}
|
||||||
|
await semaphore.acquire()
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
reader.onload = () => {
|
||||||
|
const dataUrl = reader.result
|
||||||
|
if (typeof dataUrl === "string") {
|
||||||
|
const base64String = dataUrl.split(',')[1]
|
||||||
|
reader.onload = null
|
||||||
|
reader.onerror = null
|
||||||
|
resolve(base64String)
|
||||||
|
} else {
|
||||||
|
reader.onload = null
|
||||||
|
reader.onerror = null
|
||||||
|
reject(new Error('Invalid data URL'))
|
||||||
|
}
|
||||||
|
semaphore.release()
|
||||||
|
}
|
||||||
|
reader.onerror = (error) => {
|
||||||
|
reader.onload = null
|
||||||
|
reader.onerror = null
|
||||||
|
reject(error)
|
||||||
|
semaphore.release()
|
||||||
|
}
|
||||||
|
})
|
Loading…
x
Reference in New Issue
Block a user