feat: editor

This commit is contained in:
2024-02-26 15:10:42 +07:00
parent 63db8b1423
commit 98ef1927f2
12 changed files with 256 additions and 135 deletions

View File

@@ -15,12 +15,14 @@
"@lume/utils": "workspace:^", "@lume/utils": "workspace:^",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-collapsible": "^1.0.3",
"@tanstack/query-sync-storage-persister": "^5.24.1",
"@tanstack/react-query": "^5.22.2", "@tanstack/react-query": "^5.22.2",
"@tanstack/react-query-persist-client": "^5.22.2", "@tanstack/react-query-persist-client": "^5.22.2",
"@tanstack/react-router": "^1.16.6", "@tanstack/react-router": "^1.16.6",
"i18next": "^23.10.0", "i18next": "^23.10.0",
"i18next-resources-to-backend": "^1.2.0", "i18next-resources-to-backend": "^1.2.0",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"nostr-tools": "^2.3.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^14.0.5", "react-i18next": "^14.0.5",

View File

@@ -1,6 +1,6 @@
import { useArk } from "@lume/ark"; import { useArk } from "@lume/ark";
import { ArkProvider } from "./ark"; import { ArkProvider } from "./ark";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient } from "@tanstack/react-query";
import { RouterProvider, createRouter } from "@tanstack/react-router"; import { RouterProvider, createRouter } from "@tanstack/react-router";
import React, { StrictMode } from "react"; import React, { StrictMode } from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
@@ -9,36 +9,23 @@ import "./app.css";
import i18n from "./locale"; import i18n from "./locale";
import { Toaster } from "sonner"; import { Toaster } from "sonner";
import { locale, platform } from "@tauri-apps/plugin-os"; import { locale, platform } from "@tauri-apps/plugin-os";
import { routeTree } from "./router.gen"; // auto generated file
import { get, set, del } from "idb-keyval";
import {
PersistedClient,
Persister,
} from "@tanstack/react-query-persist-client";
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"; import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
import { routeTree } from "./router.gen"; // auto generated file
function createIDBPersister(idbValidKey: IDBValidKey = "reactQuery") {
return {
persistClient: async (client: PersistedClient) => {
await set(idbValidKey, client);
},
restoreClient: async () => {
return await get<PersistedClient>(idbValidKey);
},
removeClient: async () => {
await del(idbValidKey);
},
} as Persister;
}
const persister = createIDBPersister();
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours gcTime: 1000 * 60 * 60 * 24, // 24 hours
staleTime: 1000 * 60 * 5, // 5 minutes
}, },
}, },
}); });
const persister = createSyncStoragePersister({
storage: window.localStorage,
});
const platformName = await platform(); const platformName = await platform();
const osLocale = (await locale()).slice(0, 2); const osLocale = (await locale()).slice(0, 2);

View File

@@ -6,11 +6,10 @@ import {
insertImage, insertImage,
insertMention, insertMention,
insertNostrEvent, insertNostrEvent,
isImagePath,
isImageUrl, isImageUrl,
sendNativeNotification, sendNativeNotification,
} from "@lume/utils"; } from "@lume/utils";
import { createLazyFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { MediaButton } from "./-components/media"; import { MediaButton } from "./-components/media";
@@ -34,23 +33,68 @@ import {
} from "slate-react"; } from "slate-react";
import { Contact } from "@lume/types"; import { Contact } from "@lume/types";
import { User } from "@lume/ui"; import { User } from "@lume/ui";
import { nip19 } from "nostr-tools";
import { queryOptions, useSuspenseQuery } from "@tanstack/react-query";
import { invoke } from "@tauri-apps/api/core";
export const Route = createLazyFileRoute("/editor/")({ type EditorElement = {
type: string;
children: Descendant[];
eventId?: string;
};
const contactQueryOptions = queryOptions({
queryKey: ["contacts"],
queryFn: () => invoke("get_contact_metadata"),
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
});
export const Route = createFileRoute("/editor/")({
loader: ({ context }) =>
context.queryClient.ensureQueryData(contactQueryOptions),
component: Screen, component: Screen,
pendingComponent: Pending,
}); });
function Screen() { function Screen() {
const ark = useArk(); // @ts-ignore, useless
const ref = useRef<HTMLDivElement | null>(); const { reply_to, quote } = Route.useSearch();
const [t] = useTranslation(); let initialValue: EditorElement[];
const [editorValue, setEditorValue] = useState([
if (quote) {
initialValue = [
{ {
type: "paragraph", type: "paragraph",
children: [{ text: "" }], children: [{ text: "" }],
}, },
]); {
const [contacts, setContacts] = useState<Contact[]>([]); type: "event",
eventId: `nostr:${nip19.noteEncode(reply_to)}`,
children: [{ text: "" }],
},
{
type: "paragraph",
children: [{ text: "" }],
},
];
} else {
initialValue = [
{
type: "paragraph",
children: [{ text: "" }],
},
];
}
const ark = useArk();
const ref = useRef<HTMLDivElement | null>();
const contacts = useSuspenseQuery(contactQueryOptions).data as Contact[];
const [t] = useTranslation();
const [editorValue, setEditorValue] = useState(initialValue);
const [target, setTarget] = useState<Range | undefined>(); const [target, setTarget] = useState<Range | undefined>();
const [index, setIndex] = useState(0); const [index, setIndex] = useState(0);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@@ -63,7 +107,7 @@ function Screen() {
?.filter((c) => ?.filter((c) =>
c?.profile.name?.toLowerCase().startsWith(search.toLowerCase()), c?.profile.name?.toLowerCase().startsWith(search.toLowerCase()),
) )
?.slice(0, 10); ?.slice(0, 5);
const reset = () => { const reset = () => {
// @ts-expect-error, backlog // @ts-expect-error, backlog
@@ -101,7 +145,7 @@ function Screen() {
setLoading(true); setLoading(true);
const content = serialize(editor.children); const content = serialize(editor.children);
const eventId = await ark.publish(content); const eventId = await ark.publish(content, reply_to, quote);
if (eventId) { if (eventId) {
await sendNativeNotification("You've publish new post successfully."); await sendNativeNotification("You've publish new post successfully.");
@@ -162,7 +206,7 @@ function Screen() {
data-tauri-drag-region data-tauri-drag-region
className="flex h-16 w-full shrink-0 items-center justify-end gap-3 px-2" className="flex h-16 w-full shrink-0 items-center justify-end gap-3 px-2"
> >
<MediaButton className="size-9 rounded-full bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800" /> <MediaButton className="size-9 rounded-full bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700" />
<button <button
type="button" type="button"
onClick={publish} onClick={publish}
@@ -176,8 +220,14 @@ function Screen() {
</button> </button>
</div> </div>
<div className="flex h-full min-h-0 w-full"> <div className="flex h-full min-h-0 w-full">
<div className="h-full w-full flex-1 px-2 pb-2"> <div className="flex h-full w-full flex-1 flex-col gap-2 px-2 pb-2">
<div className="h-full w-full 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"> {reply_to && !quote ? (
<div className="flex flex-col gap-2 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">
<h3 className="font-medium">Reply to:</h3>
<MentionNote eventId={reply_to} />
</div>
) : null}
<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">
<Editable <Editable
key={JSON.stringify(editorValue)} key={JSON.stringify(editorValue)}
autoFocus={true} autoFocus={true}
@@ -206,8 +256,8 @@ function Screen() {
className="flex w-full flex-col rounded-lg p-2 hover:bg-neutral-100 dark:hover:bg-neutral-900" 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.Provider pubkey={contact.pubkey}>
<User.Root className="flex w-full items-center gap-2.5"> <User.Root className="flex w-full items-center gap-2">
<User.Avatar className="size-8 shrink-0 rounded-lg object-cover" /> <User.Avatar className="size-7 shrink-0 rounded-full object-cover" />
<div className="flex w-full flex-col items-start"> <div className="flex w-full flex-col items-start">
<User.Name className="max-w-[8rem] truncate text-sm font-medium" /> <User.Name className="max-w-[8rem] truncate text-sm font-medium" />
</div> </div>
@@ -226,6 +276,15 @@ function Screen() {
); );
} }
function Pending() {
return (
<div className="flex h-full w-full items-center justify-center gap-2.5">
<LoaderIcon className="size-5 animate-spin" />
<p>Loading cache...</p>
</div>
);
}
const withNostrEvent = (editor: ReactEditor) => { const withNostrEvent = (editor: ReactEditor) => {
const { insertData, isVoid } = editor; const { insertData, isVoid } = editor;

View File

@@ -83,7 +83,7 @@ function Screen() {
onClick={() => select(account.npub)} onClick={() => select(account.npub)}
> >
<User.Provider pubkey={account.npub}> <User.Provider pubkey={account.npub}>
<User.Root className="flex h-36 w-32 flex-col items-center justify-center gap-4 rounded-xl p-2 hover:bg-white/10 dark:hover:bg-black/10"> <User.Root className="flex h-36 w-32 flex-col items-center justify-center gap-4 rounded-2xl p-2 hover:bg-white/10 dark:hover:bg-black/10">
<User.Avatar className="size-20 rounded-full object-cover" /> <User.Avatar className="size-20 rounded-full object-cover" />
<User.Name className="max-w-[5rem] truncate text-lg font-medium leading-tight text-white" /> <User.Name className="max-w-[5rem] truncate text-lg font-medium leading-tight text-white" />
</User.Root> </User.Root>
@@ -91,7 +91,7 @@ function Screen() {
</button> </button>
))} ))}
<Link to="/landing"> <Link to="/landing">
<div className="flex h-36 w-32 flex-col items-center justify-center gap-4 rounded-xl p-2 text-white hover:bg-white/10 dark:hover:bg-black/10"> <div className="flex h-36 w-32 flex-col items-center justify-center gap-4 rounded-2xl p-2 text-white hover:bg-white/10 dark:hover:bg-black/10">
<div className="flex size-20 items-center justify-center rounded-full bg-white/20 dark:bg-black/20"> <div className="flex size-20 items-center justify-center rounded-full bg-white/20 dark:bg-black/20">
<PlusIcon className="size-5" /> <PlusIcon className="size-5" />
</div> </div>

View File

@@ -1,6 +1,7 @@
import { WebviewWindow } from "@tauri-apps/api/webviewWindow"; import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
import type { import type {
Account, Account,
Contact,
Event, Event,
EventWithReplies, EventWithReplies,
Keys, Keys,
@@ -12,11 +13,10 @@ import { readFile } from "@tauri-apps/plugin-fs";
import { generateContentTags } from "@lume/utils"; import { generateContentTags } from "@lume/utils";
export class Ark { export class Ark {
public account: Account; public accounts: Account[];
public accounts: Array<Account>;
constructor() { constructor() {
this.account = { npub: "", contacts: [] }; this.accounts = [];
} }
public async get_all_accounts() { public async get_all_accounts() {
@@ -43,12 +43,6 @@ export class Ark {
npub: fullNpub, npub: fullNpub,
}); });
if (cmd) {
const contacts: string[] = await invoke("get_contact_list");
this.account.npub = npub;
this.account.contacts = contacts;
}
return cmd; return cmd;
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -71,9 +65,6 @@ export class Ark {
if (cmd) { if (cmd) {
await invoke("update_signer", { nsec: keys.nsec }); await invoke("update_signer", { nsec: keys.nsec });
const contacts: string[] = await invoke("get_contact_list");
this.account.npub = keys.npub;
this.account.contacts = contacts;
} }
return cmd; return cmd;
@@ -155,13 +146,35 @@ export class Ark {
} }
} }
public async publish(content: string) { public async publish(content: string, reply_to?: string, quote?: boolean) {
try { try {
const g = await generateContentTags(content); const g = await generateContentTags(content);
const eventContent = g.content; const eventContent = g.content;
const eventTags = g.tags; const eventTags = g.tags;
if (reply_to) {
const replyEvent = await this.get_event(reply_to);
if (quote) {
eventTags.push([
"e",
replyEvent.id,
replyEvent.relay || "",
"mention",
]);
} else {
const rootEvent = replyEvent.tags.find((ev) => ev[3] === "root");
if (rootEvent) {
eventTags.push(["e", rootEvent[1], rootEvent[2] || "", "root"]);
}
eventTags.push(["e", replyEvent.id, replyEvent.relay || "", "reply"]);
eventTags.push(["p", replyEvent.pubkey]);
}
}
const cmd: string = await invoke("publish", { const cmd: string = await invoke("publish", {
content: eventContent, content: eventContent,
tags: eventTags, tags: eventTags,
@@ -310,6 +323,16 @@ export class Ark {
} }
} }
public async get_contact_metadata() {
try {
const cmd: Contact[] = await invoke("get_contact_metadata");
return cmd;
} catch (e) {
console.error(e);
return [];
}
}
public async follow(id: string, alias?: string) { public async follow(id: string, alias?: string) {
try { try {
const cmd: string = await invoke("follow", { id, alias }); const cmd: string = await invoke("follow", { id, alias });
@@ -433,10 +456,18 @@ export class Ark {
}); });
} }
public open_editor() { public open_editor(reply_to?: string, quote: boolean = false) {
let url: string;
if (reply_to) {
url = `/editor?reply_to=${reply_to}&quote=${quote}`;
} else {
url = "/editor";
}
return new WebviewWindow("editor", { return new WebviewWindow("editor", {
title: "Editor", title: "Editor",
url: "/editor", url,
minWidth: 500, minWidth: 500,
width: 600, width: 600,
height: 400, height: 400,

View File

@@ -36,6 +36,7 @@ export interface Event {
tags: string[][]; tags: string[][];
content: string; content: string;
sig: string; sig: string;
relay?: string;
} }
export interface EventWithReplies extends Event { export interface EventWithReplies extends Event {

View File

@@ -1,8 +1,9 @@
import { ReplyIcon } from "@lume/icons"; import { ReplyIcon, ShareIcon } from "@lume/icons";
import * as Tooltip from "@radix-ui/react-tooltip"; import * as Tooltip from "@radix-ui/react-tooltip";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNoteContext } from "../provider"; import { useNoteContext } from "../provider";
import { useArk } from "@lume/ark"; import { useArk } from "@lume/ark";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
export function NoteReply() { export function NoteReply() {
const ark = useArk(); const ark = useArk();
@@ -11,17 +12,19 @@ export function NoteReply() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<DropdownMenu.Root>
<Tooltip.Provider> <Tooltip.Provider>
<Tooltip.Root delayDuration={150}> <Tooltip.Root delayDuration={150}>
<DropdownMenu.Trigger asChild>
<Tooltip.Trigger asChild> <Tooltip.Trigger asChild>
<button <button
type="button" type="button"
onClick={() => ark.open_thread(event.id)} className="size07 group inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200"
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-800 dark:text-neutral-200"
> >
<ReplyIcon className="size-5 group-hover:text-blue-500" /> <ReplyIcon className="size-5 group-hover:text-blue-500" />
</button> </button>
</Tooltip.Trigger> </Tooltip.Trigger>
</DropdownMenu.Trigger>
<Tooltip.Portal> <Tooltip.Portal>
<Tooltip.Content className="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 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] dark:bg-neutral-50 dark:text-neutral-950"> <Tooltip.Content className="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 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] dark:bg-neutral-50 dark:text-neutral-950">
{t("note.menu.viewThread")} {t("note.menu.viewThread")}
@@ -30,5 +33,31 @@ export function NoteReply() {
</Tooltip.Portal> </Tooltip.Portal>
</Tooltip.Root> </Tooltip.Root>
</Tooltip.Provider> </Tooltip.Provider>
<DropdownMenu.Portal>
<DropdownMenu.Content className="flex w-[200px] flex-col overflow-hidden rounded-xl bg-black p-1 shadow-md shadow-neutral-500/20 focus:outline-none dark:bg-white">
<DropdownMenu.Item asChild>
<button
type="button"
onClick={() => ark.open_thread(event.id)}
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
>
<ShareIcon className="size-4" />
{t("note.buttons.view")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<button
type="button"
onClick={() => ark.open_editor(event.id)}
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
>
<ReplyIcon className="size-4" />
{t("note.buttons.reply")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Arrow className="fill-black dark:fill-white" />
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
); );
} }

View File

@@ -1,8 +1,7 @@
import { LoaderIcon, ReplyIcon, RepostIcon } from "@lume/icons"; import { LoaderIcon, ReplyIcon, RepostIcon } from "@lume/icons";
import { cn, editorAtom, editorValueAtom } from "@lume/utils"; import { cn } from "@lume/utils";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import * as Tooltip from "@radix-ui/react-tooltip"; import * as Tooltip from "@radix-ui/react-tooltip";
import { useSetAtom } from "jotai";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -12,13 +11,10 @@ import { useArk } from "@lume/ark";
export function NoteRepost() { export function NoteRepost() {
const ark = useArk(); const ark = useArk();
const event = useNoteContext(); const event = useNoteContext();
const setEditorValue = useSetAtom(editorValueAtom);
const setIsEditorOpen = useSetAtom(editorAtom);
const [t] = useTranslation(); const [t] = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isRepost, setIsRepost] = useState(false); const [isRepost, setIsRepost] = useState(false);
const [open, setOpen] = useState(false);
const repost = async () => { const repost = async () => {
try { try {
@@ -39,35 +35,15 @@ export function NoteRepost() {
} }
}; };
const quote = () => {
setEditorValue([
{
type: "paragraph",
children: [{ text: "" }],
},
{
type: "event",
// @ts-expect-error, useless
eventId: `nostr:${nip19.noteEncode(event.id)}`,
children: [{ text: "" }],
},
{
type: "paragraph",
children: [{ text: "" }],
},
]);
setIsEditorOpen(true);
};
return ( return (
<DropdownMenu.Root open={open} onOpenChange={setOpen}> <DropdownMenu.Root>
<Tooltip.Provider> <Tooltip.Provider>
<Tooltip.Root delayDuration={150}> <Tooltip.Root delayDuration={150}>
<DropdownMenu.Trigger asChild> <DropdownMenu.Trigger asChild>
<Tooltip.Trigger asChild> <Tooltip.Trigger asChild>
<button <button
type="button" type="button"
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-800 dark:text-neutral-200" className="size07 group inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200"
> >
{loading ? ( {loading ? (
<LoaderIcon className="size-4 animate-spin" /> <LoaderIcon className="size-4 animate-spin" />
@@ -91,12 +67,12 @@ export function NoteRepost() {
</Tooltip.Root> </Tooltip.Root>
</Tooltip.Provider> </Tooltip.Provider>
<DropdownMenu.Portal> <DropdownMenu.Portal>
<DropdownMenu.Content className="flex w-[200px] flex-col overflow-hidden rounded-2xl bg-white/50 p-2 ring-1 ring-black/10 backdrop-blur-2xl focus:outline-none dark:bg-black/50 dark:ring-white/10"> <DropdownMenu.Content className="flex w-[200px] flex-col overflow-hidden rounded-xl bg-black p-1 shadow-md shadow-neutral-500/20 focus:outline-none dark:bg-white">
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
<button <button
type="button" type="button"
onClick={repost} onClick={repost}
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
> >
<RepostIcon className="size-4" /> <RepostIcon className="size-4" />
{t("note.buttons.repost")} {t("note.buttons.repost")}
@@ -105,13 +81,14 @@ export function NoteRepost() {
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
<button <button
type="button" type="button"
onClick={quote} onClick={() => ark.open_editor(event.id, true)}
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
> >
<ReplyIcon className="size-4" /> <ReplyIcon className="size-4" />
{t("note.buttons.quote")} {t("note.buttons.quote")}
</button> </button>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Arrow className="fill-black dark:fill-white" />
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Portal> </DropdownMenu.Portal>
</DropdownMenu.Root> </DropdownMenu.Root>

31
pnpm-lock.yaml generated
View File

@@ -78,6 +78,9 @@ importers:
'@radix-ui/react-collapsible': '@radix-ui/react-collapsible':
specifier: ^1.0.3 specifier: ^1.0.3
version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0)
'@tanstack/query-sync-storage-persister':
specifier: ^5.24.1
version: 5.24.1
'@tanstack/react-query': '@tanstack/react-query':
specifier: ^5.22.2 specifier: ^5.22.2
version: 5.22.2(react@18.2.0) version: 5.22.2(react@18.2.0)
@@ -96,6 +99,9 @@ importers:
idb-keyval: idb-keyval:
specifier: ^6.2.1 specifier: ^6.2.1
version: 6.2.1 version: 6.2.1
nostr-tools:
specifier: ^2.3.1
version: 2.3.1(typescript@5.3.3)
react: react:
specifier: ^18.2.0 specifier: ^18.2.0
version: 18.2.0 version: 18.2.0
@@ -2729,15 +2735,15 @@ packages:
resolution: {integrity: sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==} resolution: {integrity: sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==}
dependencies: dependencies:
'@noble/curves': 1.1.0 '@noble/curves': 1.1.0
'@noble/hashes': 1.3.1 '@noble/hashes': 1.3.3
'@scure/base': 1.1.1 '@scure/base': 1.1.5
dev: false dev: false
/@scure/bip39@1.2.1: /@scure/bip39@1.2.1:
resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==} resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==}
dependencies: dependencies:
'@noble/hashes': 1.3.1 '@noble/hashes': 1.3.3
'@scure/base': 1.1.1 '@scure/base': 1.1.5
dev: false dev: false
/@swc/core-darwin-arm64@1.4.2: /@swc/core-darwin-arm64@1.4.2:
@@ -2892,12 +2898,29 @@ packages:
resolution: {integrity: sha512-z3PwKFUFACMUqe1eyesCIKg3Jv1mysSrYfrEW5ww5DCDUD4zlpTKBvUDaEjsfZzL3ULrFLDM9yVUxI/fega1Qg==} resolution: {integrity: sha512-z3PwKFUFACMUqe1eyesCIKg3Jv1mysSrYfrEW5ww5DCDUD4zlpTKBvUDaEjsfZzL3ULrFLDM9yVUxI/fega1Qg==}
dev: false dev: false
/@tanstack/query-core@5.24.1:
resolution: {integrity: sha512-DZ6Nx9p7BhjkG50ayJ+MKPgff+lMeol7QYXkvuU5jr2ryW/4ok5eanaS9W5eooA4xN0A/GPHdLGOZGzArgf5Cg==}
dev: false
/@tanstack/query-persist-client-core@5.22.2: /@tanstack/query-persist-client-core@5.22.2:
resolution: {integrity: sha512-sFDgWoN54uclIDIoImPmDzxTq8HhZEt9pO0JbVHjI6LPZqunMMF9yAq9zFKrpH//jD5f+rBCQsdGyhdpUo9e8Q==} resolution: {integrity: sha512-sFDgWoN54uclIDIoImPmDzxTq8HhZEt9pO0JbVHjI6LPZqunMMF9yAq9zFKrpH//jD5f+rBCQsdGyhdpUo9e8Q==}
dependencies: dependencies:
'@tanstack/query-core': 5.22.2 '@tanstack/query-core': 5.22.2
dev: false dev: false
/@tanstack/query-persist-client-core@5.24.1:
resolution: {integrity: sha512-ayUDCSCXAq3ZYXMrVQ3c4g2Mvj+d/Q7rGkNJTvdw09DZQUUMTfZsvSayitJjOxqJl1Pex4HmZNk8PiDvrqvlRQ==}
dependencies:
'@tanstack/query-core': 5.24.1
dev: false
/@tanstack/query-sync-storage-persister@5.24.1:
resolution: {integrity: sha512-dfqgFgb+6tmdvnE1vMQbBuZOBUi7zFeQB/gQJgiADJ2IO0OXp/Ucj06sVOLm9fAPGiUBDqF+UW/xB9ipBH2+Hw==}
dependencies:
'@tanstack/query-core': 5.24.1
'@tanstack/query-persist-client-core': 5.24.1
dev: false
/@tanstack/react-query-persist-client@5.22.2(@tanstack/react-query@5.22.2)(react@18.2.0): /@tanstack/react-query-persist-client@5.22.2(@tanstack/react-query@5.22.2)(react@18.2.0):
resolution: {integrity: sha512-osAaQn2PDTaa2ApTLOAus7g8Y96LHfS2+Pgu/RoDlEJUEkX7xdEn0YuurxbnJaDJDESMfr+CH/eAX2y+lx02Fg==} resolution: {integrity: sha512-osAaQn2PDTaa2ApTLOAus7g8Y96LHfS2+Pgu/RoDlEJUEkX7xdEn0YuurxbnJaDJDESMfr+CH/eAX2y+lx02Fg==}
peerDependencies: peerDependencies:

View File

@@ -98,6 +98,7 @@ fn main() {
nostr::keys::verify_nip05, nostr::keys::verify_nip05,
nostr::metadata::get_profile, nostr::metadata::get_profile,
nostr::metadata::get_contact_list, nostr::metadata::get_contact_list,
nostr::metadata::get_contact_metadata,
nostr::metadata::create_profile, nostr::metadata::create_profile,
nostr::metadata::follow, nostr::metadata::follow,
nostr::metadata::unfollow, nostr::metadata::unfollow,
@@ -110,7 +111,6 @@ fn main() {
nostr::event::get_global_events, nostr::event::get_global_events,
nostr::event::get_event_thread, nostr::event::get_event_thread,
nostr::event::publish, nostr::event::publish,
nostr::event::reply_to,
nostr::event::repost, nostr::event::repost,
nostr::event::upvote, nostr::event::upvote,
nostr::event::downvote, nostr::event::downvote,

View File

@@ -137,25 +137,6 @@ pub async fn publish(
} }
} }
#[tauri::command]
pub async fn reply_to(
content: &str,
tags: Vec<String>,
state: State<'_, Nostr>,
) -> Result<EventId, String> {
let client = &state.client;
if let Ok(event_tags) = Tag::parse(tags) {
let event = client
.publish_text_note(content, vec![event_tags])
.await
.expect("Publish reply failed");
Ok(event)
} else {
Err("Reply failed".into())
}
}
#[tauri::command] #[tauri::command]
pub async fn repost(id: &str, pubkey: &str, state: State<'_, Nostr>) -> Result<EventId, ()> { pub async fn repost(id: &str, pubkey: &str, state: State<'_, Nostr>) -> Result<EventId, ()> {
let client = &state.client; let client = &state.client;

View File

@@ -3,6 +3,12 @@ use nostr_sdk::prelude::*;
use std::{str::FromStr, time::Duration}; use std::{str::FromStr, time::Duration};
use tauri::State; use tauri::State;
#[derive(serde::Serialize)]
pub struct CacheContact {
pubkey: String,
profile: Metadata,
}
#[tauri::command] #[tauri::command]
pub async fn get_profile(id: &str, state: State<'_, Nostr>) -> Result<Metadata, String> { pub async fn get_profile(id: &str, state: State<'_, Nostr>) -> Result<Metadata, String> {
let client = &state.client; let client = &state.client;
@@ -46,11 +52,36 @@ pub async fn get_profile(id: &str, state: State<'_, Nostr>) -> Result<Metadata,
#[tauri::command] #[tauri::command]
pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, String> { pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
let client = &state.client; let client = &state.client;
let contact_list = client.get_contact_list(Some(Duration::from_secs(10))).await;
if let Ok(list) = contact_list { if let Ok(contact_list) = client.get_contact_list(Some(Duration::from_secs(10))).await {
let v = list.into_iter().map(|f| f.public_key.to_hex()).collect(); let list = contact_list
Ok(v) .into_iter()
.map(|f| f.public_key.to_hex())
.collect();
Ok(list)
} else {
Err("Contact list not found".into())
}
}
#[tauri::command]
pub async fn get_contact_metadata(state: State<'_, Nostr>) -> Result<Vec<CacheContact>, String> {
let client = &state.client;
if let Ok(contact_list) = client
.get_contact_list_metadata(Some(Duration::from_secs(10)))
.await
{
let list: Vec<CacheContact> = contact_list
.into_iter()
.map(|(id, metadata)| CacheContact {
pubkey: id.to_hex(),
profile: metadata,
})
.collect();
Ok(list)
} else { } else {
Err("Contact list not found".into()) Err("Contact list not found".into())
} }