fix: editor crash on open

This commit is contained in:
reya 2024-05-28 14:43:34 +07:00
parent 5ca9444358
commit 32f3315344
7 changed files with 666 additions and 511 deletions

View File

@ -1,18 +1,11 @@
import { CheckCircleIcon, InfoCircleIcon, CancelCircleIcon } from "@lume/icons"; import { CheckCircleIcon, InfoCircleIcon, CancelCircleIcon } from "@lume/icons";
import type { Interests, Metadata, Settings } from "@lume/types"; import type { Settings } from "@lume/types";
import { Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import type { QueryClient } from "@tanstack/react-query"; import type { QueryClient } from "@tanstack/react-query";
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router"; import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
import type { Platform } from "@tauri-apps/plugin-os"; import type { Platform } from "@tauri-apps/plugin-os";
import type { Descendant } from "slate";
import { Toaster } from "sonner"; import { Toaster } from "sonner";
type EditorElement = {
type: string;
children: Descendant[];
eventId?: string;
};
interface RouterContext { interface RouterContext {
// System // System
queryClient: QueryClient; queryClient: QueryClient;
@ -21,13 +14,8 @@ interface RouterContext {
locale?: string; locale?: string;
// Settings // Settings
settings?: Settings; settings?: Settings;
interests?: Interests; // Accounts
// Profile
accounts?: string[]; accounts?: string[];
profile?: Metadata;
isNewUser?: boolean;
// Editor
initialValue?: EditorElement[];
} }
export const Route = createRootRouteWithContext<RouterContext>()({ export const Route = createRootRouteWithContext<RouterContext>()({

View File

@ -13,7 +13,7 @@ export function MediaButton({ className }: { className?: string }) {
const editor = useSlateStatic(); const editor = useSlateStatic();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const uploadToNostrBuild = async () => { const upload = async () => {
try { try {
// start loading // start loading
setLoading(true); setLoading(true);
@ -66,7 +66,7 @@ export function MediaButton({ className }: { className?: string }) {
<Tooltip.Trigger asChild> <Tooltip.Trigger asChild>
<button <button
type="button" type="button"
onClick={() => uploadToNostrBuild()} onClick={() => upload()}
disabled={loading} disabled={loading}
className={cn("inline-flex items-center justify-center", className)} className={cn("inline-flex items-center justify-center", className)}
> >

View File

@ -60,20 +60,26 @@ export function MentionButton({ className }: { className?: string }) {
</Tooltip.Provider> </Tooltip.Provider>
<DropdownMenu.Portal> <DropdownMenu.Portal>
<DropdownMenu.Content className="flex w-[220px] h-[220px] scrollbar-none flex-col overflow-y-auto rounded-xl bg-black py-1 shadow-md shadow-neutral-500/20 focus:outline-none dark:bg-white"> <DropdownMenu.Content className="flex w-[220px] h-[220px] scrollbar-none flex-col overflow-y-auto rounded-xl bg-black py-1 shadow-md shadow-neutral-500/20 focus:outline-none dark:bg-white">
{contacts.map((contact) => ( {contacts.length < 1 ? (
<DropdownMenu.Item <div className="w-full h-full flex items-center justify-center">
key={contact} <p className="text-sm text-white">Contact List is empty.</p>
onClick={() => select(contact)} </div>
className="shrink-0 h-11 flex items-center hover:bg-white/10 px-2" ) : (
> contacts.map((contact) => (
<User.Provider pubkey={contact}> <DropdownMenu.Item
<User.Root className="flex items-center gap-2"> key={contact}
<User.Avatar className="shrink-0 size-8 rounded-full" /> onClick={() => select(contact)}
<User.Name className="text-sm font-medium text-white dark:text-black" /> className="shrink-0 h-11 flex items-center hover:bg-white/10 px-2"
</User.Root> >
</User.Provider> <User.Provider pubkey={contact}>
</DropdownMenu.Item> <User.Root className="flex items-center gap-2">
))} <User.Avatar className="shrink-0 size-8 rounded-full" />
<User.Name className="text-sm font-medium text-white dark:text-black" />
</User.Root>
</User.Provider>
</DropdownMenu.Item>
))
)}
<DropdownMenu.Arrow className="fill-neutral-950 dark:fill-neutral-50" /> <DropdownMenu.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Portal> </DropdownMenu.Portal>

View File

@ -3,13 +3,13 @@ import { cn } from "@lume/utils";
import * as Tooltip from "@radix-ui/react-tooltip"; import * as Tooltip from "@radix-ui/react-tooltip";
import type { Dispatch, SetStateAction } from "react"; import type { Dispatch, SetStateAction } from "react";
export function NsfwToggle({ export function WarningToggle({
nsfw, warning,
setNsfw, setWarning,
className, className,
}: { }: {
nsfw: boolean; warning: boolean;
setNsfw: Dispatch<SetStateAction<boolean>>; setWarning: Dispatch<SetStateAction<boolean>>;
className?: string; className?: string;
}) { }) {
return ( return (
@ -18,11 +18,11 @@ export function NsfwToggle({
<Tooltip.Trigger asChild> <Tooltip.Trigger asChild>
<button <button
type="button" type="button"
onClick={() => setNsfw((prev) => !prev)} onClick={() => setWarning((prev) => !prev)}
className={cn( className={cn(
"inline-flex items-center justify-center", "inline-flex items-center justify-center",
className, className,
nsfw ? "bg-blue-500 text-white" : "", warning ? "bg-blue-500 text-white" : "",
)} )}
> >
<NsfwIcon className="size-4" /> <NsfwIcon className="size-4" />

View File

@ -1,4 +1,4 @@
import { ComposeFilledIcon, TrashIcon } from "@lume/icons"; import { ComposeFilledIcon } from "@lume/icons";
import { Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import { import {
cn, cn,
@ -8,7 +8,6 @@ import {
sendNativeNotification, sendNativeNotification,
} from "@lume/utils"; } from "@lume/utils";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { nip19 } from "nostr-tools";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { type Descendant, Node, Transforms, createEditor } from "slate"; import { type Descendant, Node, Transforms, createEditor } from "slate";
@ -22,16 +21,22 @@ import {
withReact, withReact,
} from "slate-react"; } from "slate-react";
import { MediaButton } from "./-components/media"; import { MediaButton } from "./-components/media";
import { NsfwToggle } from "./-components/nsfw";
import { MentionButton } from "./-components/mention"; import { MentionButton } from "./-components/mention";
import { MentionNote } from "@/components/note/mentions/note";
import { LumeEvent } from "@lume/system"; import { LumeEvent } from "@lume/system";
import { WarningToggle } from "./-components/warning";
import { MentionNote } from "@/components/note/mentions/note";
type EditorSearch = { type EditorSearch = {
reply_to: string; reply_to: string;
quote: boolean; quote: boolean;
}; };
type EditorElement = {
type: string;
children: Descendant[];
eventId?: string;
};
export const Route = createFileRoute("/editor/")({ export const Route = createFileRoute("/editor/")({
validateSearch: (search: Record<string, string>): EditorSearch => { validateSearch: (search: Record<string, string>): EditorSearch => {
return { return {
@ -39,43 +44,23 @@ export const Route = createFileRoute("/editor/")({
quote: search.quote === "true" || false, quote: search.quote === "true" || false,
}; };
}, },
beforeLoad: async ({ search }) => {
return {
initialValue: search.quote
? [
{
type: "paragraph",
children: [{ text: "" }],
},
{
type: "event",
eventId: `nostr:${nip19.noteEncode(search.reply_to)}`,
children: [{ text: "" }],
},
{
type: "paragraph",
children: [{ text: "" }],
},
]
: [
{
type: "paragraph",
children: [{ text: "" }],
},
],
};
},
component: Screen, component: Screen,
}); });
const initialValue: EditorElement[] = [
{
type: "paragraph",
children: [{ text: "" }],
},
];
function Screen() { function Screen() {
const { reply_to, quote } = Route.useSearch(); const search = Route.useSearch();
const { initialValue } = Route.useRouteContext();
const [t] = useTranslation(); const [t] = useTranslation();
const [editorValue, setEditorValue] = useState(initialValue); const [editorValue, setEditorValue] = useState(initialValue);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [nsfw, setNsfw] = useState(false); const [warning, setWarning] = useState(false);
const [editor] = useState(() => const [editor] = useState(() =>
withMentions(withNostrEvent(withImages(withReact(createEditor())))), withMentions(withNostrEvent(withImages(withReact(createEditor())))),
); );
@ -116,7 +101,12 @@ function Screen() {
setLoading(true); setLoading(true);
const content = serialize(editor.children); const content = serialize(editor.children);
const eventId = await LumeEvent.publish(content, reply_to, quote); const eventId = await LumeEvent.publish(
content,
search.reply_to,
search.quote,
warning,
);
if (eventId) { if (eventId) {
await sendNativeNotification( await sendNativeNotification(
@ -137,15 +127,15 @@ function Screen() {
}; };
return ( return (
<div className="w-full h-full"> <div className="w-full h-full flex flex-col">
<Slate editor={editor} initialValue={editorValue}> <Slate editor={editor} initialValue={editorValue}>
<div <div
data-tauri-drag-region data-tauri-drag-region
className="flex h-14 w-full shrink-0 items-center justify-end gap-2 px-2 border-b border-black/10 dark:border-white/10" className="shrink-0 flex h-14 w-full items-center justify-end gap-2 px-2 border-b border-black/10 dark:border-white/10"
> >
<NsfwToggle <WarningToggle
nsfw={nsfw} warning={warning}
setNsfw={setNsfw} setWarning={setWarning}
className="size-8 rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20" className="size-8 rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
/> />
<MentionButton className="size-8 rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20" /> <MentionButton className="size-8 rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20" />
@ -163,13 +153,13 @@ function Screen() {
{t("global.post")} {t("global.post")}
</button> </button>
</div> </div>
<div className="flex h-full w-full flex-1 flex-col"> <div className="flex-1 overflow-y-auto flex flex-col">
{reply_to && !quote ? ( {search.reply_to ? (
<div className="px-4 py-2"> <div className="px-4 py-2">
<MentionNote eventId={reply_to} /> <MentionNote eventId={search.reply_to} />
</div> </div>
) : null} ) : null}
<div className="overflow-y-auto p-4"> <div className="overflow-y-auto scrollbar-none p-4">
<Editable <Editable
key={JSON.stringify(editorValue)} key={JSON.stringify(editorValue)}
autoFocus={true} autoFocus={true}
@ -178,7 +168,7 @@ function Screen() {
spellCheck={false} spellCheck={false}
renderElement={(props) => <Element {...props} />} renderElement={(props) => <Element {...props} />}
placeholder={ placeholder={
reply_to ? "Type your reply..." : t("editor.placeholder") search.reply_to ? "Type your reply..." : t("editor.placeholder")
} }
className="focus:outline-none" className="focus:outline-none"
/> />
@ -252,35 +242,24 @@ const withImages = (editor: ReactEditor) => {
return editor; return editor;
}; };
const Image = ({ attributes, children, element }) => { const Image = ({ attributes, element, children }) => {
const editor = useSlateStatic(); const editor = useSlateStatic();
const path = ReactEditor.findPath(editor as ReactEditor, element);
const selected = useSelected(); const selected = useSelected();
const focused = useFocused(); const focused = useFocused();
const path = ReactEditor.findPath(editor as ReactEditor, element);
return ( return (
<div {...attributes}> <div {...attributes}>
{children} {children}
<div contentEditable={false} className="relative my-2"> <img
<img src={element.url}
src={element.url} alt={element.url}
alt={element.url} className={cn(
className={cn( "my-2 h-auto w-1/2 rounded-lg object-cover ring-2 outline outline-1 -outline-offset-1 outline-black/15",
"h-auto w-full rounded-lg border border-neutral-100 object-cover ring-2 dark:border-neutral-900", selected && focused ? "ring-blue-500" : "ring-transparent",
selected && focused ? "ring-blue-500" : "ring-transparent", )}
)} onClick={() => Transforms.removeNodes(editor, { at: path })}
contentEditable={false} />
/>
<button
type="button"
contentEditable={false}
onClick={() => Transforms.removeNodes(editor, { at: path })}
className="absolute right-2 top-2 inline-flex size-8 items-center justify-center rounded-lg bg-red-500 text-white hover:bg-red-600"
>
<TrashIcon className="size-4" />
</button>
</div>
</div> </div>
); );
}; };

File diff suppressed because it is too large Load Diff

View File

@ -60,17 +60,10 @@ export const insertMention = (
name: contact.profile.name || contact.profile.display_name || "anon", name: contact.profile.name || contact.profile.display_name || "anon",
children: [text], children: [text],
}; };
const extraText = [
{
type: "paragraph",
children: [text],
},
];
// @ts-ignore, idk // @ts-ignore, idk
ReactEditor.focus(editor); ReactEditor.focus(editor);
Transforms.insertNodes(editor, mention); Transforms.insertNodes(editor, mention);
Transforms.insertNodes(editor, extraText);
}; };
export const insertNostrEvent = ( export const insertNostrEvent = (