mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-23 06:54:08 +02:00
add content warning switch when writing note
This commit is contained in:
parent
070ec6a31e
commit
409f219c54
5
.changeset/tidy-turkeys-allow.md
Normal file
5
.changeset/tidy-turkeys-allow.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add content warning switch when writing note
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useMemo, useRef, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalOverlay,
|
ModalOverlay,
|
||||||
@ -6,22 +6,22 @@ import {
|
|||||||
ModalBody,
|
ModalBody,
|
||||||
Flex,
|
Flex,
|
||||||
Button,
|
Button,
|
||||||
Text,
|
|
||||||
VisuallyHiddenInput,
|
|
||||||
IconButton,
|
|
||||||
useToast,
|
useToast,
|
||||||
Box,
|
Box,
|
||||||
Heading,
|
Heading,
|
||||||
|
useDisclosure,
|
||||||
|
Input,
|
||||||
|
Switch,
|
||||||
|
ModalProps,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { Kind } from "nostr-tools";
|
||||||
|
|
||||||
import NostrPublishAction from "../../classes/nostr-publish-action";
|
import NostrPublishAction from "../../classes/nostr-publish-action";
|
||||||
import { getReferences } from "../../helpers/nostr/events";
|
|
||||||
import { useWriteRelayUrls } from "../../hooks/use-client-relays";
|
import { useWriteRelayUrls } from "../../hooks/use-client-relays";
|
||||||
import { useSigningContext } from "../../providers/signing-provider";
|
import { useSigningContext } from "../../providers/signing-provider";
|
||||||
import { DraftNostrEvent } from "../../types/nostr-event";
|
import { ArrowDownSIcon, ArrowUpSIcon } from "../icons";
|
||||||
import { ImageIcon } from "../icons";
|
|
||||||
import { NoteLink } from "../note-link";
|
|
||||||
import { NoteContents } from "../note/note-contents";
|
import { NoteContents } from "../note/note-contents";
|
||||||
import { PublishDetails } from "../publish-details";
|
import { PublishDetails } from "../publish-details";
|
||||||
import { TrustProvider } from "../../providers/trust";
|
import { TrustProvider } from "../../providers/trust";
|
||||||
@ -30,79 +30,78 @@ import { UserAvatarStack } from "../compact-user-stack";
|
|||||||
import MagicTextArea from "../magic-textarea";
|
import MagicTextArea from "../magic-textarea";
|
||||||
import { useContextEmojis } from "../../providers/emoji-provider";
|
import { useContextEmojis } from "../../providers/emoji-provider";
|
||||||
|
|
||||||
function emptyDraft(): DraftNostrEvent {
|
export default function PostModal({ isOpen, onClose }: Omit<ModalProps, "children">) {
|
||||||
return {
|
|
||||||
content: "",
|
|
||||||
kind: 1,
|
|
||||||
tags: [],
|
|
||||||
created_at: dayjs().unix(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type PostModalProps = {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
initialDraft?: Partial<DraftNostrEvent>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) => {
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { requestSignature } = useSigningContext();
|
const { requestSignature } = useSigningContext();
|
||||||
const writeRelays = useWriteRelayUrls();
|
const writeRelays = useWriteRelayUrls();
|
||||||
const [signing, setSigning] = useState(false);
|
|
||||||
const [publishAction, setPublishAction] = useState<NostrPublishAction>();
|
const [publishAction, setPublishAction] = useState<NostrPublishAction>();
|
||||||
const [draft, setDraft] = useState<DraftNostrEvent>(() => Object.assign(emptyDraft(), initialDraft));
|
|
||||||
const imageUploadRef = useRef<HTMLInputElement | null>(null);
|
|
||||||
const [uploading, setUploading] = useState(false);
|
|
||||||
const emojis = useContextEmojis();
|
const emojis = useContextEmojis();
|
||||||
|
const moreOptions = useDisclosure();
|
||||||
|
|
||||||
const uploadImage = async (imageFile: File) => {
|
const { getValues, setValue, watch, register, handleSubmit, formState } = useForm({
|
||||||
try {
|
defaultValues: {
|
||||||
if (!imageFile.type.includes("image")) throw new Error("Only images are supported");
|
content: "",
|
||||||
setUploading(true);
|
nsfw: false,
|
||||||
const payload = new FormData();
|
nsfwReason: "",
|
||||||
payload.append("fileToUpload", imageFile);
|
},
|
||||||
const response = await fetch("https://nostr.build/upload.php", { body: payload, method: "POST" }).then((res) =>
|
});
|
||||||
res.text(),
|
watch("content");
|
||||||
);
|
watch("nsfw");
|
||||||
const imageUrl = response.match(/https:\/\/nostr\.build\/i\/[\w.]+/)?.[0];
|
watch("nsfwReason");
|
||||||
if (imageUrl) {
|
|
||||||
setDraft((d) => ({ ...d, content: (d.content += imageUrl) }));
|
// const imageUploadRef = useRef<HTMLInputElement | null>(null);
|
||||||
}
|
// const [uploading, setUploading] = useState(false);
|
||||||
} catch (e) {
|
// const uploadImage = async (imageFile: File) => {
|
||||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
// try {
|
||||||
|
// if (!imageFile.type.includes("image")) throw new Error("Only images are supported");
|
||||||
|
// setUploading(true);
|
||||||
|
// const payload = new FormData();
|
||||||
|
// payload.append("fileToUpload", imageFile);
|
||||||
|
// const response = await fetch("https://nostr.build/upload.php", { body: payload, method: "POST" }).then((res) =>
|
||||||
|
// res.text(),
|
||||||
|
// );
|
||||||
|
// const imageUrl = response.match(/https:\/\/nostr\.build\/i\/[\w.]+/)?.[0];
|
||||||
|
// if (imageUrl) {
|
||||||
|
// setValue('content', getValues().content += imageUrl );
|
||||||
|
// }
|
||||||
|
// } catch (e) {
|
||||||
|
// if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
|
// }
|
||||||
|
// setUploading(false);
|
||||||
|
// };
|
||||||
|
|
||||||
|
const getDraft = useCallback(() => {
|
||||||
|
const { content, nsfw, nsfwReason } = getValues();
|
||||||
|
|
||||||
|
let updatedDraft = finalizeNote({
|
||||||
|
content: content,
|
||||||
|
kind: Kind.Text,
|
||||||
|
tags: [],
|
||||||
|
created_at: dayjs().unix(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (nsfw) {
|
||||||
|
updatedDraft.tags.push(nsfwReason ? ["content-warning", nsfwReason] : ["content-warning"]);
|
||||||
}
|
}
|
||||||
setUploading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleContentChange: React.ChangeEventHandler<HTMLTextAreaElement> = (event) => {
|
const contentMentions = getContentMentions(updatedDraft.content);
|
||||||
setDraft((d) => ({ ...d, content: event.target.value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const finalDraft = useMemo(() => {
|
|
||||||
let updatedDraft = finalizeNote(draft);
|
|
||||||
const contentMentions = getContentMentions(draft.content);
|
|
||||||
updatedDraft = createEmojiTags(updatedDraft, emojis);
|
updatedDraft = createEmojiTags(updatedDraft, emojis);
|
||||||
updatedDraft = ensureNotifyPubkeys(updatedDraft, contentMentions);
|
updatedDraft = ensureNotifyPubkeys(updatedDraft, contentMentions);
|
||||||
return updatedDraft;
|
return updatedDraft;
|
||||||
}, [draft, emojis]);
|
}, [getValues, emojis]);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const submit = handleSubmit(async () => {
|
||||||
try {
|
try {
|
||||||
setSigning(true);
|
const signed = await requestSignature(getDraft());
|
||||||
const signed = await requestSignature(finalDraft);
|
|
||||||
setSigning(false);
|
|
||||||
|
|
||||||
const pub = new NostrPublishAction("Post", writeRelays, signed);
|
const pub = new NostrPublishAction("Post", writeRelays, signed);
|
||||||
setPublishAction(pub);
|
setPublishAction(pub);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
const refs = getReferences(draft);
|
const canSubmit = getValues().content.length > 0;
|
||||||
|
const mentions = getContentMentions(getValues().content);
|
||||||
const canSubmit = draft.content.length > 0;
|
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (publishAction) {
|
if (publishAction) {
|
||||||
@ -117,35 +116,31 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{refs.replyId && (
|
|
||||||
<Text mb="2">
|
|
||||||
Replying to: <NoteLink noteId={refs.replyId} />
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
<MagicTextArea
|
<MagicTextArea
|
||||||
autoFocus
|
autoFocus
|
||||||
mb="2"
|
mb="2"
|
||||||
value={draft.content}
|
value={getValues().content}
|
||||||
onChange={handleContentChange}
|
onChange={(e) => setValue("content", e.target.value)}
|
||||||
rows={5}
|
rows={5}
|
||||||
onPaste={(e) => {
|
isRequired
|
||||||
const imageFile = Array.from(e.clipboardData.files).find((f) => f.type.includes("image"));
|
// onPaste={(e) => {
|
||||||
if (imageFile) uploadImage(imageFile);
|
// const imageFile = Array.from(e.clipboardData.files).find((f) => f.type.includes("image"));
|
||||||
}}
|
// if (imageFile) uploadImage(imageFile);
|
||||||
|
// }}
|
||||||
/>
|
/>
|
||||||
{draft.content.length > 0 && (
|
{getValues().content.length > 0 && (
|
||||||
<Box>
|
<Box>
|
||||||
<Heading size="sm">Preview:</Heading>
|
<Heading size="sm">Preview:</Heading>
|
||||||
<Box borderWidth={1} borderRadius="md" p="2">
|
<Box borderWidth={1} borderRadius="md" p="2">
|
||||||
<TrustProvider trust>
|
<TrustProvider trust>
|
||||||
<NoteContents event={finalDraft} />
|
<NoteContents event={getDraft()} />
|
||||||
</TrustProvider>
|
</TrustProvider>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<Flex gap="2" alignItems="center" justifyContent="flex-end">
|
<Flex gap="2" alignItems="center" justifyContent="flex-end">
|
||||||
<Flex mr="auto" gap="2">
|
<Flex mr="auto" gap="2">
|
||||||
<VisuallyHiddenInput
|
{/* <VisuallyHiddenInput
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
ref={imageUploadRef}
|
ref={imageUploadRef}
|
||||||
@ -160,14 +155,35 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
|
|||||||
title="Upload Image"
|
title="Upload Image"
|
||||||
onClick={() => imageUploadRef.current?.click()}
|
onClick={() => imageUploadRef.current?.click()}
|
||||||
isLoading={uploading}
|
isLoading={uploading}
|
||||||
/>
|
/> */}
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
rightIcon={moreOptions.isOpen ? <ArrowUpSIcon /> : <ArrowDownSIcon />}
|
||||||
|
onClick={moreOptions.onToggle}
|
||||||
|
>
|
||||||
|
More Options
|
||||||
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
<UserAvatarStack label="Mentions" pubkeys={getContentMentions(draft.content)} />
|
{mentions.length > 0 && <UserAvatarStack label="Mentions" pubkeys={mentions} />}
|
||||||
<Button onClick={onClose}>Cancel</Button>
|
<Button onClick={onClose}>Cancel</Button>
|
||||||
<Button colorScheme="blue" type="submit" isLoading={signing} onClick={handleSubmit} isDisabled={!canSubmit}>
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
type="submit"
|
||||||
|
isLoading={formState.isSubmitting}
|
||||||
|
onClick={submit}
|
||||||
|
isDisabled={!canSubmit}
|
||||||
|
>
|
||||||
Post
|
Post
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
{moreOptions.isOpen && (
|
||||||
|
<>
|
||||||
|
<Flex gap="2" direction="column">
|
||||||
|
<Switch {...register("nsfw")}>NSFW</Switch>
|
||||||
|
{getValues().nsfw && <Input {...register("nsfwReason")} placeholder="Reason" maxW="50%" />}
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -182,4 +198,4 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
|
|||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { PropsWithChildren, useCallback, useMemo, useState } from "react";
|
import React, { PropsWithChildren, useMemo } from "react";
|
||||||
import { useDisclosure } from "@chakra-ui/react";
|
import { useDisclosure } from "@chakra-ui/react";
|
||||||
import { ErrorBoundary } from "../components/error-boundary";
|
import { ErrorBoundary } from "../components/error-boundary";
|
||||||
import { PostModal } from "../components/post-modal";
|
import PostModal from "../components/post-modal";
|
||||||
import { DraftNostrEvent } from "../types/nostr-event";
|
import { DraftNostrEvent } from "../types/nostr-event";
|
||||||
|
|
||||||
export type PostModalContextType = {
|
export type PostModalContextType = {
|
||||||
@ -14,20 +14,12 @@ export const PostModalContext = React.createContext<PostModalContextType>({
|
|||||||
|
|
||||||
export default function PostModalProvider({ children }: PropsWithChildren) {
|
export default function PostModalProvider({ children }: PropsWithChildren) {
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const [draft, setDraft] = useState<Partial<DraftNostrEvent> | undefined>(undefined);
|
const context = useMemo(() => ({ openModal: onOpen }), [onOpen]);
|
||||||
const openModal = useCallback(
|
|
||||||
(draft?: Partial<DraftNostrEvent>) => {
|
|
||||||
setDraft(draft);
|
|
||||||
onOpen();
|
|
||||||
},
|
|
||||||
[setDraft, onOpen],
|
|
||||||
);
|
|
||||||
const context = useMemo(() => ({ openModal }), [openModal]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PostModalContext.Provider value={context}>
|
<PostModalContext.Provider value={context}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
{isOpen && <PostModal isOpen initialDraft={draft} onClose={onClose} />}
|
{isOpen && <PostModal isOpen onClose={onClose} />}
|
||||||
{children}
|
{children}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</PostModalContext.Provider>
|
</PostModalContext.Provider>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user