diff --git a/src/App.tsx b/src/App.tsx index 2a50651..f5d4939 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { ) : ( <> - + { 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({ diff --git a/src/backgroundFunctions/encryption.ts b/src/backgroundFunctions/encryption.ts index d641644..54640f1 100644 --- a/src/backgroundFunctions/encryption.ts +++ b/src/backgroundFunctions/encryption.ts @@ -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 diff --git a/src/common/ImageUploader.tsx b/src/common/ImageUploader.tsx new file mode 100644 index 0000000..f21c17f --- /dev/null +++ b/src/common/ImageUploader.tsx @@ -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 => + 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 = ({ 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((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 ( + + + {children} + + ) +} + +export default ImageUploader diff --git a/src/components/MainAvatar.tsx b/src/components/MainAvatar.tsx new file mode 100644 index 0000000..71aed3b --- /dev/null +++ b/src/components/MainAvatar.tsx @@ -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 ( + <> + + {myName?.charAt(0)} + + + + change avatar + + + + + ); + } + + if (hasAvatar) { + return ( + <> + + {myName?.charAt(0)} + + + + change avatar + + + + + ); + } + + return ( + <> + + + + set avatar + + + + + ); +}; + + +const PopoverComp = ({avatarFile, setAvatarFile, id, open, anchorEl, handleClose, publishAvatar, isLoading}) => { + return ( + + + + (500 KB max. for GIFS){" "} + + setAvatarFile(file)}> + + + {avatarFile?.name} + + + + Publish avatar + + + + ) + }; \ No newline at end of file diff --git a/src/components/Mobile/MobileHeader.tsx b/src/components/Mobile/MobileHeader.tsx index f49291e..5856259 100644 --- a/src/components/Mobile/MobileHeader.tsx +++ b/src/components/Mobile/MobileHeader.tsx @@ -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 diff --git a/src/utils/fileReading/index.ts b/src/utils/fileReading/index.ts new file mode 100644 index 0000000..c96dd74 --- /dev/null +++ b/src/utils/fileReading/index.ts @@ -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() + } +}) \ No newline at end of file