Some improments and Negentropy (#219)

* feat: adjust default window size

* feat: save window state

* feat: add window state plugin

* feat: add search

* feat: use negentropy for newsfeed

* feat: live feeds

* feat: add search user
This commit is contained in:
雨宮蓮
2024-06-30 14:26:02 +07:00
committed by GitHub
parent 968b1ada94
commit 0fec21b9ce
46 changed files with 5633 additions and 3938 deletions

View File

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

View File

@@ -1,8 +1,8 @@
import { LumeWindow, useEvent } from "@lume/system";
import { LinkIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import { User } from "@/components/user"; import { User } from "@/components/user";
import { LinkIcon } from "@lume/icons";
import { LumeWindow, useEvent } from "@lume/system";
import { Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import { cn } from "@lume/utils";
export function MentionNote({ export function MentionNote({
eventId, eventId,
@@ -40,7 +40,7 @@ export function MentionNote({
<div className="pl-4 py-3 flex flex-col w-full border-l-2 border-black/5 dark:border-white/5"> <div className="pl-4 py-3 flex flex-col w-full border-l-2 border-black/5 dark:border-white/5">
<User.Provider pubkey={data.pubkey}> <User.Provider pubkey={data.pubkey}>
<User.Root className="flex items-center gap-2 h-8"> <User.Root className="flex items-center gap-2 h-8">
<User.Avatar className="object-cover rounded-full size-6 shrink-0" /> <User.Avatar className="rounded-full size-6" />
<div className="inline-flex items-center flex-1 gap-2"> <div className="inline-flex items-center flex-1 gap-2">
<User.Name className="font-semibold text-neutral-900 dark:text-neutral-100" /> <User.Name className="font-semibold text-neutral-900 dark:text-neutral-100" />
<span className="text-neutral-600 dark:text-neutral-400">·</span> <span className="text-neutral-600 dark:text-neutral-400">·</span>

View File

@@ -96,7 +96,7 @@ export function Images({ urls }: { urls: string[] }) {
} }
return ( return (
<div className="relative pl-2 overflow-hidden group"> <div className="relative px-3 overflow-hidden group">
<div ref={emblaRef} className="w-full h-[320px]"> <div ref={emblaRef} className="w-full h-[320px]">
<div className="flex w-full gap-2 scrollbar-none"> <div className="flex w-full gap-2 scrollbar-none">
{imageUrls.map((url, index) => ( {imageUrls.map((url, index) => (

View File

@@ -20,10 +20,11 @@ export function VideoPreview({ url }: { url: string }) {
<div className="my-1"> <div className="my-1">
<video <video
className="max-h-[600px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15" className="max-h-[600px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
preload="metadata"
controls controls
muted muted
> >
<source src={url} type="video/mp4" /> <source src={`${url}#t=0.1`} type="video/mp4" />
Your browser does not support the video tag. Your browser does not support the video tag.
</video> </video>
</div> </div>

View File

@@ -5,12 +5,12 @@ export function Videos({ urls }: { urls: string[] }) {
return ( return (
<div className="group px-3"> <div className="group px-3">
<video <video
className="w-full h-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15" className="max-h-[400px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
preload="metadata" preload="metadata"
controls controls
muted muted
> >
<source src={urls[0]} type="video/mp4" /> <source src={`${urls[0]}#t=0.1`} type="video/mp4" />
Your browser does not support the video tag. Your browser does not support the video tag.
</video> </video>
</div> </div>
@@ -28,7 +28,7 @@ export function Videos({ urls }: { urls: string[] }) {
controls={false} controls={false}
muted muted
> >
<source src={item} type="video/mp4" /> <source src={`${item}#t=0.1`} type="video/mp4" />
Your browser does not support the video tag. Your browser does not support the video tag.
</video> </video>
</CarouselItem> </CarouselItem>

View File

@@ -42,7 +42,7 @@ export function NoteUser({ className }: { className?: string }) {
onClick={(e) => showContextMenu(e)} onClick={(e) => showContextMenu(e)}
className="shrink-0" className="shrink-0"
> >
<User.Avatar className="object-cover rounded-full size-8 outline outline-1 -outline-offset-1 outline-black/15" /> <User.Avatar className="rounded-full size-8" />
</button> </button>
<div className="flex items-center w-full gap-3"> <div className="flex items-center w-full gap-3">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">

View File

@@ -69,7 +69,7 @@ export const RepostNote = memo(function RepostNote({
<div className="text-sm font-medium text-neutral-800 dark:text-neutral-200"> <div className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
Reposted by Reposted by
</div> </div>
<User.Avatar className="object-cover rounded-full size-6 shrink-0 ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" /> <User.Avatar className="rounded-full size-6" />
</User.Root> </User.Root>
</User.Provider> </User.Provider>
</div> </div>

View File

@@ -10,8 +10,6 @@ export const TextNote = memo(function TextNote({
event: LumeEvent; event: LumeEvent;
className?: string; className?: string;
}) { }) {
console.log("Rendered at: ", event.id, new Date().toLocaleTimeString());
return ( return (
<Note.Provider event={event}> <Note.Provider event={event}>
<Note.Root <Note.Root

View File

@@ -2,7 +2,6 @@ import { cn } from "@lume/utils";
import * as Avatar from "@radix-ui/react-avatar"; import * as Avatar from "@radix-ui/react-avatar";
import { useRouteContext } from "@tanstack/react-router"; import { useRouteContext } from "@tanstack/react-router";
import { minidenticon } from "minidenticons"; import { minidenticon } from "minidenticons";
import { nanoid } from "nanoid";
import { useMemo } from "react"; import { useMemo } from "react";
import { useUserContext } from "./provider"; import { useUserContext } from "./provider";
@@ -22,22 +21,29 @@ export function UserAvatar({ className }: { className?: string }) {
} }
}, [user.profile?.picture]); }, [user.profile?.picture]);
const fallbackAvatar = useMemo( const fallback = useMemo(
() => () =>
`data:image/svg+xml;utf8,${encodeURIComponent( `data:image/svg+xml;utf8,${encodeURIComponent(
minidenticon(user.pubkey || nanoid(), 90, 50), minidenticon(user.pubkey, 60, 50),
)}`, )}`,
[user.pubkey], [user.pubkey],
); );
if (settings && !settings.display_avatar) { if (settings && !settings.display_avatar) {
return ( return (
<Avatar.Root className="shrink-0"> <Avatar.Root
className={cn(
"shrink-0 block overflow-hidden bg-neutral-200 dark:bg-neutral-800",
className,
)}
>
<Avatar.Fallback delayMs={120}> <Avatar.Fallback delayMs={120}>
<img <img
src={fallbackAvatar} src={fallback}
alt={user.pubkey} alt={user.pubkey}
className={cn("bg-black dark:bg-white", className)} loading="lazy"
decoding="async"
className="size-full bg-black dark:bg-white outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
/> />
</Avatar.Fallback> </Avatar.Fallback>
</Avatar.Root> </Avatar.Root>
@@ -45,19 +51,24 @@ export function UserAvatar({ className }: { className?: string }) {
} }
return ( return (
<Avatar.Root className="shrink-0"> <Avatar.Root
className={cn(
"shrink-0 block overflow-hidden bg-neutral-200 dark:bg-neutral-800",
className,
)}
>
<Avatar.Image <Avatar.Image
src={picture} src={picture}
alt={user.pubkey} alt={user.pubkey}
loading="eager" loading="lazy"
decoding="async" decoding="async"
className={cn("outline-[.5px] outline-black/5 object-cover", className)} className="w-full aspect-square object-cover outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
/> />
<Avatar.Fallback delayMs={120}> <Avatar.Fallback>
<img <img
src={fallbackAvatar} src={fallback}
alt={user.pubkey} alt={user.pubkey}
className={cn("bg-black dark:bg-white", className)} className="size-full bg-black dark:bg-white outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
/> />
</Avatar.Fallback> </Avatar.Fallback>
</Avatar.Root> </Avatar.Root>

View File

@@ -1,6 +1,6 @@
import { Column } from "@/components/column"; import { Column } from "@/components/column";
import { Toolbar } from "@/components/toolbar"; import { Toolbar } from "@/components/toolbar";
import { ArrowLeftIcon, ArrowRightIcon, PlusSquareIcon } from "@lume/icons"; import { ArrowLeftIcon, ArrowRightIcon } from "@lume/icons";
import { NostrQuery } from "@lume/system"; import { NostrQuery } from "@lume/system";
import type { ColumnEvent, LumeColumn } from "@lume/types"; import type { ColumnEvent, LumeColumn } from "@lume/types";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
@@ -45,17 +45,6 @@ function Screen() {
getCurrent().emit("child-webview", { resize: true, direction: "x" }); 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) => { const add = useDebouncedCallback((column: LumeColumn) => {
column.label = `${column.label}-${nanoid()}`; // update col label column.label = `${column.label}-${nanoid()}`; // update col label
setColumns((prev) => [column, ...prev]); setColumns((prev) => [column, ...prev]);
@@ -158,29 +147,20 @@ function Screen() {
</div> </div>
</div> </div>
<Toolbar> <Toolbar>
<div className="flex items-center h-8 gap-1 p-[2px] rounded-full bg-black/5 dark:bg-white/5"> <button
<button type="button"
type="button" onClick={() => scrollPrev()}
onClick={() => scrollPrev()} className="inline-flex items-center justify-center rounded-full size-8 hover:bg-black/5 dark:hover:bg-white/5"
className="inline-flex items-center justify-center rounded-full size-7 text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10" >
> <ArrowLeftIcon className="size-4" />
<ArrowLeftIcon className="size-4" /> </button>
</button> <button
<button type="button"
type="button" onClick={() => scrollNext()}
onClick={() => openLumeStore()} className="inline-flex items-center justify-center rounded-full size-8 hover:bg-black/5 dark:hover:bg-white/5"
className="inline-flex items-center justify-center rounded-full size-7 text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10" >
> <ArrowRightIcon className="size-4" />
<PlusSquareIcon className="size-4" /> </button>
</button>
<button
type="button"
onClick={() => scrollNext()}
className="inline-flex items-center justify-center rounded-full size-7 text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10"
>
<ArrowRightIcon className="size-4" />
</button>
</div>
</Toolbar> </Toolbar>
</div> </div>
); );

View File

@@ -1,49 +1,83 @@
import { User } from "@/components/user"; import { User } from "@/components/user";
import { ComposeFilledIcon, HorizontalDotsIcon, PlusIcon } from "@lume/icons"; import {
ChevronDownIcon,
ComposeFilledIcon,
PlusIcon,
SearchIcon,
} from "@lume/icons";
import { LumeWindow, NostrAccount } from "@lume/system"; import { LumeWindow, NostrAccount } from "@lume/system";
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
import * as Popover from "@radix-ui/react-popover";
import { Outlet, createFileRoute } from "@tanstack/react-router"; import { Outlet, createFileRoute } from "@tanstack/react-router";
import { Link } from "@tanstack/react-router"; import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
import { Menu, MenuItem } from "@tauri-apps/api/menu";
import { getCurrent } from "@tauri-apps/api/window"; import { getCurrent } from "@tauri-apps/api/window";
import { message } from "@tauri-apps/plugin-dialog"; import { message } from "@tauri-apps/plugin-dialog";
import { useCallback, useEffect, useMemo, useState } from "react"; import { memo, useCallback, useState } from "react";
export const Route = createFileRoute("/$account")({ export const Route = createFileRoute("/$account")({
beforeLoad: async () => { beforeLoad: async ({ params }) => {
const accounts = await NostrAccount.getAccounts(); const accounts = await NostrAccount.getAccounts();
return { accounts }; const otherAccounts = accounts.filter(
(account) => account !== params.account,
);
return { otherAccounts };
}, },
component: Screen, component: Screen,
}); });
function Screen() { function Screen() {
const { platform } = Route.useRouteContext();
const openLumeStore = async () => {
await getCurrent().emit("columns", {
type: "add",
column: {
label: "store",
name: "Store",
content: "/store/official",
},
});
};
return ( return (
<div className="flex flex-col w-screen h-screen"> <div className="flex flex-col w-screen h-screen">
<div <div
data-tauri-drag-region data-tauri-drag-region
className="flex h-11 shrink-0 items-center justify-between pr-2 ml-2 pl-20" className="flex h-11 shrink-0 items-center justify-between px-3"
> >
<div className="flex items-center gap-3"> <div
<Accounts /> data-tauri-drag-region
<Link className={cn(
to="/landing" "flex-1 flex items-center gap-2",
className="inline-flex items-center justify-center rounded-full size-8 shrink-0 bg-black/10 text-neutral-800 hover:bg-black/20 dark:bg-white/10 dark:text-neutral-200 dark:hover:bg-white/20" platform === "macos" ? "pl-[64px]" : "",
)}
>
<button
type="button"
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" /> <PlusIcon className="size-5" />
</Link> Column
</button>
<div id="toolbar" />
</div> </div>
<div className="flex items-center gap-2"> <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-3"
>
<button <button
type="button" type="button"
onClick={() => LumeWindow.openEditor()} 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" /> <ComposeFilledIcon className="size-4" />
New Post New Post
</button> </button>
<div id="toolbar" /> <Accounts />
</div> </div>
</div> </div>
<div className="flex-1"> <div className="flex-1">
@@ -53,34 +87,25 @@ function Screen() {
); );
} }
function Accounts() { const Accounts = memo(function Accounts() {
const navigate = Route.useNavigate(); const { otherAccounts } = Route.useRouteContext();
const { accounts } = Route.useRouteContext();
const { account } = Route.useParams(); const { account } = Route.useParams();
const [windowWidth, setWindowWidth] = useState<number>(null); const navigate = Route.useNavigate();
const sortedList = useMemo(() => {
const list = accounts;
for (const [i, item] of list.entries()) {
if (item === account) {
list.splice(i, 1);
list.unshift(item);
}
}
return list;
}, [accounts]);
const showContextMenu = useCallback( const showContextMenu = useCallback(
async (e: React.MouseEvent, npub: string) => { async (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
const menuItems = await Promise.all([ const menuItems = await Promise.all([
MenuItem.new({
text: "New Post",
action: () => LumeWindow.openEditor(),
}),
PredefinedMenuItem.new({ item: "Separator" }),
MenuItem.new({ MenuItem.new({
text: "View Profile", text: "View Profile",
action: () => LumeWindow.openProfile(npub), action: () => LumeWindow.openProfile(account),
}), }),
MenuItem.new({ MenuItem.new({
text: "Open Settings", text: "Open Settings",
@@ -94,112 +119,107 @@ function Accounts() {
await menu.popup().catch((e) => console.error(e)); await menu.popup().catch((e) => console.error(e));
}, },
[], [account],
); );
const changeAccount = async (e: React.MouseEvent, npub: string) => { const changeAccount = useCallback(
if (npub === account) { async (npub: string) => {
return showContextMenu(e, npub); // Change current account and update signer
} const select = await NostrAccount.loadAccount(npub);
// Change current account and update signer if (select) {
const select = await NostrAccount.loadAccount(npub); // Reset current columns
await getCurrent().emit("columns", { type: "reset" });
if (select) { // Redirect to new account
// Reset current columns return navigate({
await getCurrent().emit("columns", { type: "reset" }); to: "/$account/home",
params: { account: npub },
resetScroll: true,
replace: true,
});
} else {
await message("Something wrong.", { title: "Accounts", kind: "error" });
}
},
[otherAccounts],
);
// Redirect to new account return (
return navigate({ <div data-tauri-drag-region className="hidden md:flex items-center gap-3">
to: "/$account/home", {otherAccounts.map((npub) => (
params: { account: npub }, <button key={npub} type="button" onClick={(e) => changeAccount(npub)}>
resetScroll: true, <User.Provider pubkey={npub}>
replace: true, <User.Root className="shrink-0 rounded-full transition-all ease-in-out duration-150 will-change-auto hover:ring-1 hover:ring-blue-500">
}); <User.Avatar className="rounded-full size-8" />
} else { </User.Root>
await message("Something wrong.", { title: "Accounts", kind: "error" }); </User.Provider>
} </button>
}; ))}
<button
type="button"
onClick={(e) => showContextMenu(e)}
className="inline-flex items-center gap-1.5"
>
<User.Provider pubkey={account}>
<User.Root className="shrink-0 rounded-full">
<User.Avatar className="rounded-full size-8" />
</User.Root>
</User.Provider>
<ChevronDownIcon className="size-3" />
</button>
</div>
);
});
const getWindowDimensions = () => { const Search = memo(function Search() {
const { innerWidth: width, innerHeight: height } = window; const [searchType, setSearchType] = useState<"notes" | "users">("notes");
return { const [query, setQuery] = useState("");
width,
height,
};
};
useEffect(() => { const showContextMenu = useCallback(async (e: React.MouseEvent) => {
function handleResize() { e.preventDefault();
setWindowWidth(getWindowDimensions().width);
}
if (!windowWidth) { const menuItems = await Promise.all([
setWindowWidth(getWindowDimensions().width); MenuItem.new({
} text: "Notes",
action: () => setSearchType("notes"),
}),
MenuItem.new({
text: "Users",
action: () => setSearchType("users"),
}),
]);
window.addEventListener("resize", handleResize); const menu = await Menu.new({
items: menuItems,
});
return () => { await menu.popup().catch((e) => console.error(e));
window.removeEventListener("resize", handleResize);
};
}, []); }, []);
return ( return (
<div data-tauri-drag-region className="flex items-center gap-3"> <div className="h-8 w-full px-3 text-sm rounded-full inline-flex items-center bg-black/5 dark:bg-white/5">
{sortedList <button
.slice(0, windowWidth > 500 ? account.length : 2) type="button"
.map((user) => ( onClick={(e) => showContextMenu(e)}
<button className="inline-flex items-center gap-1 capitalize text-sm font-medium pr-2 border-r border-black/10 dark:border-white/10"
key={user} >
type="button" {searchType}
onClick={(e) => changeAccount(e, user)} <ChevronDownIcon className="size-3 text-black/50 dark:text-white/50" />
> </button>
<User.Provider pubkey={user}> <input
<User.Root type="text"
className={cn( name="search"
"shrink-0 rounded-full transition-all ease-in-out duration-150 will-change-auto", placeholder="Search..."
user === account onChange={(e) => setQuery(e.target.value)}
? "ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950" onKeyDown={(event) => {
: "", if (event.key === "Enter") {
)} LumeWindow.openSearch(searchType, query);
> }
<User.Avatar }}
className={cn( 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"
"aspect-square h-auto rounded-full object-cover transition-all ease-in-out duration-150 will-change-auto", />
user === account ? "w-7" : "w-8", <SearchIcon className="size-5" />
)}
/>
</User.Root>
</User.Provider>
</button>
))}
{accounts.length >= 3 && windowWidth <= 700 ? (
<Popover.Root>
<Popover.Trigger className="inline-flex items-center justify-center rounded-full size-8 shrink-0 bg-black/10 text-neutral-800 hover:bg-black/20 dark:bg-white/10 dark:text-neutral-200 dark:hover:bg-white/20">
<HorizontalDotsIcon className="size-5" />
</Popover.Trigger>
<Popover.Portal>
<Popover.Content className="flex h-11 select-none items-center justify-center rounded-md bg-black/20 p-1 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">
{sortedList.slice(2).map((user) => (
<button
key={user}
type="button"
onClick={(e) => changeAccount(e, user)}
className="inline-flex items-center justify-center rounded-md size-9 hover:bg-white/10"
>
<User.Provider pubkey={user}>
<User.Root className="rounded-full ring-1 ring-white/10">
<User.Avatar className="object-cover h-auto rounded-full size-7 aspect-square" />
</User.Root>
</User.Provider>
</button>
))}
<Popover.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
) : null}
</div> </div>
); );
} });

View File

@@ -126,7 +126,7 @@ function Screen() {
> >
<User.Provider pubkey={item}> <User.Provider pubkey={item}>
<User.Root className="flex items-center gap-2.5"> <User.Root className="flex items-center gap-2.5">
<User.Avatar className="object-cover rounded-full size-8" /> <User.Avatar className="rounded-full size-8" />
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<User.Name className="text-sm font-medium" /> <User.Name className="text-sm font-medium" />
</div> </div>
@@ -157,7 +157,7 @@ function Screen() {
> >
<User.Provider pubkey={item}> <User.Provider pubkey={item}>
<User.Root className="flex items-center gap-2.5"> <User.Root className="flex items-center gap-2.5">
<User.Avatar className="object-cover rounded-full size-8" /> <User.Avatar className="rounded-full size-8" />
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<User.Name className="text-sm font-medium" /> <User.Name className="text-sm font-medium" />
</div> </div>

View File

@@ -95,7 +95,7 @@ function Screen() {
<div className="flex flex-col w-full h-full gap-2"> <div className="flex flex-col w-full h-full gap-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<User.Avatar className="object-cover rounded-full size-7 shrink-0" /> <User.Avatar className="rounded-full size-7" />
<User.Name className="text-sm leadning-tight max-w-[15rem] truncate font-semibold" /> <User.Name className="text-sm leadning-tight max-w-[15rem] truncate font-semibold" />
</div> </div>
<button <button

View File

@@ -1,7 +1,12 @@
import { Note } from "@/components/note";
import { MentionNote } from "@/components/note/mentions/note";
import { User } from "@/components/user";
import { ComposeFilledIcon } from "@lume/icons"; import { ComposeFilledIcon } from "@lume/icons";
import { LumeEvent, useEvent } from "@lume/system";
import { Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import { cn, insertImage, insertNostrEvent, isImageUrl } from "@lume/utils"; import { cn, insertImage, insertNostrEvent, isImageUrl } from "@lume/utils";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { nip19 } from "nostr-tools";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { type Descendant, Node, Transforms, createEditor } from "slate"; import { type Descendant, Node, Transforms, createEditor } from "slate";
import { import {
@@ -14,13 +19,8 @@ import {
withReact, withReact,
} from "slate-react"; } from "slate-react";
import { MediaButton } from "./-components/media"; import { MediaButton } from "./-components/media";
import { LumeEvent, useEvent } from "@lume/system";
import { WarningButton } from "./-components/warning";
import { MentionNote } from "@/components/note/mentions/note";
import { PowButton } from "./-components/pow"; import { PowButton } from "./-components/pow";
import { User } from "@/components/user"; import { WarningButton } from "./-components/warning";
import { Note } from "@/components/note";
import { nip19 } from "nostr-tools";
type EditorSearch = { type EditorSearch = {
reply_to: string; reply_to: string;
@@ -250,7 +250,7 @@ function ChildNote({ id }: { id: string }) {
<Note.Root className="flex items-center gap-2"> <Note.Root className="flex items-center gap-2">
<User.Provider pubkey={data.pubkey}> <User.Provider pubkey={data.pubkey}>
<User.Root className="shrink-0"> <User.Root className="shrink-0">
<User.Avatar className="rounded-full size-8 shrink-0" /> <User.Avatar className="rounded-full size-8" />
</User.Root> </User.Root>
</User.Provider> </User.Provider>
<div className="content-break line-clamp-1">{data.content}</div> <div className="content-break line-clamp-1">{data.content}</div>

View File

@@ -92,7 +92,7 @@ function Screen() {
> >
<User.Provider pubkey={account}> <User.Provider pubkey={account}>
<User.Root className="flex items-center gap-2.5 p-3"> <User.Root className="flex items-center gap-2.5 p-3">
<User.Avatar className="object-cover rounded-full size-10 shrink-0" /> <User.Avatar className="rounded-full size-10" />
<div className="inline-flex flex-col items-start"> <div className="inline-flex flex-col items-start">
<User.Name className="max-w-[6rem] truncate font-medium leading-tight" /> <User.Name className="max-w-[6rem] truncate font-medium leading-tight" />
<span className="text-sm text-neutral-700 dark:text-neutral-300"> <span className="text-sm text-neutral-700 dark:text-neutral-300">

View File

@@ -2,16 +2,23 @@ import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote"; import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost"; import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text"; import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon } from "@lume/icons"; import { ArrowRightCircleIcon, ArrowUpIcon } from "@lume/icons";
import { type LumeEvent, NostrAccount, NostrQuery } from "@lume/system"; import { LumeEvent, NostrAccount, NostrQuery } from "@lume/system";
import { type ColumnRouteSearch, Kind } from "@lume/types"; import { type ColumnRouteSearch, Kind, type Meta } from "@lume/types";
import { Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import * as ScrollArea from "@radix-ui/react-scroll-area"; import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useInfiniteQuery } from "@tanstack/react-query"; import { type InfiniteData, useInfiniteQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router"; import { createFileRoute, redirect } from "@tanstack/react-router";
import { useCallback, useRef } from "react"; import { listen } from "@tauri-apps/api/event";
import { getCurrent } from "@tauri-apps/api/window";
import { useCallback, useEffect, useRef, useState } from "react";
import { Virtualizer } from "virtua"; import { Virtualizer } from "virtua";
type Payload = {
raw: string;
parsed: Meta;
};
export const Route = createFileRoute("/newsfeed")({ export const Route = createFileRoute("/newsfeed")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => { validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return { return {
@@ -40,6 +47,7 @@ export const Route = createFileRoute("/newsfeed")({
}); });
export function Screen() { export function Screen() {
const { queryClient } = Route.useRouteContext();
const { label, account } = Route.useSearch(); const { label, account } = Route.useSearch();
const { const {
data, data,
@@ -84,20 +92,31 @@ export function Screen() {
[data], [data],
); );
useEffect(() => {
const unlisten = listen("synced", async () => {
await queryClient.invalidateQueries({ queryKey: [label, account] });
});
return () => {
unlisten.then((f) => f());
};
}, []);
return ( return (
<ScrollArea.Root <ScrollArea.Root
type={"scroll"} type={"scroll"}
scrollHideDelay={300} scrollHideDelay={300}
className="overflow-hidden size-full" className="overflow-hidden size-full"
> >
<ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3"> <ScrollArea.Viewport ref={ref} className="relative h-full px-3 pb-3">
<Listerner />
<Virtualizer scrollRef={ref}> <Virtualizer scrollRef={ref}>
{isFetching && !isLoading && !isFetchingNextPage ? ( {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 w-full mb-3 h-12 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"> <div className="flex items-center justify-center gap-2">
<Spinner className="size-5" /> <Spinner className="size-5" />
<span className="text-sm font-medium"> <span className="text-sm font-medium">
Fetching new notes... Getting new notes...
</span> </span>
</div> </div>
</div> </div>
@@ -145,3 +164,69 @@ export function Screen() {
</ScrollArea.Root> </ScrollArea.Root>
); );
} }
function Listerner() {
const { queryClient } = Route.useRouteContext();
const { label, account } = Route.useSearch();
const [events, setEvents] = useState<LumeEvent[]>([]);
const pushNewEvents = async () => {
await queryClient.setQueryData(
[label, account],
(oldData: InfiniteData<LumeEvent[], number> | undefined) => {
const firstPage = oldData?.pages[0];
if (firstPage) {
return {
...oldData,
pages: [
{
...firstPage,
posts: [...events, ...firstPage],
},
...oldData.pages.slice(1),
],
};
}
},
);
await queryClient.invalidateQueries({ queryKey: [label, account] });
};
useEffect(() => {
const unlistenEvent = getCurrent().listen<Payload>("new_event", (data) => {
const event = LumeEvent.from(data.payload.raw, data.payload.parsed);
setEvents((prev) => [event, ...prev]);
});
const unlistenWindow = getCurrent().onCloseRequested(async () => {
await NostrQuery.unlisten();
await getCurrent().destroy();
});
// Listen for new event
NostrQuery.listenLocalEvent().then(() => console.log("listen"));
return () => {
unlistenEvent.then((f) => f());
unlistenWindow.then((f) => f());
};
}, []);
if (!events?.length) return null;
return (
<div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3">
<button
type="button"
onClick={() => pushNewEvents()}
className="w-max h-8 pl-2 pr-3 inline-flex items-center justify-center gap-1.5 rounded-full shadow-lg text-sm font-medium text-white bg-black dark:text-black dark:bg-white"
>
<ArrowUpIcon className="size-4" />
{events.length} new notes
</button>
</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

@@ -42,50 +42,41 @@ function Screen() {
const [account, setAccount] = useState<string>(null); const [account, setAccount] = useState<string>(null);
const [events, setEvents] = useState<LumeEvent[]>([]); const [events, setEvents] = useState<LumeEvent[]>([]);
const texts = useMemo( const { texts, zaps, reactions } = useMemo(() => {
() => events.filter((ev) => ev.kind === Kind.Text), const zaps = new Map<string, LumeEvent[]>();
[events], const reactions = new Map<string, LumeEvent[]>();
); const texts = events.filter((ev) => ev.kind === Kind.Text);
const zapEvents = events.filter((ev) => ev.kind === Kind.ZapReceipt);
const zaps = useMemo(() => { const reactEvents = events.filter(
const groups = new Map<string, LumeEvent[]>();
const list = events.filter((ev) => ev.kind === Kind.ZapReceipt);
for (const event of list) {
const rootId = event.tags.filter((tag) => tag[0] === "e")[0]?.[1];
if (rootId) {
if (groups.has(rootId)) {
groups.get(rootId).push(event);
} else {
groups.set(rootId, [event]);
}
}
}
return groups;
}, [events]);
const reactions = useMemo(() => {
const groups = new Map<string, LumeEvent[]>();
const list = events.filter(
(ev) => ev.kind === Kind.Repost || ev.kind === Kind.Reaction, (ev) => ev.kind === Kind.Repost || ev.kind === Kind.Reaction,
); );
for (const event of list) { for (const event of reactEvents) {
const rootId = event.tags.filter((tag) => tag[0] === "e")[0]?.[1]; const rootId = event.tags.filter((tag) => tag[0] === "e")[0]?.[1];
if (rootId) { if (rootId) {
if (groups.has(rootId)) { if (reactions.has(rootId)) {
groups.get(rootId).push(event); reactions.get(rootId).push(event);
} else { } else {
groups.set(rootId, [event]); reactions.set(rootId, [event]);
} }
} }
} }
return groups; for (const event of zapEvents) {
}, [events]); const rootId = event.tags.filter((tag) => tag[0] === "e")[0]?.[1];
if (rootId) {
if (zaps.has(rootId)) {
zaps.get(rootId).push(event);
} else {
zaps.set(rootId, [event]);
}
}
}
return { texts, zaps, reactions };
}, [events?.length]);
const showContextMenu = useCallback(async (e: React.MouseEvent) => { const showContextMenu = useCallback(async (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
@@ -99,10 +90,6 @@ function Screen() {
text: "New Post", text: "New Post",
action: () => LumeWindow.openEditor(), action: () => LumeWindow.openEditor(),
}), }),
MenuItem.new({
text: "Search",
action: () => LumeWindow.openSearch(),
}),
PredefinedMenuItem.new({ item: "Separator" }), PredefinedMenuItem.new({ item: "Separator" }),
MenuItem.new({ MenuItem.new({
text: "About Lume", text: "About Lume",
@@ -180,13 +167,6 @@ function Screen() {
<User.Avatar className="rounded-full size-7" /> <User.Avatar className="rounded-full size-7" />
</User.Root> </User.Root>
</User.Provider> </User.Provider>
<button
type="button"
onClick={() => LumeWindow.openSearch()}
className="inline-flex items-center justify-center rounded-full size-7 bg-black/5 dark:bg-white/5"
>
<SearchIcon className="size-4" />
</button>
<button <button
type="button" type="button"
onClick={(e) => showContextMenu(e)} onClick={(e) => showContextMenu(e)}
@@ -351,7 +331,7 @@ function RootNote({ id }: { id: string }) {
<Note.Root className="flex items-center gap-2"> <Note.Root className="flex items-center gap-2">
<User.Provider pubkey={data.pubkey}> <User.Provider pubkey={data.pubkey}>
<User.Root className="shrink-0"> <User.Root className="shrink-0">
<User.Avatar className="rounded-full size-8 shrink-0" /> <User.Avatar className="rounded-full size-8" />
</User.Root> </User.Root>
</User.Provider> </User.Provider>
<div className="line-clamp-1">{data.content}</div> <div className="line-clamp-1">{data.content}</div>
@@ -371,7 +351,7 @@ function TextNote({ event }: { event: LumeEvent }) {
<Note.Root className="flex flex-col p-2 mb-2 rounded-lg shrink-0 backdrop-blur-md bg-black/10 dark:bg-white/10"> <Note.Root className="flex flex-col p-2 mb-2 rounded-lg shrink-0 backdrop-blur-md bg-black/10 dark:bg-white/10">
<User.Provider pubkey={event.pubkey}> <User.Provider pubkey={event.pubkey}>
<User.Root className="inline-flex items-center gap-2"> <User.Root className="inline-flex items-center gap-2">
<User.Avatar className="rounded-full size-9 shrink-0" /> <User.Avatar className="rounded-full size-9" />
<div className="flex flex-col flex-1"> <div className="flex flex-col flex-1">
<div className="flex items-baseline justify-between w-full"> <div className="flex items-baseline justify-between w-full">
<User.Name className="text-sm font-semibold leading-tight" /> <User.Name className="text-sm font-semibold leading-tight" />

View File

@@ -0,0 +1,96 @@
import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { LumeEvent, NostrQuery } from "@lume/system";
import { Kind, type NostrEvent } from "@lume/types";
import { Spinner } from "@lume/ui";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { fetch } from "@tauri-apps/plugin-http";
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 { isLoading, data } = useQuery({
queryKey: ["search", query],
queryFn: async () => {
try {
const res = await fetch(
`https://api.nostr.wine/search?query=${query}&kind=1&limit=50`,
);
const content = await res.json();
const events = content.data as NostrEvent[];
const lumeEvents = await Promise.all(
events.map(async (item): Promise<LumeEvent> => {
const event = await LumeEvent.build(item);
return event;
}),
);
return lumeEvents.sort((a, b) => b.created_at - a.created_at);
} catch (e) {
throw new Error(e);
}
},
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 p-3">
<Virtualizer scrollRef={ref}>
{isLoading ? (
<div className="flex items-center justify-center w-full h-11 gap-2">
<Spinner className="size-5" />
<span className="text-sm font-medium">Searching...</span>
</div>
) : (
data.map((item) => renderItem(item))
)}
</Virtualizer>
</ScrollArea.Viewport>
);
}

View File

@@ -1,148 +1,44 @@
import { Note } from "@/components/note"; import * as ScrollArea from "@radix-ui/react-scroll-area";
import { User } from "@/components/user"; import { Outlet, createFileRoute } from "@tanstack/react-router";
import { SearchIcon } from "@lume/icons";
import { LumeEvent, LumeWindow } from "@lume/system"; type Search = {
import { Kind, type NostrEvent } from "@lume/types"; query: string;
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";
export const Route = createFileRoute("/search")({ export const Route = createFileRoute("/search")({
validateSearch: (search: Record<string, string>): Search => {
return {
query: search.query,
};
},
component: Screen, component: Screen,
}); });
function Screen() { function Screen() {
const [loading, setLoading] = useState(false); const { query } = Route.useSearch();
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 ( return (
<div data-tauri-drag-region className="flex flex-col w-full h-full"> <div className="flex flex-col h-full">
<div className="relative flex flex-col h-24 border-b shrink-0 border-black/5 dark:border-white/5"> <div
<div data-tauri-drag-region className="w-full h-4 shrink-0" /> data-tauri-drag-region
<input className="shrink-0 flex items-end gap-1 h-20 px-3 pb-3 w-full border-b border-black/10 dark:border-white/10"
value={search} >
onChange={(e) => setSearch(e.target.value)} Search result for: <span className="font-semibold">{query}</span>
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>
</div> <ScrollArea.Root
); type={"scroll"}
} scrollHideDelay={300}
className="overflow-hidden size-full flex-1"
function SearchUser({ event }: { event: LumeEvent }) { >
return ( <Outlet />
<button <ScrollArea.Scrollbar
key={event.id} className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
type="button" orientation="vertical"
onClick={() => LumeWindow.openProfile(event.pubkey)} >
className="col-span-1 p-2 rounded-lg hover:bg-black/10 dark:hover:bg-white/10" <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>
<User.Provider pubkey={event.pubkey} embedProfile={event.content}> <ScrollArea.Corner className="bg-transparent" />
<User.Root className="flex items-center gap-2"> </ScrollArea.Root>
<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> </div>
); );
} }

View File

@@ -0,0 +1,101 @@
import { User } from "@/components/user";
import { LumeWindow, NostrQuery } from "@lume/system";
import type { NostrEvent } from "@lume/types";
import { Spinner } from "@lume/ui";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { fetch } from "@tauri-apps/plugin-http";
import { useRef } from "react";
import { Virtualizer } from "virtua";
type Search = {
query: string;
};
type UserItem = {
pubkey: string;
profile: string;
};
export const Route = createFileRoute("/search/users")({
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 { isLoading, data } = useQuery({
queryKey: ["search", query],
queryFn: async () => {
try {
const res = await fetch(
`https://api.nostr.wine/search?query=${query}&kind=0&limit=100`,
);
const content = await res.json();
const events = content.data as NostrEvent[];
const users: UserItem[] = events.map((ev) => ({
pubkey: ev.pubkey,
profile: ev.content,
}));
return users;
} catch (e) {
throw new Error(e);
}
},
refetchOnWindowFocus: false,
});
const ref = useRef<HTMLDivElement>(null);
return (
<ScrollArea.Viewport ref={ref} className="h-full px-3 pt-3">
<Virtualizer scrollRef={ref}>
{isLoading ? (
<div className="flex items-center justify-center w-full h-11 gap-2">
<Spinner className="size-5" />
<span className="text-sm font-medium">Searching...</span>
</div>
) : (
data.map((item) => (
<div
key={item.pubkey}
className="w-full p-3 mb-2 overflow-hidden bg-white rounded-lg h-max dark:bg-black/20 backdrop-blur-lg shadow-primary dark:ring-1 ring-neutral-800/50"
>
<User.Provider pubkey={item.pubkey} embedProfile={item.profile}>
<User.Root className="flex flex-col w-full h-full gap-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<User.Avatar className="rounded-full size-7" />
<div className="inline-flex items-center gap-1">
<User.Name className="text-sm leadning-tight max-w-[15rem] truncate font-semibold" />
<User.NIP05 />
</div>
</div>
<button
type="button"
onClick={() => LumeWindow.openProfile(item.pubkey)}
className="inline-flex items-center justify-center w-16 text-sm font-medium rounded-md h-7 bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
>
View
</button>
</div>
<User.About className="select-text line-clamp-3 max-w-none text-neutral-800 dark:text-neutral-400" />
</User.Root>
</User.Provider>
</div>
))
)}
</Virtualizer>
</ScrollArea.Viewport>
);
}

View File

@@ -51,7 +51,7 @@ function Account({ account }: { account: string }) {
<div className="flex items-center justify-between gap-2 py-3"> <div className="flex items-center justify-between gap-2 py-3">
<User.Provider pubkey={account}> <User.Provider pubkey={account}>
<User.Root className="flex items-center gap-2"> <User.Root className="flex items-center gap-2">
<User.Avatar className="object-cover rounded-full size-8" /> <User.Avatar className="rounded-full size-8" />
<div className="flex flex-col"> <div className="flex flex-col">
<User.Name className="text-sm leading-tight" /> <User.Name className="text-sm leading-tight" />
<span className="text-sm leading-tight text-black/50 dark:text-white/50"> <span className="text-sm leading-tight text-black/50 dark:text-white/50">

View File

@@ -1,5 +1,5 @@
import { Spinner } from "@lume/ui";
import { User } from "@/components/user"; import { User } from "@/components/user";
import { Spinner } from "@lume/ui";
import { Await, defer } from "@tanstack/react-router"; import { Await, defer } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { Suspense } from "react"; import { Suspense } from "react";
@@ -52,7 +52,7 @@ export function Screen() {
<div className="flex h-full w-full flex-col gap-2"> <div className="flex h-full w-full flex-col gap-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<User.Avatar className="size-10 shrink-0 rounded-full object-cover" /> <User.Avatar className="size-10 rounded-full" />
<User.Name className="leadning-tight max-w-[15rem] truncate font-semibold" /> <User.Name className="leadning-tight max-w-[15rem] truncate font-semibold" />
</div> </div>
<User.Button className="inline-flex h-8 w-20 items-center justify-center rounded-lg bg-black/10 text-sm font-medium hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20" /> <User.Button className="inline-flex h-8 w-20 items-center justify-center rounded-lg bg-black/10 text-sm font-medium hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20" />

View File

@@ -25,11 +25,11 @@
"@tauri-apps/plugin-dialog": "2.0.0-beta.5", "@tauri-apps/plugin-dialog": "2.0.0-beta.5",
"@tauri-apps/plugin-fs": "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-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-os": "github:tauri-apps/tauri-plugin-os#v2",
"@tauri-apps/plugin-process": "2.0.0-beta.5", "@tauri-apps/plugin-process": "2.0.0-beta.5",
"@tauri-apps/plugin-shell": "2.0.0-beta.6", "@tauri-apps/plugin-shell": "2.0.0-beta.6",
"@tauri-apps/plugin-updater": "2.0.0-beta.5", "@tauri-apps/plugin-updater": "2.0.0-beta.5",
"@tauri-apps/plugin-upload": "2.0.0-beta.6" "@tauri-apps/plugin-upload": "2.0.0-beta.6",
"@tauri-apps/plugin-window-state": "2.0.0-beta.6"
} }
} }

View File

@@ -148,7 +148,7 @@ try {
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async checkContact(hex: string) : Promise<Result<boolean, null>> { async checkContact(hex: string) : Promise<Result<boolean, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("check_contact", { hex }) }; return { status: "ok", data: await TAURI_INVOKE("check_contact", { hex }) };
} catch (e) { } catch (e) {
@@ -300,14 +300,6 @@ try {
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async unlistenEventReply(id: string) : Promise<Result<null, null>> {
try {
return { status: "ok", data: await TAURI_INVOKE("unlisten_event_reply", { id }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getEventsBy(publicKey: string, asOf: string | null) : Promise<Result<RichEvent[], string>> { async getEventsBy(publicKey: string, asOf: string | null) : Promise<Result<RichEvent[], string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("get_events_by", { publicKey, asOf }) }; return { status: "ok", data: await TAURI_INVOKE("get_events_by", { publicKey, asOf }) };
@@ -324,6 +316,14 @@ try {
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async listenLocalEvent(label: string) : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("listen_local_event", { label }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getGroupEvents(publicKeys: string[], until: string | null) : Promise<Result<RichEvent[], string>> { async getGroupEvents(publicKeys: string[], until: string | null) : Promise<Result<RichEvent[], string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("get_group_events", { publicKeys, until }) }; return { status: "ok", data: await TAURI_INVOKE("get_group_events", { publicKeys, until }) };
@@ -388,6 +388,14 @@ try {
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async unlisten(id: string) : Promise<Result<null, null>> {
try {
return { status: "ok", data: await TAURI_INVOKE("unlisten", { id }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async showInFolder(path: string) : Promise<void> { async showInFolder(path: string) : Promise<void> {
await TAURI_INVOKE("show_in_folder", { path }); await TAURI_INVOKE("show_in_folder", { path });
}, },

View File

@@ -200,7 +200,7 @@ export class LumeEvent {
} }
public async unlistenEventReply() { public async unlistenEventReply() {
const query = await commands.unlistenEventReply(this.id); const query = await commands.unlisten(this.id);
if (query.status === "ok") { if (query.status === "ok") {
return query.data; return query.data;
@@ -271,6 +271,17 @@ export class LumeEvent {
} }
} }
static async build(event: NostrEvent) {
const query = await commands.getEventMeta(event.content);
if (query.status === "ok") {
event.meta = query.data;
return new LumeEvent(event);
} else {
return new LumeEvent(event);
}
}
static from(raw: string, parsed?: Meta) { static from(raw: string, parsed?: Meta) {
const nostrEvent: NostrEvent = JSON.parse(raw); const nostrEvent: NostrEvent = JSON.parse(raw);
@@ -280,6 +291,6 @@ export class LumeEvent {
nostrEvent.meta = null; nostrEvent.meta = null;
} }
return new this(nostrEvent); return new LumeEvent(nostrEvent);
} }
} }

View File

@@ -1,5 +1,6 @@
import type { LumeColumn, Metadata, NostrEvent, Relay } from "@lume/types"; import type { LumeColumn, Metadata, NostrEvent, Relay } from "@lume/types";
import { resolveResource } from "@tauri-apps/api/path"; import { resolveResource } from "@tauri-apps/api/path";
import { getCurrent } from "@tauri-apps/api/window";
import { open } from "@tauri-apps/plugin-dialog"; import { open } from "@tauri-apps/plugin-dialog";
import { readFile, readTextFile } from "@tauri-apps/plugin-fs"; import { readFile, readTextFile } from "@tauri-apps/plugin-fs";
import { relaunch } from "@tauri-apps/plugin-process"; import { relaunch } from "@tauri-apps/plugin-process";
@@ -206,6 +207,17 @@ export class NostrQuery {
} }
} }
static async listenLocalEvent() {
const label = getCurrent().label;
const query = await commands.listenLocalEvent(label);
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
}
static async getGroupEvents(pubkeys: string[], asOf?: number) { static async getGroupEvents(pubkeys: string[], asOf?: number) {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined; const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const query = await commands.getGroupEvents(pubkeys, until); const query = await commands.getGroupEvents(pubkeys, until);
@@ -403,4 +415,15 @@ export class NostrQuery {
throw new Error(query.error); throw new Error(query.error);
} }
} }
static async unlisten(id?: string) {
const label = id ? id : getCurrent().label;
const query = await commands.unlisten(label);
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
}
} }

View File

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

View File

@@ -15,6 +15,7 @@
"@tailwindcss/typography": "^0.5.13", "@tailwindcss/typography": "^0.5.13",
"tailwind-gradient-mask-image": "^1.2.0", "tailwind-gradient-mask-image": "^1.2.0",
"tailwind-scrollbar": "^3.1.0", "tailwind-scrollbar": "^3.1.0",
"tailwindcss": "^3.4.4" "tailwindcss": "^3.4.4",
"tailwindcss-content-visibility": "^0.2.0"
} }
} }

View File

@@ -49,7 +49,7 @@ const config = {
require("@tailwindcss/forms"), require("@tailwindcss/forms"),
require("@tailwindcss/typography"), require("@tailwindcss/typography"),
require("tailwind-gradient-mask-image"), require("tailwind-gradient-mask-image"),
require("tailwind-scrollbar")({ nocompatible: true }), require("tailwindcss-content-visibility"),
], ],
}; };

View File

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

View File

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

7465
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

35
src-tauri/Cargo.lock generated
View File

@@ -2811,6 +2811,7 @@ dependencies = [
"tauri-plugin-theme", "tauri-plugin-theme",
"tauri-plugin-updater", "tauri-plugin-updater",
"tauri-plugin-upload", "tauri-plugin-upload",
"tauri-plugin-window-state",
"tauri-specta", "tauri-specta",
"tokio", "tokio",
"url", "url",
@@ -5373,7 +5374,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-clipboard-manager" name = "tauri-plugin-clipboard-manager"
version = "2.1.0-beta.4" version = "2.1.0-beta.4"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#29751ee939fc8d26df07e4da3ad7f5c2aa0926ba" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#03d3cc3677bbebd3a8a4f1ab07f9a3bec671b7f5"
dependencies = [ dependencies = [
"arboard", "arboard",
"image 0.24.9", "image 0.24.9",
@@ -5404,7 +5405,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-dialog" name = "tauri-plugin-dialog"
version = "2.0.0-beta.9" version = "2.0.0-beta.9"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#29751ee939fc8d26df07e4da3ad7f5c2aa0926ba" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#03d3cc3677bbebd3a8a4f1ab07f9a3bec671b7f5"
dependencies = [ dependencies = [
"dunce", "dunce",
"log", "log",
@@ -5421,7 +5422,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-fs" name = "tauri-plugin-fs"
version = "2.0.0-beta.9" version = "2.0.0-beta.9"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#29751ee939fc8d26df07e4da3ad7f5c2aa0926ba" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#03d3cc3677bbebd3a8a4f1ab07f9a3bec671b7f5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"glob", "glob",
@@ -5439,7 +5440,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-http" name = "tauri-plugin-http"
version = "2.0.0-beta.10" version = "2.0.0-beta.10"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#29751ee939fc8d26df07e4da3ad7f5c2aa0926ba" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#03d3cc3677bbebd3a8a4f1ab07f9a3bec671b7f5"
dependencies = [ dependencies = [
"data-url", "data-url",
"http", "http",
@@ -5459,7 +5460,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-notification" name = "tauri-plugin-notification"
version = "2.0.0-beta.8" version = "2.0.0-beta.8"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#29751ee939fc8d26df07e4da3ad7f5c2aa0926ba" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#03d3cc3677bbebd3a8a4f1ab07f9a3bec671b7f5"
dependencies = [ dependencies = [
"log", "log",
"notify-rust", "notify-rust",
@@ -5477,7 +5478,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-os" name = "tauri-plugin-os"
version = "2.0.0-beta.6" version = "2.0.0-beta.6"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#29751ee939fc8d26df07e4da3ad7f5c2aa0926ba" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#03d3cc3677bbebd3a8a4f1ab07f9a3bec671b7f5"
dependencies = [ dependencies = [
"gethostname", "gethostname",
"log", "log",
@@ -5494,7 +5495,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-process" name = "tauri-plugin-process"
version = "2.0.0-beta.6" version = "2.0.0-beta.6"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#29751ee939fc8d26df07e4da3ad7f5c2aa0926ba" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#03d3cc3677bbebd3a8a4f1ab07f9a3bec671b7f5"
dependencies = [ dependencies = [
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
@@ -5503,7 +5504,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-shell" name = "tauri-plugin-shell"
version = "2.0.0-beta.7" version = "2.0.0-beta.7"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#29751ee939fc8d26df07e4da3ad7f5c2aa0926ba" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#03d3cc3677bbebd3a8a4f1ab07f9a3bec671b7f5"
dependencies = [ dependencies = [
"encoding_rs", "encoding_rs",
"log", "log",
@@ -5541,7 +5542,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-updater" name = "tauri-plugin-updater"
version = "2.0.0-beta.8" version = "2.0.0-beta.8"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#29751ee939fc8d26df07e4da3ad7f5c2aa0926ba" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#03d3cc3677bbebd3a8a4f1ab07f9a3bec671b7f5"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"dirs-next", "dirs-next",
@@ -5569,7 +5570,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-upload" name = "tauri-plugin-upload"
version = "2.0.0-beta.7" version = "2.0.0-beta.7"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#29751ee939fc8d26df07e4da3ad7f5c2aa0926ba" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#03d3cc3677bbebd3a8a4f1ab07f9a3bec671b7f5"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"log", "log",
@@ -5584,6 +5585,20 @@ dependencies = [
"tokio-util", "tokio-util",
] ]
[[package]]
name = "tauri-plugin-window-state"
version = "2.0.0-beta.9"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#03d3cc3677bbebd3a8a4f1ab07f9a3bec671b7f5"
dependencies = [
"bitflags 2.6.0",
"log",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror",
]
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.0.0-beta.18" version = "2.0.0-beta.18"

View File

@@ -17,12 +17,13 @@ serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
monitor = { git = "https://github.com/ahkohd/tauri-toolkit", branch = "v2" } monitor = { git = "https://github.com/ahkohd/tauri-toolkit", branch = "v2" }
tauri = { version = "2.0.0-beta", features = [ tauri = { version = "2.0.0-beta", features = [
"unstable", "unstable",
"tray-icon", "tray-icon",
"macos-private-api", "macos-private-api",
"native-tls-vendored", "native-tls-vendored",
"protocol-asset", "protocol-asset",
] } ] }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-clipboard-manager = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } tauri-plugin-clipboard-manager = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-dialog = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } tauri-plugin-dialog = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-fs = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } tauri-plugin-fs = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }

View File

@@ -0,0 +1,51 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "desktop-capability",
"description": "Capability for the column",
"platforms": [
"linux",
"macOS",
"windows"
],
"windows": [
"column-*"
],
"permissions": [
"resources:default",
"tray:default",
"os:allow-locale",
"os:allow-os-type",
"clipboard-manager:allow-write-text",
"dialog:allow-open",
"dialog:allow-ask",
"dialog:allow-message",
"fs:allow-read-file",
"menu:default",
"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/*"
}
]
}
]
}

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","panel","splash","settings","search","nwc","activity","zap-*","event-*","user-*","editor-*","column-*"],"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","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

@@ -142,7 +142,7 @@
"identifier": { "identifier": {
"oneOf": [ "oneOf": [
{ {
"description": "fs:default -> # Tauri `fs` default permissions\n\nThis configuration file defines the default permissions granted\nto the filesystem.\n\n### Granted Permissions\n\nThis default permission set enables all read-related commands and\nallows access to the `$APP` folder and sub directories created in it.\nThe location of the `$APP` folder depends on the operating system,\nwhere the application is run.\n\nIn general the `$APP` folder needs to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\n### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n", "description": "fs:default -> This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"fs:default" "fs:default"
@@ -1373,6 +1373,13 @@
"fs:allow-write-text-file" "fs:allow-write-text-file"
] ]
}, },
{
"description": "fs:create-app-specific-dirs -> This permissions allows to create the application specific directories.\n",
"type": "string",
"enum": [
"fs:create-app-specific-dirs"
]
},
{ {
"description": "fs:deny-copy-file -> Denies the copy_file command without any pre-configured scope.", "description": "fs:deny-copy-file -> Denies the copy_file command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -1562,6 +1569,13 @@
"fs:read-all" "fs:read-all"
] ]
}, },
{
"description": "fs:read-app-specific-dirs-recursive -> This permission allows recursive read functionality on the application\nspecific base directories. \n",
"type": "string",
"enum": [
"fs:read-app-specific-dirs-recursive"
]
},
{ {
"description": "fs:read-dirs -> This enables directory read and file metadata related commands without any pre-configured accessible paths.", "description": "fs:read-dirs -> This enables directory read and file metadata related commands without any pre-configured accessible paths.",
"type": "string", "type": "string",
@@ -2190,7 +2204,7 @@
"identifier": { "identifier": {
"oneOf": [ "oneOf": [
{ {
"description": "http:default -> Allows all fetch operations", "description": "http:default -> This permission set configures what kind of\nfetch operations are available from the http plugin.\n\nThis enables all fetch operations but does not\nallow explicitly any origins to be fetched. This needs to\nbe manually configured before usage.\n\n#### Granted Permissions\n\nAll fetch operations are enabled.\n\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"http:default" "http:default"
@@ -2313,6 +2327,7 @@
"identifier": { "identifier": {
"oneOf": [ "oneOf": [
{ {
"description": "shell:default -> This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"shell:default" "shell:default"
@@ -2546,6 +2561,7 @@
] ]
}, },
{ {
"description": "clipboard-manager:default -> No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"clipboard-manager:default" "clipboard-manager:default"
@@ -2656,6 +2672,7 @@
] ]
}, },
{ {
"description": "dialog:default -> This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"dialog:default" "dialog:default"
@@ -3852,7 +3869,7 @@
] ]
}, },
{ {
"description": "fs:default -> # Tauri `fs` default permissions\n\nThis configuration file defines the default permissions granted\nto the filesystem.\n\n### Granted Permissions\n\nThis default permission set enables all read-related commands and\nallows access to the `$APP` folder and sub directories created in it.\nThe location of the `$APP` folder depends on the operating system,\nwhere the application is run.\n\nIn general the `$APP` folder needs to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\n### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n", "description": "fs:default -> This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"fs:default" "fs:default"
@@ -4026,6 +4043,13 @@
"fs:allow-write-text-file" "fs:allow-write-text-file"
] ]
}, },
{
"description": "fs:create-app-specific-dirs -> This permissions allows to create the application specific directories.\n",
"type": "string",
"enum": [
"fs:create-app-specific-dirs"
]
},
{ {
"description": "fs:deny-copy-file -> Denies the copy_file command without any pre-configured scope.", "description": "fs:deny-copy-file -> Denies the copy_file command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -4215,6 +4239,13 @@
"fs:read-all" "fs:read-all"
] ]
}, },
{
"description": "fs:read-app-specific-dirs-recursive -> This permission allows recursive read functionality on the application\nspecific base directories. \n",
"type": "string",
"enum": [
"fs:read-app-specific-dirs-recursive"
]
},
{ {
"description": "fs:read-dirs -> This enables directory read and file metadata related commands without any pre-configured accessible paths.", "description": "fs:read-dirs -> This enables directory read and file metadata related commands without any pre-configured accessible paths.",
"type": "string", "type": "string",
@@ -4783,7 +4814,7 @@
] ]
}, },
{ {
"description": "http:default -> Allows all fetch operations", "description": "http:default -> This permission set configures what kind of\nfetch operations are available from the http plugin.\n\nThis enables all fetch operations but does not\nallow explicitly any origins to be fetched. This needs to\nbe manually configured before usage.\n\n#### Granted Permissions\n\nAll fetch operations are enabled.\n\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"http:default" "http:default"
@@ -5238,12 +5269,61 @@
] ]
}, },
{ {
"description": "notification:default -> Allows requesting permission, checking permission state and sending notifications", "description": "notification:default -> This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"notification:default" "notification:default"
] ]
}, },
{
"description": "notification:allow-batch -> Enables the batch command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-batch"
]
},
{
"description": "notification:allow-cancel -> Enables the cancel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-cancel"
]
},
{
"description": "notification:allow-check-permissions -> Enables the check_permissions command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-check-permissions"
]
},
{
"description": "notification:allow-create-channel -> Enables the create_channel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-create-channel"
]
},
{
"description": "notification:allow-delete-channel -> Enables the delete_channel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-delete-channel"
]
},
{
"description": "notification:allow-get-active -> Enables the get_active command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-get-active"
]
},
{
"description": "notification:allow-get-pending -> Enables the get_pending command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-get-pending"
]
},
{ {
"description": "notification:allow-is-permission-granted -> Enables the is_permission_granted command without any pre-configured scope.", "description": "notification:allow-is-permission-granted -> Enables the is_permission_granted command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5251,6 +5331,13 @@
"notification:allow-is-permission-granted" "notification:allow-is-permission-granted"
] ]
}, },
{
"description": "notification:allow-list-channels -> Enables the list_channels command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-list-channels"
]
},
{ {
"description": "notification:allow-notify -> Enables the notify command without any pre-configured scope.", "description": "notification:allow-notify -> Enables the notify command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5258,6 +5345,13 @@
"notification:allow-notify" "notification:allow-notify"
] ]
}, },
{
"description": "notification:allow-permission-state -> Enables the permission_state command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-permission-state"
]
},
{ {
"description": "notification:allow-register-action-types -> Enables the register_action_types command without any pre-configured scope.", "description": "notification:allow-register-action-types -> Enables the register_action_types command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5272,6 +5366,13 @@
"notification:allow-register-listener" "notification:allow-register-listener"
] ]
}, },
{
"description": "notification:allow-remove-active -> Enables the remove_active command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-remove-active"
]
},
{ {
"description": "notification:allow-request-permission -> Enables the request_permission command without any pre-configured scope.", "description": "notification:allow-request-permission -> Enables the request_permission command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5279,6 +5380,62 @@
"notification:allow-request-permission" "notification:allow-request-permission"
] ]
}, },
{
"description": "notification:allow-show -> Enables the show command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-show"
]
},
{
"description": "notification:deny-batch -> Denies the batch command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-batch"
]
},
{
"description": "notification:deny-cancel -> Denies the cancel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-cancel"
]
},
{
"description": "notification:deny-check-permissions -> Denies the check_permissions command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-check-permissions"
]
},
{
"description": "notification:deny-create-channel -> Denies the create_channel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-create-channel"
]
},
{
"description": "notification:deny-delete-channel -> Denies the delete_channel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-delete-channel"
]
},
{
"description": "notification:deny-get-active -> Denies the get_active command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-get-active"
]
},
{
"description": "notification:deny-get-pending -> Denies the get_pending command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-get-pending"
]
},
{ {
"description": "notification:deny-is-permission-granted -> Denies the is_permission_granted command without any pre-configured scope.", "description": "notification:deny-is-permission-granted -> Denies the is_permission_granted command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5286,6 +5443,13 @@
"notification:deny-is-permission-granted" "notification:deny-is-permission-granted"
] ]
}, },
{
"description": "notification:deny-list-channels -> Denies the list_channels command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-list-channels"
]
},
{ {
"description": "notification:deny-notify -> Denies the notify command without any pre-configured scope.", "description": "notification:deny-notify -> Denies the notify command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5293,6 +5457,13 @@
"notification:deny-notify" "notification:deny-notify"
] ]
}, },
{
"description": "notification:deny-permission-state -> Denies the permission_state command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-permission-state"
]
},
{ {
"description": "notification:deny-register-action-types -> Denies the register_action_types command without any pre-configured scope.", "description": "notification:deny-register-action-types -> Denies the register_action_types command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5307,6 +5478,13 @@
"notification:deny-register-listener" "notification:deny-register-listener"
] ]
}, },
{
"description": "notification:deny-remove-active -> Denies the remove_active command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-remove-active"
]
},
{ {
"description": "notification:deny-request-permission -> Denies the request_permission command without any pre-configured scope.", "description": "notification:deny-request-permission -> Denies the request_permission command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5315,6 +5493,14 @@
] ]
}, },
{ {
"description": "notification:deny-show -> Denies the show command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-show"
]
},
{
"description": "os:default -> This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"os:default" "os:default"
@@ -5552,6 +5738,7 @@
] ]
}, },
{ {
"description": "process:default -> This permission set configures which\nprocess feeatures are by default exposed.\n\n#### Granted Permissions\n\nThis enables to quit via `allow-exit` and restart via `allow-restart`\nthe application.\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"process:default" "process:default"
@@ -5607,6 +5794,7 @@
] ]
}, },
{ {
"description": "shell:default -> This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"shell:default" "shell:default"
@@ -5878,7 +6066,7 @@
] ]
}, },
{ {
"description": "updater:default -> Allows checking for new updates and installing them", "description": "updater:default -> This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"updater:default" "updater:default"
@@ -5941,6 +6129,7 @@
] ]
}, },
{ {
"description": "upload:default -> This permission set configures what kind of\noperations are available from the upload plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"upload:default" "upload:default"
@@ -7037,6 +7226,55 @@
"enum": [ "enum": [
"window:deny-unminimize" "window:deny-unminimize"
] ]
},
{
"description": "window-state:default -> This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n",
"type": "string",
"enum": [
"window-state:default"
]
},
{
"description": "window-state:allow-filename -> Enables the filename command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:allow-filename"
]
},
{
"description": "window-state:allow-restore-state -> Enables the restore_state command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:allow-restore-state"
]
},
{
"description": "window-state:allow-save-window-state -> Enables the save_window_state command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:allow-save-window-state"
]
},
{
"description": "window-state:deny-filename -> Denies the filename command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:deny-filename"
]
},
{
"description": "window-state:deny-restore-state -> Denies the restore_state command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:deny-restore-state"
]
},
{
"description": "window-state:deny-save-window-state -> Denies the save_window_state command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:deny-save-window-state"
]
} }
] ]
}, },

View File

@@ -142,7 +142,7 @@
"identifier": { "identifier": {
"oneOf": [ "oneOf": [
{ {
"description": "fs:default -> # Tauri `fs` default permissions\n\nThis configuration file defines the default permissions granted\nto the filesystem.\n\n### Granted Permissions\n\nThis default permission set enables all read-related commands and\nallows access to the `$APP` folder and sub directories created in it.\nThe location of the `$APP` folder depends on the operating system,\nwhere the application is run.\n\nIn general the `$APP` folder needs to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\n### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n", "description": "fs:default -> This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"fs:default" "fs:default"
@@ -1373,6 +1373,13 @@
"fs:allow-write-text-file" "fs:allow-write-text-file"
] ]
}, },
{
"description": "fs:create-app-specific-dirs -> This permissions allows to create the application specific directories.\n",
"type": "string",
"enum": [
"fs:create-app-specific-dirs"
]
},
{ {
"description": "fs:deny-copy-file -> Denies the copy_file command without any pre-configured scope.", "description": "fs:deny-copy-file -> Denies the copy_file command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -1562,6 +1569,13 @@
"fs:read-all" "fs:read-all"
] ]
}, },
{
"description": "fs:read-app-specific-dirs-recursive -> This permission allows recursive read functionality on the application\nspecific base directories. \n",
"type": "string",
"enum": [
"fs:read-app-specific-dirs-recursive"
]
},
{ {
"description": "fs:read-dirs -> This enables directory read and file metadata related commands without any pre-configured accessible paths.", "description": "fs:read-dirs -> This enables directory read and file metadata related commands without any pre-configured accessible paths.",
"type": "string", "type": "string",
@@ -2190,7 +2204,7 @@
"identifier": { "identifier": {
"oneOf": [ "oneOf": [
{ {
"description": "http:default -> Allows all fetch operations", "description": "http:default -> This permission set configures what kind of\nfetch operations are available from the http plugin.\n\nThis enables all fetch operations but does not\nallow explicitly any origins to be fetched. This needs to\nbe manually configured before usage.\n\n#### Granted Permissions\n\nAll fetch operations are enabled.\n\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"http:default" "http:default"
@@ -2313,6 +2327,7 @@
"identifier": { "identifier": {
"oneOf": [ "oneOf": [
{ {
"description": "shell:default -> This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"shell:default" "shell:default"
@@ -2546,6 +2561,7 @@
] ]
}, },
{ {
"description": "clipboard-manager:default -> No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"clipboard-manager:default" "clipboard-manager:default"
@@ -2656,6 +2672,7 @@
] ]
}, },
{ {
"description": "dialog:default -> This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"dialog:default" "dialog:default"
@@ -3852,7 +3869,7 @@
] ]
}, },
{ {
"description": "fs:default -> # Tauri `fs` default permissions\n\nThis configuration file defines the default permissions granted\nto the filesystem.\n\n### Granted Permissions\n\nThis default permission set enables all read-related commands and\nallows access to the `$APP` folder and sub directories created in it.\nThe location of the `$APP` folder depends on the operating system,\nwhere the application is run.\n\nIn general the `$APP` folder needs to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\n### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n", "description": "fs:default -> This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"fs:default" "fs:default"
@@ -4026,6 +4043,13 @@
"fs:allow-write-text-file" "fs:allow-write-text-file"
] ]
}, },
{
"description": "fs:create-app-specific-dirs -> This permissions allows to create the application specific directories.\n",
"type": "string",
"enum": [
"fs:create-app-specific-dirs"
]
},
{ {
"description": "fs:deny-copy-file -> Denies the copy_file command without any pre-configured scope.", "description": "fs:deny-copy-file -> Denies the copy_file command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -4215,6 +4239,13 @@
"fs:read-all" "fs:read-all"
] ]
}, },
{
"description": "fs:read-app-specific-dirs-recursive -> This permission allows recursive read functionality on the application\nspecific base directories. \n",
"type": "string",
"enum": [
"fs:read-app-specific-dirs-recursive"
]
},
{ {
"description": "fs:read-dirs -> This enables directory read and file metadata related commands without any pre-configured accessible paths.", "description": "fs:read-dirs -> This enables directory read and file metadata related commands without any pre-configured accessible paths.",
"type": "string", "type": "string",
@@ -4783,7 +4814,7 @@
] ]
}, },
{ {
"description": "http:default -> Allows all fetch operations", "description": "http:default -> This permission set configures what kind of\nfetch operations are available from the http plugin.\n\nThis enables all fetch operations but does not\nallow explicitly any origins to be fetched. This needs to\nbe manually configured before usage.\n\n#### Granted Permissions\n\nAll fetch operations are enabled.\n\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"http:default" "http:default"
@@ -5238,12 +5269,61 @@
] ]
}, },
{ {
"description": "notification:default -> Allows requesting permission, checking permission state and sending notifications", "description": "notification:default -> This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"notification:default" "notification:default"
] ]
}, },
{
"description": "notification:allow-batch -> Enables the batch command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-batch"
]
},
{
"description": "notification:allow-cancel -> Enables the cancel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-cancel"
]
},
{
"description": "notification:allow-check-permissions -> Enables the check_permissions command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-check-permissions"
]
},
{
"description": "notification:allow-create-channel -> Enables the create_channel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-create-channel"
]
},
{
"description": "notification:allow-delete-channel -> Enables the delete_channel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-delete-channel"
]
},
{
"description": "notification:allow-get-active -> Enables the get_active command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-get-active"
]
},
{
"description": "notification:allow-get-pending -> Enables the get_pending command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-get-pending"
]
},
{ {
"description": "notification:allow-is-permission-granted -> Enables the is_permission_granted command without any pre-configured scope.", "description": "notification:allow-is-permission-granted -> Enables the is_permission_granted command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5251,6 +5331,13 @@
"notification:allow-is-permission-granted" "notification:allow-is-permission-granted"
] ]
}, },
{
"description": "notification:allow-list-channels -> Enables the list_channels command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-list-channels"
]
},
{ {
"description": "notification:allow-notify -> Enables the notify command without any pre-configured scope.", "description": "notification:allow-notify -> Enables the notify command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5258,6 +5345,13 @@
"notification:allow-notify" "notification:allow-notify"
] ]
}, },
{
"description": "notification:allow-permission-state -> Enables the permission_state command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-permission-state"
]
},
{ {
"description": "notification:allow-register-action-types -> Enables the register_action_types command without any pre-configured scope.", "description": "notification:allow-register-action-types -> Enables the register_action_types command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5272,6 +5366,13 @@
"notification:allow-register-listener" "notification:allow-register-listener"
] ]
}, },
{
"description": "notification:allow-remove-active -> Enables the remove_active command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-remove-active"
]
},
{ {
"description": "notification:allow-request-permission -> Enables the request_permission command without any pre-configured scope.", "description": "notification:allow-request-permission -> Enables the request_permission command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5279,6 +5380,62 @@
"notification:allow-request-permission" "notification:allow-request-permission"
] ]
}, },
{
"description": "notification:allow-show -> Enables the show command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-show"
]
},
{
"description": "notification:deny-batch -> Denies the batch command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-batch"
]
},
{
"description": "notification:deny-cancel -> Denies the cancel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-cancel"
]
},
{
"description": "notification:deny-check-permissions -> Denies the check_permissions command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-check-permissions"
]
},
{
"description": "notification:deny-create-channel -> Denies the create_channel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-create-channel"
]
},
{
"description": "notification:deny-delete-channel -> Denies the delete_channel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-delete-channel"
]
},
{
"description": "notification:deny-get-active -> Denies the get_active command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-get-active"
]
},
{
"description": "notification:deny-get-pending -> Denies the get_pending command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-get-pending"
]
},
{ {
"description": "notification:deny-is-permission-granted -> Denies the is_permission_granted command without any pre-configured scope.", "description": "notification:deny-is-permission-granted -> Denies the is_permission_granted command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5286,6 +5443,13 @@
"notification:deny-is-permission-granted" "notification:deny-is-permission-granted"
] ]
}, },
{
"description": "notification:deny-list-channels -> Denies the list_channels command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-list-channels"
]
},
{ {
"description": "notification:deny-notify -> Denies the notify command without any pre-configured scope.", "description": "notification:deny-notify -> Denies the notify command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5293,6 +5457,13 @@
"notification:deny-notify" "notification:deny-notify"
] ]
}, },
{
"description": "notification:deny-permission-state -> Denies the permission_state command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-permission-state"
]
},
{ {
"description": "notification:deny-register-action-types -> Denies the register_action_types command without any pre-configured scope.", "description": "notification:deny-register-action-types -> Denies the register_action_types command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5307,6 +5478,13 @@
"notification:deny-register-listener" "notification:deny-register-listener"
] ]
}, },
{
"description": "notification:deny-remove-active -> Denies the remove_active command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-remove-active"
]
},
{ {
"description": "notification:deny-request-permission -> Denies the request_permission command without any pre-configured scope.", "description": "notification:deny-request-permission -> Denies the request_permission command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5315,6 +5493,14 @@
] ]
}, },
{ {
"description": "notification:deny-show -> Denies the show command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-show"
]
},
{
"description": "os:default -> This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"os:default" "os:default"
@@ -5552,6 +5738,7 @@
] ]
}, },
{ {
"description": "process:default -> This permission set configures which\nprocess feeatures are by default exposed.\n\n#### Granted Permissions\n\nThis enables to quit via `allow-exit` and restart via `allow-restart`\nthe application.\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"process:default" "process:default"
@@ -5607,6 +5794,7 @@
] ]
}, },
{ {
"description": "shell:default -> This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"shell:default" "shell:default"
@@ -5878,7 +6066,7 @@
] ]
}, },
{ {
"description": "updater:default -> Allows checking for new updates and installing them", "description": "updater:default -> This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"updater:default" "updater:default"
@@ -5941,6 +6129,7 @@
] ]
}, },
{ {
"description": "upload:default -> This permission set configures what kind of\noperations are available from the upload plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n",
"type": "string", "type": "string",
"enum": [ "enum": [
"upload:default" "upload:default"
@@ -7037,6 +7226,55 @@
"enum": [ "enum": [
"window:deny-unminimize" "window:deny-unminimize"
] ]
},
{
"description": "window-state:default -> This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n",
"type": "string",
"enum": [
"window-state:default"
]
},
{
"description": "window-state:allow-filename -> Enables the filename command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:allow-filename"
]
},
{
"description": "window-state:allow-restore-state -> Enables the restore_state command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:allow-restore-state"
]
},
{
"description": "window-state:allow-save-window-state -> Enables the save_window_state command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:allow-save-window-state"
]
},
{
"description": "window-state:deny-filename -> Denies the filename command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:deny-filename"
]
},
{
"description": "window-state:deny-restore-state -> Denies the restore_state command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:deny-restore-state"
]
},
{
"description": "window-state:deny-save-window-state -> Denies the save_window_state command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:deny-save-window-state"
]
} }
] ]
}, },

View File

@@ -71,6 +71,10 @@ impl Default for Settings {
} }
} }
pub const FETCH_LIMIT: usize = 20;
pub const NEWSFEED_NEG_LIMIT: usize = 256;
pub const NOTIFICATION_NEG_LIMIT: usize = 64;
fn main() { fn main() {
let mut ctx = tauri::generate_context!(); let mut ctx = tauri::generate_context!();
@@ -113,9 +117,9 @@ fn main() {
nostr::event::get_event_from, nostr::event::get_event_from,
nostr::event::get_replies, nostr::event::get_replies,
nostr::event::listen_event_reply, nostr::event::listen_event_reply,
nostr::event::unlisten_event_reply,
nostr::event::get_events_by, nostr::event::get_events_by,
nostr::event::get_local_events, nostr::event::get_local_events,
nostr::event::listen_local_event,
nostr::event::get_group_events, nostr::event::get_group_events,
nostr::event::get_global_events, nostr::event::get_global_events,
nostr::event::get_hashtag_events, nostr::event::get_hashtag_events,
@@ -124,6 +128,7 @@ fn main() {
nostr::event::repost, nostr::event::repost,
nostr::event::event_to_bech32, nostr::event::event_to_bech32,
nostr::event::user_to_bech32, nostr::event::user_to_bech32,
nostr::event::unlisten,
commands::folder::show_in_folder, commands::folder::show_in_folder,
commands::window::create_column, commands::window::create_column,
commands::window::close_column, commands::window::close_column,
@@ -263,6 +268,11 @@ fn main() {
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_upload::init()) .plugin(tauri_plugin_upload::init())
.plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_updater::Builder::new().build())
.plugin(
tauri_plugin_window_state::Builder::new()
.with_denylist(&["panel"])
.build(),
)
.invoke_handler(invoke_handler) .invoke_handler(invoke_handler)
.build(ctx) .build(ctx)
.expect("error while running tauri application") .expect("error while running tauri application")

View File

@@ -7,7 +7,7 @@ use specta::Type;
use tauri::State; use tauri::State;
use crate::nostr::utils::{create_event_tags, dedup_event, parse_event, Meta}; use crate::nostr::utils::{create_event_tags, dedup_event, parse_event, Meta};
use crate::Nostr; use crate::{Nostr, FETCH_LIMIT};
#[derive(Debug, Clone, Serialize, Type)] #[derive(Debug, Clone, Serialize, Type)]
pub struct RichEvent { pub struct RichEvent {
@@ -197,18 +197,6 @@ pub async fn listen_event_reply(id: &str, state: State<'_, Nostr>) -> Result<(),
Ok(()) Ok(())
} }
#[tauri::command]
#[specta::specta]
pub async fn unlisten_event_reply(id: &str, state: State<'_, Nostr>) -> Result<(), ()> {
let client = &state.client;
let sub_id = SubscriptionId::new(id);
// Remove subscription
client.unsubscribe(sub_id).await;
Ok(())
}
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn get_events_by( pub async fn get_events_by(
@@ -227,7 +215,7 @@ pub async fn get_events_by(
let filter = Filter::new() let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost]) .kinds(vec![Kind::TextNote, Kind::Repost])
.author(author) .author(author)
.limit(20) .limit(FETCH_LIMIT)
.until(until); .until(until);
match client.get_events_of(vec![filter], None).await { match client.get_events_of(vec![filter], None).await {
@@ -275,17 +263,13 @@ pub async fn get_local_events(
let filter = Filter::new() let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost]) .kinds(vec![Kind::TextNote, Kind::Repost])
.limit(20) .limit(64)
.until(as_of) .until(as_of)
.authors(authors); .authors(authors);
match client match client.database().query(vec![filter], Order::Desc).await {
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
.await
{
Ok(events) => { Ok(events) => {
let dedup = dedup_event(&events); let dedup = dedup_event(&events);
let futures = dedup.into_iter().map(|ev| async move { let futures = dedup.into_iter().map(|ev| async move {
let raw = ev.as_json(); let raw = ev.as_json();
let parsed = if ev.kind == Kind::TextNote { let parsed = if ev.kind == Kind::TextNote {
@@ -296,7 +280,6 @@ pub async fn get_local_events(
RichEvent { raw, parsed } RichEvent { raw, parsed }
}); });
let rich_events = join_all(futures).await; let rich_events = join_all(futures).await;
Ok(rich_events) Ok(rich_events)
@@ -305,6 +288,31 @@ pub async fn get_local_events(
} }
} }
#[tauri::command]
#[specta::specta]
pub async fn listen_local_event(label: &str, state: State<'_, Nostr>) -> Result<(), String> {
let client = &state.client;
let contact_list = state
.contact_list
.lock()
.map_err(|err| err.to_string())?
.clone();
let authors: Vec<PublicKey> = contact_list.into_iter().map(|f| f.public_key).collect();
let sub_id = SubscriptionId::new(label);
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.authors(authors)
.since(Timestamp::now());
// Subscribe
client.subscribe_with_id(sub_id, vec![filter], None).await;
Ok(())
}
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn get_group_events( pub async fn get_group_events(
@@ -332,7 +340,7 @@ pub async fn get_group_events(
let filter = Filter::new() let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost]) .kinds(vec![Kind::TextNote, Kind::Repost])
.limit(20) .limit(FETCH_LIMIT)
.until(as_of) .until(as_of)
.authors(authors); .authors(authors);
@@ -376,7 +384,7 @@ pub async fn get_global_events(
let filter = Filter::new() let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost]) .kinds(vec![Kind::TextNote, Kind::Repost])
.limit(20) .limit(FETCH_LIMIT)
.until(as_of); .until(as_of);
match client match client
@@ -417,7 +425,7 @@ pub async fn get_hashtag_events(
}; };
let filter = Filter::new() let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost]) .kinds(vec![Kind::TextNote, Kind::Repost])
.limit(20) .limit(FETCH_LIMIT)
.until(as_of) .until(as_of)
.hashtags(hashtags); .hashtags(hashtags);
@@ -663,3 +671,15 @@ pub async fn user_to_bech32(user: &str, state: State<'_, Nostr>) -> Result<Strin
}, },
} }
} }
#[tauri::command]
#[specta::specta]
pub async fn unlisten(id: &str, state: State<'_, Nostr>) -> Result<(), ()> {
let client = &state.client;
let sub_id = SubscriptionId::new(id);
// Remove subscription
client.unsubscribe(sub_id).await;
Ok(())
}

View File

@@ -10,7 +10,7 @@ use tauri_plugin_notification::NotificationExt;
use crate::nostr::event::RichEvent; use crate::nostr::event::RichEvent;
use crate::nostr::utils::parse_event; use crate::nostr::utils::parse_event;
use crate::{Nostr, Settings}; use crate::{Nostr, Settings, NEWSFEED_NEG_LIMIT, NOTIFICATION_NEG_LIMIT};
#[derive(Serialize, Type)] #[derive(Serialize, Type)]
pub struct Account { pub struct Account {
@@ -242,8 +242,9 @@ pub async fn load_account(
}; };
// Get user's contact list // Get user's contact list
let contacts = client.get_contact_list(None).await.unwrap(); if let Ok(contacts) = client.get_contact_list(None).await {
*state.contact_list.lock().unwrap() = contacts; *state.contact_list.lock().unwrap() = contacts
};
// Create a subscription for notification // Create a subscription for notification
let sub_id = SubscriptionId::new("notification"); let sub_id = SubscriptionId::new("notification");
@@ -299,8 +300,9 @@ pub async fn load_account(
let window = handle.get_window("main").unwrap(); let window = handle.get_window("main").unwrap();
let state = window.state::<Nostr>(); let state = window.state::<Nostr>();
let client = &state.client; let client = &state.client;
let contact_list = state.contact_list.lock().unwrap().clone();
let filter = Filter::new() let notification = Filter::new()
.pubkey(public_key) .pubkey(public_key)
.kinds(vec![ .kinds(vec![
Kind::TextNote, Kind::TextNote,
@@ -308,11 +310,39 @@ pub async fn load_account(
Kind::Reaction, Kind::Reaction,
Kind::ZapReceipt, Kind::ZapReceipt,
]) ])
.limit(500); .limit(NOTIFICATION_NEG_LIMIT);
match client.reconcile(filter, NegentropyOptions::default()).await { match client
Ok(_) => println!("Sync notification done."), .reconcile(notification, NegentropyOptions::default())
.await
{
Ok(_) => {
if handle.emit_to(EventTarget::Any, "synced", true).is_err() {
println!("Emit event failed.")
}
}
Err(_) => println!("Sync notification failed."), Err(_) => println!("Sync notification failed."),
};
if !contact_list.is_empty() {
let authors: Vec<PublicKey> = contact_list.into_iter().map(|f| f.public_key).collect();
let newsfeed = Filter::new()
.authors(authors)
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(NEWSFEED_NEG_LIMIT);
match client
.reconcile(newsfeed, NegentropyOptions::default())
.await
{
Ok(_) => {
if handle.emit_to(EventTarget::Any, "synced", true).is_err() {
println!("Emit event failed.")
}
}
Err(_) => println!("Sync newsfeed failed."),
}
} }
}); });
@@ -324,7 +354,7 @@ pub async fn load_account(
let client = &state.client; let client = &state.client;
// Handle notifications // Handle notifications
if client client
.handle_notifications(|notification| async { .handle_notifications(|notification| async {
if let RelayPoolNotification::Message { message, .. } = notification { if let RelayPoolNotification::Message { message, .. } = notification {
if let RelayMessage::Event { if let RelayMessage::Event {
@@ -415,6 +445,24 @@ pub async fn load_account(
{ {
println!("Emit new notification failed.") println!("Emit new notification failed.")
} }
} else if id.starts_with("column-") {
let raw = event.as_json();
let parsed = if event.kind == Kind::TextNote {
Some(parse_event(&event.content).await)
} else {
None
};
if app
.emit_to(
EventTarget::window(id),
"new_event",
RichEvent { raw, parsed },
)
.is_err()
{
println!("Emit new notification failed.")
}
} else { } else {
println!("new event: {}", event.as_json()) println!("new event: {}", event.as_json())
} }
@@ -425,10 +473,6 @@ pub async fn load_account(
Ok(false) Ok(false)
}) })
.await .await
.is_ok()
{
print!("Listing for new event...");
}
}); });
Ok(true) Ok(true)

View File

@@ -187,13 +187,15 @@ pub async fn is_contact_list_empty(state: State<'_, Nostr>) -> Result<bool, ()>
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn check_contact(hex: &str, state: State<'_, Nostr>) -> Result<bool, ()> { pub async fn check_contact(hex: &str, state: State<'_, Nostr>) -> Result<bool, String> {
let contact_list = state.contact_list.lock().unwrap(); let contact_list = state.contact_list.lock().unwrap();
let public_key = PublicKey::from_str(hex).unwrap();
match contact_list.iter().position(|x| x.public_key == public_key) { match PublicKey::from_str(hex) {
Some(_) => Ok(true), Ok(public_key) => match contact_list.iter().position(|x| x.public_key == public_key) {
None => Ok(false), Some(_) => Ok(true),
None => Ok(false),
},
Err(err) => Err(err.to_string()),
} }
} }

View File

@@ -1,35 +1,39 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/schema.json", "$schema": "../node_modules/@tauri-apps/cli/schema.json",
"app": { "app": {
"windows": [ "windows": [
{ {
"title": "Lume", "title": "Lume",
"label": "main", "label": "main",
"titleBarStyle": "Overlay", "titleBarStyle": "Overlay",
"width": 500, "width": 1045,
"height": 800, "height": 800,
"minWidth": 500, "minWidth": 500,
"minHeight": 800, "minHeight": 800,
"hiddenTitle": true, "hiddenTitle": true,
"windowEffects": { "windowEffects": {
"state": "followsWindowActiveState", "state": "followsWindowActiveState",
"effects": ["underWindowBackground"] "effects": [
} "underWindowBackground"
}, ]
{ }
"title": "Lume Panel", },
"label": "panel", {
"url": "/panel", "title": "Lume Panel",
"width": 350, "label": "panel",
"height": 500, "url": "/panel",
"fullscreen": false, "width": 350,
"resizable": false, "height": 500,
"visible": false, "fullscreen": false,
"decorations": false, "resizable": false,
"windowEffects": { "visible": false,
"effects": ["popover"] "decorations": false,
} "windowEffects": {
} "effects": [
] "popover"
} ]
}
}
]
}
} }