diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 000000000..4a7ad40a4 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +pnpm 9.15.5 +nodejs 20.18.0 diff --git a/src/hooks/use-input-upload-file.ts b/src/hooks/use-input-upload-file.ts new file mode 100644 index 000000000..4bdba1dea --- /dev/null +++ b/src/hooks/use-input-upload-file.ts @@ -0,0 +1,37 @@ +import { ChangeEventHandler, ClipboardEventHandler, useCallback } from "react"; +import useUploadFile from "./use-upload-file"; +import { UseFormSetValue } from "react-hook-form"; + +export function useInputUploadFileWithForm(setValue: UseFormSetValue, field: string) { + const setText = useCallback((text: string) => setValue(field, text), [setValue]); + return useInputUploadFile(setText); +} + +export default function useInputUploadFile(setText: (text: string) => void) { + const { uploadFile, uploading } = useUploadFile(); + + const privateUploadFile = useCallback(async (file: File) => { + const imageUrl = await uploadFile(file); + + if (imageUrl) + setText(imageUrl); + }, [uploadFile]) + + const onFileInputChange = useCallback>( + (e) => { + const img = e.target.files?.[0]; + if (img) privateUploadFile(img); + }, + [privateUploadFile], + ); + + const onPaste = useCallback>( + (e) => { + const imageFile = Array.from(e.clipboardData.files).find((f) => f.type.includes("image")); + if (imageFile) privateUploadFile(imageFile); + }, + [privateUploadFile], + ); + + return { uploadFile, uploading, onPaste, onFileInputChange }; +} diff --git a/src/hooks/use-textarea-upload-file.ts b/src/hooks/use-textarea-upload-file.ts index 16b11263d..067d89972 100644 --- a/src/hooks/use-textarea-upload-file.ts +++ b/src/hooks/use-textarea-upload-file.ts @@ -1,16 +1,9 @@ import { ChangeEventHandler, ClipboardEventHandler, MutableRefObject, useCallback, useState } from "react"; -import { useToast } from "@chakra-ui/react"; -import { useActiveAccount } from "applesauce-react/hooks"; -import { nostrBuildUploadImage } from "../helpers/media-upload/nostr-build"; import { RefType } from "../components/magic-textarea"; -import { useSigningContext } from "../providers/global/signing-provider"; import { UseFormGetValues, UseFormSetValue } from "react-hook-form"; -import useAppSettings from "./use-user-app-settings"; -import useUsersMediaServers from "./use-user-media-servers"; -import { simpleMultiServerUpload } from "../helpers/media-upload/blossom"; -import { stripSensitiveMetadataOnFile } from "../helpers/image"; import insertTextIntoMagicTextarea from "../helpers/magic-textarea"; +import useUploadFile from "./use-upload-file"; export function useTextAreaUploadFileWithForm( ref: MutableRefObject, @@ -41,52 +34,29 @@ export function useTextAreaInsertTextWithForm( } export default function useTextAreaUploadFile(insertText: (url: string) => void) { - const toast = useToast(); - const account = useActiveAccount(); - const { mediaUploadService } = useAppSettings(); - const { servers: mediaServers } = useUsersMediaServers(account?.pubkey); - const { requestSignature } = useSigningContext(); + const { uploadFile, uploading } = useUploadFile(); - const [uploading, setUploading] = useState(false); - const uploadFile = useCallback( - async (file: File) => { - setUploading(true); - try { - const safeFile = await stripSensitiveMetadataOnFile(file); - if (mediaUploadService === "nostr.build") { - const response = await nostrBuildUploadImage(safeFile, requestSignature); - const imageUrl = response.url; - insertText(imageUrl); - } else if (mediaUploadService === "blossom" && mediaServers.length) { - const blob = await simpleMultiServerUpload( - mediaServers.map((s) => s.toString()), - safeFile, - requestSignature, - ); - insertText(blob.url); - } - } catch (e) { - if (e instanceof Error) toast({ description: e.message, status: "error" }); - } - setUploading(false); - }, - [insertText, toast, setUploading, mediaServers, mediaUploadService], - ); + const privateUploadFile = useCallback(async (file: File) => { + const imageUrl = await uploadFile(file); + + if (imageUrl) + insertText(imageUrl); + }, [uploadFile]) const onFileInputChange = useCallback>( (e) => { const img = e.target.files?.[0]; - if (img) uploadFile(img); + if (img) privateUploadFile(img); }, - [uploadFile], + [privateUploadFile], ); const onPaste = useCallback>( (e) => { const imageFile = Array.from(e.clipboardData.files).find((f) => f.type.includes("image")); - if (imageFile) uploadFile(imageFile); + if (imageFile) privateUploadFile(imageFile); }, - [uploadFile], + [privateUploadFile], ); return { uploadFile, uploading, onPaste, onFileInputChange }; diff --git a/src/hooks/use-upload-file.ts b/src/hooks/use-upload-file.ts new file mode 100644 index 000000000..371b87718 --- /dev/null +++ b/src/hooks/use-upload-file.ts @@ -0,0 +1,52 @@ +import { useCallback, useState } from "react"; +import { stripSensitiveMetadataOnFile } from "../helpers/image"; +import { nostrBuildUploadImage } from "../helpers/media-upload/nostr-build"; +import { useToast } from "@chakra-ui/react"; +import useUsersMediaServers from "./use-user-media-servers"; +import { useSigningContext } from "../providers/global/signing-provider"; +import { useActiveAccount } from "applesauce-react/hooks"; +import useAppSettings from "./use-user-app-settings"; +import { simpleMultiServerUpload } from "~/helpers/media-upload/blossom"; + +export default function useUploadFile() { + const toast = useToast(); + const account = useActiveAccount(); + const { mediaUploadService } = useAppSettings(); + const { servers: mediaServers } = useUsersMediaServers(account?.pubkey); + const { requestSignature } = useSigningContext(); + + const [uploading, setUploading] = useState(false); + + const uploadFile = useCallback( + async (file: File) => { + let imageUrl: string | undefined = undefined; + + setUploading(true); + try { + const safeFile = await stripSensitiveMetadataOnFile(file); + if (mediaUploadService === "nostr.build") { + const response = await nostrBuildUploadImage(safeFile, requestSignature); + imageUrl = response.url; + } else if (mediaUploadService === "blossom" && mediaServers.length) { + const blob = await simpleMultiServerUpload( + mediaServers.map((s) => s.toString()), + safeFile, + requestSignature, + ); + imageUrl = blob.url; + } + } catch (e) { + if (e instanceof Error) toast({ description: e.message, status: "error" }); + } + setUploading(false); + + return imageUrl; + }, + [toast, setUploading, mediaServers, mediaUploadService], + ); + + return { + uploadFile, + uploading, + }; +} diff --git a/src/views/profile/edit.tsx b/src/views/profile/edit.tsx index 3575f47a2..0e258baf8 100644 --- a/src/views/profile/edit.tsx +++ b/src/views/profile/edit.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo } from "react"; +import { useEffect, useMemo, useRef } from "react"; import { Avatar, Button, @@ -9,11 +9,15 @@ import { Input, Link, Textarea, + InputGroup, + InputRightElement, + IconButton, + VisuallyHiddenInput, } from "@chakra-ui/react"; import { useForm } from "react-hook-form"; import { ProfileContent, unixNow } from "applesauce-core/helpers"; -import { ExternalLinkIcon } from "../../components/icons"; +import { ExternalLinkIcon, OutboxIcon } from "../../components/icons"; import { isLNURL } from "../../helpers/lnurl"; import { useReadRelays } from "../../hooks/use-client-relays"; import { useActiveAccount } from "applesauce-react/hooks"; @@ -24,6 +28,7 @@ import lnurlMetadataService from "../../services/lnurl-metadata"; import VerticalPageLayout from "../../components/vertical-page-layout"; import { COMMON_CONTACT_RELAYS } from "../../const"; import { usePublishEvent } from "../../providers/global/publish-provider"; +import { useInputUploadFileWithForm } from "../../hooks/use-input-upload-file"; const isEmail = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; @@ -55,6 +60,7 @@ const MetadataForm = ({ defaultValues, onSubmit }: MetadataFormProps) => { reset, handleSubmit, watch, + setValue, formState: { errors, isSubmitting }, } = useForm({ mode: "onBlur", @@ -65,6 +71,12 @@ const MetadataForm = ({ defaultValues, onSubmit }: MetadataFormProps) => { reset(defaultValues); }, [defaultValues]); + const pictureUploadManage = useInputUploadFileWithForm(setValue, "picture"); + const pictureUploadRef = useRef(null); + + const bannerUploadManage = useInputUploadFileWithForm(setValue, "banner"); + const bannerUploadRef = useRef(null); + return ( @@ -113,24 +125,62 @@ const MetadataForm = ({ defaultValues, onSubmit }: MetadataFormProps) => { Picture - + + + + } + title="Upload picture" + aria-label="Upload picture" + onClick={() => pictureUploadRef.current?.click()} + /> + + + Banner - + + + + } + title="Upload baner" + aria-label="Upload banner" + onClick={() => bannerUploadRef.current?.click()} + /> + + +