From b7572d373e342b758f6a624d6b5352266d2775f3 Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Fri, 3 May 2024 09:43:11 -0500 Subject: [PATCH] add wiki edit view --- .changeset/clever-nails-invite.md | 2 +- .changeset/curvy-cherries-exercise.md | 2 +- src/app.tsx | 2 + src/views/wiki/components/markdown-editor.tsx | 88 +++++++++++++++++ src/views/wiki/components/wioki-page-menu.tsx | 18 +++- src/views/wiki/create.tsx | 4 +- src/views/wiki/edit.tsx | 98 +++++++++++++++++++ src/views/wiki/page.tsx | 28 ++++-- 8 files changed, 226 insertions(+), 16 deletions(-) create mode 100644 src/views/wiki/components/markdown-editor.tsx create mode 100644 src/views/wiki/edit.tsx diff --git a/.changeset/clever-nails-invite.md b/.changeset/clever-nails-invite.md index 20ddfa4f8..c9fa7babf 100644 --- a/.changeset/clever-nails-invite.md +++ b/.changeset/clever-nails-invite.md @@ -2,4 +2,4 @@ "nostrudel": minor --- -Add simple wiki pages +Add wiki pages diff --git a/.changeset/curvy-cherries-exercise.md b/.changeset/curvy-cherries-exercise.md index 6f828e87a..e9a135781 100644 --- a/.changeset/curvy-cherries-exercise.md +++ b/.changeset/curvy-cherries-exercise.md @@ -2,4 +2,4 @@ "nostrudel": minor --- -Add date picker to notifictions +Add date picker to notifications diff --git a/src/app.tsx b/src/app.tsx index c2a784139..18027264f 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -126,6 +126,7 @@ const WikiTopicView = lazy(() => import("./views/wiki/topic")); const WikiSearchView = lazy(() => import("./views/wiki/search")); const WikiCompareView = lazy(() => import("./views/wiki/compare")); const CreateWikiPageView = lazy(() => import("./views/wiki/create")); +const EditWikiPageView = lazy(() => import("./views/wiki/edit")); const overrideReactTextareaAutocompleteStyles = css` .rta__autocomplete { @@ -321,6 +322,7 @@ const router = createHashRouter([ { path: "search", element: }, { path: "topic/:topic", element: }, { path: "page/:naddr", element: }, + { path: "edit/:topic", element: }, { path: "compare/:topic/:a/:b", element: }, { path: "create", element: }, { path: "", element: }, diff --git a/src/views/wiki/components/markdown-editor.tsx b/src/views/wiki/components/markdown-editor.tsx new file mode 100644 index 000000000..bac2b161d --- /dev/null +++ b/src/views/wiki/components/markdown-editor.tsx @@ -0,0 +1,88 @@ +import { useMemo, useRef, useState } from "react"; +import { VisuallyHidden } from "@chakra-ui/react"; +import SimpleMDE, { SimpleMDEReactProps } from "react-simplemde-editor"; +import ReactDOMServer from "react-dom/server"; + +import EasyMDE from "easymde"; +import "easymde/dist/easymde.min.css"; + +import useUsersMediaServers from "../../../hooks/use-user-media-servers"; +import useAppSettings from "../../../hooks/use-app-settings"; +import useCurrentAccount from "../../../hooks/use-current-account"; + +import { CharkaMarkdown } from "./markdown"; +import { useSigningContext } from "../../../providers/global/signing-provider"; +import { uploadFileToServers } from "../../../helpers/media-upload/blossom"; + +export default function MarkdownEditor({ options, ...props }: SimpleMDEReactProps) { + const account = useCurrentAccount(); + const { requestSignature } = useSigningContext(); + const { mediaUploadService } = useAppSettings(); + const { servers } = useUsersMediaServers(account?.pubkey); + + const [_, setPreview] = useState(); + const previewRef = useRef(null); + const customOptions = useMemo(() => { + const uploads = mediaUploadService === "blossom"; + async function imageUploadFunction(file: File, onSuccess: (url: string) => void, onError: (error: string) => void) { + if (!servers) return onError("No media servers set"); + try { + const blob = await uploadFileToServers(servers, file, requestSignature); + if (blob) onSuccess(blob.url); + } catch (error) { + if (error instanceof Error) onError(error.message); + } + } + + return { + minHeight: "60vh", + uploadImage: uploads, + imageUploadFunction: uploads ? imageUploadFunction : undefined, + toolbar: [ + "undo", + "redo", + "|", + "bold", + "italic", + "heading", + "|", + "quote", + "unordered-list", + "ordered-list", + "table", + "code", + "|", + "link", + "image", + ...(uploads + ? [ + { + name: "upload-image", + title: "Upload Image", + className: "fa fa-upload", + action: EasyMDE.drawUploadedImage, + }, + ] + : []), + "|", + "preview", + "side-by-side", + "fullscreen", + "|", + "guide", + ], + previewRender(text, element) { + return previewRef.current?.innerHTML || ReactDOMServer.renderToString({text}); + }, + } satisfies SimpleMDEReactProps["options"]; + }, [servers, requestSignature, setPreview]); + + return ( + <> + + + {props.value ?? ""} + + + ); +} diff --git a/src/views/wiki/components/wioki-page-menu.tsx b/src/views/wiki/components/wioki-page-menu.tsx index f53167ba8..a64da47ed 100644 --- a/src/views/wiki/components/wioki-page-menu.tsx +++ b/src/views/wiki/components/wioki-page-menu.tsx @@ -1,19 +1,29 @@ import { NostrEvent } from "nostr-tools"; +import { Link as RouterLink } from "react-router-dom"; +import { MenuItem } from "@chakra-ui/react"; import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button"; import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app"; import CopyShareLinkMenuItem from "../../../components/common-menu-items/copy-share-link"; import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code"; import DebugEventMenuItem from "../../../components/debug-modal/debug-event-menu-item"; +import useCurrentAccount from "../../../hooks/use-current-account"; +import { EditIcon } from "../../../components/icons"; +import { getPageTopic } from "../../../helpers/nostr/wiki"; + +export default function WikiPageMenu({ page, ...props }: { page: NostrEvent } & Omit) { + const account = useCurrentAccount(); -export default function WikiPageMenu({ - page, - ...props -}: { page: NostrEvent } & Omit) { return ( <> + {account?.pubkey === page.pubkey && ( + }> + Edit Page + + )} + diff --git a/src/views/wiki/create.tsx b/src/views/wiki/create.tsx index 89efb31dc..8db2478b4 100644 --- a/src/views/wiki/create.tsx +++ b/src/views/wiki/create.tsx @@ -23,6 +23,7 @@ import { useSigningContext } from "../../providers/global/signing-provider"; import useCurrentAccount from "../../hooks/use-current-account"; import { CharkaMarkdown } from "./components/markdown"; import useCacheForm from "../../hooks/use-cache-form"; +import useReplaceableEvent from "../../hooks/use-replaceable-event"; export default function CreateWikiPageView() { const account = useCurrentAccount(); @@ -63,6 +64,7 @@ export default function CreateWikiPageView() { tags: [ ["d", values.topic], ["title", values.title], + ["published_at", String(dayjs().unix())], ], created_at: dayjs().unix(), }; @@ -152,7 +154,7 @@ export default function CreateWikiPageView() { Title - + setValue("content", v)} options={options} /> diff --git a/src/views/wiki/edit.tsx b/src/views/wiki/edit.tsx new file mode 100644 index 000000000..fe64454b8 --- /dev/null +++ b/src/views/wiki/edit.tsx @@ -0,0 +1,98 @@ +import { Button, Flex, FormControl, FormLabel, Heading, Input, Spinner, useToast } from "@chakra-ui/react"; +import { Navigate, useNavigate, useParams } from "react-router-dom"; +import { useForm } from "react-hook-form"; +import { NostrEvent } from "nostr-tools"; + +import { WIKI_RELAYS } from "../../const"; +import useCacheForm from "../../hooks/use-cache-form"; +import useReplaceableEvent from "../../hooks/use-replaceable-event"; +import useCurrentAccount from "../../hooks/use-current-account"; +import { WIKI_PAGE_KIND, getPageTitle, getPageTopic } from "../../helpers/nostr/wiki"; +import { getSharableEventAddress } from "../../helpers/nip19"; +import { usePublishEvent } from "../../providers/global/publish-provider"; +import VerticalPageLayout from "../../components/vertical-page-layout"; +import MarkdownEditor from "./components/markdown-editor"; +import { ErrorBoundary } from "../../components/error-boundary"; +import { cloneEvent } from "../../helpers/nostr/event"; + +function EditWikiPagePage({ page }: { page: NostrEvent }) { + const toast = useToast(); + const publish = usePublishEvent(); + const navigate = useNavigate(); + + const topic = getPageTopic(page); + + const { register, setValue, getValues, handleSubmit, watch, formState, reset } = useForm({ + defaultValues: { content: page.content, title: getPageTitle(page) ?? topic }, + mode: "all", + }); + + const clearFormCache = useCacheForm( + "wiki-" + topic, + // @ts-expect-error + getValues, + setValue, + formState, + ); + + watch("content"); + register("content", { + minLength: 10, + required: true, + }); + + const submit = handleSubmit(async (values) => { + try { + const draft = cloneEvent(WIKI_PAGE_KIND, page); + draft.content = values.content; + + const pub = await publish("Publish Page", draft, WIKI_RELAYS, false); + clearFormCache(); + navigate(`/wiki/page/${getSharableEventAddress(pub.event)}`); + } catch (error) { + if (error instanceof Error) toast({ description: error.message, status: "error" }); + } + }); + + return ( + + Create Page + + + Topic + + + + Title + + + + setValue("content", v)} /> + + {formState.isDirty && } + + + + + ); +} + +export default function EditWikiPageView() { + const account = useCurrentAccount(); + if (!account) return ; + + const topic = useParams().topic; + if (!topic) return ; + + const page = useReplaceableEvent({ kind: WIKI_PAGE_KIND, pubkey: account.pubkey, identifier: topic }); + + if (!page) return ; + + return ( + + + + ); +} diff --git a/src/views/wiki/page.tsx b/src/views/wiki/page.tsx index cb081daff..ee246b9c3 100644 --- a/src/views/wiki/page.tsx +++ b/src/views/wiki/page.tsx @@ -25,7 +25,6 @@ import useSubject from "../../hooks/use-subject"; import useWikiTopicTimeline from "./hooks/use-wiki-topic-timeline"; import WikiPageResult from "./components/wiki-page-result"; import Timestamp from "../../components/timestamp"; -import DebugEventButton from "../../components/debug-modal/debug-event-button"; import WikiPageHeader from "./components/wiki-page-header"; import { WIKI_RELAYS } from "../../const"; import GitBranch01 from "../../components/icons/git-branch-01"; @@ -36,6 +35,7 @@ import ZapBubbles from "../../components/note/timeline-note/components/zap-bubbl import QuoteRepostButton from "../../components/note/quote-repost-button"; import WikiPageMenu from "./components/wioki-page-menu"; import EventVoteButtons from "../../components/reactions/event-vote-buttions"; +import useCurrentAccount from "../../hooks/use-current-account"; function ForkAlert({ page, address }: { page: NostrEvent; address: nip19.AddressPointer }) { const topic = getPageTopic(page); @@ -85,6 +85,7 @@ function DeferAlert({ page, address }: { page: NostrEvent; address: nip19.Addres } function WikiPagePage({ page }: { page: NostrEvent }) { + const account = useCurrentAccount(); const topic = getPageTopic(page); const timeline = useWikiTopicTimeline(topic); @@ -105,20 +106,29 @@ function WikiPagePage({ page }: { page: NostrEvent }) { - - + + {getPageTitle(page)} by - - - - - - - + + + {page.pubkey === account?.pubkey && ( + + )} + + + + + + + + {address && }