mirror of
https://github.com/luminous-devs/lume.git
synced 2024-10-01 09:21:07 +00:00
feat: add search
This commit is contained in:
parent
207e063d56
commit
8aac349a2e
@ -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({
|
||||
|
@ -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]);
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
122
apps/desktop2/src/routes/search.notes.tsx
Normal file
122
apps/desktop2/src/routes/search.notes.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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 });
|
||||
},
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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,
|
||||
|
@ -1,5 +1,4 @@
|
||||
export * from "./src/constants";
|
||||
export * from "./src/delay";
|
||||
export * from "./src/formater";
|
||||
export * from "./src/editor";
|
||||
export * from "./src/cn";
|
||||
|
@ -1 +0,0 @@
|
||||
export const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
@ -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
|
||||
|
@ -11,7 +11,7 @@
|
||||
"main",
|
||||
"panel",
|
||||
"settings",
|
||||
"search",
|
||||
"search-*",
|
||||
"zap-*",
|
||||
"event-*",
|
||||
"user-*",
|
||||
|
@ -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"]}}
|
@ -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,
|
||||
|
@ -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()),
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user