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:^",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@tanstack/query-sync-storage-persister": "^5.24.1",
"@tanstack/react-query": "^5.22.2",
"@tanstack/react-query-persist-client": "^5.22.2",
"@tanstack/react-router": "^1.16.6",
"i18next": "^23.10.0",
"i18next-resources-to-backend": "^1.2.0",
"idb-keyval": "^6.2.1",
"nostr-tools": "^2.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.0.5",

View File

@@ -1,6 +1,6 @@
import { useArk } from "@lume/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 React, { StrictMode } from "react";
import ReactDOM from "react-dom/client";
@@ -9,36 +9,23 @@ import "./app.css";
import i18n from "./locale";
import { Toaster } from "sonner";
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 { 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({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
staleTime: 1000 * 60 * 5, // 5 minutes
},
},
});
const persister = createSyncStoragePersister({
storage: window.localStorage,
});
const platformName = await platform();
const osLocale = (await locale()).slice(0, 2);

View File

@@ -6,11 +6,10 @@ import {
insertImage,
insertMention,
insertNostrEvent,
isImagePath,
isImageUrl,
sendNativeNotification,
} from "@lume/utils";
import { createLazyFileRoute } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { MediaButton } from "./-components/media";
@@ -34,23 +33,68 @@ import {
} from "slate-react";
import { Contact } from "@lume/types";
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,
pendingComponent: Pending,
});
function Screen() {
// @ts-ignore, useless
const { reply_to, quote } = Route.useSearch();
let initialValue: EditorElement[];
if (quote) {
initialValue = [
{
type: "paragraph",
children: [{ text: "" }],
},
{
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([
{
type: "paragraph",
children: [{ text: "" }],
},
]);
const [contacts, setContacts] = useState<Contact[]>([]);
const [editorValue, setEditorValue] = useState(initialValue);
const [target, setTarget] = useState<Range | undefined>();
const [index, setIndex] = useState(0);
const [search, setSearch] = useState("");
@@ -63,7 +107,7 @@ function Screen() {
?.filter((c) =>
c?.profile.name?.toLowerCase().startsWith(search.toLowerCase()),
)
?.slice(0, 10);
?.slice(0, 5);
const reset = () => {
// @ts-expect-error, backlog
@@ -101,7 +145,7 @@ function Screen() {
setLoading(true);
const content = serialize(editor.children);
const eventId = await ark.publish(content);
const eventId = await ark.publish(content, reply_to, quote);
if (eventId) {
await sendNativeNotification("You've publish new post successfully.");
@@ -162,7 +206,7 @@ function Screen() {
data-tauri-drag-region
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
type="button"
onClick={publish}
@@ -176,8 +220,14 @@ function Screen() {
</button>
</div>
<div className="flex h-full min-h-0 w-full">
<div className="h-full w-full flex-1 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">
<div className="flex h-full w-full flex-1 flex-col gap-2 px-2 pb-2">
{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
key={JSON.stringify(editorValue)}
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"
>
<User.Provider pubkey={contact.pubkey}>
<User.Root className="flex w-full items-center gap-2.5">
<User.Avatar className="size-8 shrink-0 rounded-lg object-cover" />
<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>
@@ -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 { insertData, isVoid } = editor;

View File

@@ -83,7 +83,7 @@ function Screen() {
onClick={() => select(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.Name className="max-w-[5rem] truncate text-lg font-medium leading-tight text-white" />
</User.Root>
@@ -91,7 +91,7 @@ function Screen() {
</button>
))}
<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">
<PlusIcon className="size-5" />
</div>

View File

@@ -1,6 +1,7 @@
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
import type {
Account,
Contact,
Event,
EventWithReplies,
Keys,
@@ -12,11 +13,10 @@ import { readFile } from "@tauri-apps/plugin-fs";
import { generateContentTags } from "@lume/utils";
export class Ark {
public account: Account;
public accounts: Array<Account>;
public accounts: Account[];
constructor() {
this.account = { npub: "", contacts: [] };
this.accounts = [];
}
public async get_all_accounts() {
@@ -43,12 +43,6 @@ export class Ark {
npub: fullNpub,
});
if (cmd) {
const contacts: string[] = await invoke("get_contact_list");
this.account.npub = npub;
this.account.contacts = contacts;
}
return cmd;
} catch (e) {
console.error(e);
@@ -71,9 +65,6 @@ export class Ark {
if (cmd) {
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;
@@ -155,13 +146,35 @@ export class Ark {
}
}
public async publish(content: string) {
public async publish(content: string, reply_to?: string, quote?: boolean) {
try {
const g = await generateContentTags(content);
const eventContent = g.content;
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", {
content: eventContent,
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) {
try {
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", {
title: "Editor",
url: "/editor",
url,
minWidth: 500,
width: 600,
height: 400,

View File

@@ -36,6 +36,7 @@ export interface Event {
tags: string[][];
content: string;
sig: string;
relay?: string;
}
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 { useTranslation } from "react-i18next";
import { useNoteContext } from "../provider";
import { useArk } from "@lume/ark";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
export function NoteReply() {
const ark = useArk();
@@ -11,24 +12,52 @@ export function NoteReply() {
const { t } = useTranslation();
return (
<Tooltip.Provider>
<Tooltip.Root delayDuration={150}>
<Tooltip.Trigger asChild>
<button
type="button"
onClick={() => ark.open_thread(event.id)}
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" />
</button>
</Tooltip.Trigger>
<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">
{t("note.menu.viewThread")}
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
<DropdownMenu.Root>
<Tooltip.Provider>
<Tooltip.Root delayDuration={150}>
<DropdownMenu.Trigger asChild>
<Tooltip.Trigger asChild>
<button
type="button"
className="size07 group inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200"
>
<ReplyIcon className="size-5 group-hover:text-blue-500" />
</button>
</Tooltip.Trigger>
</DropdownMenu.Trigger>
<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">
{t("note.menu.viewThread")}
<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-[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 { cn, editorAtom, editorValueAtom } from "@lume/utils";
import { cn } from "@lume/utils";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useSetAtom } from "jotai";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
@@ -12,13 +11,10 @@ import { useArk } from "@lume/ark";
export function NoteRepost() {
const ark = useArk();
const event = useNoteContext();
const setEditorValue = useSetAtom(editorValueAtom);
const setIsEditorOpen = useSetAtom(editorAtom);
const [t] = useTranslation();
const [loading, setLoading] = useState(false);
const [isRepost, setIsRepost] = useState(false);
const [open, setOpen] = useState(false);
const repost = async () => {
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 (
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
<DropdownMenu.Root>
<Tooltip.Provider>
<Tooltip.Root delayDuration={150}>
<DropdownMenu.Trigger asChild>
<Tooltip.Trigger asChild>
<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 ? (
<LoaderIcon className="size-4 animate-spin" />
@@ -91,12 +67,12 @@ export function NoteRepost() {
</Tooltip.Root>
</Tooltip.Provider>
<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>
<button
type="button"
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" />
{t("note.buttons.repost")}
@@ -105,13 +81,14 @@ export function NoteRepost() {
<DropdownMenu.Item asChild>
<button
type="button"
onClick={quote}
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"
onClick={() => ark.open_editor(event.id, true)}
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.quote")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Arrow className="fill-black dark:fill-white" />
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>

31
pnpm-lock.yaml generated
View File

@@ -78,6 +78,9 @@ importers:
'@radix-ui/react-collapsible':
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)
'@tanstack/query-sync-storage-persister':
specifier: ^5.24.1
version: 5.24.1
'@tanstack/react-query':
specifier: ^5.22.2
version: 5.22.2(react@18.2.0)
@@ -96,6 +99,9 @@ importers:
idb-keyval:
specifier: ^6.2.1
version: 6.2.1
nostr-tools:
specifier: ^2.3.1
version: 2.3.1(typescript@5.3.3)
react:
specifier: ^18.2.0
version: 18.2.0
@@ -2729,15 +2735,15 @@ packages:
resolution: {integrity: sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==}
dependencies:
'@noble/curves': 1.1.0
'@noble/hashes': 1.3.1
'@scure/base': 1.1.1
'@noble/hashes': 1.3.3
'@scure/base': 1.1.5
dev: false
/@scure/bip39@1.2.1:
resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==}
dependencies:
'@noble/hashes': 1.3.1
'@scure/base': 1.1.1
'@noble/hashes': 1.3.3
'@scure/base': 1.1.5
dev: false
/@swc/core-darwin-arm64@1.4.2:
@@ -2892,12 +2898,29 @@ packages:
resolution: {integrity: sha512-z3PwKFUFACMUqe1eyesCIKg3Jv1mysSrYfrEW5ww5DCDUD4zlpTKBvUDaEjsfZzL3ULrFLDM9yVUxI/fega1Qg==}
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:
resolution: {integrity: sha512-sFDgWoN54uclIDIoImPmDzxTq8HhZEt9pO0JbVHjI6LPZqunMMF9yAq9zFKrpH//jD5f+rBCQsdGyhdpUo9e8Q==}
dependencies:
'@tanstack/query-core': 5.22.2
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):
resolution: {integrity: sha512-osAaQn2PDTaa2ApTLOAus7g8Y96LHfS2+Pgu/RoDlEJUEkX7xdEn0YuurxbnJaDJDESMfr+CH/eAX2y+lx02Fg==}
peerDependencies:

View File

@@ -98,6 +98,7 @@ fn main() {
nostr::keys::verify_nip05,
nostr::metadata::get_profile,
nostr::metadata::get_contact_list,
nostr::metadata::get_contact_metadata,
nostr::metadata::create_profile,
nostr::metadata::follow,
nostr::metadata::unfollow,
@@ -110,7 +111,6 @@ fn main() {
nostr::event::get_global_events,
nostr::event::get_event_thread,
nostr::event::publish,
nostr::event::reply_to,
nostr::event::repost,
nostr::event::upvote,
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]
pub async fn repost(id: &str, pubkey: &str, state: State<'_, Nostr>) -> Result<EventId, ()> {
let client = &state.client;

View File

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