add avatar

This commit is contained in:
PhilReact 2024-09-24 20:28:45 +03:00
parent 5e2778d475
commit ac5a6b251b
7 changed files with 405 additions and 2 deletions

View File

@ -90,6 +90,7 @@ import { requestQueueGroupJoinRequests } from "./components/Group/GroupJoinReque
import { DrawerComponent } from "./components/Drawer/Drawer";
import { LitecoinQRCode } from "./components/LitecoinQRCode";
import { Settings } from "./components/Group/Settings";
import { MainAvatar } from "./components/MainAvatar";
type extStates =
| "not-authenticated"
@ -1263,7 +1264,7 @@ function App() {
</>
) : (
<>
<img src={Logo2} />
<MainAvatar myName={userInfo?.name} />
<Spacer height="32px" />
<TextP
sx={{

View File

@ -5,6 +5,7 @@ import {
decryptGroupEncryption,
encryptAndPublishSymmetricKeyGroupChat,
publishGroupEncryptedResource,
publishOnQDN,
uint8ArrayToObject,
} from "./backgroundFunctions/encryption";
import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from "./constants/codes";
@ -3841,6 +3842,24 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => {
return true;
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": {
const { groups, directs } = request.payload;
handleActiveGroupDataFromSocket({

View File

@ -152,6 +152,24 @@ export const publishGroupEncryptedResource = async ({encryptedData, identifier})
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) {
const length = uint8Array.length

View 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

View 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>
)
};

View File

@ -321,7 +321,7 @@ const Header = ({
display: "flex",
justifyContent: "center",
alignItems: "center",
zIndex: 500,
zIndex: 6,
width: "30px", // Adjust as needed
height: "30px", // Adjust as needed
backgroundColor: "#232428", // Circle background

View 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()
}
})