mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-28 20:43:33 +02:00
fix media uploads in reply form and stream chat
This commit is contained in:
@@ -80,7 +80,7 @@ const ReactionNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>((
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<NotificationIconEntry ref={ref} icon={<Heart boxSize={8} color="red.400" />}>
|
<NotificationIconEntry ref={ref} icon={<Heart boxSize={8} color="red.400" />}>
|
||||||
<Flex gap="2" alignItems="center">
|
<Flex gap="2" alignItems="center" pl="2">
|
||||||
<AvatarGroup size="sm">
|
<AvatarGroup size="sm">
|
||||||
<UserAvatarLink pubkey={event.pubkey} />
|
<UserAvatarLink pubkey={event.pubkey} />
|
||||||
</AvatarGroup>
|
</AvatarGroup>
|
||||||
@@ -123,7 +123,7 @@ const ZapNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ eve
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<NotificationIconEntry ref={ref} icon={<LightningIcon boxSize={8} color="yellow.400" />}>
|
<NotificationIconEntry ref={ref} icon={<LightningIcon boxSize={8} color="yellow.400" />}>
|
||||||
<Flex gap="2" alignItems="center">
|
<Flex gap="2" alignItems="center" pl="2">
|
||||||
<AvatarGroup size="sm">
|
<AvatarGroup size="sm">
|
||||||
<UserAvatarLink pubkey={zap.request.pubkey} />
|
<UserAvatarLink pubkey={zap.request.pubkey} />
|
||||||
</AvatarGroup>
|
</AvatarGroup>
|
||||||
|
@@ -1,19 +1,18 @@
|
|||||||
import { useCallback, useMemo, useRef } from "react";
|
import { useMemo, useRef } from "react";
|
||||||
import { Box, Button, Flex, useToast } from "@chakra-ui/react";
|
import { Box, Button, Flex, useToast } from "@chakra-ui/react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { ParsedStream, buildChatMessage } from "../../../../helpers/nostr/stream";
|
import { ParsedStream, buildChatMessage } from "../../../../helpers/nostr/stream";
|
||||||
import { unique } from "../../../../helpers/array";
|
import { unique } from "../../../../helpers/array";
|
||||||
import { useSigningContext } from "../../../../providers/global/signing-provider";
|
|
||||||
import { createEmojiTags, ensureNotifyContentMentions } from "../../../../helpers/nostr/post";
|
import { createEmojiTags, ensureNotifyContentMentions } from "../../../../helpers/nostr/post";
|
||||||
import { useContextEmojis } from "../../../../providers/global/emoji-provider";
|
import { useContextEmojis } from "../../../../providers/global/emoji-provider";
|
||||||
import { MagicInput, RefType } from "../../../../components/magic-textarea";
|
import { MagicInput, RefType } from "../../../../components/magic-textarea";
|
||||||
import StreamZapButton from "../../components/stream-zap-button";
|
import StreamZapButton from "../../components/stream-zap-button";
|
||||||
import { nostrBuildUploadImage } from "../../../../helpers/media-upload/nostr-build";
|
|
||||||
import { useUserInbox } from "../../../../hooks/use-user-mailboxes";
|
import { useUserInbox } from "../../../../hooks/use-user-mailboxes";
|
||||||
import { usePublishEvent } from "../../../../providers/global/publish-provider";
|
import { usePublishEvent } from "../../../../providers/global/publish-provider";
|
||||||
import { useReadRelays } from "../../../../hooks/use-client-relays";
|
import { useReadRelays } from "../../../../hooks/use-client-relays";
|
||||||
import { useAdditionalRelayContext } from "../../../../providers/local/additional-relay-context";
|
import { useAdditionalRelayContext } from "../../../../providers/local/additional-relay-context";
|
||||||
|
import { useTextAreaUploadFileWithForm } from "../../../../hooks/use-textarea-upload-file";
|
||||||
|
|
||||||
export default function ChatMessageForm({ stream, hideZapButton }: { stream: ParsedStream; hideZapButton?: boolean }) {
|
export default function ChatMessageForm({ stream, hideZapButton }: { stream: ParsedStream; hideZapButton?: boolean }) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -24,7 +23,6 @@ export default function ChatMessageForm({ stream, hideZapButton }: { stream: Par
|
|||||||
|
|
||||||
const relays = useMemo(() => unique([...streamRelays, ...hostReadRelays]), [hostReadRelays, streamRelays]);
|
const relays = useMemo(() => unique([...streamRelays, ...hostReadRelays]), [hostReadRelays, streamRelays]);
|
||||||
|
|
||||||
const { requestSignature } = useSigningContext();
|
|
||||||
const { setValue, handleSubmit, formState, reset, getValues, watch } = useForm({
|
const { setValue, handleSubmit, formState, reset, getValues, watch } = useForm({
|
||||||
defaultValues: { content: "" },
|
defaultValues: { content: "" },
|
||||||
});
|
});
|
||||||
@@ -37,25 +35,7 @@ export default function ChatMessageForm({ stream, hideZapButton }: { stream: Par
|
|||||||
});
|
});
|
||||||
|
|
||||||
const textAreaRef = useRef<RefType | null>(null);
|
const textAreaRef = useRef<RefType | null>(null);
|
||||||
const uploadImage = useCallback(
|
const { onPaste } = useTextAreaUploadFileWithForm(textAreaRef, getValues, setValue);
|
||||||
async (imageFile: File) => {
|
|
||||||
try {
|
|
||||||
if (!imageFile.type.includes("image")) throw new Error("Only images are supported");
|
|
||||||
|
|
||||||
const response = await nostrBuildUploadImage(imageFile, requestSignature);
|
|
||||||
const imageUrl = response.url;
|
|
||||||
|
|
||||||
const content = getValues().content;
|
|
||||||
const position = textAreaRef.current?.getCaretPosition();
|
|
||||||
if (position !== undefined) {
|
|
||||||
setValue("content", content.slice(0, position) + imageUrl + content.slice(position), { shouldDirty: true });
|
|
||||||
} else setValue("content", content + imageUrl, { shouldDirty: true });
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[setValue, getValues],
|
|
||||||
);
|
|
||||||
|
|
||||||
watch("content");
|
watch("content");
|
||||||
|
|
||||||
@@ -70,10 +50,8 @@ export default function ChatMessageForm({ stream, hideZapButton }: { stream: Par
|
|||||||
isRequired
|
isRequired
|
||||||
value={getValues().content}
|
value={getValues().content}
|
||||||
onChange={(e) => setValue("content", e.target.value, { shouldDirty: true })}
|
onChange={(e) => setValue("content", e.target.value, { shouldDirty: true })}
|
||||||
onPaste={(e) => {
|
// @ts-expect-error
|
||||||
const file = Array.from(e.clipboardData.files).find((f) => f.type.includes("image"));
|
onPaste={onPaste}
|
||||||
if (file) uploadImage(file);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Button colorScheme="primary" type="submit" isLoading={formState.isSubmitting}>
|
<Button colorScheme="primary" type="submit" isLoading={formState.isSubmitting}>
|
||||||
Send
|
Send
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useMemo, useRef, useState } from "react";
|
import { useMemo, useRef } from "react";
|
||||||
import { Box, Button, ButtonGroup, Flex, IconButton, VisuallyHiddenInput, useToast } from "@chakra-ui/react";
|
import { Box, Button, ButtonGroup, Flex, IconButton, VisuallyHiddenInput } from "@chakra-ui/react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useThrottle } from "react-use";
|
import { useThrottle } from "react-use";
|
||||||
import { kinds } from "nostr-tools";
|
import { kinds } from "nostr-tools";
|
||||||
@@ -16,16 +16,15 @@ import {
|
|||||||
getPubkeysMentionedInContent,
|
getPubkeysMentionedInContent,
|
||||||
} from "../../../helpers/nostr/post";
|
} from "../../../helpers/nostr/post";
|
||||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||||
import { useSigningContext } from "../../../providers/global/signing-provider";
|
|
||||||
import MagicTextArea, { RefType } from "../../../components/magic-textarea";
|
import MagicTextArea, { RefType } from "../../../components/magic-textarea";
|
||||||
import { useContextEmojis } from "../../../providers/global/emoji-provider";
|
import { useContextEmojis } from "../../../providers/global/emoji-provider";
|
||||||
import { TrustProvider } from "../../../providers/local/trust-provider";
|
import { TrustProvider } from "../../../providers/local/trust-provider";
|
||||||
import { nostrBuildUploadImage } from "../../../helpers/media-upload/nostr-build";
|
|
||||||
import { UploadImageIcon } from "../../../components/icons";
|
import { UploadImageIcon } from "../../../components/icons";
|
||||||
import { unique } from "../../../helpers/array";
|
import { unique } from "../../../helpers/array";
|
||||||
import { usePublishEvent } from "../../../providers/global/publish-provider";
|
import { usePublishEvent } from "../../../providers/global/publish-provider";
|
||||||
import { TextNoteContents } from "../../../components/note/timeline-note/text-note-contents";
|
import { TextNoteContents } from "../../../components/note/timeline-note/text-note-contents";
|
||||||
import useCacheForm from "../../../hooks/use-cache-form";
|
import useCacheForm from "../../../hooks/use-cache-form";
|
||||||
|
import { useTextAreaUploadFileWithForm } from "../../../hooks/use-textarea-upload-file";
|
||||||
|
|
||||||
export type ReplyFormProps = {
|
export type ReplyFormProps = {
|
||||||
item: ThreadItem;
|
item: ThreadItem;
|
||||||
@@ -35,11 +34,9 @@ export type ReplyFormProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function ReplyForm({ item, onCancel, onSubmitted, replyKind = kinds.ShortTextNote }: ReplyFormProps) {
|
export default function ReplyForm({ item, onCancel, onSubmitted, replyKind = kinds.ShortTextNote }: ReplyFormProps) {
|
||||||
const toast = useToast();
|
|
||||||
const publish = usePublishEvent();
|
const publish = usePublishEvent();
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
const emojis = useContextEmojis();
|
const emojis = useContextEmojis();
|
||||||
const { requestSignature } = useSigningContext();
|
|
||||||
|
|
||||||
const threadMembers = useMemo(() => getThreadMembers(item, account?.pubkey), [item, account?.pubkey]);
|
const threadMembers = useMemo(() => getThreadMembers(item, account?.pubkey), [item, account?.pubkey]);
|
||||||
const { setValue, getValues, watch, handleSubmit, formState, reset } = useForm({
|
const { setValue, getValues, watch, handleSubmit, formState, reset } = useForm({
|
||||||
@@ -57,28 +54,7 @@ export default function ReplyForm({ item, onCancel, onSubmitted, replyKind = kin
|
|||||||
|
|
||||||
const textAreaRef = useRef<RefType | null>(null);
|
const textAreaRef = useRef<RefType | null>(null);
|
||||||
const imageUploadRef = useRef<HTMLInputElement | null>(null);
|
const imageUploadRef = useRef<HTMLInputElement | null>(null);
|
||||||
const [uploading, setUploading] = useState(false);
|
const { onPaste, onFileInputChange, uploading } = useTextAreaUploadFileWithForm(textAreaRef, getValues, setValue);
|
||||||
const uploadImage = useCallback(
|
|
||||||
async (imageFile: File) => {
|
|
||||||
try {
|
|
||||||
if (!imageFile.type.includes("image")) throw new Error("Only images are supported");
|
|
||||||
|
|
||||||
setUploading(true);
|
|
||||||
const response = await nostrBuildUploadImage(imageFile, requestSignature);
|
|
||||||
const imageUrl = response.url;
|
|
||||||
|
|
||||||
const content = getValues().content;
|
|
||||||
const position = textAreaRef.current?.getCaretPosition();
|
|
||||||
if (position !== undefined) {
|
|
||||||
setValue("content", content.slice(0, position) + imageUrl + content.slice(position), { shouldDirty: true });
|
|
||||||
} else setValue("content", content + imageUrl, { shouldDirty: true });
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
|
||||||
}
|
|
||||||
setUploading(false);
|
|
||||||
},
|
|
||||||
[setValue, getValues],
|
|
||||||
);
|
|
||||||
|
|
||||||
const draft = useMemo(() => {
|
const draft = useMemo(() => {
|
||||||
let updated = finalizeNote({ kind: replyKind, content: getValues().content, created_at: dayjs().unix(), tags: [] });
|
let updated = finalizeNote({ kind: replyKind, content: getValues().content, created_at: dayjs().unix(), tags: [] });
|
||||||
@@ -109,24 +85,13 @@ export default function ReplyForm({ item, onCancel, onSubmitted, replyKind = kin
|
|||||||
value={getValues().content}
|
value={getValues().content}
|
||||||
onChange={(e) => setValue("content", e.target.value, { shouldDirty: true })}
|
onChange={(e) => setValue("content", e.target.value, { shouldDirty: true })}
|
||||||
instanceRef={(inst) => (textAreaRef.current = inst)}
|
instanceRef={(inst) => (textAreaRef.current = inst)}
|
||||||
onPaste={(e) => {
|
onPaste={onPaste}
|
||||||
const imageFile = Array.from(e.clipboardData.files).find((f) => f.type.includes("image"));
|
|
||||||
if (imageFile) uploadImage(imageFile);
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.ctrlKey && e.key === "Enter" && formRef.current) formRef.current.requestSubmit();
|
if (e.ctrlKey && e.key === "Enter" && formRef.current) formRef.current.requestSubmit();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Flex gap="2" alignItems="center">
|
<Flex gap="2" alignItems="center">
|
||||||
<VisuallyHiddenInput
|
<VisuallyHiddenInput type="file" accept="image/*" ref={imageUploadRef} onChange={onFileInputChange} />
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
ref={imageUploadRef}
|
|
||||||
onChange={(e) => {
|
|
||||||
const img = e.target.files?.[0];
|
|
||||||
if (img) uploadImage(img);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<UploadImageIcon />}
|
icon={<UploadImageIcon />}
|
||||||
aria-label="Upload Image"
|
aria-label="Upload Image"
|
||||||
|
Reference in New Issue
Block a user