feat: add search

This commit is contained in:
reya 2024-06-27 14:20:51 +07:00
parent 207e063d56
commit 8aac349a2e
17 changed files with 341 additions and 230 deletions

View File

@ -1,12 +1,11 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { StrictMode } from "react";
import { type } from "@tauri-apps/plugin-os";
import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import { routeTree } from "./router.gen"; // auto generated file
import "./app.css";
// Set up a Router instance
const queryClient = new QueryClient();
const platform = type();
const router = createRouter({

View File

@ -1,6 +1,6 @@
import { Column } from "@/components/column";
import { Toolbar } from "@/components/toolbar";
import { ArrowLeftIcon, ArrowRightIcon, PlusSquareIcon } from "@lume/icons";
import { ArrowLeftIcon, ArrowRightIcon } from "@lume/icons";
import { NostrQuery } from "@lume/system";
import type { ColumnEvent, LumeColumn } from "@lume/types";
import { createFileRoute } from "@tanstack/react-router";
@ -45,17 +45,6 @@ function Screen() {
getCurrent().emit("child-webview", { resize: true, direction: "x" });
}, []);
const openLumeStore = useDebouncedCallback(async () => {
await getCurrent().emit("columns", {
type: "add",
column: {
label: "store",
name: "Store",
content: "/store/official",
},
});
}, 150);
const add = useDebouncedCallback((column: LumeColumn) => {
column.label = `${column.label}-${nanoid()}`; // update col label
setColumns((prev) => [column, ...prev]);

View File

@ -1,12 +1,17 @@
import { User } from "@/components/user";
import { ChevronDownIcon, ComposeFilledIcon, PlusIcon } from "@lume/icons";
import {
ChevronDownIcon,
ComposeFilledIcon,
PlusIcon,
SearchIcon,
} from "@lume/icons";
import { LumeWindow, NostrAccount } from "@lume/system";
import { cn } from "@lume/utils";
import { Outlet, createFileRoute } from "@tanstack/react-router";
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
import { getCurrent } from "@tauri-apps/api/window";
import { message } from "@tauri-apps/plugin-dialog";
import { memo, useCallback } from "react";
import { memo, useCallback, useState } from "react";
export const Route = createFileRoute("/$account")({
beforeLoad: async ({ params }) => {
@ -23,6 +28,17 @@ export const Route = createFileRoute("/$account")({
function Screen() {
const { platform } = Route.useRouteContext();
const openLumeStore = async () => {
await getCurrent().emit("columns", {
type: "add",
column: {
label: "store",
name: "Store",
content: "/store/official",
},
});
};
return (
<div className="flex flex-col w-screen h-screen">
<div
@ -38,22 +54,25 @@ function Screen() {
>
<button
type="button"
className="inline-flex items-center justify-center gap-1 rounded-full text-sm font-medium h-8 w-max pl-1.5 pr-3 bg-black/5 dark:bg-white/5 hover:bg-black/10 dark:hover:bg-white/10"
onClick={() => openLumeStore()}
className="inline-flex items-center justify-center gap-0.5 rounded-full text-sm font-medium h-8 w-max pl-1.5 pr-3 bg-black/5 dark:bg-white/5 hover:bg-black/10 dark:hover:bg-white/10"
>
<PlusIcon className="size-5" />
Add column
Column
</button>
<div id="toolbar" />
</div>
<div data-tauri-drag-region className="hidden md:flex md:flex-1" />
<div data-tauri-drag-region className="hidden md:flex md:flex-1">
<Search />
</div>
<div
data-tauri-drag-region
className="flex-1 flex items-center justify-end gap-2"
className="flex-1 flex items-center justify-end gap-3"
>
<button
type="button"
onClick={() => LumeWindow.openEditor()}
className="inline-flex items-center justify-center h-8 gap-1 px-3 text-sm font-medium text-white bg-blue-500 rounded-full w-max hover:bg-blue-600"
className="inline-flex items-center justify-center h-8 gap-1 px-3 text-sm font-medium bg-black/5 dark:bg-white/5 rounded-full w-max hover:bg-blue-500 hover:text-white"
>
<ComposeFilledIcon className="size-4" />
New Post
@ -127,7 +146,7 @@ const Accounts = memo(function Accounts() {
);
return (
<div data-tauri-drag-region className="hidden md:flex items-center gap-2">
<div data-tauri-drag-region className="hidden md:flex items-center gap-3">
{otherAccounts.map((npub) => (
<button key={npub} type="button" onClick={(e) => changeAccount(npub)}>
<User.Provider pubkey={npub}>
@ -152,3 +171,55 @@ const Accounts = memo(function Accounts() {
</div>
);
});
const Search = memo(function Search() {
const [searchType, setSearchType] = useState<"notes" | "users">("notes");
const [query, setQuery] = useState("");
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
e.preventDefault();
const menuItems = await Promise.all([
MenuItem.new({
text: "Notes",
action: () => setSearchType("notes"),
}),
MenuItem.new({
text: "Users",
action: () => setSearchType("users"),
}),
]);
const menu = await Menu.new({
items: menuItems,
});
await menu.popup().catch((e) => console.error(e));
}, []);
return (
<div className="h-8 w-full px-3 text-sm rounded-full inline-flex items-center bg-black/5 dark:bg-white/5">
<button
type="button"
onClick={(e) => showContextMenu(e)}
className="inline-flex items-center gap-1 capitalize text-sm font-medium pr-2 border-r border-black/10 dark:border-white/10"
>
{searchType}
<ChevronDownIcon className="size-3 text-black/50 dark:text-white/50" />
</button>
<input
type="text"
name="search"
placeholder="Search..."
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
LumeWindow.openSearch(searchType, query);
}
}}
className="h-full w-full px-3 text-sm rounded-full border-none ring-0 focus:ring-0 focus:outline-none bg-transparent placeholder:text-black/50 dark:placeholder:text-black/50"
/>
<SearchIcon className="size-5" />
</div>
);
});

View File

@ -1,52 +0,0 @@
import { ZapIcon } from "@lume/icons";
import { NostrAccount } from "@lume/system";
import { Container } from "@lume/ui";
import { createLazyFileRoute } from "@tanstack/react-router";
import { useState } from "react";
export const Route = createLazyFileRoute("/nwc")({
component: Screen,
});
function Screen() {
const [uri, setUri] = useState("");
const [isDone, setIsDone] = useState(false);
const save = async () => {
const nwc = await NostrAccount.setWallet(uri);
setIsDone(nwc);
};
return (
<Container withDrag>
<div className="flex-1 w-full h-full px-5">
<div className="flex flex-col gap-2">
<div>
<h3 className="text-2xl font-light">
Connect <span className="font-semibold">bitcoin wallet</span> to
start zapping to your favorite content and creator.
</h3>
</div>
</div>
<div className="flex flex-col gap-2 mt-10">
<div className="flex flex-col gap-1.5">
<label>Paste a Nostr Wallet Connect connection string</label>
<textarea
value={uri}
onChange={(e) => setUri(e.target.value)}
placeholder="nostrconnect://"
className="w-full h-24 px-3 bg-transparent rounded-lg border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/>
</div>
<button
type="button"
onClick={save}
className="inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-lg bg-blue-500 px-5 font-medium text-white hover:bg-blue-600"
>
Save & Connect
</button>
</div>
</div>
</Container>
);
}

View File

@ -0,0 +1,122 @@
import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon } from "@lume/icons";
import { type LumeEvent, NostrQuery } from "@lume/system";
import { Kind } from "@lume/types";
import { Spinner } from "@lume/ui";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useInfiniteQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { useCallback, useRef } from "react";
import { Virtualizer } from "virtua";
type Search = {
query: string;
};
export const Route = createFileRoute("/search/notes")({
validateSearch: (search: Record<string, string>): Search => {
return {
query: search.query,
};
},
beforeLoad: async () => {
const settings = await NostrQuery.getUserSettings();
return { settings };
},
component: Screen,
});
function Screen() {
const { query } = Route.useSearch();
const {
data,
isLoading,
isFetching,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
} = useInfiniteQuery({
queryKey: ["search", query],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await NostrQuery.searchEvent(query, pageParam);
return events;
},
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
select: (data) => data?.pages.flat(),
refetchOnWindowFocus: false,
});
const ref = useRef<HTMLDivElement>(null);
const renderItem = useCallback(
(event: LumeEvent) => {
if (!event) return;
switch (event.kind) {
case Kind.Repost:
return <RepostNote key={event.id} event={event} className="mb-3" />;
default: {
if (event.isConversation) {
return (
<Conversation key={event.id} className="mb-3" event={event} />
);
}
if (event.isQuote) {
return <Quote key={event.id} event={event} className="mb-3" />;
}
return <TextNote key={event.id} event={event} className="mb-3" />;
}
}
},
[data],
);
return (
<ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
<Virtualizer scrollRef={ref}>
{isFetching && !isLoading && !isFetchingNextPage ? (
<div className="flex items-center justify-center w-full mb-3 h-11 bg-black/10 dark:bg-white/10 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<div className="flex items-center justify-center gap-2">
<Spinner className="size-5" />
<span className="text-sm font-medium">Fetching new notes...</span>
</div>
</div>
) : null}
{isLoading ? (
<div className="flex items-center justify-center w-full h-16 gap-2">
<Spinner className="size-5" />
<span className="text-sm font-medium">Loading...</span>
</div>
) : !data.length ? (
<div className="flex items-center justify-center">
Yo. You're catching up on all notes
</div>
) : (
data.map((item) => renderItem(item))
)}
{data?.length && hasNextPage ? (
<div>
<button
type="button"
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading}
className="inline-flex items-center justify-center w-full gap-2 px-3 font-medium h-9 rounded-xl bg-black/5 hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
>
{isFetchingNextPage ? (
<Spinner className="size-5" />
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
</>
)}
</button>
</div>
) : null}
</Virtualizer>
</ScrollArea.Viewport>
);
}

View File

@ -1,148 +1,25 @@
import { Note } from "@/components/note";
import { User } from "@/components/user";
import { SearchIcon } from "@lume/icons";
import { LumeEvent, LumeWindow } from "@lume/system";
import { Kind, type NostrEvent } from "@lume/types";
import { Spinner } from "@lume/ui";
import { createFileRoute } from "@tanstack/react-router";
import { message } from "@tauri-apps/plugin-dialog";
import { useEffect, useState } from "react";
import { useDebounce } from "use-debounce";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { Outlet, createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/search")({
component: Screen,
});
function Screen() {
const [loading, setLoading] = useState(false);
const [events, setEvents] = useState<LumeEvent[]>([]);
const [search, setSearch] = useState("");
const [searchValue] = useDebounce(search, 500);
const searchEvents = async () => {
try {
setLoading(true);
const query = `https://api.nostr.wine/search?query=${searchValue}&kind=0,1`;
const res = await fetch(query);
const content = await res.json();
const events = content.data as NostrEvent[];
const lumeEvents = events.map((ev) => new LumeEvent(ev));
const sorted = lumeEvents.sort((a, b) => b.created_at - a.created_at);
setLoading(false);
setEvents(sorted);
} catch (e) {
setLoading(false);
await message(String(e), {
title: "Search",
kind: "error",
});
}
};
useEffect(() => {
if (searchValue.length >= 3 && searchValue.length < 500) {
searchEvents();
}
}, [searchValue]);
return (
<div data-tauri-drag-region className="flex flex-col w-full h-full">
<div className="relative flex flex-col h-24 border-b shrink-0 border-black/5 dark:border-white/5">
<div data-tauri-drag-region className="w-full h-4 shrink-0" />
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") searchEvents();
}}
placeholder="Search anything..."
className="w-full h-20 px-3 pt-10 text-lg bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-neutral-500 dark:placeholder:text-neutral-600"
/>
</div>
<div className="flex-1 p-3 overflow-y-auto scrollbar-none">
{loading ? (
<div className="flex items-center justify-center w-full h-full">
<Spinner />
</div>
) : events.length ? (
<div className="flex flex-col gap-5">
<div className="flex flex-col gap-1.5">
<div className="text-sm font-medium text-neutral-700 dark:text-neutral-300 shrink-0">
Users
</div>
<div className="flex flex-col flex-1 gap-1">
{events
.filter((ev) => ev.kind === Kind.Metadata)
.map((event) => (
<SearchUser key={event.pubkey} event={event} />
))}
</div>
</div>
<div className="flex flex-col gap-1.5">
<div className="text-sm font-medium text-neutral-700 dark:text-neutral-300 shrink-0">
Notes
</div>
<div className="flex flex-col flex-1 gap-3">
{events
.filter((ev) => ev.kind === Kind.Text)
.map((event) => (
<SearchNote key={event.id} event={event} />
))}
</div>
</div>
</div>
) : null}
{!loading && !events.length ? (
<div className="flex flex-col items-center justify-center h-full gap-3">
<div className="inline-flex items-center justify-center rounded-full size-16 bg-black/10 dark:bg-white/10">
<SearchIcon className="size-6" />
</div>
Try searching for people, notes, or keywords
</div>
) : null}
</div>
</div>
);
}
function SearchUser({ event }: { event: LumeEvent }) {
return (
<button
key={event.id}
type="button"
onClick={() => LumeWindow.openProfile(event.pubkey)}
className="col-span-1 p-2 rounded-lg hover:bg-black/10 dark:hover:bg-white/10"
<ScrollArea.Root
type={"scroll"}
scrollHideDelay={300}
className="overflow-hidden size-full"
>
<User.Provider pubkey={event.pubkey} embedProfile={event.content}>
<User.Root className="flex items-center gap-2">
<User.Avatar className="rounded-full size-9 shrink-0" />
<div className="inline-flex items-center gap-1.5">
<User.Name className="font-semibold" />
<User.NIP05 />
</div>
</User.Root>
</User.Provider>
</button>
);
}
function SearchNote({ event }: { event: LumeEvent }) {
return (
<div className="bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<Note.Provider event={event}>
<Note.Root>
<div className="flex items-center justify-between px-3 h-14">
<Note.User />
<Note.Menu />
</div>
<Note.Content className="px-3" quote={false} mention={false} />
<div className="flex items-center gap-4 px-3 mt-3 h-14">
<Note.Open />
</div>
</Note.Root>
</Note.Provider>
</div>
<Outlet />
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
);
}

View File

@ -25,7 +25,6 @@
"@tauri-apps/plugin-dialog": "2.0.0-beta.5",
"@tauri-apps/plugin-fs": "2.0.0-beta.5",
"@tauri-apps/plugin-http": "2.0.0-beta.5",
"@tauri-apps/plugin-notification": "2.0.0-beta.5",
"@tauri-apps/plugin-os": "github:tauri-apps/tauri-plugin-os#v2",
"@tauri-apps/plugin-process": "2.0.0-beta.5",
"@tauri-apps/plugin-shell": "2.0.0-beta.6",

View File

@ -388,6 +388,22 @@ try {
else return { status: "error", error: e as any };
}
},
async searchEvent(until: string | null, query: string) : Promise<Result<RichEvent[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("search_event", { until, query }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async searchUser(until: string | null, query: string) : Promise<Result<string[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("search_user", { until, query }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async showInFolder(path: string) : Promise<void> {
await TAURI_INVOKE("show_in_folder", { path });
},

View File

@ -243,6 +243,31 @@ export class NostrQuery {
}
}
static async searchEvent(searchQuery: string, asOf?: number) {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const query = await commands.searchEvent(until, searchQuery);
if (query.status === "ok") {
const data = NostrQuery.#toLumeEvents(query.data);
return data;
} else {
return [];
}
}
static async searchUser(searchQuery: string, asOf?: number) {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const query = await commands.searchUser(until, searchQuery);
if (query.status === "ok") {
const events: NostrEvent[] = query.data.map((item) => JSON.parse(item));
const meta: Metadata[] = events.map((event) => JSON.parse(event.content));
return meta;
} else {
return [];
}
}
static async verifyNip05(pubkey: string, nip05?: string) {
const query = await commands.verifyNip05(pubkey, nip05);

View File

@ -129,11 +129,17 @@ export class LumeWindow {
}
}
static async openSearch() {
const label = "search";
static async openSearch(searchType: "notes" | "users", searchQuery: string) {
const url = `/search/${searchType}/?query=${searchQuery}`;
const label = `search-${searchQuery
.toLowerCase()
.replace(/[^\w ]+/g, "")
.replace(/ +/g, "_")
.replace(/_+/g, "_")}`;
const query = await commands.openWindow({
label,
url: "/search",
url,
title: "Search",
width: 400,
height: 600,

View File

@ -1,5 +1,4 @@
export * from "./src/constants";
export * from "./src/delay";
export * from "./src/formater";
export * from "./src/editor";
export * from "./src/cn";

View File

@ -1 +0,0 @@
export const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));

View File

@ -23,9 +23,6 @@ importers:
'@tauri-apps/plugin-http':
specifier: 2.0.0-beta.5
version: 2.0.0-beta.5
'@tauri-apps/plugin-notification':
specifier: 2.0.0-beta.5
version: 2.0.0-beta.5
'@tauri-apps/plugin-os':
specifier: github:tauri-apps/tauri-plugin-os#v2
version: https://codeload.github.com/tauri-apps/tauri-plugin-os/tar.gz/6ca6f068998b4e8c6f84cdefbc152357a8daafa7
@ -1702,9 +1699,6 @@ packages:
'@tauri-apps/plugin-http@2.0.0-beta.5':
resolution: {integrity: sha512-zRWWWCNw/TdewCSrBY8Racm2dwMpkzMOnFmHGExEIoklqUUIq3FU/hkqcX/67Nplf+t1y+WljOxKpCHbm1fOQw==}
'@tauri-apps/plugin-notification@2.0.0-beta.5':
resolution: {integrity: sha512-5zYK2aT1ZvR+LnuwsnTvg28iEhI7FiZSPvBqQ8y6fj28T4oIHVox19Pk4YRyTyfyJN1nZclXxVAqWLSXLI9SKQ==}
'@tauri-apps/plugin-os@https://codeload.github.com/tauri-apps/tauri-plugin-os/tar.gz/6ca6f068998b4e8c6f84cdefbc152357a8daafa7':
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-os/tar.gz/6ca6f068998b4e8c6f84cdefbc152357a8daafa7}
version: 2.0.0-beta.5
@ -4897,10 +4891,6 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.0.0-beta.13
'@tauri-apps/plugin-notification@2.0.0-beta.5':
dependencies:
'@tauri-apps/api': 2.0.0-beta.13
'@tauri-apps/plugin-os@https://codeload.github.com/tauri-apps/tauri-plugin-os/tar.gz/6ca6f068998b4e8c6f84cdefbc152357a8daafa7':
dependencies:
'@tauri-apps/api': 2.0.0-beta.13

View File

@ -11,7 +11,7 @@
"main",
"panel",
"settings",
"search",
"search-*",
"zap-*",
"event-*",
"user-*",

View File

@ -1 +1 @@
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","panel","settings","search","zap-*","event-*","user-*","editor-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","os:allow-os-type","updater:default","updater:allow-check","updater:allow-download-and-install","window:allow-start-dragging","window:allow-create","window:allow-close","window:allow-destroy","window:allow-set-focus","window:allow-center","window:allow-minimize","window:allow-maximize","window:allow-set-size","window:allow-set-focus","window:allow-start-dragging","decorum:allow-show-snap-overlay","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","webview:allow-create-webview-window","webview:allow-create-webview","webview:allow-set-webview-size","webview:allow-set-webview-position","webview:allow-webview-close","dialog:allow-open","dialog:allow-ask","dialog:allow-message","process:allow-restart","process:allow-exit","fs:allow-read-file","theme:allow-set-theme","theme:allow-get-theme","menu:allow-new","menu:allow-popup","http:default","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["linux","macOS","windows"]}}
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","panel","settings","search-*","zap-*","event-*","user-*","editor-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","os:allow-os-type","updater:default","updater:allow-check","updater:allow-download-and-install","window:allow-start-dragging","window:allow-create","window:allow-close","window:allow-destroy","window:allow-set-focus","window:allow-center","window:allow-minimize","window:allow-maximize","window:allow-set-size","window:allow-set-focus","window:allow-start-dragging","decorum:allow-show-snap-overlay","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","webview:allow-create-webview-window","webview:allow-create-webview","webview:allow-set-webview-size","webview:allow-set-webview-position","webview:allow-webview-close","dialog:allow-open","dialog:allow-ask","dialog:allow-message","process:allow-restart","process:allow-exit","fs:allow-read-file","theme:allow-set-theme","theme:allow-get-theme","menu:allow-new","menu:allow-popup","http:default","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["linux","macOS","windows"]}}

View File

@ -124,6 +124,8 @@ fn main() {
nostr::event::repost,
nostr::event::event_to_bech32,
nostr::event::user_to_bech32,
nostr::event::search_event,
nostr::event::search_user,
commands::folder::show_in_folder,
commands::window::create_column,
commands::window::close_column,

View File

@ -663,3 +663,72 @@ pub async fn user_to_bech32(user: &str, state: State<'_, Nostr>) -> Result<Strin
},
}
}
#[tauri::command]
#[specta::specta]
pub async fn search_event(
until: Option<&str>,
query: &str,
state: State<'_, Nostr>,
) -> Result<Vec<RichEvent>, String> {
let client = &state.client;
let as_of = match until {
Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?,
None => Timestamp::now(),
};
let filter = Filter::new()
.search(query)
.kinds(vec![Kind::TextNote])
.limit(20)
.until(as_of);
match client.get_events_of(vec![filter], None).await {
Ok(events) => {
let futures = events.into_iter().map(|ev| async move {
let raw = ev.as_json();
let parsed = if ev.kind == Kind::TextNote {
Some(parse_event(&ev.content).await)
} else {
None
};
RichEvent { raw, parsed }
});
let rich_events = join_all(futures).await;
Ok(rich_events)
}
Err(err) => Err(err.to_string()),
}
}
#[tauri::command]
#[specta::specta]
pub async fn search_user(
until: Option<&str>,
query: &str,
state: State<'_, Nostr>,
) -> Result<Vec<String>, String> {
let client = &state.client;
let as_of = match until {
Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?,
None => Timestamp::now(),
};
let filter = Filter::new()
.search(query)
.kinds(vec![Kind::Metadata])
.limit(20)
.until(as_of);
match client.get_events_of(vec![filter], None).await {
Ok(events) => {
let json = events
.into_iter()
.map(|ev| ev.as_json())
.collect::<Vec<_>>();
Ok(json)
}
Err(err) => Err(err.to_string()),
}
}