feat: editor
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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() {
|
||||
const ark = useArk();
|
||||
const ref = useRef<HTMLDivElement | null>();
|
||||
// @ts-ignore, useless
|
||||
const { reply_to, quote } = Route.useSearch();
|
||||
|
||||
const [t] = useTranslation();
|
||||
const [editorValue, setEditorValue] = useState([
|
||||
let initialValue: EditorElement[];
|
||||
|
||||
if (quote) {
|
||||
initialValue = [
|
||||
{
|
||||
type: "paragraph",
|
||||
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 [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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}"e=${quote}`;
|
||||
} else {
|
||||
url = "/editor";
|
||||
}
|
||||
|
||||
return new WebviewWindow("editor", {
|
||||
title: "Editor",
|
||||
url: "/editor",
|
||||
url,
|
||||
minWidth: 500,
|
||||
width: 600,
|
||||
height: 400,
|
||||
|
||||
1
packages/types/index.d.ts
vendored
1
packages/types/index.d.ts
vendored
@@ -36,6 +36,7 @@ export interface Event {
|
||||
tags: string[][];
|
||||
content: string;
|
||||
sig: string;
|
||||
relay?: string;
|
||||
}
|
||||
|
||||
export interface EventWithReplies extends Event {
|
||||
|
||||
@@ -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,17 +12,19 @@ export function NoteReply() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root delayDuration={150}>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<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"
|
||||
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")}
|
||||
@@ -30,5 +33,31 @@ export function NoteReply() {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
31
pnpm-lock.yaml
generated
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user