mirror of
https://github.com/lumehq/lume.git
synced 2025-07-09 18:00:43 +02:00
feat: improve editor
This commit is contained in:
@ -10,9 +10,8 @@ import { useSlateStatic } from "slate-react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export function MediaButton({ className }: { className?: string }) {
|
export function MediaButton({ className }: { className?: string }) {
|
||||||
const { ark } = useRouteContext({ strict: false });
|
|
||||||
const editor = useSlateStatic();
|
const editor = useSlateStatic();
|
||||||
|
const { ark } = useRouteContext({ strict: false });
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const uploadToNostrBuild = async () => {
|
const uploadToNostrBuild = async () => {
|
||||||
|
83
apps/desktop2/src/routes/editor/-components/mention.tsx
Normal file
83
apps/desktop2/src/routes/editor/-components/mention.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { MentionIcon } from "@lume/icons";
|
||||||
|
import { cn, insertMention } from "@lume/utils";
|
||||||
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||||
|
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRouteContext } from "@tanstack/react-router";
|
||||||
|
import { User } from "@lume/ui";
|
||||||
|
import { useSlateStatic } from "slate-react";
|
||||||
|
import type { Contact } from "@lume/types";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export function MentionButton({ className }: { className?: string }) {
|
||||||
|
const editor = useSlateStatic();
|
||||||
|
const { ark } = useRouteContext({ strict: false });
|
||||||
|
const [contacts, setContacts] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const select = async (user: string) => {
|
||||||
|
try {
|
||||||
|
const metadata = await ark.get_profile(user);
|
||||||
|
const contact: Contact = { pubkey: user, profile: metadata };
|
||||||
|
|
||||||
|
insertMention(editor, contact);
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(String(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getContacts() {
|
||||||
|
const data = await ark.get_contact_list();
|
||||||
|
setContacts(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
getContacts();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<Tooltip.Provider>
|
||||||
|
<Tooltip.Root delayDuration={150}>
|
||||||
|
<DropdownMenu.Trigger asChild>
|
||||||
|
<Tooltip.Trigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<MentionIcon className="size-4" />
|
||||||
|
</button>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<Tooltip.Portal>
|
||||||
|
<Tooltip.Content className="inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-950 px-3.5 text-sm text-neutral-50 will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade dark:bg-neutral-50 dark:text-neutral-950">
|
||||||
|
Mention
|
||||||
|
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Portal>
|
||||||
|
</Tooltip.Root>
|
||||||
|
</Tooltip.Provider>
|
||||||
|
<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">
|
||||||
|
{contacts.map((contact) => (
|
||||||
|
<DropdownMenu.Item
|
||||||
|
key={contact}
|
||||||
|
onClick={() => select(contact)}
|
||||||
|
className="shrink-0 h-11 flex items-center hover:bg-white/10 px-2"
|
||||||
|
>
|
||||||
|
<User.Provider pubkey={contact}>
|
||||||
|
<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.Content>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
);
|
||||||
|
}
|
40
apps/desktop2/src/routes/editor/-components/pow.tsx
Normal file
40
apps/desktop2/src/routes/editor/-components/pow.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { NsfwIcon } from "@lume/icons";
|
||||||
|
import { cn } from "@lume/utils";
|
||||||
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||||
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
|
export function PowToggle({
|
||||||
|
pow,
|
||||||
|
setPow,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
pow: boolean;
|
||||||
|
setPow: Dispatch<SetStateAction<boolean>>;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Tooltip.Provider>
|
||||||
|
<Tooltip.Root delayDuration={150}>
|
||||||
|
<Tooltip.Trigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPow((prev) => !prev)}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center",
|
||||||
|
className,
|
||||||
|
pow ? "bg-blue-500 text-white" : "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<NsfwIcon className="size-4" />
|
||||||
|
</button>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Portal>
|
||||||
|
<Tooltip.Content className="inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-950 px-3.5 text-sm text-neutral-50 will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade dark:bg-neutral-50 dark:text-neutral-950">
|
||||||
|
Proof of Work
|
||||||
|
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Portal>
|
||||||
|
</Tooltip.Root>
|
||||||
|
</Tooltip.Provider>
|
||||||
|
);
|
||||||
|
}
|
@ -1,27 +1,18 @@
|
|||||||
import { ComposeFilledIcon, TrashIcon } from "@lume/icons";
|
import { ComposeFilledIcon, TrashIcon } from "@lume/icons";
|
||||||
import { Spinner, User } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { MentionNote } from "@lume/ui/src/note/mentions/note";
|
import { MentionNote } from "@lume/ui/src/note/mentions/note";
|
||||||
import {
|
import {
|
||||||
Portal,
|
|
||||||
cn,
|
cn,
|
||||||
insertImage,
|
insertImage,
|
||||||
insertMention,
|
|
||||||
insertNostrEvent,
|
insertNostrEvent,
|
||||||
isImageUrl,
|
isImageUrl,
|
||||||
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 { nip19 } from "nostr-tools";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import { type Descendant, Node, Transforms, createEditor } from "slate";
|
||||||
type Descendant,
|
|
||||||
Editor,
|
|
||||||
Node,
|
|
||||||
Range,
|
|
||||||
Transforms,
|
|
||||||
createEditor,
|
|
||||||
} from "slate";
|
|
||||||
import {
|
import {
|
||||||
Editable,
|
Editable,
|
||||||
ReactEditor,
|
ReactEditor,
|
||||||
@ -33,6 +24,7 @@ import {
|
|||||||
} from "slate-react";
|
} from "slate-react";
|
||||||
import { MediaButton } from "./-components/media";
|
import { MediaButton } from "./-components/media";
|
||||||
import { NsfwToggle } from "./-components/nsfw";
|
import { NsfwToggle } from "./-components/nsfw";
|
||||||
|
import { MentionButton } from "./-components/mention";
|
||||||
|
|
||||||
type EditorSearch = {
|
type EditorSearch = {
|
||||||
reply_to: string;
|
reply_to: string;
|
||||||
@ -73,32 +65,20 @@ export const Route = createFileRoute("/editor/")({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
pendingComponent: Pending,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const ref = useRef<HTMLDivElement | null>();
|
|
||||||
const { reply_to, quote } = Route.useSearch();
|
const { reply_to, quote } = Route.useSearch();
|
||||||
const { ark, initialValue, contacts } = Route.useRouteContext();
|
const { ark, initialValue } = Route.useRouteContext();
|
||||||
|
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const [editorValue, setEditorValue] = useState(initialValue);
|
const [editorValue, setEditorValue] = useState(initialValue);
|
||||||
const [target, setTarget] = useState<Range | undefined>();
|
|
||||||
const [index, setIndex] = useState(0);
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [nsfw, setNsfw] = useState(false);
|
const [nsfw, setNsfw] = useState(false);
|
||||||
const [editor] = useState(() =>
|
const [editor] = useState(() =>
|
||||||
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
|
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
|
||||||
);
|
);
|
||||||
|
|
||||||
const filters =
|
|
||||||
contacts
|
|
||||||
?.filter((c) =>
|
|
||||||
c?.profile.name?.toLowerCase().startsWith(search.toLowerCase()),
|
|
||||||
)
|
|
||||||
?.slice(0, 5) ?? [];
|
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
// @ts-expect-error, backlog
|
// @ts-expect-error, backlog
|
||||||
editor.children = [{ type: "paragraph", children: [{ text: "" }] }];
|
editor.children = [{ type: "paragraph", children: [{ text: "" }] }];
|
||||||
@ -138,11 +118,15 @@ function Screen() {
|
|||||||
const eventId = await ark.publish(content, reply_to, quote);
|
const eventId = await ark.publish(content, reply_to, quote);
|
||||||
|
|
||||||
if (eventId) {
|
if (eventId) {
|
||||||
await sendNativeNotification("You've publish new post successfully.");
|
await sendNativeNotification(
|
||||||
|
"Your note has been published successfully.",
|
||||||
|
"Lume",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop loading
|
// stop loading
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
// reset form
|
// reset form
|
||||||
reset();
|
reset();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -151,58 +135,20 @@ function Screen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (target && filters.length > 0) {
|
|
||||||
const el = ref.current;
|
|
||||||
const domRange = ReactEditor.toDOMRange(editor, target);
|
|
||||||
const rect = domRange.getBoundingClientRect();
|
|
||||||
el.style.top = `${rect.top + window.scrollY + 24}px`;
|
|
||||||
el.style.left = `${rect.left + window.scrollX}px`;
|
|
||||||
}
|
|
||||||
}, [filters.length, editor, index, search, target]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-screen flex-col bg-gradient-to-tr from-neutral-200 to-neutral-100 dark:from-neutral-950 dark:to-neutral-900">
|
<div className="w-full h-full">
|
||||||
<Slate
|
<Slate editor={editor} initialValue={editorValue}>
|
||||||
editor={editor}
|
|
||||||
initialValue={editorValue}
|
|
||||||
onChange={() => {
|
|
||||||
const { selection } = editor;
|
|
||||||
|
|
||||||
if (selection && Range.isCollapsed(selection)) {
|
|
||||||
const [start] = Range.edges(selection);
|
|
||||||
const wordBefore = Editor.before(editor, start, { unit: "word" });
|
|
||||||
const before = wordBefore && Editor.before(editor, wordBefore);
|
|
||||||
const beforeRange = before && Editor.range(editor, before, start);
|
|
||||||
const beforeText =
|
|
||||||
beforeRange && Editor.string(editor, beforeRange);
|
|
||||||
const beforeMatch = beforeText?.match(/^@(\w+)$/);
|
|
||||||
const after = Editor.after(editor, start);
|
|
||||||
const afterRange = Editor.range(editor, start, after);
|
|
||||||
const afterText = Editor.string(editor, afterRange);
|
|
||||||
const afterMatch = afterText.match(/^(\s|$)/);
|
|
||||||
|
|
||||||
if (beforeMatch && afterMatch) {
|
|
||||||
setTarget(beforeRange);
|
|
||||||
setSearch(beforeMatch[1]);
|
|
||||||
setIndex(0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTarget(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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"
|
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"
|
||||||
>
|
>
|
||||||
<NsfwToggle
|
<NsfwToggle
|
||||||
nsfw={nsfw}
|
nsfw={nsfw}
|
||||||
setNsfw={setNsfw}
|
setNsfw={setNsfw}
|
||||||
className="size-8 rounded-full bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
className="size-8 rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
|
||||||
/>
|
/>
|
||||||
<MediaButton className="size-8 rounded-full bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700" />
|
<MentionButton className="size-8 rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20" />
|
||||||
|
<MediaButton className="size-8 rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20" />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => publish()}
|
onClick={() => publish()}
|
||||||
@ -216,10 +162,13 @@ function Screen() {
|
|||||||
{t("global.post")}
|
{t("global.post")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full min-h-0 w-full">
|
<div className="flex h-full w-full flex-1 flex-col">
|
||||||
<div className="flex h-full w-full flex-1 flex-col gap-2 px-2 pb-2">
|
{reply_to && !quote ? (
|
||||||
{reply_to && !quote ? <MentionNote eventId={reply_to} /> : null}
|
<div className="px-4 py-2">
|
||||||
<div className="h-full w-full flex-1 overflow-hidden overflow-y-auto rounded-xl bg-white p-5 shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/5">
|
<MentionNote eventId={reply_to} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="overflow-y-auto p-4">
|
||||||
<Editable
|
<Editable
|
||||||
key={JSON.stringify(editorValue)}
|
key={JSON.stringify(editorValue)}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
@ -232,37 +181,6 @@ function Screen() {
|
|||||||
}
|
}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
/>
|
/>
|
||||||
{target && filters.length > 0 && (
|
|
||||||
<Portal>
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className="absolute left-[-9999px] top-[-9999px] z-10 w-[250px] rounded-xl border border-neutral-50 bg-white p-2 shadow-lg dark:border-neutral-900 dark:bg-neutral-950"
|
|
||||||
>
|
|
||||||
{filters.map((contact) => (
|
|
||||||
<button
|
|
||||||
key={contact.pubkey}
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
Transforms.select(editor, target);
|
|
||||||
insertMention(editor, contact);
|
|
||||||
setTarget(null);
|
|
||||||
}}
|
|
||||||
className="flex w-full flex-col rounded-lg p-2 hover:bg-neutral-100 dark:hover:bg-neutral-900"
|
|
||||||
>
|
|
||||||
<User.Provider pubkey={contact.pubkey}>
|
|
||||||
<User.Root className="flex w-full items-center gap-2">
|
|
||||||
<User.Avatar className="size-7 shrink-0 rounded-full object-cover" />
|
|
||||||
<div className="flex w-full flex-col items-start">
|
|
||||||
<User.Name className="max-w-[8rem] truncate text-sm font-medium" />
|
|
||||||
</div>
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Portal>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Slate>
|
</Slate>
|
||||||
@ -270,20 +188,6 @@ function Screen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Pending() {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-tauri-drag-region
|
|
||||||
className="flex h-full w-full items-center justify-center gap-2.5"
|
|
||||||
>
|
|
||||||
<button type="button" disabled>
|
|
||||||
<Spinner className="size-5" />
|
|
||||||
</button>
|
|
||||||
<p>Loading cache...</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const withNostrEvent = (editor: ReactEditor) => {
|
const withNostrEvent = (editor: ReactEditor) => {
|
||||||
const { insertData, isVoid } = editor;
|
const { insertData, isVoid } = editor;
|
||||||
|
|
||||||
@ -429,7 +333,7 @@ const Element = (props) => {
|
|||||||
return <Event {...props} />;
|
return <Event {...props} />;
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<p {...attributes} className="text-lg">
|
<p {...attributes} className="text-[15px]">
|
||||||
{children}
|
{children}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
@ -66,7 +66,7 @@ function Screen() {
|
|||||||
<div className="flex items-center justify-center gap-6">
|
<div className="flex items-center justify-center gap-6">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="inline-flex size-6 items-center justify-center">
|
<div className="inline-flex size-6 items-center justify-center">
|
||||||
<Spinner className="size-6" />
|
<Spinner className="size-6 text-white" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -276,6 +276,7 @@ export class Ark {
|
|||||||
|
|
||||||
if (quote) {
|
if (quote) {
|
||||||
eventTags.push(["e", replyEvent.id, relayHint, "mention"]);
|
eventTags.push(["e", replyEvent.id, relayHint, "mention"]);
|
||||||
|
eventTags.push(["q", replyEvent.id]);
|
||||||
} else {
|
} else {
|
||||||
const rootEvent = replyEvent.tags.find((ev) => ev[3] === "root");
|
const rootEvent = replyEvent.tags.find((ev) => ev[3] === "root");
|
||||||
|
|
||||||
@ -809,8 +810,8 @@ export class Ark {
|
|||||||
label,
|
label,
|
||||||
title: "Editor",
|
title: "Editor",
|
||||||
url,
|
url,
|
||||||
width: 500,
|
width: 560,
|
||||||
height: 360,
|
height: 340,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(String(e));
|
throw new Error(String(e));
|
||||||
|
@ -4,21 +4,13 @@ export function MentionIcon(
|
|||||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
d="M11.85 13.251c-3.719.065-6.427 2.567-7.18 5.915-.13.575.338 1.084.927 1.084h6.901m-.647-6.999l.147-.001c.353 0 .696.022 1.03.064m-1.177-.063a7.889 7.889 0 00-1.852.249m3.028-.186c.334.042.658.104.972.186m-.972-.186a7.475 7.475 0 011.972.524m3.25 1.412v3m0 0v3m0-3h-3m3 0h3m-5.5-11.75a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
|
d="M16.868 19.867A9.25 9.25 0 1 1 21.25 12c0 1.98-.984 4.024-3.279 3.816a3.312 3.312 0 0 1-2.978-3.767l.53-3.646m-.585 4.077c-.308 2.188-2.109 3.744-4.023 3.474-1.914-.269-3.217-2.26-2.91-4.448.308-2.187 2.11-3.743 4.023-3.474 1.914.27 3.217 2.26 2.91 4.448Z"
|
||||||
></path>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ export function NoteReply({ large = false }: { large?: boolean }) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ReplyIcon className="shrink-0 size-4" />
|
<ReplyIcon className="shrink-0 size-4" />
|
||||||
|
{large ? "Reply" : null}
|
||||||
</button>
|
</button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Portal>
|
<Tooltip.Portal>
|
||||||
|
@ -187,9 +187,9 @@ pub async fn publish(
|
|||||||
state: State<'_, Nostr>,
|
state: State<'_, Nostr>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let final_tags = tags.into_iter().map(|val| Tag::parse(&val).unwrap());
|
let event_tags = tags.into_iter().map(|val| Tag::parse(&val).unwrap());
|
||||||
|
|
||||||
match client.publish_text_note(content, final_tags).await {
|
match client.publish_text_note(content, event_tags).await {
|
||||||
Ok(event_id) => Ok(event_id.to_bech32().unwrap()),
|
Ok(event_id) => Ok(event_id.to_bech32().unwrap()),
|
||||||
Err(err) => Err(err.to_string()),
|
Err(err) => Err(err.to_string()),
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use tauri::TitleBarStyle;
|
use tauri::TitleBarStyle;
|
||||||
use tauri::{Manager, Runtime, WebviewUrl, WebviewWindowBuilder};
|
use tauri::{
|
||||||
|
utils::config::WindowEffectsConfig, window::Effect, Manager, Runtime, WebviewUrl,
|
||||||
|
WebviewWindowBuilder,
|
||||||
|
};
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
|
||||||
pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
||||||
@ -60,18 +63,25 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
|||||||
let _ =
|
let _ =
|
||||||
WebviewWindowBuilder::new(app, "editor-0", WebviewUrl::App(PathBuf::from("editor")))
|
WebviewWindowBuilder::new(app, "editor-0", WebviewUrl::App(PathBuf::from("editor")))
|
||||||
.title("Editor")
|
.title("Editor")
|
||||||
.min_inner_size(500., 400.)
|
.min_inner_size(560., 340.)
|
||||||
.inner_size(600., 400.)
|
.inner_size(560., 340.)
|
||||||
.hidden_title(true)
|
.hidden_title(true)
|
||||||
.title_bar_style(TitleBarStyle::Overlay)
|
.title_bar_style(TitleBarStyle::Overlay)
|
||||||
|
.transparent(true)
|
||||||
|
.effects(WindowEffectsConfig {
|
||||||
|
state: None,
|
||||||
|
effects: vec![Effect::WindowBackground],
|
||||||
|
radius: None,
|
||||||
|
color: None,
|
||||||
|
})
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
let _ =
|
let _ =
|
||||||
WebviewWindowBuilder::new(app, "editor-0", WebviewUrl::App(PathBuf::from("editor")))
|
WebviewWindowBuilder::new(app, "editor-0", WebviewUrl::App(PathBuf::from("editor")))
|
||||||
.title("Editor")
|
.title("Editor")
|
||||||
.min_inner_size(500., 400.)
|
.min_inner_size(560., 340.)
|
||||||
.inner_size(600., 400.)
|
.inner_size(560., 340.)
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
@ -92,6 +102,13 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
|||||||
.minimizable(false)
|
.minimizable(false)
|
||||||
.resizable(false)
|
.resizable(false)
|
||||||
.title_bar_style(TitleBarStyle::Overlay)
|
.title_bar_style(TitleBarStyle::Overlay)
|
||||||
|
.transparent(true)
|
||||||
|
.effects(WindowEffectsConfig {
|
||||||
|
state: None,
|
||||||
|
effects: vec![Effect::WindowBackground],
|
||||||
|
radius: None,
|
||||||
|
color: None,
|
||||||
|
})
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
@ -131,6 +148,13 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
|||||||
.hidden_title(true)
|
.hidden_title(true)
|
||||||
.resizable(false)
|
.resizable(false)
|
||||||
.minimizable(false)
|
.minimizable(false)
|
||||||
|
.transparent(true)
|
||||||
|
.effects(WindowEffectsConfig {
|
||||||
|
state: None,
|
||||||
|
effects: vec![Effect::WindowBackground],
|
||||||
|
radius: None,
|
||||||
|
color: None,
|
||||||
|
})
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
Reference in New Issue
Block a user