add wiki edit view

This commit is contained in:
hzrd149 2024-05-03 09:43:11 -05:00
parent 3c57aca4a9
commit b7572d373e
8 changed files with 226 additions and 16 deletions

View File

@ -2,4 +2,4 @@
"nostrudel": minor
---
Add simple wiki pages
Add wiki pages

View File

@ -2,4 +2,4 @@
"nostrudel": minor
---
Add date picker to notifictions
Add date picker to notifications

View File

@ -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: <WikiSearchView /> },
{ path: "topic/:topic", element: <WikiTopicView /> },
{ path: "page/:naddr", element: <WikiPageView /> },
{ path: "edit/:topic", element: <EditWikiPageView /> },
{ path: "compare/:topic/:a/:b", element: <WikiCompareView /> },
{ path: "create", element: <CreateWikiPageView /> },
{ path: "", element: <WikiHomeView /> },

View File

@ -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<HTMLElement>();
const previewRef = useRef<HTMLDivElement | null>(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(<CharkaMarkdown>{text}</CharkaMarkdown>);
},
} satisfies SimpleMDEReactProps["options"];
}, [servers, requestSignature, setPreview]);
return (
<>
<SimpleMDE options={customOptions} {...props} />
<VisuallyHidden>
<CharkaMarkdown ref={previewRef}>{props.value ?? ""}</CharkaMarkdown>
</VisuallyHidden>
</>
);
}

View File

@ -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<MenuIconButtonProps, "children">) {
const account = useCurrentAccount();
export default function WikiPageMenu({
page,
...props
}: { page: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
return (
<>
<DotsMenuButton {...props}>
<OpenInAppMenuItem event={page} />
{account?.pubkey === page.pubkey && (
<MenuItem as={RouterLink} to={`/wiki/edit/${getPageTopic(page)}`} icon={<EditIcon />}>
Edit Page
</MenuItem>
)}
<CopyShareLinkMenuItem event={page} />
<CopyEmbedCodeMenuItem event={page} />

View File

@ -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() {
</FormControl>
<FormControl isRequired>
<FormLabel>Title</FormLabel>
<Input {...register("title", { required: true })} />
<Input {...register("title", { required: true })} autoComplete="off" />
</FormControl>
</Flex>
<SimpleMDE value={getValues().content} onChange={(v) => setValue("content", v)} options={options} />

98
src/views/wiki/edit.tsx Normal file
View File

@ -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 (
<VerticalPageLayout as="form" h="full" onSubmit={submit}>
<Heading>Create Page</Heading>
<Flex gap="2" wrap={{ base: "wrap", md: "nowrap" }}>
<FormControl w={{ base: "full", md: "sm" }} isRequired flexShrink={0}>
<FormLabel>Topic</FormLabel>
<Input readOnly value={topic} />
</FormControl>
<FormControl isRequired>
<FormLabel>Title</FormLabel>
<Input {...register("title", { required: true })} autoComplete="off" />
</FormControl>
</Flex>
<MarkdownEditor value={getValues().content} onChange={(v) => setValue("content", v)} />
<Flex gap="2" justifyContent="flex-end">
{formState.isDirty && <Button onClick={() => reset()}>Clear</Button>}
<Button onClick={() => navigate(-1)}>Cancel</Button>
<Button colorScheme="primary" type="submit" isLoading={formState.isSubmitting}>
Publish
</Button>
</Flex>
</VerticalPageLayout>
);
}
export default function EditWikiPageView() {
const account = useCurrentAccount();
if (!account) return <Navigate to="/" />;
const topic = useParams().topic;
if (!topic) return <Navigate to="/wiki" />;
const page = useReplaceableEvent({ kind: WIKI_PAGE_KIND, pubkey: account.pubkey, identifier: topic });
if (!page) return <Spinner />;
return (
<ErrorBoundary>
<EditWikiPagePage page={page} />
</ErrorBoundary>
);
}

View File

@ -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 }) {
<VerticalPageLayout>
<WikiPageHeader />
<Flex wrap="wrap">
<Box>
<Flex gap="2" wrap="wrap">
<Box flex={1}>
<Heading>{getPageTitle(page)}</Heading>
<Text>
by <UserLink pubkey={page.pubkey} /> - <Timestamp timestamp={page.created_at} />
</Text>
</Box>
<Flex alignItems="flex-end" gap="2" ml="auto">
<EventVoteButtons event={page} inline chevrons={false} />
<ButtonGroup size="sm">
<QuoteRepostButton event={page} />
<NoteZapButton event={page} showEventPreview={false} />
<WikiPageMenu page={page} aria-label="Page Options" />
<Flex direction="column" gap="2" ml="auto">
<ButtonGroup ml="auto" size="sm">
{page.pubkey === account?.pubkey && (
<Button as={RouterLink} colorScheme="primary" to={`/wiki/edit/${getPageTopic(page)}`}>
Edit
</Button>
)}
</ButtonGroup>
<Flex alignItems="flex-end" gap="2" ml="auto">
<EventVoteButtons event={page} inline chevrons={false} />
<ButtonGroup size="sm">
<QuoteRepostButton event={page} />
<NoteZapButton event={page} showEventPreview={false} />
<WikiPageMenu page={page} aria-label="Page Options" />
</ButtonGroup>
</Flex>
</Flex>
</Flex>
{address && <ForkAlert page={page} address={address} />}