feat: support nip-36

This commit is contained in:
reya 2024-04-16 07:49:44 +07:00
parent 09b143cb08
commit 94d400cab2
16 changed files with 253 additions and 150 deletions

View File

@ -19,6 +19,7 @@
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/query-sync-storage-persister": "^5.29.0",
"@tanstack/react-query": "^5.29.0",
"@tanstack/react-query-persist-client": "^5.29.0",

View File

@ -10,7 +10,7 @@ import { getCurrent } from "@tauri-apps/api/webviewWindow";
import { readTextFile } from "@tauri-apps/plugin-fs";
import { nanoid } from "nanoid";
import { useEffect, useRef, useState } from "react";
import { useDebounce, useDebouncedCallback } from "use-debounce";
import { useDebouncedCallback } from "use-debounce";
import { VList, VListHandle } from "virtua";
export const Route = createFileRoute("/$account/home")({

View File

@ -2,8 +2,15 @@ import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
import { type Ark } from "@lume/ark";
import { type QueryClient } from "@tanstack/react-query";
import { type Platform } from "@tauri-apps/plugin-os";
import { Account, Interests, Settings } from "@lume/types";
import type { Account, Interests, Settings } from "@lume/types";
import { Spinner } from "@lume/ui";
import { type Descendant } from "slate";
type EditorElement = {
type: string;
children: Descendant[];
eventId?: string;
};
interface RouterContext {
ark: Ark;
@ -13,6 +20,7 @@ interface RouterContext {
settings?: Settings;
interests?: Interests;
accounts?: Account[];
initialValue?: EditorElement[];
}
export const Route = createRootRouteWithContext<RouterContext>()({

View File

@ -69,6 +69,13 @@ function Screen() {
}));
};
const toggleNsfw = () => {
setNewSettings((prev) => ({
...prev,
nsfw: !newSettings.nsfw,
}));
};
const submit = async () => {
try {
// start loading
@ -167,18 +174,28 @@ function Screen() {
</p>
</div>
</div>
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-50 px-5 py-4 dark:bg-neutral-950">
<p className="text-sm text-neutral-700 dark:text-neutral-300">
There are many more settings you can configure from the 'Settings'
Screen. Be sure to visit it later.
</p>
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-neutral-900">
<Switch.Root
checked={newSettings.nsfw}
onClick={() => toggleNsfw()}
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
>
<Switch.Thumb className="block size-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
<div className="flex-1">
<h3 className="font-semibold">Filter sensitive content</h3>
<p className="text-sm text-neutral-700 dark:text-neutral-300">
By default, Lume will display all content which have Content
Warning tag, it's may include NSFW content.
</p>
</div>
</div>
</div>
<button
type="button"
onClick={submit}
disabled={loading}
className="inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
className="mb-1 inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
>
{t("global.continue")}
</button>

View File

@ -7,6 +7,7 @@ import { getCurrent } from "@tauri-apps/api/window";
import { UnlistenFn } from "@tauri-apps/api/event";
import { useRouteContext } from "@tanstack/react-router";
import { Spinner } from "@lume/ui";
import * as Tooltip from "@radix-ui/react-tooltip";
export function MediaButton({ className }: { className?: string }) {
const { ark } = useRouteContext({ strict: false });
@ -16,14 +17,13 @@ export function MediaButton({ className }: { className?: string }) {
const uploadToNostrBuild = async () => {
try {
// start loading
setLoading(true);
const image = await ark.upload();
insertImage(editor, image);
if (image) {
insertImage(editor, image);
}
// reset loading
setLoading(false);
} catch (e) {
setLoading(false);
@ -63,17 +63,29 @@ export function MediaButton({ className }: { className?: string }) {
}, []);
return (
<button
type="button"
onClick={() => uploadToNostrBuild()}
disabled={loading}
className={cn("inline-flex items-center justify-center", className)}
>
{loading ? (
<Spinner className="size-5" />
) : (
<AddMediaIcon className="size-5" />
)}
</button>
<Tooltip.Provider>
<Tooltip.Root delayDuration={150}>
<Tooltip.Trigger asChild>
<button
type="button"
onClick={() => uploadToNostrBuild()}
disabled={loading}
className={cn("inline-flex items-center justify-center", className)}
>
{loading ? (
<Spinner className="size-4" />
) : (
<AddMediaIcon 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">
Upload media
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}

View File

@ -0,0 +1,40 @@
import { NsfwIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import * as Tooltip from "@radix-ui/react-tooltip";
import { Dispatch, SetStateAction } from "react";
export function NsfwToggle({
nsfw,
setNsfw,
className,
}: {
nsfw: boolean;
setNsfw: Dispatch<SetStateAction<boolean>>;
className?: string;
}) {
return (
<Tooltip.Provider>
<Tooltip.Root delayDuration={150}>
<Tooltip.Trigger asChild>
<button
type="button"
onClick={() => setNsfw((prev) => !prev)}
className={cn(
"inline-flex items-center justify-center",
className,
nsfw ? "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">
Mark as sensitive content
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}

View File

@ -1,4 +1,4 @@
import { LoaderIcon, TrashIcon } from "@lume/icons";
import { ComposeFilledIcon, NsfwIcon, TrashIcon } from "@lume/icons";
import {
Portal,
cn,
@ -35,11 +35,11 @@ import { Spinner, User } from "@lume/ui";
import { nip19 } from "nostr-tools";
import { queryOptions, useSuspenseQuery } from "@tanstack/react-query";
import { invoke } from "@tauri-apps/api/core";
import { NsfwToggle } from "./-components/nsfw";
type EditorElement = {
type: string;
children: Descendant[];
eventId?: string;
type EditorSearch = {
reply_to: string;
quote: boolean;
};
const contactQueryOptions = queryOptions({
@ -51,46 +51,48 @@ const contactQueryOptions = queryOptions({
});
export const Route = createFileRoute("/editor/")({
loader: ({ context }) =>
context.queryClient.ensureQueryData(contactQueryOptions),
validateSearch: (search: Record<string, string>): EditorSearch => {
return {
reply_to: search.reply_to,
quote: search.quote === "true" ?? false,
};
},
beforeLoad: async ({ search }) => {
return {
initialValue: search.quote
? [
{
type: "paragraph",
children: [{ text: "" }],
},
{
type: "event",
eventId: `nostr:${nip19.noteEncode(search.reply_to)}`,
children: [{ text: "" }],
},
{
type: "paragraph",
children: [{ text: "" }],
},
]
: [
{
type: "paragraph",
children: [{ text: "" }],
},
],
};
},
loader: ({ context }) => {
context.queryClient.ensureQueryData(contactQueryOptions);
},
component: Screen,
pendingComponent: Pending,
});
function Screen() {
// @ts-ignore, useless
const { reply_to, quote } = Route.useSearch();
const { ark } = Route.useRouteContext();
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 ref = useRef<HTMLDivElement | null>();
const contacts = useSuspenseQuery(contactQueryOptions).data as Contact[];
const { ark, initialValue } = Route.useRouteContext();
const [t] = useTranslation();
const [editorValue, setEditorValue] = useState(initialValue);
@ -98,10 +100,14 @@ function Screen() {
const [index, setIndex] = useState(0);
const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false);
const [nsfw, setNsfw] = useState(false);
const [editor] = useState(() =>
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
);
const ref = useRef<HTMLDivElement | null>();
const contacts = useSuspenseQuery(contactQueryOptions).data as Contact[];
const filters = contacts
?.filter((c) =>
c?.profile.name?.toLowerCase().startsWith(search.toLowerCase()),
@ -204,15 +210,25 @@ function Screen() {
>
<div
data-tauri-drag-region
className="flex h-16 w-full shrink-0 items-center justify-end gap-3 px-2"
className="flex h-14 w-full shrink-0 items-center justify-end gap-2 px-2"
>
<MediaButton className="size-9 rounded-full bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700" />
<NsfwToggle
nsfw={nsfw}
setNsfw={setNsfw}
className="size-8 rounded-full bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
/>
<MediaButton className="size-8 rounded-full bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700" />
<button
type="button"
onClick={publish}
className="inline-flex h-9 w-24 items-center justify-center rounded-full bg-blue-500 px-3 font-medium text-white hover:bg-blue-600"
className="inline-flex h-8 w-max items-center justify-center gap-1 rounded-full bg-blue-500 px-3 text-sm font-medium text-white hover:bg-blue-600"
>
{loading ? <Spinner className="size-5" /> : t("global.post")}
{loading ? (
<Spinner className="size-4" />
) : (
<ComposeFilledIcon className="size-4" />
)}
{t("global.post")}
</button>
</div>
<div className="flex h-full min-h-0 w-full">

View File

@ -23,9 +23,11 @@ enum NSTORE_KEYS {
export class Ark {
public windows: WebviewWindow[];
public settings: Settings;
constructor() {
this.windows = [];
this.settings = undefined;
}
public async get_all_accounts() {
@ -144,7 +146,6 @@ export class Ark {
if (asOf && asOf > 0) until = asOf.toString();
const dedup = true;
const seenIds = new Set<string>();
const dedupQueue = new Set<string>();
@ -155,31 +156,37 @@ export class Ark {
global: isGlobal,
});
if (dedup) {
for (const event of nostrEvents) {
const tags = event.tags
.filter((el) => el[0] === "e")
?.map((item) => item[1]);
for (const event of nostrEvents) {
const tags = event.tags
.filter((el) => el[0] === "e")
?.map((item) => item[1]);
if (tags.length) {
for (const tag of tags) {
if (seenIds.has(tag)) {
dedupQueue.add(event.id);
break;
}
seenIds.add(tag);
if (tags.length) {
for (const tag of tags) {
if (seenIds.has(tag)) {
dedupQueue.add(event.id);
break;
}
seenIds.add(tag);
}
}
return nostrEvents
.filter((event) => !dedupQueue.has(event.id))
.sort((a, b) => b.created_at - a.created_at);
}
return nostrEvents;
const events = nostrEvents
.filter((event) => !dedupQueue.has(event.id))
.sort((a, b) => b.created_at - a.created_at);
if (this.settings?.nsfw) {
return events.filter(
(event) =>
event.tags.filter((event) => event[0] === "content-warning")
.length > 0,
);
}
return events;
} catch (e) {
console.error(String(e));
console.info(String(e));
return [];
}
}
@ -229,7 +236,12 @@ export class Ark {
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
}
public async publish(content: string, reply_to?: string, quote?: boolean) {
public async publish(
content: string,
reply_to?: string,
quote?: boolean,
nsfw?: boolean,
) {
try {
const g = await generateContentTags(content);
@ -238,26 +250,34 @@ export class Ark {
if (reply_to) {
const replyEvent = await this.get_event(reply_to);
const relayHint =
replyEvent.tags.find((ev) => ev[0] === "e")?.[0][2] ?? "";
if (quote) {
eventTags.push([
"e",
replyEvent.id,
replyEvent.relay || "",
"mention",
]);
eventTags.push(["e", replyEvent.id, relayHint, "mention"]);
} else {
const rootEvent = replyEvent.tags.find((ev) => ev[3] === "root");
if (rootEvent) {
eventTags.push(["e", rootEvent[1], rootEvent[2] || "", "root"]);
eventTags.push([
"e",
rootEvent[1],
rootEvent[2] || relayHint,
"root",
]);
}
eventTags.push(["e", replyEvent.id, replyEvent.relay || "", "reply"]);
eventTags.push(["e", replyEvent.id, relayHint, "reply"]);
eventTags.push(["p", replyEvent.pubkey]);
}
}
if (nsfw) {
eventTags.push(["L", "content-warning"]);
eventTags.push(["l", "reason", "content-warning"]);
eventTags.push(["content-warning", "nsfw"]);
}
const cmd: string = await invoke("publish", {
content: eventContent,
tags: eventTags,
@ -605,6 +625,7 @@ export class Ark {
key: NSTORE_KEYS.settings,
});
const settings: Settings = cmd ? JSON.parse(cmd) : null;
this.settings = settings;
return settings;
} catch {
const defaultSettings: Settings = {
@ -612,7 +633,9 @@ export class Ark {
enhancedPrivacy: false,
notification: false,
zap: false,
nsfw: false,
};
this.settings = defaultSettings;
return defaultSettings;
}
}

View File

@ -123,3 +123,4 @@ export * from "./src/laurel";
export * from "./src/quote";
export * from "./src/key";
export * from "./src/remote";
export * from "./src/nsfw";

View File

@ -1,20 +1,13 @@
export function AddMediaIcon(props: JSX.IntrinsicElements["svg"]) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 22v-3m0 0v-3m0 3h-3m3 0h3m0-5.648V11l-.001-1m-9.464 11H10c-.756 0-1.41 0-1.983-.01M22 10H21c-1.393 0-2.09 0-2.676.06A11.5 11.5 0 008.06 20.324c-.02.2-.034.415-.043.665M22 10c-.008-2.15-.068-3.336-.544-4.27a5 5 0 00-2.185-2.185C18.2 3 16.8 3 14 3h-4c-2.8 0-4.2 0-5.27.545A5 5 0 002.545 5.73C2 6.8 2 8.2 2 11v2c0 2.8 0 4.2.545 5.27a5 5 0 002.185 2.185c.78.398 1.738.505 3.287.534M7.5 9.5a1 1 0 110-2 1 1 0 010 2z"
/>
</svg>
);
return (
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M15.25 8.75v-4a2 2 0 0 0-2-2h-8.5a2 2 0 0 0-2 2v8.5a2 2 0 0 0 2 2h4M3.1 11.9l1.794-1.176a2 2 0 0 1 2.206.01l1.279.852M6 6.25h.5m8 8.75h.5M6.75 6.25a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0Zm7 6.95v3.6l2.8-1.8-2.8-1.8Zm5.5 8.05h-8.5a2 2 0 0 1-2-2v-8.5a2 2 0 0 1 2-2h8.5a2 2 0 0 1 2 2v8.5a2 2 0 0 1-2 2Z"
/>
</svg>
);
}

View File

@ -0,0 +1,13 @@
export function NsfwIcon(props: JSX.IntrinsicElements["svg"]) {
return (
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M4.75 18.75v1.5a1 1 0 0 0 1 1h12.5a1 1 0 0 0 1-1v-1.5a2 2 0 0 0-2-2H6.75a2 2 0 0 0-2 2Zm2-2V12a5.25 5.25 0 0 1 10.5 0v4.75M12 1.75v1.025M21.225 12h1.025M2.775 12H1.75m16.773-6.523.725-.725m-13.771.725-.725-.725M12 16.75v-3"
/>
</svg>
);
}

View File

@ -3,6 +3,8 @@ export interface Settings {
enhancedPrivacy: boolean;
autoUpdate: boolean;
zap: boolean;
nsfw: boolean;
[key: string]: string | number | boolean;
}
export interface Keys {

View File

@ -20,7 +20,6 @@ export const Note = {
Pin: NotePin,
Content: NoteContent,
Zap: NoteZap,
Pin: NotePin,
Child: NoteChild,
Thread: NoteThread,
};

View File

@ -87,6 +87,9 @@ importers:
'@radix-ui/react-switch':
specifier: ^1.0.3
version: 1.0.3(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-tooltip':
specifier: ^1.0.7
version: 1.0.7(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
'@tanstack/query-sync-storage-persister':
specifier: ^5.29.0
version: 5.29.0
@ -259,7 +262,7 @@ importers:
version: 1.0.7(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-tooltip':
specifier: ^1.0.7
version: 1.0.7(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
version: 1.0.7(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
'@tanstack/react-query':
specifier: ^5.29.0
version: 5.29.0(react@18.2.0)
@ -410,7 +413,7 @@ importers:
version: 1.0.7(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-tooltip':
specifier: ^1.0.7
version: 1.0.7(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
version: 1.0.7(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
'@tanstack/react-query':
specifier: ^5.29.0
version: 5.29.0(react@18.2.0)
@ -2283,7 +2286,7 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-tooltip@1.0.7(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0):
/@radix-ui/react-tooltip@1.0.7(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==}
peerDependencies:
'@types/react': '*'
@ -2308,8 +2311,9 @@ packages:
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.75)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.75)(react@18.2.0)
'@radix-ui/react-visually-hidden': 1.0.3(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.75
'@types/react-dom': 18.2.24
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
@ -2416,7 +2420,7 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-visually-hidden@1.0.3(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0):
/@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==}
peerDependencies:
'@types/react': '*'
@ -2432,6 +2436,7 @@ packages:
'@babel/runtime': 7.24.4
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.75
'@types/react-dom': 18.2.24
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false

View File

@ -126,8 +126,6 @@ fn main() {
nostr::event::get_event_thread,
nostr::event::publish,
nostr::event::repost,
nostr::event::upvote,
nostr::event::downvote,
commands::folder::show_in_folder,
commands::folder::get_accounts,
commands::opg::fetch_opg,

View File

@ -222,16 +222,15 @@ pub async fn get_event_thread(id: &str, state: State<'_, Nostr>) -> Result<Vec<E
#[tauri::command]
pub async fn publish(
content: &str,
tags: Vec<Vec<String>>,
tags: Vec<Vec<&str>>,
state: State<'_, Nostr>,
) -> Result<String, String> {
let client = &state.client;
let final_tags = tags.into_iter().map(|val| Tag::parse(&val).unwrap());
if let Ok(event_id) = client.publish_text_note(content, final_tags).await {
Ok(event_id.to_bech32().unwrap())
} else {
Err("Publish text note failed".into())
match client.publish_text_note(content, final_tags).await {
Ok(event_id) => Ok(event_id.to_bech32().unwrap()),
Err(err) => Err(err.to_string()),
}
}
@ -246,27 +245,3 @@ pub async fn repost(raw: &str, state: State<'_, Nostr>) -> Result<EventId, Strin
Err("Repost failed".into())
}
}
#[tauri::command]
pub async fn upvote(raw: &str, state: State<'_, Nostr>) -> Result<EventId, String> {
let client = &state.client;
let event = Event::from_json(raw).unwrap();
if let Ok(event_id) = client.like(&event).await {
Ok(event_id)
} else {
Err("Upvote failed".into())
}
}
#[tauri::command]
pub async fn downvote(raw: &str, state: State<'_, Nostr>) -> Result<EventId, String> {
let client = &state.client;
let event = Event::from_json(raw).unwrap();
if let Ok(event_id) = client.dislike(&event).await {
Ok(event_id)
} else {
Err("Downvote failed".into())
}
}