Add insert gif button

This commit is contained in:
hzrd149 2024-11-26 20:01:19 -06:00
parent 9c9a1b588c
commit 5403d37a49
16 changed files with 422 additions and 105 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add insert gif button

View File

@ -0,0 +1,136 @@
import { useEffect, useState } from "react";
import {
Button,
ButtonGroup,
Flex,
FlexProps,
Image,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
ModalProps,
SimpleGrid,
} from "@chakra-ui/react";
import { kinds, NostrEvent } from "nostr-tools";
import { getEventUID, getTagValue } from "applesauce-core/helpers";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { useReadRelays } from "../../hooks/use-client-relays";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import SearchRelayPicker, { useSearchRelay } from "../../views/search/components/search-relay-picker";
import useEventIntersectionRef from "../../hooks/use-event-intersection-ref";
import { ListId, usePeopleListSelect } from "../../providers/local/people-list-provider";
function GifCard({ gif, onClick }: Omit<FlexProps, "children" | "onClick"> & { gif: NostrEvent; onClick: () => void }) {
const url = getTagValue(gif, "url");
const thumb = getTagValue(gif, "thumb");
const ref = useEventIntersectionRef(gif);
if (!url) return null;
return (
<Flex ref={ref} alignItems="center" justifyContent="center" position="relative">
{/* <Flex direction="column" h="full">
<UserAvatarLink pubkey={gif.pubkey} size="sm" />
<SingleZapButton event={gif} showEventPreview={false} variant="ghost" size="sm" />
<DebugEventButton event={gif} position="absolute" variant="ghost" size="sm" mt="auto" />
</Flex> */}
<button onClick={onClick}>
<Image src={thumb || url} rounded="md" minH="20" minW="20" />
</button>
</Flex>
);
}
type GifPickerProps = Omit<ModalProps, "children"> & { onSelect: (gif: NostrEvent) => void };
export default function GifPickerModal({ onClose, isOpen, onSelect, ...props }: GifPickerProps) {
const [search, setSearch] = useState<string>();
const [searchRelayUrl, setSearchRelayUrl] = useState<string>();
const [list, setList] = useState<ListId>("global");
const { selected, setSelected, filter, listId } = usePeopleListSelect(list, setList);
const searchRelay = useSearchRelay(searchRelayUrl);
const [debounceSearch, setDebounceSearch] = useState<string>();
useEffect(() => {
setDebounceSearch(undefined);
const t = setTimeout(() => setDebounceSearch(search), 600);
return () => clearTimeout(t);
}, [search, setDebounceSearch]);
const baseFilter = {
kinds: [kinds.FileMetadata],
"#m": ["image/gif"],
...filter,
};
const readRelays = useReadRelays();
const { loader, timeline } = useTimelineLoader(
[listId, "gifs", searchRelay?.url ?? "all", debounceSearch ?? "all"].join("-"),
searchRelay !== undefined ? [searchRelay] : readRelays,
debounceSearch !== undefined ? { ...baseFilter, search: debounceSearch } : baseFilter,
);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<Modal isOpen={isOpen} onClose={onClose} scrollBehavior="inside" size={{ base: "full", md: "6xl" }} {...props}>
<ModalOverlay />
<IntersectionObserverProvider callback={callback}>
<ModalContent>
<ModalHeader p="2" pr="16">
<Flex gap="2" wrap="wrap">
<Input
type="search"
maxW="sm"
placeholder="Search gifs"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<SearchRelayPicker value={searchRelayUrl} onChange={(e) => setSearchRelayUrl(e.target.value)} />
<Button type="submit">Search</Button>
</Flex>
<ButtonGroup size="xs">
<Button colorScheme={selected === "global" ? "primary" : undefined} onClick={() => setSelected("global")}>
Global
</Button>
<Button
colorScheme={selected === "following" ? "primary" : undefined}
onClick={() => setSelected("following")}
>
Follows
</Button>
<Button colorScheme={selected === "self" ? "primary" : undefined} onClick={() => setSelected("self")}>
Personal
</Button>
</ButtonGroup>
</ModalHeader>
<ModalCloseButton />
<ModalBody p="4">
<SimpleGrid columns={{ base: 1, sm: 2, md: 3, lg: 4 }} gap="2">
{timeline.map((gif) => (
<GifCard
key={getEventUID(gif)}
gif={gif}
onClick={() => {
onSelect(gif);
onClose();
}}
/>
))}
</SimpleGrid>
</ModalBody>
</ModalContent>
</IntersectionObserverProvider>
</Modal>
);
}

View File

@ -0,0 +1,35 @@
import { IconButton, IconButtonProps, useDisclosure } from "@chakra-ui/react";
import Clapperboard from "../icons/clapperboard";
import GifPickerModal from "./gif-picker-modal";
import { NostrEvent } from "nostr-tools";
import { useCallback } from "react";
import { getTagValue } from "applesauce-core/helpers";
export default function InsertGifButton({
onSelect,
onSelectURL,
...props
}: Omit<IconButtonProps, "icon" | "onSelect"> & {
onSelect?: (gif: NostrEvent) => void;
onSelectURL?: (url: string) => void;
}) {
const modal = useDisclosure();
const handleSelect = useCallback(
(gif: NostrEvent) => {
if (onSelect) onSelect(gif);
if (onSelectURL) {
const url = getTagValue(gif, "url");
if (url) onSelectURL(url);
}
},
[onSelect, onSelectURL],
);
return (
<>
<IconButton icon={<Clapperboard boxSize={5} />} onClick={modal.onOpen} {...props} />
{modal.isOpen && <GifPickerModal onClose={modal.onClose} isOpen onSelect={handleSelect} />}
</>
);
}

View File

@ -0,0 +1,46 @@
import { ButtonProps, IconButton, useDisclosure } from "@chakra-ui/react";
import { NostrEvent } from "../../types/nostr-event";
import { LightningIcon } from "../icons";
import ZapModal from "../event-zap-modal";
import useUserLNURLMetadata from "../../hooks/use-user-lnurl-metadata";
export type SingleZapButton = Omit<ButtonProps, "children"> & {
event: NostrEvent;
allowComment?: boolean;
showEventPreview?: boolean;
};
export default function SingleZapButton({ event, allowComment, showEventPreview, ...props }: SingleZapButton) {
const { metadata } = useUserLNURLMetadata(event.pubkey);
const { isOpen, onOpen, onClose } = useDisclosure();
const canZap = !!metadata?.allowsNostr || event.tags.some((t) => t[0] === "zap");
if (!canZap) return null;
return (
<>
<IconButton
icon={<LightningIcon color="yellow.400" verticalAlign="sub" />}
aria-label="Zap"
title="Zap"
{...props}
onClick={onOpen}
isDisabled={!canZap}
/>
{isOpen && (
<ZapModal
isOpen={isOpen}
pubkey={event.pubkey}
event={event}
onClose={onClose}
onZapped={onClose}
allowComment={allowComment}
showEmbed={showEventPreview}
/>
)}
</>
);
}

View File

@ -54,7 +54,7 @@ import ZapSplitCreator, { fillRemainingPercent } from "./zap-split-creator";
import { EventSplit } from "../../helpers/nostr/zaps";
import useCurrentAccount from "../../hooks/use-current-account";
import useCacheForm from "../../hooks/use-cache-form";
import { useTextAreaUploadFileWithForm } from "../../hooks/use-textarea-upload-file";
import useTextAreaUploadFile, { useTextAreaInsertTextWithForm } from "../../hooks/use-textarea-upload-file";
import MinePOW from "../pow/mine-pow";
import useAppSettings from "../../hooks/use-app-settings";
import { ErrorBoundary } from "../error-boundary";
@ -62,6 +62,8 @@ import { useFinalizeDraft, usePublishEvent } from "../../providers/global/publis
import { TextNoteContents } from "../note/timeline-note/text-note-contents";
import localSettings from "../../services/local-settings";
import useLocalStorageDisclosure from "../../hooks/use-localstorage-disclosure";
import InsertGifButton from "../gif/insert-gif-button";
import InsertImageButton from "./insert-image-button";
type FormValues = {
subject: string;
@ -162,7 +164,8 @@ export default function PostModal({
const imageUploadRef = useRef<HTMLInputElement | null>(null);
const textAreaRef = useRef<RefType | null>(null);
const { onPaste, onFileInputChange, uploading } = useTextAreaUploadFileWithForm(textAreaRef, getValues, setValue);
const insertText = useTextAreaInsertTextWithForm(textAreaRef, getValues, setValue);
const { onPaste } = useTextAreaUploadFile(insertText);
const publishPost = async (unsigned?: UnsignedEvent) => {
unsigned = unsigned || draft || (await updateDraft());
@ -240,19 +243,8 @@ export default function PostModal({
)}
<Flex gap="2" alignItems="center" justifyContent="flex-end">
<Flex mr="auto" gap="2">
<VisuallyHiddenInput
type="file"
accept="image/*,audio/*,video/*"
ref={imageUploadRef}
onChange={onFileInputChange}
/>
<IconButton
icon={<UploadImageIcon boxSize={6} />}
aria-label="Upload Image"
title="Upload Image"
onClick={() => imageUploadRef.current?.click()}
isLoading={uploading}
/>
<InsertImageButton onUploaded={insertText} aria-label="Upload image" />
<InsertGifButton onSelectURL={insertText} aria-label="Add gif" />
<Button
variant="link"
rightIcon={moreOptions.isOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}

View File

@ -0,0 +1,30 @@
import { useRef } from "react";
import { IconButton, IconButtonProps, VisuallyHiddenInput } from "@chakra-ui/react";
import { UploadImageIcon } from "../icons";
import useTextAreaUploadFile from "../../hooks/use-textarea-upload-file";
export default function InsertImageButton({
onUploaded,
...props
}: Omit<IconButtonProps, "icon"> & { onUploaded: (url: string) => void }) {
const imageUploadRef = useRef<HTMLInputElement | null>(null);
const { onFileInputChange, uploading } = useTextAreaUploadFile(onUploaded);
return (
<>
<VisuallyHiddenInput
type="file"
accept="image/*,audio/*,video/*"
ref={imageUploadRef}
onChange={onFileInputChange}
/>
<IconButton
icon={<UploadImageIcon boxSize={6} />}
onClick={() => imageUploadRef.current?.click()}
isLoading={uploading}
{...props}
/>
</>
);
}

View File

@ -0,0 +1,29 @@
import { RefType } from "../components/magic-textarea";
export default function insertTextIntoMagicTextarea(
instance: RefType,
getText: () => string,
setText: (text: string) => void,
text: string,
) {
const content = getText();
const position = instance.getCaretPosition();
if (position !== undefined) {
let inject = text;
// add a space before
if (position >= 1 && content.slice(position - 1, position) !== " ") inject = " " + inject;
// add a space after
if (position < content.length && content.slice(position, position + 1) !== " ") inject = inject + " ";
setText(content.slice(0, position) + inject + content.slice(position));
} else {
let inject = text;
// add a space before if there isn't one
if (content.slice(content.length - 1) !== " ") inject = " " + inject;
setText(content + inject + " ");
}
}

View File

@ -8,7 +8,5 @@ export default function useSearchRelays() {
const searchRelayList = useUserSearchRelayList(account?.pubkey);
const searchRelays = searchRelayList ? getRelaysFromList(searchRelayList) : DEFAULT_SEARCH_RELAYS;
// TODO: maybe add localRelay into the list if it supports NIP-50
return searchRelays;
}

View File

@ -10,56 +10,43 @@ import useUsersMediaServers from "./use-user-media-servers";
import { simpleMultiServerUpload } from "../helpers/media-upload/blossom";
import useCurrentAccount from "./use-current-account";
import { stripSensitiveMetadataOnFile } from "../helpers/image";
import insertTextIntoMagicTextarea from "../helpers/magic-textarea";
export function useTextAreaUploadFileWithForm(
ref: MutableRefObject<RefType | null>,
getValues: UseFormGetValues<any>,
setValue: UseFormSetValue<any>,
) {
const getText = useCallback(() => getValues().content, [getValues]);
const setText = useCallback(
(text: string) => setValue("content", text, { shouldDirty: true, shouldTouch: true }),
[setValue],
);
return useTextAreaUploadFile(ref, getText, setText);
const insertText = useTextAreaInsertTextWithForm(ref, getValues, setValue);
return useTextAreaUploadFile(insertText);
}
export default function useTextAreaUploadFile(
export function useTextAreaInsertTextWithForm(
ref: MutableRefObject<RefType | null>,
getText: () => string,
setText: (text: string) => void,
getValues: UseFormGetValues<any>,
setValue: UseFormSetValue<any>,
field = "content",
) {
const getText = useCallback(() => getValues()[field], [getValues, field]);
const setText = useCallback(
(text: string) => setValue(field, text, { shouldDirty: true, shouldTouch: true }),
[setValue, field],
);
return useCallback(
(text: string) => {
if (ref.current) insertTextIntoMagicTextarea(ref.current, getText, setText, text);
},
[setText, getText],
);
}
export default function useTextAreaUploadFile(insertText: (url: string) => void) {
const toast = useToast();
const account = useCurrentAccount();
const { mediaUploadService } = useAppSettings();
const { servers: mediaServers } = useUsersMediaServers(account?.pubkey);
const { requestSignature } = useSigningContext();
const insertURL = useCallback(
(url: string) => {
const content = getText();
const position = ref.current?.getCaretPosition();
if (position !== undefined) {
let inject = url;
// add a space before
if (position >= 1 && content.slice(position - 1, position) !== " ") inject = " " + inject;
// add a space after
if (position < content.length && content.slice(position, position + 1) !== " ") inject = inject + " ";
setText(content.slice(0, position) + inject + content.slice(position));
} else {
let inject = url;
// add a space before if there isn't one
if (content.slice(content.length - 1) !== " ") inject = " " + inject;
setText(content + inject + " ");
}
},
[setText, getText],
);
const [uploading, setUploading] = useState(false);
const uploadFile = useCallback(
async (file: File) => {
@ -69,21 +56,21 @@ export default function useTextAreaUploadFile(
if (mediaUploadService === "nostr.build") {
const response = await nostrBuildUploadImage(safeFile, requestSignature);
const imageUrl = response.url;
insertURL(imageUrl);
insertText(imageUrl);
} else if (mediaUploadService === "blossom" && mediaServers.length) {
const blob = await simpleMultiServerUpload(
mediaServers.map((s) => s.toString()),
safeFile,
requestSignature,
);
insertURL(blob.url);
insertText(blob.url);
}
} catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
setUploading(false);
},
[insertURL, toast, setUploading, mediaServers, mediaUploadService],
[insertText, toast, setUploading, mediaServers, mediaUploadService],
);
const onFileInputChange = useCallback<ChangeEventHandler<HTMLInputElement>>(

View File

@ -1,11 +1,12 @@
import { useEffect, useMemo } from "react";
import { usePrevious, useUnmount } from "react-use";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import { useStoreQuery } from "applesauce-react/hooks";
import { Queries } from "applesauce-core";
import { Filter } from "nostr-tools";
import timelineCacheService from "../services/timeline-cache";
import { EventFilter } from "../classes/timeline-loader";
import { useStoreQuery } from "applesauce-react/hooks";
import { Queries } from "applesauce-core";
type Options = {
eventFilter?: EventFilter;
@ -14,7 +15,7 @@ type Options = {
export default function useTimelineLoader(
key: string,
relays: Iterable<string>,
relays: Iterable<string | AbstractRelay>,
filters: Filter | Filter[] | undefined,
opts?: Options,
) {
@ -27,7 +28,11 @@ export default function useTimelineLoader(
useEffect(() => {
loader.setRelays(relays);
loader.triggerChunkLoad();
}, [Array.from(relays).join("|")]);
}, [
Array.from(relays)
.map((t) => (typeof t === "string" ? t : t.url))
.join("|"),
]);
// update filters
useEffect(() => {

View File

@ -7,7 +7,7 @@ import useReplaceableEvent from "../../hooks/use-replaceable-event";
import { NostrEvent } from "../../types/nostr-event";
import useRouteSearchValue from "../../hooks/use-route-search-value";
export type ListId = "following" | "global" | string;
export type ListId = "following" | "global" | "self" | string;
export type Person = { pubkey: string; relay?: string };
export type PeopleListContextType = {
@ -34,11 +34,40 @@ function useListCoordinate(listId: ListId) {
return useMemo(() => {
if (listId === "following") return account ? `${kinds.Contacts}:${account.pubkey}` : undefined;
if (listId === "self") return undefined;
if (listId === "global") return undefined;
return listId;
}, [listId, account]);
}
export function usePeopleListSelect(selected: ListId, onChange: (list: ListId) => void): PeopleListContextType {
const account = useCurrentAccount();
const listId = useListCoordinate(selected);
const listEvent = useReplaceableEvent(listId, [], { alwaysRequest: true });
const people = listEvent && getPubkeysFromList(listEvent);
const filter = useMemo<Filter | undefined>(() => {
if (selected === "global") return {};
if (selected === "self") {
if (account) return { authors: [account.pubkey] };
else return {};
}
if (!people) return undefined;
return { authors: people.map((p) => p.pubkey) };
}, [people, selected, account]);
return {
people,
selected,
listId,
listEvent,
setSelected: onChange,
filter,
};
}
export type PeopleListProviderProps = PropsWithChildren & {
initList?: ListId;
};
@ -54,28 +83,7 @@ export default function PeopleListProvider({ children, initList }: PeopleListPro
[peopleParam.setValue],
);
const listId = useListCoordinate(selected);
const listEvent = useReplaceableEvent(listId, [], { alwaysRequest: true });
const people = listEvent && getPubkeysFromList(listEvent);
const filter = useMemo<Filter | undefined>(() => {
if (selected === "global") return {};
if (!people) return undefined;
return { authors: people.map((p) => p.pubkey) };
}, [people, selected]);
const context = useMemo(
() => ({
people,
selected,
listId,
listEvent,
setSelected,
filter,
}),
[selected, setSelected, people, listEvent],
);
const context = usePeopleListSelect(selected, setSelected);
return <PeopleListContext.Provider value={context}>{children}</PeopleListContext.Provider>;
}

View File

@ -1,16 +1,18 @@
import { useRef, useState } from "react";
import { useCallback, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import dayjs from "dayjs";
import { kinds } from "nostr-tools";
import { Button, Flex, FlexProps, Heading } from "@chakra-ui/react";
import { Button, ButtonGroup, Flex, FlexProps, Heading } from "@chakra-ui/react";
import MagicTextArea, { RefType } from "../../../components/magic-textarea";
import { useTextAreaUploadFileWithForm } from "../../../hooks/use-textarea-upload-file";
import useTextAreaUploadFile, { useTextAreaInsertTextWithForm } from "../../../hooks/use-textarea-upload-file";
import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
import { createEmojiTags, ensureNotifyPubkeys, getPubkeysMentionedInContent } from "../../../helpers/nostr/post";
import { useContextEmojis } from "../../../providers/global/emoji-provider";
import { usePublishEvent } from "../../../providers/global/publish-provider";
import InsertGifButton from "../../../components/gif/insert-gif-button";
import InsertImageButton from "../../../components/post-modal/insert-image-button";
export default function ChannelMessageForm({
channel,
@ -31,7 +33,8 @@ export default function ChannelMessageForm({
const componentRef = useRef<RefType | null>(null);
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
const { onPaste } = useTextAreaUploadFileWithForm(componentRef, getValues, setValue);
const insertText = useTextAreaInsertTextWithForm(componentRef, getValues, setValue);
const { onPaste } = useTextAreaUploadFile(insertText);
const sendMessage = handleSubmit(async (values) => {
if (!values.content) return;
@ -79,7 +82,13 @@ export default function ChannelMessageForm({
if ((e.ctrlKey || e.metaKey) && e.key === "Enter" && formRef.current) formRef.current.requestSubmit();
}}
/>
<Button type="submit">Send</Button>
<Flex gap="2" direction="column">
<Button type="submit">Send</Button>
<ButtonGroup size="sm">
<InsertImageButton onUploaded={insertText} aria-label="Upload image" />
<InsertGifButton onSelectURL={insertText} aria-label="Add gif" />
</ButtonGroup>
</Flex>
</>
)}
</Flex>

View File

@ -6,12 +6,13 @@ import { kinds } from "nostr-tools";
import { Button, Flex, FlexProps, Heading } from "@chakra-ui/react";
import { useSigningContext } from "../../../providers/global/signing-provider";
import MagicTextArea, { RefType } from "../../../components/magic-textarea";
import { useTextAreaUploadFileWithForm } from "../../../hooks/use-textarea-upload-file";
import useTextAreaUploadFile, { useTextAreaInsertTextWithForm } from "../../../hooks/use-textarea-upload-file";
import { DraftNostrEvent } from "../../../types/nostr-event";
import useUserMailboxes from "../../../hooks/use-user-mailboxes";
import { usePublishEvent } from "../../../providers/global/publish-provider";
import useCacheForm from "../../../hooks/use-cache-form";
import decryptionCacheService from "../../../services/decryption-cache";
import InsertGifButton from "../../../components/gif/insert-gif-button";
export default function SendMessageForm({
pubkey,
@ -36,7 +37,8 @@ export default function SendMessageForm({
const autocompleteRef = useRef<RefType | null>(null);
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
const { onPaste } = useTextAreaUploadFileWithForm(autocompleteRef, getValues, setValue);
const insertText = useTextAreaInsertTextWithForm(autocompleteRef, getValues, setValue);
const { onPaste } = useTextAreaUploadFile(insertText);
const userMailboxes = useUserMailboxes(pubkey);
const sendMessage = handleSubmit(async (values) => {
@ -96,7 +98,10 @@ export default function SendMessageForm({
if ((e.ctrlKey || e.metaKey) && e.key === "Enter" && formRef.current) formRef.current.requestSubmit();
}}
/>
<Button type="submit">Send</Button>
<Flex gap="2" direction="column">
<Button type="submit">Send</Button>
<InsertGifButton onSelectURL={insertText} aria-label="Add gif" />
</Flex>
</>
)}
</Flex>

View File

@ -0,0 +1,35 @@
import { forwardRef } from "react";
import { Select, SelectProps } from "@chakra-ui/react";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import useSearchRelays from "../../../hooks/use-search-relays";
import { useRelayInfo } from "../../../hooks/use-relay-info";
import { localRelay } from "../../../services/local-relay";
import WasmRelay from "../../../services/wasm-relay";
import relayPoolService from "../../../services/relay-pool";
export function useSearchRelay(relay?: string) {
if (!relay) return undefined;
if (relay === "local") return localRelay as AbstractRelay;
else return relayPoolService.requestRelay(relay);
}
const SearchRelayPicker = forwardRef<any, Omit<SelectProps, "children">>(({ value, onChange, ...props }) => {
const searchRelays = useSearchRelays();
const { info: localRelayInfo } = useRelayInfo(localRelay instanceof AbstractRelay ? localRelay : undefined, true);
const localSearchSupported =
localRelay instanceof WasmRelay ||
(localRelay instanceof AbstractRelay && !!localRelayInfo?.supported_nips?.includes(50));
return (
<Select w="auto" value={value} onChange={onChange} {...props}>
{localSearchSupported && <option value="local">Local Relay</option>}
{searchRelays.map((url) => (
<option key={url} value={url}>
{url}
</option>
))}
</Select>
);
});
export default SearchRelayPicker;

View File

@ -12,7 +12,8 @@ import { useUserInbox } from "../../../../hooks/use-user-mailboxes";
import { usePublishEvent } from "../../../../providers/global/publish-provider";
import { useReadRelays } from "../../../../hooks/use-client-relays";
import { useAdditionalRelayContext } from "../../../../providers/local/additional-relay-context";
import { useTextAreaUploadFileWithForm } from "../../../../hooks/use-textarea-upload-file";
import useTextAreaUploadFile, { useTextAreaInsertTextWithForm } from "../../../../hooks/use-textarea-upload-file";
import InsertGifButton from "../../../../components/gif/insert-gif-button";
export default function ChatMessageForm({ stream, hideZapButton }: { stream: ParsedStream; hideZapButton?: boolean }) {
const toast = useToast();
@ -35,7 +36,8 @@ export default function ChatMessageForm({ stream, hideZapButton }: { stream: Par
});
const textAreaRef = useRef<RefType | null>(null);
const { onPaste } = useTextAreaUploadFileWithForm(textAreaRef, getValues, setValue);
const insertText = useTextAreaInsertTextWithForm(textAreaRef, getValues, setValue);
const { onPaste } = useTextAreaUploadFile(insertText);
watch("content");
@ -43,6 +45,7 @@ export default function ChatMessageForm({ stream, hideZapButton }: { stream: Par
<>
<Box borderRadius="md" flexShrink={0} display="flex" gap="2" px="2" pb="2">
<Flex as="form" onSubmit={sendMessage} gap="2" flex={1}>
<InsertGifButton onSelectURL={insertText} aria-label="Add gif" />
<MagicInput
instanceRef={(inst) => (textAreaRef.current = inst)}
placeholder="Message"

View File

@ -1,5 +1,5 @@
import { useMemo, useRef } from "react";
import { Box, Button, ButtonGroup, Flex, IconButton, VisuallyHiddenInput } from "@chakra-ui/react";
import { Box, Button, ButtonGroup, Flex } from "@chakra-ui/react";
import { useForm } from "react-hook-form";
import { useThrottle } from "react-use";
import { kinds } from "nostr-tools";
@ -20,12 +20,13 @@ import useCurrentAccount from "../../../hooks/use-current-account";
import MagicTextArea, { RefType } from "../../../components/magic-textarea";
import { useContextEmojis } from "../../../providers/global/emoji-provider";
import { TrustProvider } from "../../../providers/local/trust-provider";
import { UploadImageIcon } from "../../../components/icons";
import { unique } from "../../../helpers/array";
import { usePublishEvent } from "../../../providers/global/publish-provider";
import { TextNoteContents } from "../../../components/note/timeline-note/text-note-contents";
import useCacheForm from "../../../hooks/use-cache-form";
import { useTextAreaUploadFileWithForm } from "../../../hooks/use-textarea-upload-file";
import useTextAreaUploadFile, { useTextAreaInsertTextWithForm } from "../../../hooks/use-textarea-upload-file";
import InsertGifButton from "../../../components/gif/insert-gif-button";
import InsertImageButton from "../../../components/post-modal/insert-image-button";
export type ReplyFormProps = {
item: ThreadItem;
@ -55,8 +56,8 @@ export default function ReplyForm({ item, onCancel, onSubmitted, replyKind = kin
watch("content");
const textAreaRef = useRef<RefType | null>(null);
const imageUploadRef = useRef<HTMLInputElement | null>(null);
const { onPaste, onFileInputChange, uploading } = useTextAreaUploadFileWithForm(textAreaRef, getValues, setValue);
const insertText = useTextAreaInsertTextWithForm(textAreaRef, getValues, setValue);
const { onPaste } = useTextAreaUploadFile(insertText);
const draft = useMemo(() => {
let updated = finalizeNote({ kind: replyKind, content: getValues().content, created_at: dayjs().unix(), tags: [] });
@ -93,15 +94,8 @@ export default function ReplyForm({ item, onCancel, onSubmitted, replyKind = kin
}}
/>
<Flex gap="2" alignItems="center">
<VisuallyHiddenInput type="file" accept="image/*" ref={imageUploadRef} onChange={onFileInputChange} />
<IconButton
icon={<UploadImageIcon />}
aria-label="Upload Image"
title="Upload Image"
onClick={() => imageUploadRef.current?.click()}
isLoading={uploading}
size="sm"
/>
<InsertImageButton onUploaded={insertText} size="sm" aria-label="Upload image" />
<InsertGifButton onSelectURL={insertText} aria-label="Add gif" size="sm" />
<UserAvatarStack label="Notify" pubkeys={notifyPubkeys} />
<ButtonGroup size="sm" ml="auto">
{onCancel && <Button onClick={onCancel}>Cancel</Button>}