mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-29 16:30:55 +00:00
fix: editor crash on open
This commit is contained in:
parent
5ca9444358
commit
32f3315344
@ -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>()({
|
||||||
|
@ -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)}
|
||||||
>
|
>
|
||||||
|
@ -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>
|
||||||
|
@ -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" />
|
@ -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
@ -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 = (
|
||||||
|
Loading…
Reference in New Issue
Block a user