Compare commits

...
18 Commits
Author SHA1 Message Date
reya 72da83d648 feat: add force quit command 2024-07-02 17:24:55 +07:00
reya 783a4538a4 Revert "chore: update ci"
This reverts commit 04706a6d7c.
2024-07-02 15:54:00 +07:00
reya 15e62cad11 fix: temporary include devtools in release build 2024-07-02 14:58:00 +07:00
reya c52b20ca80 chore: bump version 2024-07-02 14:36:16 +07:00
reya 04706a6d7c chore: update ci 2024-07-02 14:07:12 +07:00
reya 0755cbeb6c chore: bump version 2024-07-02 13:03:45 +07:00
雨宮蓮andGitHub 8eb01c8bbf Improve column management (#221)
* wip: redesign store

* feat: update trending column

* feat: add more functions
2024-07-02 12:51:50 +07:00
reya ed4f89ff66 feat: add option to toggle window transparent 2024-07-02 08:49:52 +07:00
reya d9fe647f8e feat: few improvements 2024-07-01 14:41:33 +07:00
reya 843c2d52e7 refactor: tray panel 2024-07-01 13:04:32 +07:00
reya 017a3676a4 feat: optimize spawn thread 2024-06-30 21:06:04 +07:00
reya fcb70c0e9a chore: update deps 2024-06-30 14:37:26 +07:00
雨宮蓮andGitHub 0fec21b9ce 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
2024-06-30 14:26:02 +07:00
XIAO YUandGitHub 968b1ada94 refactor: improve relay management code structure (#220) 2024-06-29 07:41:16 +07:00
reya 5c9b599b1e feat: use memo for some components 2024-06-26 17:49:36 +07:00
雨宮蓮andGitHub 717c3e17df Event Subscriptions (#218)
* feat: improve create column command

* refactor: thread

* feat: add window virtualized to event screen

* chore: update deps

* fix: window decoration

* feat: improve mention ntoe

* feat: add subscription to event screen
2024-06-26 14:51:50 +07:00
XIAO YUandGitHub a4540a0802 refactor: improve error handling in event.rs (#217) 2024-06-26 07:57:53 +07:00
reya 31bacc2646 chore: remove unused code 2024-06-24 11:09:32 +07:00
104 changed files with 8174 additions and 6551 deletions
+11 -11
View File
@@ -1,14 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lume Desktop</title>
</head>
<body
class="relative h-screen w-screen cursor-default select-none overflow-hidden font-sans text-black antialiased dark:text-white"
>
<div id="root" class="h-full w-full"></div>
<script type="module" src="/src/app.tsx"></script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lume Desktop</title>
</head>
<body
class="relative h-screen w-screen cursor-default select-none overflow-hidden font-sans text-black antialiased dark:text-white"
>
<div id="root" class="h-full w-full"></div>
<script type="module" src="/src/app.tsx"></script>
</body>
</html>
+14 -14
View File
@@ -14,16 +14,16 @@
"@lume/system": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/query-persist-client-core": "^5.45.0",
"@tanstack/react-query": "^5.45.0",
"@tanstack/react-router": "^1.38.1",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@tanstack/query-persist-client-core": "^5.49.1",
"@tanstack/react-query": "^5.49.2",
"@tanstack/react-router": "^1.43.3",
"embla-carousel-react": "^8.1.5",
"i18next": "^23.11.5",
"i18next-resources-to-backend": "^1.2.1",
@@ -45,16 +45,16 @@
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@tanstack/router-devtools": "^1.38.1",
"@tanstack/router-vite-plugin": "^1.38.0",
"@tanstack/router-devtools": "^1.43.3",
"@tanstack/router-vite-plugin": "^1.43.1",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.7.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"postcss": "^8.4.39",
"tailwindcss": "^3.4.4",
"typescript": "^5.4.5",
"vite": "^5.3.1",
"typescript": "^5.5.2",
"vite": "^5.3.2",
"vite-plugin-top-level-await": "^1.4.1",
"vite-tsconfig-paths": "^4.3.2"
}
+6 -17
View File
@@ -1,30 +1,19 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { RouterProvider, createRouter } from "@tanstack/react-router";
import React, { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import { I18nextProvider } from "react-i18next";
import "./app.css";
import { type } from "@tauri-apps/plugin-os";
import i18n from "./locale";
import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import { routeTree } from "./router.gen"; // auto generated file
import "./app.css";
const queryClient = new QueryClient();
const os = await type();
// Set up a Router instance
const platform = type();
const router = createRouter({
routeTree,
context: {
queryClient,
platform: os,
},
context: { queryClient, platform },
Wrap: ({ children }) => {
return (
<I18nextProvider i18n={i18n} defaultNS={"translation"}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</I18nextProvider>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
},
});
-41
View File
@@ -1,41 +0,0 @@
import { User } from "@/components/user";
import { NostrAccount } from "@lume/system";
import { getBitcoinDisplayValues } from "@lume/utils";
import { useEffect, useMemo, useState } from "react";
export function Balance({ account }: { account: string }) {
const [balance, setBalance] = useState(0);
const value = useMemo(() => getBitcoinDisplayValues(balance), [balance]);
useEffect(() => {
async function getBalance() {
const val = await NostrAccount.getBalance();
setBalance(val);
}
getBalance();
}, []);
return (
<div
data-tauri-drag-region
className="flex h-16 items-center justify-end px-3"
>
<div className="flex items-center gap-2">
<div className="text-end">
<div className="text-sm leading-tight text-neutral-700 dark:text-neutral-300">
Your balance
</div>
<div className="font-medium leading-tight">
{value.bitcoinFormatted}
</div>
</div>
<User.Provider pubkey={account}>
<User.Root>
<User.Avatar className="size-9 rounded-full" />
</User.Root>
</User.Provider>
</div>
</div>
);
}
+77 -28
View File
@@ -1,17 +1,17 @@
import { CancelIcon, CheckIcon } from "@lume/icons";
import { CheckIcon, HorizontalDotsIcon } from "@lume/icons";
import type { LumeColumn } from "@lume/types";
import { cn } from "@lume/utils";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
import { getCurrent } from "@tauri-apps/api/webviewWindow";
import { useCallback, useEffect, useRef, useState } from "react";
import { memo, useCallback, useEffect, useRef, useState } from "react";
type WindowEvent = {
scroll: boolean;
resize: boolean;
};
export function Column({
export const Column = memo(function Column({
column,
account,
}: {
@@ -44,7 +44,7 @@ export function Column({
useEffect(() => {
if (!isCreated) return;
const unlisten = listen<WindowEvent>("child-webview", (data) => {
const unlisten = listen<WindowEvent>("child_webview", (data) => {
if (data.payload.scroll) repositionWebview();
if (data.payload.resize) repositionWebview().then(() => resizeWebview());
});
@@ -60,15 +60,17 @@ export function Column({
const rect = container.current.getBoundingClientRect();
const url = `${column.content}?account=${account}&label=${column.label}&name=${column.name}`;
// create new webview
invoke("create_column", {
const prop = {
label: webviewLabel,
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
url,
}).then(() => {
};
// create new webview
invoke("create_column", { column: prop }).then(() => {
console.log("created: ", webviewLabel);
setIsCreated(true);
});
@@ -82,23 +84,24 @@ export function Column({
}, [account]);
return (
<div className="h-full w-[500px] shrink-0 p-2">
<div
className={cn(
"flex flex-col w-full h-full rounded-xl",
column.label !== "open"
? "bg-black/5 dark:bg-white/5 backdrop-blur-sm"
: "",
)}
>
<Header label={column.label} name={column.name} />
<div className="h-full w-[480px] shrink-0 p-2">
<div className="flex flex-col w-full h-full rounded-xl bg-black/5 dark:bg-white/10">
<Header
label={column.label}
webview={webviewLabel}
name={column.name}
/>
<div ref={container} className="flex-1 w-full h-full" />
</div>
</div>
);
}
});
function Header({ label, name }: { label: string; name: string }) {
function Header({
label,
webview,
name,
}: { label: string; webview: string; name: string }) {
const [title, setTitle] = useState(name);
const [isChanged, setIsChanged] = useState(false);
@@ -114,10 +117,56 @@ function Header({ label, name }: { label: string; name: string }) {
setIsChanged(false);
};
const close = async () => {
const mainWindow = getCurrent();
await mainWindow.emit("columns", { type: "remove", label });
};
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
e.preventDefault();
const menuItems = await Promise.all([
MenuItem.new({
text: "Reload",
action: async () => {
await invoke("reload_column", { label: webview });
},
}),
MenuItem.new({
text: "Open in new window",
action: () => console.log("not implemented."),
}),
PredefinedMenuItem.new({ item: "Separator" }),
MenuItem.new({
text: "Move left",
action: async () => {
await getCurrent().emit("columns", {
type: "move",
label,
direction: "left",
});
},
}),
MenuItem.new({
text: "Move right",
action: async () => {
await getCurrent().emit("columns", {
type: "move",
label,
direction: "right",
});
},
}),
PredefinedMenuItem.new({ item: "Separator" }),
MenuItem.new({
text: "Close",
action: async () => {
await getCurrent().emit("columns", { type: "remove", label });
},
}),
]);
const menu = await Menu.new({
items: menuItems,
});
await menu.popup().catch((e) => console.error(e));
}, []);
useEffect(() => {
if (title.length !== name.length) setIsChanged(true);
@@ -126,7 +175,7 @@ function Header({ label, name }: { label: string; name: string }) {
return (
<div className="flex items-center justify-between w-full px-1 h-9 shrink-0">
<div className="size-7" />
<div className="flex items-center justify-center shrink-0 h-9">
<div className="flex items-center justify-center shrink-0 h-7">
<div className="relative flex items-center gap-2">
<div
contentEditable
@@ -149,10 +198,10 @@ function Header({ label, name }: { label: string; name: string }) {
</div>
<button
type="button"
onClick={() => close()}
className="inline-flex items-center justify-center rounded-lg size-7 hover:bg-black/10 dark:hover:bg-white/10 text-neutral-600 dark:text-neutral-400 hover:text-neutral-800 dark:hover:text-neutral-200"
onClick={(e) => showContextMenu(e)}
className="inline-flex items-center justify-center rounded-lg size-7 hover:bg-black/10 dark:hover:bg-white/10"
>
<CancelIcon className="size-4" />
<HorizontalDotsIcon className="size-5" />
</button>
</div>
);
@@ -1,10 +1,10 @@
import { ThreadIcon } from "@lume/icons";
import { Note } from "@/components/note";
import { cn } from "@lume/utils";
import { ThreadIcon } from "@lume/icons";
import type { LumeEvent } from "@lume/system";
import { useMemo } from "react";
import { cn } from "@lume/utils";
import { memo, useMemo } from "react";
export function Conversation({
export const Conversation = memo(function Conversation({
event,
className,
}: {
@@ -17,7 +17,7 @@ export function Conversation({
<Note.Provider event={event}>
<Note.Root
className={cn(
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50",
"bg-white dark:bg-black/20 rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50",
className,
)}
>
@@ -44,4 +44,4 @@ export function Conversation({
</Note.Root>
</Note.Provider>
);
}
});
@@ -17,7 +17,7 @@ export function NoteReply({ large = false }: { large?: boolean }) {
className={cn(
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
large
? "rounded-full bg-neutral-100 dark:bg-white/10 h-7 gap-1.5 w-24 text-sm font-medium hover:text-blue-500 hover:bg-neutral-200 dark:hover:bg-white/20"
? "rounded-full h-7 gap-1.5 w-20 text-sm font-medium hover:bg-black/10 dark:hover:bg-white/10"
: "size-7",
)}
>
@@ -64,9 +64,9 @@ export function NoteRepost({ large = false }: { large?: boolean }) {
type="button"
onClick={(e) => showContextMenu(e)}
className={cn(
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200 rounded-full",
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
large
? "bg-neutral-100 dark:bg-white/10 h-7 gap-1.5 w-24 text-sm font-medium hover:text-blue-500 hover:bg-neutral-200 dark:hover:bg-white/20"
? "rounded-full h-7 gap-1.5 w-24 text-sm font-medium hover:bg-black/10 dark:hover:bg-white/10"
: "size-7",
)}
>
@@ -13,11 +13,11 @@ export function NoteZap({ large = false }: { large?: boolean }) {
return (
<button
type="button"
onClick={() => LumeWindow.openZap(event.id, event.pubkey)}
onClick={() => LumeWindow.openZap(event.id)}
className={cn(
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
large
? "rounded-full bg-neutral-100 dark:bg-white/10 h-7 gap-1.5 w-24 text-sm font-medium hover:text-blue-500 hover:bg-neutral-200 dark:hover:bg-white/20"
? "rounded-full h-7 gap-1.5 w-20 text-sm font-medium hover:bg-black/10 dark:hover:bg-white/10"
: "size-7",
)}
>
@@ -97,7 +97,7 @@ export function NoteContent({
return (
<div className="relative flex flex-col gap-2">
{blurred ? (
<div className="absolute inset-0 z-10 flex items-center justify-center w-full h-full bg-black/80 backdrop-blur-xl">
<div className="absolute inset-0 z-10 flex items-center justify-center w-full h-full bg-black/80 backdrop-blur-lg">
<div className="flex flex-col items-center justify-center gap-2 text-center">
<p className="text-sm text-white/60">
The content is hidden because the author
@@ -118,7 +118,7 @@ export function NoteContent({
<div
className={cn(
"select-text text-pretty content-break overflow-hidden",
event.meta?.content.length > 400
event.meta?.content.length > 500
? "max-h-[250px] gradient-mask-b-0"
: "",
className,
@@ -1,8 +1,6 @@
import { LumeWindow, useEvent } from "@lume/system";
import { LinkIcon } from "@lume/icons";
import { useTranslation } from "react-i18next";
import { cn } from "@lume/utils";
import { User } from "@/components/user";
import { LinkIcon } from "@lume/icons";
import { LumeWindow, useEvent } from "@lume/system";
import { Spinner } from "@lume/ui";
export function MentionNote({
@@ -12,65 +10,67 @@ export function MentionNote({
eventId: string;
openable?: boolean;
}) {
const { t } = useTranslation();
const { isLoading, isError, data } = useEvent(eventId);
if (isLoading) {
return (
<div className="flex items-center justify-center w-full h-20 mt-2 border rounded-xl border-black/10 dark:border-white/10">
<Spinner className="size-5" />
<div className="py-2">
<div className="pl-4 py-3 flex flex-col w-full border-l-2 border-black/5 dark:border-white/5">
<Spinner className="size-5" />
</div>
</div>
);
}
if (isError || !data) {
return (
<div className="w-full p-3 mt-2 border rounded-xl border-black/10 dark:border-white/10">
{t("note.error")}
<div className="py-2">
<div className="pl-4 py-3 flex flex-col w-full border-l-2 border-black/5 dark:border-white/5">
<p className="text-sm font-medium text-red-500">
Event not found with your current relay set
</p>
</div>
</div>
);
}
return (
<div className="flex flex-col w-full border rounded-lg cursor-default border-black/10 dark:border-white/10">
<User.Provider pubkey={data.pubkey}>
<User.Root className="flex items-center gap-2 px-3 h-11">
<User.Avatar className="object-cover rounded-full size-6 shrink-0" />
<div className="inline-flex items-center flex-1 gap-2">
<User.Name className="font-semibold text-neutral-900 dark:text-neutral-100" />
<span className="text-neutral-600 dark:text-neutral-400">·</span>
<User.Time
time={data.created_at}
className="text-neutral-600 dark:text-neutral-400"
/>
</div>
</User.Root>
</User.Provider>
<div
className={cn(
"px-3 select-text whitespace-normal text-pretty content-break leading-normal",
data.content.length > 400 ? "max-h-[150px] gradient-mask-b-0" : "",
)}
>
{data.content}
</div>
{openable ? (
<div className="flex items-center justify-end px-2 h-11">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
LumeWindow.openEvent(data);
}}
className="z-10 inline-flex items-center justify-center gap-1 text-sm rounded-full h-7 w-28 bg-black/10 dark:bg-white/10 text-neutral-600 hover:text-blue-500 dark:text-neutral-400"
>
View post
<LinkIcon className="size-4" />
</button>
<div className="py-2">
<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.Root className="flex items-center gap-2 h-8">
<User.Avatar className="rounded-full size-6" />
<div className="inline-flex items-center flex-1 gap-2">
<User.Name className="font-semibold text-neutral-900 dark:text-neutral-100" />
<span className="text-neutral-600 dark:text-neutral-400">·</span>
<User.Time
time={data.created_at}
className="text-neutral-600 dark:text-neutral-400"
/>
</div>
</User.Root>
</User.Provider>
<div className="select-text text-pretty line-clamp-3 content-break leading-normal">
{data.content}
</div>
) : (
<div className="h-3" />
)}
{openable ? (
<div className="flex items-center justify-start mt-3">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
LumeWindow.openEvent(data);
}}
className="inline-flex items-center gap-1 text-blue-500 text-sm"
>
View post
<LinkIcon className="size-3" />
</button>
</div>
) : (
<div className="h-3" />
)}
</div>
</div>
);
}
@@ -35,7 +35,7 @@ export function ImagePreview({ url }: { url: string }) {
loading="lazy"
decoding="async"
style={{ contentVisibility: "auto" }}
className="max-h-[600px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
className="max-h-[400px] max-w-[400px] h-auto w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
onClick={() => open(url)}
onKeyDown={() => open(url)}
onError={({ currentTarget }) => {
@@ -96,8 +96,8 @@ export function Images({ urls }: { urls: string[] }) {
}
return (
<div className="relative pl-2 overflow-hidden group">
<div ref={emblaRef} className="w-full">
<div className="relative px-3 overflow-hidden group">
<div ref={emblaRef} className="w-full h-[320px]">
<div className="flex w-full gap-2 scrollbar-none">
{imageUrls.map((url, index) => (
<LazyImage
@@ -109,10 +109,7 @@ export function Images({ urls }: { urls: string[] }) {
))}
</div>
</div>
<div
aria-hidden
className="absolute z-10 items-center justify-between hidden w-full px-5 transform -translate-x-1/2 -translate-y-1/2 group-hover:flex left-1/2 top-1/2"
>
<div className="absolute z-10 items-center justify-between hidden w-full px-5 transform -translate-x-1/2 -translate-y-1/2 group-hover:flex left-1/2 top-1/2">
<button
type="button"
disabled={!emblaApi?.canScrollPrev}
@@ -20,10 +20,11 @@ export function VideoPreview({ url }: { url: string }) {
<div className="my-1">
<video
className="max-h-[600px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
preload="metadata"
controls
muted
>
<source src={url} type="video/mp4" />
<source src={`${url}#t=0.1`} type="video/mp4" />
Your browser does not support the video tag.
</video>
</div>
@@ -1,36 +1,18 @@
import { Carousel, CarouselItem } from "@lume/ui";
export function Videos({ urls }: { urls: string[] }) {
if (urls.length === 1) {
return (
<div className="group px-3">
return (
<div className="group px-3">
{urls.map((url) => (
<video
className="w-full h-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
key={url}
className="max-h-[400px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
preload="metadata"
controls
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.
</video>
</div>
);
}
return (
<Carousel
items={urls}
renderItem={({ item, isSnapPoint }) => (
<CarouselItem key={item} isSnapPoint={isSnapPoint}>
<video
className="w-full h-full object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
controls={false}
muted
>
<source src={item} type="video/mp4" />
Your browser does not support the video tag.
</video>
</CarouselItem>
)}
/>
))}
</div>
);
}
+1 -1
View File
@@ -42,7 +42,7 @@ export function NoteUser({ className }: { className?: string }) {
onClick={(e) => showContextMenu(e)}
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>
<div className="flex items-center w-full gap-3">
<div className="flex items-center gap-1">
+6 -5
View File
@@ -1,9 +1,10 @@
import { QuoteIcon } from "@lume/icons";
import { Note } from "@/components/note";
import { cn } from "@lume/utils";
import { QuoteIcon } from "@lume/icons";
import type { LumeEvent } from "@lume/system";
import { cn } from "@lume/utils";
import { memo } from "react";
export function Quote({
export const Quote = memo(function Quote({
event,
className,
}: {
@@ -14,7 +15,7 @@ export function Quote({
<Note.Provider event={event}>
<Note.Root
className={cn(
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50",
"bg-white dark:bg-black/20 rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50",
className,
)}
>
@@ -40,4 +41,4 @@ export function Quote({
</Note.Root>
</Note.Provider>
);
}
});
+7 -6
View File
@@ -1,11 +1,12 @@
import { Spinner } from "@lume/ui";
import { Note } from "@/components/note";
import { User } from "@/components/user";
import { type LumeEvent, NostrQuery } from "@lume/system";
import { Spinner } from "@lume/ui";
import { cn } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
import { type LumeEvent, NostrQuery } from "@lume/system";
import { memo } from "react";
export function RepostNote({
export const RepostNote = memo(function RepostNote({
event,
className,
}: {
@@ -32,7 +33,7 @@ export function RepostNote({
return (
<Note.Root
className={cn(
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50",
"bg-white dark:bg-black/20 rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50",
className,
)}
>
@@ -68,7 +69,7 @@ export function RepostNote({
<div className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
Reposted by
</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.Provider>
</div>
@@ -78,4 +79,4 @@ export function RepostNote({
)}
</Note.Root>
);
}
});
+5 -4
View File
@@ -1,8 +1,9 @@
import { cn } from "@lume/utils";
import { Note } from "@/components/note";
import type { LumeEvent } from "@lume/system";
import { cn } from "@lume/utils";
import { memo } from "react";
export function TextNote({
export const TextNote = memo(function TextNote({
event,
className,
}: {
@@ -13,7 +14,7 @@ export function TextNote({
<Note.Provider event={event}>
<Note.Root
className={cn(
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50",
"bg-white dark:bg-black/20 rounded-xl shadow-primary dark:ring-1 dark:ring-white/5",
className,
)}
>
@@ -31,4 +32,4 @@ export function TextNote({
</Note.Root>
</Note.Provider>
);
}
});
+23 -12
View File
@@ -2,7 +2,6 @@ import { cn } from "@lume/utils";
import * as Avatar from "@radix-ui/react-avatar";
import { useRouteContext } from "@tanstack/react-router";
import { minidenticon } from "minidenticons";
import { nanoid } from "nanoid";
import { useMemo } from "react";
import { useUserContext } from "./provider";
@@ -22,22 +21,29 @@ export function UserAvatar({ className }: { className?: string }) {
}
}, [user.profile?.picture]);
const fallbackAvatar = useMemo(
const fallback = useMemo(
() =>
`data:image/svg+xml;utf8,${encodeURIComponent(
minidenticon(user.pubkey || nanoid(), 90, 50),
minidenticon(user.pubkey, 60, 50),
)}`,
[user.pubkey],
);
if (settings && !settings.display_avatar) {
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}>
<img
src={fallbackAvatar}
src={fallback}
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.Root>
@@ -45,19 +51,24 @@ export function UserAvatar({ className }: { className?: string }) {
}
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
src={picture}
alt={user.pubkey}
loading="eager"
loading="lazy"
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
src={fallbackAvatar}
src={fallback}
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.Root>
+52 -30
View File
@@ -1,6 +1,6 @@
import { Column } from "@/components/column";
import { Toolbar } from "@/components/toolbar";
import { ArrowLeftIcon, ArrowRightIcon, PlusSquareIcon } from "@lume/icons";
import { ArrowLeftIcon, ArrowRightIcon, PlusIcon } from "@lume/icons";
import { NostrQuery } from "@lume/system";
import type { ColumnEvent, LumeColumn } from "@lume/types";
import { createFileRoute } from "@tanstack/react-router";
@@ -38,23 +38,23 @@ function Screen() {
}, [emblaApi]);
const emitScrollEvent = useCallback(() => {
getCurrent().emit("child-webview", { scroll: true });
getCurrent().emit("child_webview", { scroll: true });
}, []);
const emitResizeEvent = useCallback(() => {
getCurrent().emit("child-webview", { resize: true, direction: "x" });
getCurrent().emit("child_webview", { resize: true, direction: "x" });
}, []);
const openLumeStore = useDebouncedCallback(async () => {
const openLumeStore = useCallback(async () => {
await getCurrent().emit("columns", {
type: "add",
column: {
label: "store",
name: "Store",
content: "/store/official",
name: "Column Gallery",
content: "/store",
},
});
}, 150);
}, []);
const add = useDebouncedCallback((column: LumeColumn) => {
column.label = `${column.label}-${nanoid()}`; // update col label
@@ -65,6 +65,23 @@ function Screen() {
setColumns((prev) => prev.filter((t) => t.label !== label));
}, 150);
const move = useDebouncedCallback(
(label: string, direction: "left" | "right") => {
const newCols = [...columns];
const col = newCols.find((el) => el.label === label);
const colIndex = newCols.findIndex((el) => el.label === label);
newCols.splice(colIndex, 1);
if (direction === "left") newCols.splice(colIndex - 1, 0, col);
if (direction === "right") newCols.splice(colIndex + 1, 0, col);
setColumns(newCols);
},
150,
);
const updateName = useDebouncedCallback((label: string, title: string) => {
const currentColIndex = columns.findIndex((col) => col.label === label);
@@ -135,6 +152,8 @@ function Screen() {
if (data.payload.type === "reset") reset();
if (data.payload.type === "add") add(data.payload.column);
if (data.payload.type === "remove") remove(data.payload.label);
if (data.payload.type === "move")
move(data.payload.label, data.payload.direction);
if (data.payload.type === "set_title")
updateName(data.payload.label, data.payload.title);
});
@@ -155,32 +174,35 @@ function Screen() {
account={account}
/>
))}
<div className="shrink-0 p-2 h-full w-[480px]">
<div className="size-full bg-black/5 dark:bg-white/5 rounded-xl flex items-center justify-center">
<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" />
Add Column
</button>
</div>
</div>
</div>
</div>
<Toolbar>
<div className="flex items-center h-8 gap-1 p-[2px] rounded-full bg-black/5 dark:bg-white/5">
<button
type="button"
onClick={() => scrollPrev()}
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" />
</button>
<button
type="button"
onClick={() => openLumeStore()}
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"
>
<PlusSquareIcon className="size-4" />
</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>
<button
type="button"
onClick={() => scrollPrev()}
className="inline-flex items-center justify-center rounded-full size-8 hover:bg-black/5 dark:hover:bg-white/5"
>
<ArrowLeftIcon className="size-4" />
</button>
<button
type="button"
onClick={() => scrollNext()}
className="inline-flex items-center justify-center rounded-full size-8 hover:bg-black/5 dark:hover:bg-white/5"
>
<ArrowRightIcon className="size-4" />
</button>
</Toolbar>
</div>
);
+159 -136
View File
@@ -1,91 +1,119 @@
import { User } from "@/components/user";
import { ComposeFilledIcon, HorizontalDotsIcon, PlusIcon } from "@lume/icons";
import { LumeWindow, NostrAccount } from "@lume/system";
import {
ChevronDownIcon,
ComposeFilledIcon,
PlusIcon,
SearchIcon,
} from "@lume/icons";
import { LumeWindow, NostrAccount, NostrQuery } from "@lume/system";
import { cn } from "@lume/utils";
import * as Popover from "@radix-ui/react-popover";
import { Outlet, createFileRoute } from "@tanstack/react-router";
import { Link } from "@tanstack/react-router";
import { Menu, MenuItem } from "@tauri-apps/api/menu";
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
import { getCurrent } from "@tauri-apps/api/window";
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")({
beforeLoad: async () => {
beforeLoad: async ({ params }) => {
const settings = await NostrQuery.getUserSettings();
const accounts = await NostrAccount.getAccounts();
return { accounts };
const otherAccounts = accounts.filter(
(account) => account !== params.account,
);
return { otherAccounts, settings };
},
component: Screen,
});
function Screen() {
const { platform } = Route.useRouteContext();
const { settings, platform } = Route.useRouteContext();
const openLumeStore = async () => {
await getCurrent().emit("columns", {
type: "add",
column: {
label: "store",
name: "Column Gallery",
content: "/store",
},
});
};
return (
<div className="flex flex-col w-screen h-screen">
<div
data-tauri-drag-region
className={cn(
"flex h-11 shrink-0 items-center justify-between pr-2",
platform === "macos" ? "ml-2 pl-20" : "pl-4",
)}
className="flex h-11 shrink-0 items-center justify-between px-3"
>
<div className="flex items-center gap-3">
<Accounts />
<Link
to="/landing"
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"
<div
data-tauri-drag-region
className={cn(
"flex-1 flex items-center gap-2",
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" />
</Link>
Column
</button>
<div id="toolbar" />
</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
type="button"
onClick={() => LumeWindow.openEditor()}
className="inline-flex items-center justify-center h-8 gap-1 px-3 text-sm font-medium text-white bg-blue-500 rounded-full w-max hover:bg-blue-600"
className="inline-flex items-center justify-center h-8 gap-1 px-3 text-sm font-medium bg-black/5 dark:bg-white/5 rounded-full w-max hover:bg-blue-500 hover:text-white"
>
<ComposeFilledIcon className="size-4" />
New Post
</button>
<div id="toolbar" />
<Accounts />
</div>
</div>
<div className="flex-1">
<div
className={cn(
"flex-1",
settings.vibrancy
? ""
: "bg-white dark:bg-black border-t border-black/20 dark:border-white/20",
)}
>
<Outlet />
</div>
</div>
);
}
function Accounts() {
const navigate = Route.useNavigate();
const { accounts } = Route.useRouteContext();
const Accounts = memo(function Accounts() {
const { otherAccounts } = Route.useRouteContext();
const { account } = Route.useParams();
const [windowWidth, setWindowWidth] = useState<number>(null);
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 navigate = Route.useNavigate();
const showContextMenu = useCallback(
async (e: React.MouseEvent, npub: string) => {
async (e: React.MouseEvent) => {
e.preventDefault();
const menuItems = await Promise.all([
MenuItem.new({
text: "New Post",
action: () => LumeWindow.openEditor(),
}),
PredefinedMenuItem.new({ item: "Separator" }),
MenuItem.new({
text: "View Profile",
action: () => LumeWindow.openProfile(npub),
action: () => LumeWindow.openProfile(account),
}),
MenuItem.new({
text: "Open Settings",
@@ -99,112 +127,107 @@ function Accounts() {
await menu.popup().catch((e) => console.error(e));
},
[],
[account],
);
const changeAccount = async (e: React.MouseEvent, npub: string) => {
if (npub === account) {
return showContextMenu(e, npub);
}
const changeAccount = useCallback(
async (npub: string) => {
// Change current account and update signer
const select = await NostrAccount.loadAccount(npub);
// Change current account and update signer
const select = await NostrAccount.loadAccount(npub);
if (select) {
// Reset current columns
await getCurrent().emit("columns", { type: "reset" });
if (select) {
// Reset current columns
await getCurrent().emit("columns", { type: "reset" });
// Redirect to new account
return navigate({
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 navigate({
to: "/$account/home",
params: { account: npub },
resetScroll: true,
replace: true,
});
} else {
await message("Something wrong.", { title: "Accounts", kind: "error" });
}
};
return (
<div data-tauri-drag-region className="hidden md:flex items-center gap-3">
{otherAccounts.map((npub) => (
<button key={npub} type="button" onClick={(e) => changeAccount(npub)}>
<User.Provider pubkey={npub}>
<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" />
</User.Root>
</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 { innerWidth: width, innerHeight: height } = window;
return {
width,
height,
};
};
const Search = memo(function Search() {
const [searchType, setSearchType] = useState<"notes" | "users">("notes");
const [query, setQuery] = useState("");
useEffect(() => {
function handleResize() {
setWindowWidth(getWindowDimensions().width);
}
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
e.preventDefault();
if (!windowWidth) {
setWindowWidth(getWindowDimensions().width);
}
const menuItems = await Promise.all([
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 () => {
window.removeEventListener("resize", handleResize);
};
await menu.popup().catch((e) => console.error(e));
}, []);
return (
<div data-tauri-drag-region className="flex items-center gap-3">
{sortedList
.slice(0, windowWidth > 500 ? account.length : 2)
.map((user) => (
<button
key={user}
type="button"
onClick={(e) => changeAccount(e, user)}
>
<User.Provider pubkey={user}>
<User.Root
className={cn(
"shrink-0 rounded-full transition-all ease-in-out duration-150 will-change-auto",
user === account
? "ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950"
: "",
)}
>
<User.Avatar
className={cn(
"aspect-square h-auto rounded-full object-cover transition-all ease-in-out duration-150 will-change-auto",
user === account ? "w-7" : "w-8",
)}
/>
</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 className="h-8 w-full px-3 text-sm rounded-full inline-flex items-center bg-black/5 dark:bg-white/5">
<button
type="button"
onClick={(e) => showContextMenu(e)}
className="inline-flex items-center gap-1 capitalize text-sm font-medium pr-2 border-r border-black/10 dark:border-white/10 text-black/50 dark:text-white/50"
>
{searchType}
<ChevronDownIcon className="size-3" />
</button>
<input
type="text"
name="search"
placeholder="Search..."
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
LumeWindow.openSearch(searchType, query);
}
}}
className="h-full w-full px-3 text-sm rounded-full border-none ring-0 focus:ring-0 focus:outline-none bg-transparent placeholder:text-black/50 dark:placeholder:text-white/50"
/>
<SearchIcon className="size-5" />
</div>
);
}
});
+2 -10
View File
@@ -1,19 +1,11 @@
import type { Settings } from "@lume/system";
import { Spinner } from "@lume/ui";
import type { QueryClient } from "@tanstack/react-query";
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
import type { Platform } from "@tauri-apps/plugin-os";
import type { OsType } from "@tauri-apps/plugin-os";
interface RouterContext {
// System
queryClient: QueryClient;
// App info
platform?: Platform;
locale?: string;
// Settings
settings?: Settings;
// Accounts
accounts?: string[];
platform: OsType;
}
export const Route = createRootRouteWithContext<RouterContext>()({
@@ -7,7 +7,6 @@ import { invoke } from "@tauri-apps/api/core";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { message } from "@tauri-apps/plugin-dialog";
import { useState } from "react";
import { useTranslation } from "react-i18next";
export const Route = createFileRoute("/auth/$account/backup")({
component: Screen,
@@ -15,20 +14,18 @@ export const Route = createFileRoute("/auth/$account/backup")({
function Screen() {
const { account } = Route.useParams();
const { t } = useTranslation();
const navigate = useNavigate();
const [key, setKey] = useState(null);
const [passphase, setPassphase] = useState("");
const [copied, setCopied] = useState(false);
const [loading, setLoading] = useState(false);
const [confirm, setConfirm] = useState({ c1: false, c2: false, c3: false });
const navigate = useNavigate();
const [confirm, setConfirm] = useState({ c1: false, c2: false });
const submit = async () => {
try {
if (key) {
if (!confirm.c1 || !confirm.c2 || !confirm.c3) {
if (!confirm.c1 || !confirm.c2) {
return await message("You need to confirm before continue", {
title: "Backup",
kind: "info",
@@ -137,7 +134,7 @@ function Screen() {
className="text-sm leading-none text-neutral-800 dark:text-neutral-200"
htmlFor="confirm1"
>
{t("backup.confirm1")}
I will make sure keep it safe and not sharing with anyone.
</label>
</div>
<div className="flex items-center gap-2">
@@ -157,27 +154,7 @@ function Screen() {
className="text-sm leading-none text-neutral-800 dark:text-neutral-200"
htmlFor="confirm2"
>
{t("backup.confirm2")}
</label>
</div>
<div className="flex items-center gap-2">
<Checkbox.Root
checked={confirm.c3}
onCheckedChange={() =>
setConfirm((state) => ({ ...state, c3: !state.c3 }))
}
className="flex items-center justify-center rounded-md outline-none appearance-none size-6 bg-neutral-100 dark:bg-white/10 dark:hover:bg-white/20"
id="confirm3"
>
<Checkbox.Indicator className="text-blue-500">
<CheckIcon className="size-4" />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="text-sm leading-none text-neutral-800 dark:text-neutral-200"
htmlFor="confirm3"
>
{t("backup.confirm3")}
I understand I cannot recover private key.
</label>
</div>
</div>
@@ -191,7 +168,7 @@ function Screen() {
disabled={loading}
className="inline-flex items-center justify-center w-full font-semibold text-white bg-blue-500 rounded-lg h-11 shrink-0 hover:bg-blue-600 disabled:opacity-50"
>
{loading ? <Spinner /> : t("global.continue")}
{loading ? <Spinner /> : "Continue"}
</button>
</div>
</div>
@@ -7,21 +7,18 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { message } from "@tauri-apps/plugin-dialog";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
export const Route = createFileRoute("/auth/create-profile")({
component: Screen,
loader: async () => {
const account = await NostrAccount.createAccount();
return account;
},
component: Screen,
});
function Screen() {
const account = Route.useLoaderData();
const navigate = useNavigate();
const { t } = useTranslation();
const { register, handleSubmit } = useForm();
const [picture, setPicture] = useState<string>("");
@@ -87,7 +84,7 @@ function Screen() {
>
<div className="flex flex-col gap-1">
<label htmlFor="display_name" className="font-medium">
{t("user.displayName")} *
Display Name *
</label>
<input
type={"text"}
@@ -99,7 +96,7 @@ function Screen() {
</div>
<div className="flex flex-col gap-1">
<label htmlFor="name" className="font-medium">
{t("user.name")}
Name
</label>
<input
type={"text"}
@@ -111,7 +108,7 @@ function Screen() {
</div>
<div className="flex flex-col gap-1">
<label htmlFor="about" className="font-medium">
{t("user.bio")}
Bio
</label>
<textarea
{...register("about")}
@@ -122,7 +119,7 @@ function Screen() {
</div>
<div className="flex flex-col gap-1">
<label htmlFor="website" className="font-medium">
{t("user.website")}
Website
</label>
<input
type="url"
@@ -136,7 +133,7 @@ function Screen() {
type="submit"
className="inline-flex items-center justify-center w-full mt-3 font-semibold text-white bg-blue-500 rounded-lg h-11 shrink-0 hover:bg-blue-600 disabled:opacity-50"
>
{loading ? <Spinner /> : t("global.continue")}
{loading ? <Spinner /> : "Continue"}
</button>
</form>
</div>
+32 -13
View File
@@ -28,6 +28,13 @@ function Screen() {
const onSubmit = async (data: { url: string; purpose: string }) => {
try {
if (!data.url.startsWith("wss://") || !data.url.startsWith("ws://")) {
return await message("Relay must be starts with wss:// or ws://", {
title: "Bootstrap Relays",
kind: "info",
});
}
const relay: Relay = { url: data.url, purpose: data.purpose };
setRelays((prev) => [...prev, relay]);
reset();
@@ -50,12 +57,21 @@ function Screen() {
}, [bootstrapRelays]);
return (
<div className="flex flex-col items-center justify-center w-screen h-screen">
<div className="w-full max-w-sm mx-auto lg:max-w-lg">
<div className="text-center h-11">
<h1 className="font-semibold">Customize Bootstrap Relays</h1>
<div
data-tauri-drag-region
className="relative flex flex-col items-center justify-between w-full h-full"
>
<div
data-tauri-drag-region
className="absolute top-0 left-0 h-14 w-full"
/>
<div className="flex items-end justify-center flex-1 w-full px-4 pb-10">
<div className="text-center">
<h2 className="text-2xl font-semibold">Customize Bootstrap Relays</h2>
</div>
<div className="flex flex-col w-full px-2 bg-white rounded-xl shadow-primary backdrop-blur-lg dark:bg-white/20 dark:ring-1 ring-neutral-800/50">
</div>
<div className="flex flex-col items-center flex-1 w-full">
<div className="flex flex-col w-full max-w-sm mx-auto p-3 overflow-hidden bg-white divide-y divide-neutral-100 dark:divide-white/5 rounded-xl shadow-primary dark:bg-white/10 dark:ring-1 ring-white/15">
{relays.map((relay) => (
<div
key={relay.url}
@@ -117,15 +133,18 @@ function Screen() {
</form>
</div>
</div>
<button
type="button"
onClick={() => save()}
disabled={isLoading}
className="inline-flex items-center justify-center w-full h-10 mt-4 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
>
{isLoading ? <Spinner /> : "Save & Relaunch"}
</button>
<div className="w-full max-w-sm mx-auto">
<button
type="button"
onClick={() => save()}
disabled={isLoading}
className="inline-flex items-center justify-center w-full h-9 mt-4 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
>
{isLoading ? <Spinner /> : "Save & Relaunch"}
</button>
</div>
</div>
<div className="flex-1" />
</div>
);
}
+5 -5
View File
@@ -96,7 +96,7 @@ function Screen() {
/>
</div>
<div className="flex flex-col items-center w-full gap-3">
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] flex flex-col gap-3 bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl">
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] flex flex-col gap-3 bg-black/5 dark:bg-white/5 rounded-xl">
<div className="flex gap-2">
<input
name="npub"
@@ -122,11 +122,11 @@ function Screen() {
key={item}
type="button"
onClick={() => toggleUser(item)}
className="inline-flex items-center justify-between px-3 py-2 bg-white rounded-lg dark:bg-black/20 backdrop-blur-lg shadow-primary dark:ring-1 ring-neutral-800/50"
className="inline-flex items-center justify-between px-3 py-2 bg-white rounded-lg dark:bg-black/20 shadow-primary dark:ring-1 ring-neutral-800/50"
>
<User.Provider pubkey={item}>
<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">
<User.Name className="text-sm font-medium" />
</div>
@@ -153,11 +153,11 @@ function Screen() {
key={item}
type="button"
onClick={() => toggleUser(item)}
className="inline-flex items-center justify-between px-3 py-2 bg-white rounded-lg dark:bg-black/20 backdrop-blur-lg shadow-primary dark:ring-1 ring-neutral-800/50"
className="inline-flex items-center justify-between px-3 py-2 bg-white rounded-lg dark:bg-black/20 shadow-primary dark:ring-1 ring-neutral-800/50"
>
<User.Provider pubkey={item}>
<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">
<User.Name className="text-sm font-medium" />
</div>
@@ -49,7 +49,7 @@ function Screen() {
};
return (
<div className="overflow-y-auto scrollbar-none p-2 shrink-0 h-[450px] bg-white dark:bg-white/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<div className="overflow-y-auto scrollbar-none p-2 shrink-0 h-[450px] bg-white dark:bg-white/20 rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<div className="flex flex-col justify-between h-full">
<div className="flex-1 flex flex-col gap-1.5 justify-center px-5">
<p className="font-semibold text-neutral-500">
@@ -68,7 +68,7 @@ function Screen() {
return (
<div className="flex flex-col items-center w-full gap-3">
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl">
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] bg-black/5 dark:bg-white/5 rounded-xl">
<Suspense
fallback={
<div className="flex flex-col items-center justify-center w-full h-20 gap-1">
@@ -88,14 +88,14 @@ function Screen() {
users.profiles.map((item: { pubkey: string }) => (
<div
key={item.pubkey}
className="w-full p-2 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"
className="w-full p-2 mb-2 overflow-hidden bg-white rounded-lg h-max dark:bg-black/20shadow-primary dark:ring-1 ring-neutral-800/50"
>
<User.Provider pubkey={item.pubkey}>
<User.Root>
<div 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="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" />
</div>
<button
+2 -2
View File
@@ -75,14 +75,14 @@ function Screen() {
<span className="text-sm font-medium">Added: {topics.length}</span>
</div>
<div className="flex flex-col items-center w-full gap-3">
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl">
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] bg-black/5 dark:bg-white/5 rounded-xl">
<div className="flex flex-col gap-3">
{TOPICS.map((topic) => (
<button
key={topic.title}
type="button"
onClick={() => toggleTopic(topic)}
className="flex items-center justify-between px-3 bg-white border border-transparent rounded-lg h-11 dark:bg-black/20 backdrop-blur-lg hover:border-blue-500 shadow-primary dark:ring-1 ring-neutral-800/50"
className="flex items-center justify-between px-3 bg-white border border-transparent rounded-lg h-11 dark:bg-black/20 hover:border-blue-500 shadow-primary dark:ring-1 ring-neutral-800/50"
>
<div className="inline-flex items-center gap-1">
<div>{topic.icon}</div>
+7 -7
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 { LumeEvent, useEvent } from "@lume/system";
import { Spinner } from "@lume/ui";
import { cn, insertImage, insertNostrEvent, isImageUrl } from "@lume/utils";
import { createFileRoute } from "@tanstack/react-router";
import { nip19 } from "nostr-tools";
import { useEffect, useState } from "react";
import { type Descendant, Node, Transforms, createEditor } from "slate";
import {
@@ -14,13 +19,8 @@ import {
withReact,
} from "slate-react";
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 { User } from "@/components/user";
import { Note } from "@/components/note";
import { nip19 } from "nostr-tools";
import { WarningButton } from "./-components/warning";
type EditorSearch = {
reply_to: string;
@@ -250,7 +250,7 @@ function ChildNote({ id }: { id: string }) {
<Note.Root className="flex items-center gap-2">
<User.Provider pubkey={data.pubkey}>
<User.Root className="shrink-0">
<User.Avatar className="rounded-full size-8 shrink-0" />
<User.Avatar className="rounded-full size-8" />
</User.Root>
</User.Provider>
<div className="content-break line-clamp-1">{data.content}</div>
@@ -1,82 +0,0 @@
import { Note } from "@/components/note";
import { type LumeEvent, NostrQuery } from "@lume/system";
import { Box, Container, Spinner } from "@lume/ui";
import { createFileRoute } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { WindowVirtualizer } from "virtua";
import { Reply } from "./-components/reply";
export const Route = createFileRoute("/events/$eventId")({
beforeLoad: async () => {
const settings = await NostrQuery.getUserSettings();
return { settings };
},
loader: async ({ params }) => {
const event = await NostrQuery.getEvent(params.eventId);
return event;
},
component: Screen,
});
function Screen() {
const event = Route.useLoaderData();
const [reload, setReload] = useState(false);
const [replies, setReplies] = useState<LumeEvent[]>(null);
useEffect(() => {
let mounted = true;
if (event) {
event.getAllReplies().then((data) => {
if (mounted) setReplies(data);
});
}
return () => {
mounted = false;
};
}, [event]);
return (
<Container withDrag>
<Box className="scrollbar-none">
<WindowVirtualizer>
<Note.Provider event={event}>
<Note.Root>
<div className="flex items-center justify-between px-3 h-14">
<Note.User />
<Note.Menu />
</div>
<Note.ContentLarge className="px-3" />
<div className="flex items-center justify-end gap-2 px-3 mt-4 h-11">
<Note.Reply large />
<Note.Repost large />
<Note.Zap large />
</div>
</Note.Root>
</Note.Provider>
<div className="flex flex-col">
<div className="flex items-center px-3 text-sm font-semibold border-t h-11 text-neutral-700 dark:text-neutral-300 border-neutral-100 dark:border-neutral-900">
Replies ({replies?.length ?? 0})
</div>
{!replies ? (
<Spinner />
) : !replies.length ? (
<div className="flex items-center justify-center w-full">
<div className="flex flex-col items-center justify-center gap-2 py-6">
<h3 className="text-3xl">👋</h3>
<p className="leading-none text-neutral-600 dark:text-neutral-400">
Be the first to Reply!
</p>
</div>
</div>
) : (
replies.map((event) => <Reply key={event.id} event={event} />)
)}
</div>
</WindowVirtualizer>
</Box>
</Container>
);
}
+143
View File
@@ -0,0 +1,143 @@
import { Note } from "@/components/note";
import { LumeEvent, NostrQuery } from "@lume/system";
import type { Meta } from "@lume/types";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { createFileRoute } from "@tanstack/react-router";
import { getCurrent } from "@tauri-apps/api/window";
import { useEffect, useRef, useState } from "react";
import { Virtualizer } from "virtua";
import NoteParent from "./-components/parent";
type Payload = {
raw: string;
parsed: Meta;
};
export const Route = createFileRoute("/events/$id")({
beforeLoad: async () => {
const settings = await NostrQuery.getUserSettings();
return { settings };
},
loader: async ({ params }) => {
const event = await NostrQuery.getEvent(params.id);
return event;
},
component: Screen,
});
function Screen() {
const ref = useRef<HTMLDivElement>(null);
return (
<div className="h-full flex flex-col">
<div
data-tauri-drag-region
className="shrink-0 h-8 w-full border-b border-black/5 dark:border-white/5"
/>
<ScrollArea.Root
type={"scroll"}
scrollHideDelay={300}
className="overflow-hidden size-full flex-1"
>
<ScrollArea.Viewport ref={ref} className="h-full p-3">
<RootEvent />
<Virtualizer scrollRef={ref}>
<ReplyList />
</Virtualizer>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
</div>
);
}
function RootEvent() {
const event = Route.useLoaderData();
return (
<Note.Provider event={event}>
<Note.Root className="bg-white dark:bg-black/10 rounded-xl shadow-primary dark:ring-1 dark:ring-white/5">
<div className="flex items-center justify-between px-3 h-14">
<Note.User />
<Note.Menu />
</div>
<Note.ContentLarge className="px-3" />
<div className="flex items-center gap-2 px-3 mt-6 h-12 rounded-b-xl bg-neutral-50 dark:bg-white/5">
<Note.Reply large />
<Note.Repost large />
<Note.Zap large />
</div>
</Note.Root>
</Note.Provider>
);
}
function ReplyList() {
const event = Route.useLoaderData();
const [replies, setReplies] = useState<LumeEvent[]>([]);
useEffect(() => {
const unlistenEvent = getCurrent().listen<Payload>("new_reply", (data) => {
const event = LumeEvent.from(data.payload.raw, data.payload.parsed);
setReplies((prev) => [event, ...prev]);
});
const unlistenWindow = getCurrent().onCloseRequested(async () => {
await event.unlistenEventReply();
await getCurrent().destroy();
});
return () => {
unlistenEvent.then((f) => f());
unlistenWindow.then((f) => f());
};
}, []);
useEffect(() => {
let mounted = true;
async function getReplies() {
const data = await event.getEventReplies();
if (mounted) {
setReplies(data);
// Start listen for new reply
event.listenEventReply();
}
}
getReplies();
return () => {
mounted = false;
};
}, []);
return (
<div>
<div className="flex items-center text-sm font-semibold h-14 text-neutral-600 dark:text-white/30">
All replies
</div>
<div className="flex flex-col gap-3">
{!replies.length ? (
<div className="flex items-center justify-center w-full">
<div className="flex flex-col items-center justify-center gap-2 py-4">
<h3 className="text-3xl">👋</h3>
<p className="leading-none text-neutral-600 dark:text-neutral-400">
Be the first to Reply!
</p>
</div>
</div>
) : (
replies.map((event) => <NoteParent key={event.id} event={event} />)
)}
</div>
</div>
);
}
@@ -0,0 +1,41 @@
import { Note } from "@/components/note";
import type { LumeEvent } from "@lume/system";
import NoteParent from "./parent";
import { memo } from "react";
const NoteChild = memo(function NoteChild({ event }: { event: LumeEvent }) {
return (
<Note.Provider event={event}>
<Note.Root className="flex flex-col gap-6 mb-3">
<div>
<div className="flex items-center justify-between">
<Note.User />
<Note.Menu />
</div>
<div className="flex gap-2">
<div className="w-8 shrink-0" />
<div className="flex-1 flex flex-col gap-2">
<Note.ContentLarge />
<div className="flex items-center gap-1">
<Note.Reply />
<Note.Repost />
<Note.Zap />
</div>
</div>
</div>
</div>
{event.replies?.length ? (
<div className="flex flex-col gap-3 pl-4">
<div className="flex flex-col pl-6 border-l border-black/10 dark:border-white/10">
{event.replies?.map((childEvent) => (
<NoteParent key={childEvent.id} event={childEvent} />
))}
</div>
</div>
) : null}
</Note.Root>
</Note.Provider>
);
});
export default NoteChild;
@@ -0,0 +1,41 @@
import { Note } from "@/components/note";
import type { LumeEvent } from "@lume/system";
import NoteChild from "./child";
import { memo } from "react";
const NoteParent = memo(function NoteParent({ event }: { event: LumeEvent }) {
return (
<Note.Provider event={event}>
<Note.Root className="flex flex-col gap-6 mb-3">
<div>
<div className="flex items-center justify-between">
<Note.User />
<Note.Menu />
</div>
<div className="flex gap-2">
<div className="w-8 shrink-0" />
<div className="flex-1 flex flex-col gap-2">
<Note.ContentLarge />
<div className="flex items-center gap-1">
<Note.Reply />
<Note.Repost />
<Note.Zap />
</div>
</div>
</div>
</div>
{event.replies?.length ? (
<div className="flex flex-col gap-3 pl-4">
<div className="flex flex-col gap-3 pl-6 border-l border-black/10 dark:border-white/10">
{event.replies?.map((childEvent) => (
<NoteChild key={childEvent.id} event={childEvent} />
))}
</div>
</div>
) : null}
</Note.Root>
</Note.Provider>
);
});
export default NoteParent;
@@ -1,36 +0,0 @@
import { Note } from "@/components/note";
import type { LumeEvent } from "@lume/system";
import { cn } from "@lume/utils";
import { SubReply } from "./subReply";
export function Reply({ event }: { event: LumeEvent }) {
return (
<Note.Provider event={event}>
<Note.Root className="border-t border-neutral-100 dark:border-neutral-900">
<div className="flex items-center justify-between px-3 h-14">
<Note.User />
<Note.Menu />
</div>
<Note.ContentLarge className="px-3" />
<div className="flex items-center gap-4 px-3 mt-3 h-14">
<Note.Reply />
<Note.Repost />
<Note.Zap />
</div>
<div
className={cn(
event.replies?.length > 0
? "py-2 pl-3 flex flex-col gap-3 divide-y divide-neutral-100 bg-neutral-50 dark:bg-white/5 border-l-2 border-blue-500 dark:divide-neutral-900"
: "",
)}
>
{event.replies?.length > 0
? event.replies?.map((childEvent) => (
<SubReply key={childEvent.id} event={childEvent} />
))
: null}
</div>
</Note.Root>
</Note.Provider>
);
}
@@ -1,50 +0,0 @@
import type { EventWithReplies } from "@lume/types";
import { Spinner } from "@lume/ui";
import { cn } from "@lume/utils";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Reply } from "./reply";
import { LumeEvent } from "@lume/system";
export function ReplyList({
eventId,
className,
}: {
eventId: string;
className?: string;
}) {
const [t] = useTranslation();
const [data, setData] = useState<null | EventWithReplies[]>(null);
useEffect(() => {
async function getReplies() {
const events = await LumeEvent.getReplies(eventId);
setData(events);
}
getReplies();
}, [eventId]);
return (
<div className={cn("flex flex-col", className)}>
<div className="h-11 flex px-3 items-center text-sm font-semibold text-neutral-700 dark:text-neutral-300 border-t border-neutral-100 dark:border-neutral-900">
Replies ({data?.length ?? 0})
</div>
{!data ? (
<div className="flex h-16 items-center justify-center p-3">
<Spinner className="size-5" />
</div>
) : data.length === 0 ? (
<div className="flex w-full items-center justify-center">
<div className="flex flex-col items-center justify-center gap-2 py-6">
<h3 className="text-3xl">👋</h3>
<p className="leading-none text-neutral-600 dark:text-neutral-400">
{t("note.reply.empty")}
</p>
</div>
</div>
) : (
data.map((event) => <Reply key={event.id} event={event} />)
)}
</div>
);
}
@@ -1,26 +0,0 @@
import type { NostrEvent } from "@lume/types";
import { Note } from "@/components/note";
export function SubReply({
event,
}: {
event: NostrEvent;
rootEventId?: string;
}) {
return (
<Note.Provider event={event}>
<Note.Root>
<div className="px-3 h-14 flex items-center justify-between">
<Note.User />
<Note.Menu />
</div>
<Note.ContentLarge className="px-3" />
<div className="mt-3 flex items-center gap-4 px-3">
<Note.Reply />
<Note.Repost />
<Note.Zap />
</div>
</Note.Root>
</Note.Provider>
);
}
+2 -2
View File
@@ -81,11 +81,11 @@ export function Screen() {
<ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
<Virtualizer scrollRef={ref}>
{isFetching && !isLoading && !isFetchingNextPage ? (
<div className="flex items-center justify-center w-full mb-3 h-11 bg-black/10 dark:bg-white/10 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<div className="flex items-center justify-center w-full mb-3 h-12 bg-black/5 dark:bg-white/5 rounded-xl">
<div className="flex items-center justify-center gap-2">
<Spinner className="size-5" />
<span className="text-sm font-medium">
Fetching new notes...
Getting new notes...
</span>
</div>
</div>
+2 -2
View File
@@ -95,11 +95,11 @@ export function Screen() {
<ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
<Virtualizer scrollRef={ref}>
{isFetching && !isLoading && !isFetchingNextPage ? (
<div className="flex items-center justify-center w-full mb-3 h-11 bg-black/10 dark:bg-white/10 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<div className="flex items-center justify-center w-full mb-3 h-12 bg-black/5 dark:bg-white/5 rounded-xl">
<div className="flex items-center justify-center gap-2">
<Spinner className="size-5" />
<span className="text-sm font-medium">
Fetching new notes...
Getting new notes...
</span>
</div>
</div>
+7 -3
View File
@@ -67,8 +67,12 @@ function Screen() {
return (
<div
data-tauri-drag-region
className="flex flex-col items-center justify-between w-full h-full"
className="relative flex flex-col items-center justify-between w-full h-full"
>
<div
data-tauri-drag-region
className="absolute top-0 left-0 h-14 w-full"
/>
<div className="flex items-end justify-center flex-1 w-full px-4 pb-10">
<div className="text-center">
<h2 className="mb-1 text-lg text-neutral-700 dark:text-neutral-300">
@@ -78,7 +82,7 @@ function Screen() {
</div>
</div>
<div className="flex flex-col items-center flex-1 w-full gap-3">
<div className="flex flex-col w-full max-w-sm mx-auto overflow-hidden bg-white divide-y divide-neutral-100 dark:divide-white/5 rounded-xl shadow-primary backdrop-blur-lg dark:bg-white/10 dark:ring-1 ring-white/15">
<div className="flex flex-col w-full max-w-sm mx-auto overflow-hidden bg-white divide-y divide-neutral-100 dark:divide-white/5 rounded-xl shadow-primary dark:bg-white/10 dark:ring-1 ring-white/15">
{context.accounts.map((account) => (
<div
key={account}
@@ -88,7 +92,7 @@ function Screen() {
>
<User.Provider pubkey={account}>
<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">
<User.Name className="max-w-[6rem] truncate font-medium leading-tight" />
<span className="text-sm text-neutral-700 dark:text-neutral-300">
+1 -1
View File
@@ -12,7 +12,7 @@ function Screen() {
className="flex flex-col items-center justify-center w-screen h-screen"
>
<div className="w-full max-w-xs mx-auto lg:max-w-md">
<div className="flex flex-col w-full gap-2 px-2 bg-white rounded-xl shadow-primary backdrop-blur-lg dark:bg-white/20 dark:ring-1 ring-neutral-800/50">
<div className="flex flex-col w-full gap-2 px-2 bg-white rounded-xl shadow-primary dark:bg-white/20 dark:ring-1 ring-neutral-800/50">
<div className="flex items-center h-20 border-b border-neutral-100 dark:border-white/5">
<Link
to="/auth/create-profile"
+87 -9
View File
@@ -2,16 +2,23 @@ import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon } from "@lume/icons";
import { type LumeEvent, NostrAccount, NostrQuery } from "@lume/system";
import { type ColumnRouteSearch, Kind } from "@lume/types";
import { ArrowRightCircleIcon, ArrowUpIcon } from "@lume/icons";
import { LumeEvent, NostrAccount, NostrQuery } from "@lume/system";
import { type ColumnRouteSearch, Kind, type Meta } from "@lume/types";
import { Spinner } from "@lume/ui";
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 { 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";
type Payload = {
raw: string;
parsed: Meta;
};
export const Route = createFileRoute("/newsfeed")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return {
@@ -40,6 +47,7 @@ export const Route = createFileRoute("/newsfeed")({
});
export function Screen() {
const { queryClient } = Route.useRouteContext();
const { label, account } = Route.useSearch();
const {
data,
@@ -57,7 +65,6 @@ export function Screen() {
},
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
select: (data) => data?.pages.flat(),
refetchOnWindowFocus: false,
});
const ref = useRef<HTMLDivElement>(null);
@@ -84,20 +91,31 @@ export function Screen() {
[data],
);
useEffect(() => {
const unlisten = listen("synced", async () => {
await queryClient.invalidateQueries({ queryKey: [label, account] });
});
return () => {
unlisten.then((f) => f());
};
}, []);
return (
<ScrollArea.Root
type={"scroll"}
scrollHideDelay={300}
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}>
{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/5 dark:bg-white/5 rounded-xl">
<div className="flex items-center justify-center gap-2">
<Spinner className="size-5" />
<span className="text-sm font-medium">
Fetching new notes...
Getting new notes...
</span>
</div>
</div>
@@ -145,3 +163,63 @@ export function Screen() {
</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 unlisten = getCurrent().listen<Payload>("new_event", (data) => {
const event = LumeEvent.from(data.payload.raw, data.payload.parsed);
setEvents((prev) => [event, ...prev]);
});
NostrQuery.listenLocalEvent().then(() => console.log("listen"));
return () => {
unlisten.then((f) => f());
NostrQuery.unlisten().then(() => console.log("unlisten"));
};
}, []);
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>
);
}
-52
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>
);
}
+4 -4
View File
@@ -22,7 +22,7 @@ function Screen() {
</p>
</div>
<div className="px-3 flex flex-col gap-3">
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10 backdrop-blur-lg">
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10">
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
01.
</div>
@@ -45,7 +45,7 @@ function Screen() {
</video>
</div>
</div>
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10 backdrop-blur-lg">
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10">
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
02.
</div>
@@ -68,7 +68,7 @@ function Screen() {
</video>
</div>
</div>
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10 backdrop-blur-lg">
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10">
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
03.
</div>
@@ -91,7 +91,7 @@ function Screen() {
</video>
</div>
</div>
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10 backdrop-blur-lg">
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10">
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
04.
</div>
@@ -1,13 +1,9 @@
import { Note } from "@/components/note";
import { User } from "@/components/user";
import {
HorizontalDotsIcon,
InfoIcon,
RepostIcon,
SearchIcon,
} from "@lume/icons";
import { HorizontalDotsIcon, InfoIcon, RepostIcon } from "@lume/icons";
import { type LumeEvent, LumeWindow, NostrQuery, useEvent } from "@lume/system";
import { Kind } from "@lume/types";
import { Spinner } from "@lume/ui";
import {
checkForAppUpdates,
decodeZapInvoice,
@@ -15,77 +11,64 @@ import {
} from "@lume/utils";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import * as Tabs from "@radix-ui/react-tabs";
import { useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core";
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
import { getCurrent } from "@tauri-apps/api/window";
import { exit } from "@tauri-apps/plugin-process";
import { open } from "@tauri-apps/plugin-shell";
import {
type ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { type ReactNode, useCallback, useEffect, useRef } from "react";
import { Virtualizer } from "virtua";
interface EmitAccount {
account: string;
}
export const Route = createFileRoute("/panel")({
export const Route = createFileRoute("/panel/$account")({
component: Screen,
});
function Screen() {
const [account, setAccount] = useState<string>(null);
const [events, setEvents] = useState<LumeEvent[]>([]);
const { account } = Route.useParams();
const { queryClient } = Route.useRouteContext();
const { isLoading, data } = useQuery({
queryKey: ["notification", account],
queryFn: async () => {
const events = await NostrQuery.getNotifications();
return events;
},
select: (events) => {
const zaps = new Map<string, LumeEvent[]>();
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 reactEvents = events.filter(
(ev) => ev.kind === Kind.Repost || ev.kind === Kind.Reaction,
);
const texts = useMemo(
() => events.filter((ev) => ev.kind === Kind.Text),
[events],
);
for (const event of reactEvents) {
const rootId = event.tags.filter((tag) => tag[0] === "e")[0]?.[1];
const zaps = useMemo(() => {
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]);
if (rootId) {
if (reactions.has(rootId)) {
reactions.get(rootId).push(event);
} else {
reactions.set(rootId, [event]);
}
}
}
}
return groups;
}, [events]);
for (const event of zapEvents) {
const rootId = event.tags.filter((tag) => tag[0] === "e")[0]?.[1];
const reactions = useMemo(() => {
const groups = new Map<string, LumeEvent[]>();
const list = events.filter(
(ev) => ev.kind === Kind.Repost || ev.kind === Kind.Reaction,
);
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]);
if (rootId) {
if (zaps.has(rootId)) {
zaps.get(rootId).push(event);
} else {
zaps.set(rootId, [event]);
}
}
}
}
return groups;
}, [events]);
return { texts, zaps, reactions };
},
});
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
e.preventDefault();
@@ -99,10 +82,6 @@ function Screen() {
text: "New Post",
action: () => LumeWindow.openEditor(),
}),
MenuItem.new({
text: "Search",
action: () => LumeWindow.openSearch(),
}),
PredefinedMenuItem.new({ item: "Separator" }),
MenuItem.new({
text: "About Lume",
@@ -119,7 +98,7 @@ function Screen() {
PredefinedMenuItem.new({ item: "Separator" }),
MenuItem.new({
text: "Quit",
action: async () => await exit(0),
action: async () => await invoke("force_quit"),
}),
]);
@@ -131,45 +110,29 @@ function Screen() {
}, []);
useEffect(() => {
if (account?.length && account?.startsWith("npub1")) {
NostrQuery.getNotifications()
.then((data) => {
const sorted = data.sort((a, b) => b.created_at - a.created_at);
setEvents(sorted);
})
.catch((e) => console.log(e));
}
}, [account]);
useEffect(() => {
const unlistenLoad = getCurrent().listen<EmitAccount>(
"load-notification",
(data) => {
setAccount(data.payload.account);
},
);
const unlistenNewEvent = getCurrent().listen("notification", (data) => {
const unlisten = getCurrent().listen("notification", async (data) => {
const event: LumeEvent = JSON.parse(data.payload as string);
setEvents((prev) => [event, ...prev]);
await queryClient.setQueryData(
["notification", account],
(data: LumeEvent[]) => [event, ...data],
);
});
return () => {
unlistenLoad.then((f) => f());
unlistenNewEvent.then((f) => f());
unlisten.then((f) => f());
};
}, []);
if (!account) {
if (isLoading) {
return (
<div className="flex items-center justify-center w-full h-full text-sm">
Please log in.
<div className="size-full flex items-center justify-center">
<Spinner />
</div>
);
}
return (
<div className="flex flex-col w-full h-full">
<div className="flex flex-col size-full overflow-hidden">
<div className="flex items-center justify-between px-4 border-b h-11 shrink-0 border-black/5 dark:border-white/5">
<div>
<h1 className="text-sm font-semibold">Notifications</h1>
@@ -180,13 +143,6 @@ function Screen() {
<User.Avatar className="rounded-full size-7" />
</User.Root>
</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
type="button"
onClick={(e) => showContextMenu(e)}
@@ -196,11 +152,8 @@ function Screen() {
</button>
</div>
</div>
<Tabs.Root
defaultValue="replies"
className="flex-1 overflow-x-hidden overflow-y-auto scrollbar-none"
>
<Tabs.List className="flex items-center">
<Tabs.Root defaultValue="replies" className="flex flex-col h-full">
<Tabs.List className="h-8 shrink-0 flex items-center">
<Tabs.Trigger
className="flex-1 inline-flex h-8 items-center justify-center gap-2 px-2 text-sm font-medium border-b border-black/10 dark:border-white/10 data-[state=active]:border-black/30 dark:data-[state=active]:border-white/30 data-[state=inactive]:opacity-50"
value="replies"
@@ -220,18 +173,22 @@ function Screen() {
Zaps
</Tabs.Trigger>
</Tabs.List>
<div className="h-full">
<ScrollArea.Root
type={"scroll"}
scrollHideDelay={300}
className="min-h-0 flex-1 overflow-x-hidden"
>
<Tab value="replies">
{texts.map((event, index) => (
{data.texts.map((event, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
<TextNote key={event.id + index} event={event} />
))}
</Tab>
<Tab value="reactions">
{[...reactions.entries()].map(([root, events]) => (
{[...data.reactions.entries()].map(([root, events]) => (
<div
key={root}
className="flex flex-col gap-1 p-2 mb-2 rounded-lg shrink-0 backdrop-blur-md bg-black/10 dark:bg-white/10"
className="flex flex-col gap-1 p-2 mb-2 rounded-lg shrink-0 bg-black/10 dark:bg-white/10"
>
<div className="flex flex-col flex-1 min-w-0 gap-2">
<div className="flex items-center gap-2 pb-2 border-b border-black/5 dark:border-white/5">
@@ -240,7 +197,7 @@ function Screen() {
<div className="flex flex-wrap items-center gap-3">
{events.map((event) => (
<User.Provider key={event.id} pubkey={event.pubkey}>
<User.Root className="shrink-0 flex rounded-full h-8 bg-black/10 dark:bg-white/10 backdrop-blur-md p-[2px]">
<User.Root className="shrink-0 flex rounded-full h-8 bg-black/10 dark:bg-white/10 p-[2px]">
<User.Avatar className="flex-1 rounded-full size-7" />
<div className="inline-flex items-center justify-center flex-1 text-xs truncate rounded-full size-7">
{event.kind === Kind.Reaction ? (
@@ -262,10 +219,10 @@ function Screen() {
))}
</Tab>
<Tab value="zaps">
{[...zaps.entries()].map(([root, events]) => (
{[...data.zaps.entries()].map(([root, events]) => (
<div
key={root}
className="flex flex-col gap-1 p-2 mb-2 rounded-lg shrink-0 backdrop-blur-md bg-black/10 dark:bg-white/10"
className="flex flex-col gap-1 p-2 mb-2 rounded-lg shrink-0 bg-black/10 dark:bg-white/10"
>
<div className="flex flex-col flex-1 min-w-0 gap-2">
<div className="flex items-center gap-2 pb-2 border-b border-black/5 dark:border-white/5">
@@ -277,8 +234,8 @@ function Screen() {
key={event.id}
pubkey={event.tags.find((tag) => tag[0] === "P")[1]}
>
<User.Root className="shrink-0 flex gap-1.5 rounded-full h-8 bg-black/10 dark:bg-white/10 backdrop-blur-md p-[2px]">
<User.Avatar className="flex-1 rounded-full size-7" />
<User.Root className="shrink-0 flex gap-1.5 rounded-full h-8 bg-black/10 dark:bg-white/10 p-[2px]">
<User.Avatar className="rounded-full size-7" />
<div className="flex-1 h-7 w-max pr-1.5 rounded-full inline-flex items-center justify-center text-sm truncate">
{decodeZapInvoice(event.tags).bitcoinFormatted}
</div>
@@ -290,7 +247,14 @@ function Screen() {
</div>
))}
</Tab>
</div>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
</Tabs.Root>
</div>
);
@@ -301,22 +265,9 @@ function Tab({ value, children }: { value: string; children: ReactNode[] }) {
return (
<Tabs.Content value={value} className="size-full">
<ScrollArea.Root
type={"scroll"}
scrollHideDelay={300}
className="overflow-hidden size-full"
>
<ScrollArea.Viewport ref={ref} className="h-full px-2 pt-2">
<Virtualizer scrollRef={ref}>{children}</Virtualizer>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
<ScrollArea.Viewport ref={ref} className="h-full px-2 pt-2">
<Virtualizer scrollRef={ref}>{children}</Virtualizer>
</ScrollArea.Viewport>
</Tabs.Content>
);
}
@@ -351,7 +302,7 @@ function RootNote({ id }: { id: string }) {
<Note.Root className="flex items-center gap-2">
<User.Provider pubkey={data.pubkey}>
<User.Root className="shrink-0">
<User.Avatar className="rounded-full size-8 shrink-0" />
<User.Avatar className="rounded-full size-8" />
</User.Root>
</User.Provider>
<div className="line-clamp-1">{data.content}</div>
@@ -368,10 +319,10 @@ function TextNote({ event }: { event: LumeEvent }) {
return (
<Note.Provider event={event}>
<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 bg-black/10 dark:bg-white/10">
<User.Provider pubkey={event.pubkey}>
<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 items-baseline justify-between w-full">
<User.Name className="text-sm font-semibold leading-tight" />
@@ -384,7 +335,7 @@ function TextNote({ event }: { event: LumeEvent }) {
Reply to:
</span>
<div className="inline-flex items-baseline gap-1">
{pTags.map((replyTo) => (
{[...new Set(pTags)].map((replyTo) => (
<User.Provider key={replyTo} pubkey={replyTo}>
<User.Root>
<User.Name className="font-medium leading-tight" />
+96
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>
);
}
+32 -136
View File
@@ -1,148 +1,44 @@
import { Note } from "@/components/note";
import { User } from "@/components/user";
import { SearchIcon } from "@lume/icons";
import { LumeEvent, LumeWindow } from "@lume/system";
import { Kind, type NostrEvent } from "@lume/types";
import { Spinner } from "@lume/ui";
import { createFileRoute } from "@tanstack/react-router";
import { message } from "@tauri-apps/plugin-dialog";
import { useEffect, useState } from "react";
import { useDebounce } from "use-debounce";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { Outlet, createFileRoute } from "@tanstack/react-router";
type Search = {
query: string;
};
export const Route = createFileRoute("/search")({
validateSearch: (search: Record<string, string>): Search => {
return {
query: search.query,
};
},
component: Screen,
});
function Screen() {
const [loading, setLoading] = useState(false);
const [events, setEvents] = useState<LumeEvent[]>([]);
const [search, setSearch] = useState("");
const [searchValue] = useDebounce(search, 500);
const searchEvents = async () => {
try {
setLoading(true);
const query = `https://api.nostr.wine/search?query=${searchValue}&kind=0,1`;
const res = await fetch(query);
const content = await res.json();
const events = content.data as NostrEvent[];
const lumeEvents = events.map((ev) => new LumeEvent(ev));
const sorted = lumeEvents.sort((a, b) => b.created_at - a.created_at);
setLoading(false);
setEvents(sorted);
} catch (e) {
setLoading(false);
await message(String(e), {
title: "Search",
kind: "error",
});
}
};
useEffect(() => {
if (searchValue.length >= 3 && searchValue.length < 500) {
searchEvents();
}
}, [searchValue]);
const { query } = Route.useSearch();
return (
<div data-tauri-drag-region className="flex flex-col w-full h-full">
<div className="relative flex flex-col h-24 border-b shrink-0 border-black/5 dark:border-white/5">
<div data-tauri-drag-region className="w-full h-4 shrink-0" />
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") searchEvents();
}}
placeholder="Search anything..."
className="w-full h-20 px-3 pt-10 text-lg bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-neutral-500 dark:placeholder:text-neutral-600"
/>
</div>
<div className="flex-1 p-3 overflow-y-auto scrollbar-none">
{loading ? (
<div className="flex items-center justify-center w-full h-full">
<Spinner />
</div>
) : events.length ? (
<div className="flex flex-col gap-5">
<div className="flex flex-col gap-1.5">
<div className="text-sm font-medium text-neutral-700 dark:text-neutral-300 shrink-0">
Users
</div>
<div className="flex flex-col flex-1 gap-1">
{events
.filter((ev) => ev.kind === Kind.Metadata)
.map((event) => (
<SearchUser key={event.pubkey} event={event} />
))}
</div>
</div>
<div className="flex flex-col gap-1.5">
<div className="text-sm font-medium text-neutral-700 dark:text-neutral-300 shrink-0">
Notes
</div>
<div className="flex flex-col flex-1 gap-3">
{events
.filter((ev) => ev.kind === Kind.Text)
.map((event) => (
<SearchNote key={event.id} event={event} />
))}
</div>
</div>
</div>
) : null}
{!loading && !events.length ? (
<div className="flex flex-col items-center justify-center h-full gap-3">
<div className="inline-flex items-center justify-center rounded-full size-16 bg-black/10 dark:bg-white/10">
<SearchIcon className="size-6" />
</div>
Try searching for people, notes, or keywords
</div>
) : null}
<div className="flex flex-col h-full">
<div
data-tauri-drag-region
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"
>
Search result for: <span className="font-semibold">{query}</span>
</div>
</div>
);
}
function SearchUser({ event }: { event: LumeEvent }) {
return (
<button
key={event.id}
type="button"
onClick={() => LumeWindow.openProfile(event.pubkey)}
className="col-span-1 p-2 rounded-lg hover:bg-black/10 dark:hover:bg-white/10"
>
<User.Provider pubkey={event.pubkey} embedProfile={event.content}>
<User.Root className="flex items-center gap-2">
<User.Avatar className="rounded-full size-9 shrink-0" />
<div className="inline-flex items-center gap-1.5">
<User.Name className="font-semibold" />
<User.NIP05 />
</div>
</User.Root>
</User.Provider>
</button>
);
}
function SearchNote({ event }: { event: LumeEvent }) {
return (
<div className="bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<Note.Provider event={event}>
<Note.Root>
<div className="flex items-center justify-between px-3 h-14">
<Note.User />
<Note.Menu />
</div>
<Note.Content className="px-3" quote={false} mention={false} />
<div className="flex items-center gap-4 px-3 mt-3 h-14">
<Note.Open />
</div>
</Note.Root>
</Note.Provider>
<ScrollArea.Root
type={"scroll"}
scrollHideDelay={300}
className="overflow-hidden size-full flex-1"
>
<Outlet />
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
</div>
);
}
+101
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 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>
);
}
+3 -12
View File
@@ -8,15 +8,12 @@ import {
import { cn } from "@lume/utils";
import { Link } from "@tanstack/react-router";
import { Outlet, createFileRoute } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
export const Route = createFileRoute("/settings")({
component: Screen,
});
function Screen() {
const { t } = useTranslation();
return (
<div className="flex flex-col w-full h-full">
<div
@@ -36,9 +33,7 @@ function Screen() {
)}
>
<SettingsIcon className="size-5 shrink-0" />
<p className="text-sm font-medium">
{t("settings.general.title")}
</p>
<p className="text-sm font-medium">General</p>
</div>
);
}}
@@ -55,9 +50,7 @@ function Screen() {
)}
>
<UserIcon className="size-5 shrink-0" />
<p className="text-sm font-medium">
{t("settings.user.title")}
</p>
<p className="text-sm font-medium">User</p>
</div>
);
}}
@@ -108,9 +101,7 @@ function Screen() {
)}
>
<SecureIcon className="size-5 shrink-0" />
<p className="text-sm font-medium">
{t("settings.backup.title")}
</p>
<p className="text-sm font-medium">Backup</p>
</div>
);
}}
+1 -1
View File
@@ -51,7 +51,7 @@ function Account({ account }: { account: string }) {
<div className="flex items-center justify-between gap-2 py-3">
<User.Provider pubkey={account}>
<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">
<User.Name className="text-sm leading-tight" />
<span className="text-sm leading-tight text-black/50 dark:text-white/50">
@@ -130,6 +130,28 @@ function Screen() {
</select>
</div>
</div>
<div className="flex items-start justify-between w-full gap-4 py-3">
<div className="flex-1">
<h3 className="font-medium">Vibrancy Effect</h3>
<p className="text-sm text-neutral-700 dark:text-neutral-300">
Make the window transparent.
</p>
</div>
<div className="flex justify-end w-36 shrink-0">
<Switch.Root
checked={settings.vibrancy}
onClick={() =>
setSettings((prev) => ({
...prev,
vibrancy: !prev.vibrancy,
}))
}
className="relative h-7 w-12 shrink-0 cursor-default rounded-full bg-black/10 outline-none data-[state=checked]:bg-blue-500 dark:bg-white/10"
>
<Switch.Thumb className="block size-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
</div>
</div>
<div className="flex items-start justify-between w-full gap-4 py-3">
<div className="flex-1">
<h3 className="font-medium">Zap Button</h3>
@@ -1,21 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/store/community")({
component: Screen,
});
function Screen() {
return (
<div className="flex h-full flex-col items-center justify-center gap-3 p-3">
<div className="size-24 bg-blue-100 flex flex-col items-center justify-end overflow-hidden dark:bg-blue-900 rounded-full">
<div className="w-12 h-16 bg-gradient-to-b from-blue-500 dark:from-blue-200 to-blue-50 dark:to-blue-900 rounded-t-lg" />
</div>
<div className="text-center">
<h1 className="font-semibold text-lg">Coming Soon</h1>
<p className="text-sm text-neutral-700 dark:text-neutral-300 leading-tight">
Enhance your experience <br /> by adding column shared by community.
</p>
</div>
</div>
);
}
@@ -1,69 +0,0 @@
import type { LumeColumn } from "@lume/types";
import { createFileRoute } from "@tanstack/react-router";
import { resolveResource } from "@tauri-apps/api/path";
import { getCurrent } from "@tauri-apps/api/window";
import { readTextFile } from "@tauri-apps/plugin-fs";
export const Route = createFileRoute("/store/official")({
beforeLoad: async () => {
const resourcePath = await resolveResource(
"resources/official_columns.json",
);
const officialColumns: LumeColumn[] = JSON.parse(
await readTextFile(resourcePath),
);
return {
officialColumns,
};
},
component: Screen,
});
function Screen() {
const { officialColumns } = Route.useRouteContext();
const install = async (column: LumeColumn) => {
const mainWindow = getCurrent();
await mainWindow.emit("columns", { type: "add", column });
};
return (
<div className="flex flex-col gap-3 p-3">
{officialColumns.map((column) => (
<div
key={column.label}
className="relative h-[200px] w-full overflow-hidden rounded-xl bg-gradient-to-tr from-orange-100 to-blue-200 px-3 pt-3"
>
{column.cover ? (
<img
src={column.cover}
srcSet={column.coverRetina}
alt={column.name}
loading="lazy"
decoding="async"
className="absolute left-0 top-0 z-10 h-full w-full object-cover"
/>
) : null}
<div className="absolute bottom-0 left-0 z-20 h-16 w-full bg-black/40 px-3 backdrop-blur-xl">
<div className="flex h-full items-center justify-between">
<div>
<h1 className="font-semibold text-white">{column.name}</h1>
<p className="max-w-[24rem] truncate text-sm text-white/80">
{column.description}
</p>
</div>
<button
type="button"
onClick={() => install(column)}
className="inline-flex h-8 w-16 shrink-0 items-center justify-center rounded-full bg-white/20 text-sm font-medium text-white hover:bg-white hover:text-blue-500"
>
Add
</button>
</div>
</div>
</div>
))}
</div>
);
}
+91 -35
View File
@@ -1,48 +1,104 @@
import { GlobalIcon, LaurelIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import { Link } from "@tanstack/react-router";
import { Outlet, createFileRoute } from "@tanstack/react-router";
import { CommunityIcon, LaurelIcon } from "@lume/icons";
import type { LumeColumn } from "@lume/types";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { createFileRoute } from "@tanstack/react-router";
import { resolveResource } from "@tauri-apps/api/path";
import { getCurrent } from "@tauri-apps/api/window";
import { readTextFile } from "@tauri-apps/plugin-fs";
export const Route = createFileRoute("/store")({
beforeLoad: async () => {
const path = "resources/official_columns.json";
const resourcePath = await resolveResource(path);
const fileContent = await readTextFile(resourcePath);
const officialColumns: LumeColumn[] = JSON.parse(fileContent);
return {
officialColumns,
};
},
component: Screen,
});
function Screen() {
const { officialColumns } = Route.useRouteContext();
const install = async (column: LumeColumn) => {
const mainWindow = getCurrent();
await mainWindow.emit("columns", { type: "add", column });
};
return (
<div className="flex flex-col h-full">
<div className="px-3 mt-2 mb-1">
<div className="inline-flex items-center w-full gap-1 p-1 rounded-lg shrink-0 bg-black/5 dark:bg-white/5">
<Link to="/store/official" className="flex-1">
{({ isActive }) => (
<div
className={cn(
"inline-flex h-8 w-full items-center justify-center gap-1.5 rounded-md text-sm font-medium leading-tight",
isActive ? "bg-neutral-50 dark:bg-white/10" : "opacity-50",
)}
>
<div className="size-full">
<ScrollArea.Root
type={"scroll"}
scrollHideDelay={300}
className="flex-1 overflow-hidden size-full"
>
<ScrollArea.Viewport className="h-full px-3 ">
<div className="flex flex-col gap-3 mb-10">
<div className="inline-flex items-center gap-1.5 font-semibold leading-tight">
<div className="size-7 rounded-md inline-flex items-center justify-center bg-black/10 dark:bg-white/10">
<LaurelIcon className="size-4" />
Official
</div>
)}
</Link>
<Link to="/store/community" className="flex-1">
{({ isActive }) => (
<div
className={cn(
"inline-flex h-8 w-full items-center justify-center gap-1.5 rounded-md text-sm font-medium leading-tight",
isActive ? "bg-neutral-50 dark:bg-white/10" : "opacity-50",
)}
>
<GlobalIcon className="size-4" />
Community
Official
</div>
<div className="grid grid-cols-3 gap-4">
{officialColumns.map((column) => (
<div
key={column.label}
className="relative group flex flex-col w-full aspect-square overflow-hidden bg-white dark:bg-black/20 rounded-xl shadow-primary dark:ring-1 dark:ring-white/5"
>
<div className="hidden group-hover:flex items-center justify-center absolute inset-0 size-full rounded-xl bg-white/20 dark:bg-black/20 backdrop-blur-md">
<button
type="button"
onClick={() => install(column)}
className="w-16 h-8 inline-flex items-center justify-center rounded-full bg-black dark:bg-white text-white dark:text-black text-sm font-semibold"
>
Add
</button>
</div>
<div className="flex-1">
{column.cover ? (
<img
src={column.cover}
srcSet={column.coverRetina}
alt={column.name}
loading="lazy"
decoding="async"
className="size-full object-cover"
/>
) : null}
</div>
<div className="shrink-0 h-9 px-3 flex items-center">
<h3 className="text-sm font-semibold truncate w-full">
{column.name}
</h3>
</div>
</div>
))}
</div>
</div>
<div className="flex flex-col gap-3">
<div className="inline-flex items-center gap-1.5 font-semibold leading-tight">
<div className="size-7 rounded-md inline-flex items-center justify-center bg-black/10 dark:bg-white/10">
<CommunityIcon className="size-4" />
</div>
)}
</Link>
</div>
</div>
<div className="flex-1 overflow-y-auto scrollbar-none">
<Outlet />
</div>
Community
</div>
<div className="w-full h-20 rounded-xl flex items-center justify-center text-sm font-medium bg-black/5 dark:bg-white/5">
Coming Soon.
</div>
</div>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
</div>
);
}
+2 -2
View File
@@ -105,11 +105,11 @@ export function Screen() {
<ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
<Virtualizer scrollRef={ref}>
{isFetching && !isLoading && !isFetchingNextPage ? (
<div className="flex items-center justify-center w-full mb-3 h-11 bg-black/10 dark:bg-white/10 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<div className="flex items-center justify-center w-full mb-3 h-12 bg-black/5 dark:bg-white/5 rounded-xl">
<div className="flex items-center justify-center gap-2">
<Spinner className="size-5" />
<span className="text-sm font-medium">
Fetching new notes...
Getting new notes...
</span>
</div>
</div>
+26 -8
View File
@@ -1,11 +1,14 @@
import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { LumeEvent } from "@lume/system";
import type { NostrEvent } from "@lume/types";
import { Kind, type NostrEvent } from "@lume/types";
import { Spinner } from "@lume/ui";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { Await, createFileRoute } from "@tanstack/react-router";
import { defer } from "@tanstack/react-router";
import { Suspense, useRef } from "react";
import { Suspense, useCallback, useRef } from "react";
import { Virtualizer } from "virtua";
export const Route = createFileRoute("/trending/notes")({
@@ -21,7 +24,9 @@ export const Route = createFileRoute("/trending/notes")({
const events: NostrEvent[] = res.notes.map(
(item: { event: NostrEvent }) => item.event,
);
const lumeEvents = events.map((ev) => new LumeEvent(ev));
const lumeEvents = Promise.all(
events.map(async (ev) => await LumeEvent.build(ev)),
);
return lumeEvents;
}),
),
@@ -35,7 +40,24 @@ export const Route = createFileRoute("/trending/notes")({
export function Screen() {
const { data } = Route.useLoaderData();
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" />;
}
}
}, []);
return (
<ScrollArea.Root
@@ -60,11 +82,7 @@ export function Screen() {
}
>
<Await promise={data}>
{(notes) =>
notes.map((event) => (
<TextNote key={event.id} event={event} className="mb-3" />
))
}
{(notes) => notes.map((event) => renderItem(event))}
</Await>
</Suspense>
</Virtualizer>
+7 -7
View File
@@ -25,14 +25,14 @@ function Screen() {
return (
<div className="flex flex-col h-full">
<div className="inline-flex items-center w-full gap-1 px-3 h-11 shrink-0">
<div className="inline-flex items-center w-full h-full gap-1">
<div className="shrink-0 h-11 flex items-center w-full gap-1 px-3">
<div className="flex w-full h-full gap-1">
<Link to="/trending/notes" search={search}>
{({ isActive }) => (
<div
className={cn(
"inline-flex h-7 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium",
isActive ? "bg-neutral-50 dark:bg-white/10" : "opacity-50",
"inline-flex h-8 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium",
isActive ? "bg-black/10 dark:bg-white/10" : "opacity-50",
)}
>
<ArticleIcon className="size-4" />
@@ -44,8 +44,8 @@ function Screen() {
{({ isActive }) => (
<div
className={cn(
"inline-flex h-7 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium",
isActive ? "bg-neutral-50 dark:bg-white/10" : "opacity-50",
"inline-flex h-8 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium",
isActive ? "bg-black/10 dark:bg-white/10" : "opacity-50",
)}
>
<GroupFeedsIcon className="size-4" />
@@ -55,7 +55,7 @@ function Screen() {
</Link>
</div>
</div>
<div className="flex-1 w-full h-full p-2 overflow-y-auto scrollbar-none">
<div className="flex-1 w-full h-full overflow-y-auto scrollbar-none">
<Outlet />
</div>
</div>
+3 -3
View File
@@ -1,5 +1,5 @@
import { Spinner } from "@lume/ui";
import { User } from "@/components/user";
import { Spinner } from "@lume/ui";
import { Await, defer } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
import { Suspense } from "react";
@@ -45,14 +45,14 @@ export function Screen() {
users.profiles.map((item: { pubkey: string }) => (
<div
key={item.pubkey}
className="h-max w-full overflow-hidden mb-3 p-2 bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl"
className="h-max w-full overflow-hidden mb-3 p-2 bg-black/5 dark:bg-white/5 rounded-xl"
>
<User.Provider pubkey={item.pubkey}>
<User.Root>
<div className="flex h-full w-full flex-col gap-2">
<div className="flex items-center justify-between">
<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" />
</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" />
+1 -1
View File
@@ -50,7 +50,7 @@ function Screen() {
return (
<Container withDrag>
<Box className="px-0 scrollbar-none bg-black/5 dark:bg-white/5 backdrop-blur-sm">
<Box className="px-0 scrollbar-none bg-black/5 dark:bg-white/5">
<WindowVirtualizer>
<User.Provider pubkey={pubkey}>
<User.Root>
+2 -2
View File
@@ -13,12 +13,12 @@
"@astrojs/check": "^0.5.10",
"@astrojs/tailwind": "^5.1.0",
"@fontsource/alice": "^5.0.13",
"astro": "^4.10.2",
"astro": "^4.11.3",
"astro-seo-meta": "^4.1.1",
"astro-seo-schema": "^4.0.2",
"schema-dts": "^1.1.2",
"tailwindcss": "^3.4.4",
"typescript": "^5.4.5"
"typescript": "^5.5.2"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.13"
+4 -4
View File
@@ -11,7 +11,7 @@
"tauri": "tauri"
},
"devDependencies": {
"@biomejs/biome": "^1.8.1",
"@biomejs/biome": "^1.8.3",
"@tauri-apps/cli": "2.0.0-beta.20",
"turbo": "^1.13.4"
},
@@ -25,11 +25,11 @@
"@tauri-apps/plugin-dialog": "2.0.0-beta.5",
"@tauri-apps/plugin-fs": "2.0.0-beta.5",
"@tauri-apps/plugin-http": "2.0.0-beta.5",
"@tauri-apps/plugin-notification": "2.0.0-beta.5",
"@tauri-apps/plugin-os": "2.0.0-beta.5",
"@tauri-apps/plugin-os": "github:tauri-apps/tauri-plugin-os#v2",
"@tauri-apps/plugin-process": "2.0.0-beta.5",
"@tauri-apps/plugin-shell": "2.0.0-beta.6",
"@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"
}
}
+1 -1
View File
@@ -9,6 +9,6 @@
"devDependencies": {
"@lume/tsconfig": "workspace:*",
"@types/react": "^18.3.3",
"typescript": "^5.4.5"
"typescript": "^5.5.2"
}
}
+3 -3
View File
@@ -5,8 +5,8 @@
"main": "./src/index.ts",
"dependencies": {
"@lume/utils": "workspace:^",
"@tanstack/query-persist-client-core": "^5.45.0",
"@tanstack/react-query": "^5.45.0",
"@tanstack/query-persist-client-core": "^5.49.1",
"@tanstack/react-query": "^5.49.2",
"nostr-tools": "^2.7.0",
"react": "^18.3.1"
},
@@ -14,6 +14,6 @@
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.3.3",
"typescript": "^5.4.5"
"typescript": "^5.5.2"
}
}
+34 -53
View File
@@ -1,9 +1,8 @@
import type { Metadata } from "@lume/types";
import { Window } from "@tauri-apps/api/window";
import { type Result, commands } from "./commands";
export class NostrAccount {
static async getAccounts() {
export const NostrAccount = {
getAccounts: async () => {
const query = await commands.getAccounts();
if (query.status === "ok") {
@@ -11,9 +10,8 @@ export class NostrAccount {
} else {
return [];
}
}
static async loadAccount(npub: string) {
},
loadAccount: async (npub: string) => {
const bunker: string = localStorage.getItem(`${npub}_bunker`);
let query: Result<boolean, string>;
@@ -24,16 +22,12 @@ export class NostrAccount {
}
if (query.status === "ok") {
const panel = Window.getByLabel("panel");
panel.emit("load-notification", { account: npub }); // trigger load notification
return query.data;
} else {
throw new Error(query.error);
}
}
static async createAccount() {
},
createAccount: async () => {
const query = await commands.createAccount();
if (query.status === "ok") {
@@ -41,9 +35,8 @@ export class NostrAccount {
} else {
throw new Error(query.error);
}
}
static async createProfile(profile: Metadata) {
},
createProfile: async (profile: Metadata) => {
const query = await commands.createProfile(
profile.name || "",
profile.display_name || "",
@@ -60,9 +53,8 @@ export class NostrAccount {
} else {
throw new Error(query.error);
}
}
static async saveAccount(nsec: string, password = "") {
},
saveAccount: async (nsec: string, password = "") => {
const query = await commands.saveAccount(nsec, password);
if (query.status === "ok") {
@@ -70,9 +62,8 @@ export class NostrAccount {
} else {
throw new Error(query.error);
}
}
static async connectRemoteAccount(uri: string) {
},
connectRemoteAccount: async (uri: string) => {
const connect = await commands.connectRemoteAccount(uri);
if (connect.status === "ok") {
@@ -87,9 +78,8 @@ export class NostrAccount {
} else {
throw new Error(connect.error);
}
}
static async setContactList(pubkeys: string[]) {
},
setContactList: async (pubkeys: string[]) => {
const query = await commands.setContactList(pubkeys);
if (query.status === "ok") {
@@ -97,9 +87,8 @@ export class NostrAccount {
} else {
throw new Error(query.error);
}
}
static async loadWallet() {
},
loadWallet: async () => {
const query = await commands.loadWallet();
if (query.status === "ok") {
@@ -107,9 +96,8 @@ export class NostrAccount {
} else {
throw new Error(query.error);
}
}
static async setWallet(uri: string) {
},
setWallet: async (uri: string) => {
const query = await commands.setWallet(uri);
if (query.status === "ok") {
@@ -117,9 +105,8 @@ export class NostrAccount {
} else {
throw new Error(query.error);
}
}
static async removeWallet() {
},
removeWallet: async () => {
const query = await commands.removeWallet();
if (query.status === "ok") {
@@ -127,9 +114,8 @@ export class NostrAccount {
} else {
throw new Error(query.error);
}
}
static async getProfile() {
},
getProfile: async () => {
const query = await commands.getCurrentProfile();
if (query.status === "ok") {
@@ -137,9 +123,8 @@ export class NostrAccount {
} else {
return null;
}
}
static async getContactList() {
},
getContactList: async () => {
const query = await commands.getContactList();
if (query.status === "ok") {
@@ -147,9 +132,8 @@ export class NostrAccount {
} else {
return [];
}
}
static async isContactListEmpty() {
},
isContactListEmpty: async () => {
const query = await commands.isContactListEmpty();
if (query.status === "ok") {
@@ -157,9 +141,8 @@ export class NostrAccount {
} else {
return true;
}
}
static async checkContact(pubkey: string) {
},
checkContact: async (pubkey: string) => {
const query = await commands.checkContact(pubkey);
if (query.status === "ok") {
@@ -167,9 +150,8 @@ export class NostrAccount {
} else {
throw new Error(query.error);
}
}
static async toggleContact(pubkey: string, alias?: string) {
},
toggleContact: async (pubkey: string, alias?: string) => {
const query = await commands.toggleContact(pubkey, alias);
if (query.status === "ok") {
@@ -177,9 +159,8 @@ export class NostrAccount {
} else {
throw new Error(query.error);
}
}
static async f2f(npub: string) {
},
f2f: async (npub: string) => {
const query = await commands.friendToFriend(npub);
if (query.status === "ok") {
@@ -187,5 +168,5 @@ export class NostrAccount {
} else {
throw new Error(query.error);
}
}
}
},
};
+47 -14
View File
@@ -4,7 +4,7 @@
/** user-defined commands **/
export const commands = {
async getRelays() : Promise<Result<Relays, null>> {
async getRelays() : Promise<Result<Relays, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_relays") };
} catch (e) {
@@ -12,7 +12,7 @@ try {
else return { status: "error", error: e as any };
}
},
async connectRelay(relay: string) : Promise<Result<boolean, null>> {
async connectRelay(relay: string) : Promise<Result<boolean, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("connect_relay", { relay }) };
} catch (e) {
@@ -20,7 +20,7 @@ try {
else return { status: "error", error: e as any };
}
},
async removeRelay(relay: string) : Promise<Result<boolean, null>> {
async removeRelay(relay: string) : Promise<Result<boolean, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("remove_relay", { relay }) };
} catch (e) {
@@ -28,7 +28,7 @@ try {
else return { status: "error", error: e as any };
}
},
async getBootstrapRelays() : Promise<Result<string[], null>> {
async getBootstrapRelays() : Promise<Result<string[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_bootstrap_relays") };
} catch (e) {
@@ -52,7 +52,7 @@ try {
else return { status: "error", error: e as any };
}
},
async createAccount() : Promise<Result<Account, null>> {
async createAccount() : Promise<Result<Account, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("create_account") };
} catch (e) {
@@ -148,7 +148,7 @@ try {
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 {
return { status: "ok", data: await TAURI_INVOKE("check_contact", { hex }) };
} catch (e) {
@@ -292,6 +292,14 @@ try {
else return { status: "error", error: e as any };
}
},
async listenEventReply(id: string) : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("listen_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>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_events_by", { publicKey, asOf }) };
@@ -308,6 +316,14 @@ try {
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>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_group_events", { publicKeys, until }) };
@@ -372,18 +388,23 @@ try {
else return { status: "error", error: e as any };
}
},
async showInFolder(path: string) : Promise<void> {
await TAURI_INVOKE("show_in_folder", { path });
},
async createColumn(label: string, x: number, y: number, width: number, height: number, url: string) : Promise<Result<string, string>> {
async unlisten(id: string) : Promise<Result<null, null>> {
try {
return { status: "ok", data: await TAURI_INVOKE("create_column", { label, x, y, width, height, url }) };
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 closeColumn(label: string) : Promise<Result<boolean, null>> {
async createColumn(column: Column) : Promise<Result<string, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("create_column", { column }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async closeColumn(label: string) : Promise<Result<boolean, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("close_column", { label }) };
} catch (e) {
@@ -407,6 +428,14 @@ try {
else return { status: "error", error: e as any };
}
},
async reloadColumn(label: string) : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("reload_column", { label }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async openWindow(window: Window) : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("open_window", { window }) };
@@ -418,6 +447,9 @@ try {
async openMainWindow() : Promise<void> {
await TAURI_INVOKE("open_main_window");
},
async forceQuit() : Promise<void> {
await TAURI_INVOKE("force_quit");
},
async setBadge(count: number) : Promise<void> {
await TAURI_INVOKE("set_badge", { count });
}
@@ -434,11 +466,12 @@ await TAURI_INVOKE("set_badge", { count });
/** user-defined types **/
export type Account = { npub: string; nsec: string }
export type Column = { label: string; url: string; x: number; y: number; width: number; height: number }
export type Meta = { content: string; images: string[]; videos: string[]; events: string[]; mentions: string[]; hashtags: string[] }
export type Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null }
export type RichEvent = { raw: string; parsed: Meta | null }
export type Settings = { proxy: string | null; image_resize_service: string | null; use_relay_hint: boolean; content_warning: boolean; display_avatar: boolean; display_zap_button: boolean; display_repost_button: boolean; display_media: boolean }
export type Window = { label: string; title: string; url: string; width: number; height: number; maximizable: boolean; minimizable: boolean }
export type Settings = { proxy: string | null; image_resize_service: string | null; use_relay_hint: boolean; content_warning: boolean; display_avatar: boolean; display_zap_button: boolean; display_repost_button: boolean; display_media: boolean; vibrancy: boolean }
export type Window = { label: string; title: string; url: string; width: number; height: number; maximizable: boolean; minimizable: boolean; hidden_title: boolean }
/** tauri-specta globals **/
+103 -36
View File
@@ -1,10 +1,4 @@
import type {
EventTag,
EventWithReplies,
Kind,
Meta,
NostrEvent,
} from "@lume/types";
import type { EventTag, Kind, Meta, NostrEvent } from "@lume/types";
import { type Result, commands } from "./commands";
export class LumeEvent {
@@ -110,52 +104,82 @@ export class LumeEvent {
}
}
public async getAllReplies() {
public async getEventReplies() {
const query = await commands.getReplies(this.id);
if (query.status === "ok") {
const events = query.data.map((item) => {
const nostrEvent: NostrEvent = JSON.parse(item.raw);
const events = query.data
// Create Lume Events
.map((item) => LumeEvent.from(item.raw, item.parsed))
// Filter quote event
.filter(
(ev) =>
!ev.tags.filter((t) => t[0] === "q" || t[3] === "mention").length,
);
if (item.parsed) {
nostrEvent.meta = item.parsed;
} else {
nostrEvent.meta = null;
}
const lumeEvent = new LumeEvent(nostrEvent);
return lumeEvent;
});
if (events.length > 0) {
const replies = new Set();
if (events.length > 1) {
const removeQueues = new Set();
for (const event of events) {
const tags = event.tags.filter(
(el) => el[0] === "e" && el[1] !== this.id && el[3] !== "mention",
(t) => t[0] === "e" && t[1] !== this.id,
);
if (tags.length > 0) {
for (const tag of tags) {
const rootIndex = events.findIndex((el) => el.id === tag[1]);
if (tags.length === 1) {
const index = events.findIndex((ev) => ev.id === tags[0][1]);
if (rootIndex !== -1) {
const rootEvent = events[rootIndex];
if (index !== -1) {
const rootEvent = events[index];
if (rootEvent?.replies) {
rootEvent.replies.push(event);
} else {
rootEvent.replies = [event];
if (rootEvent.replies?.length) {
rootEvent.replies.push(event);
} else {
rootEvent.replies = [event];
}
// Add current event to queue
removeQueues.add(event.id);
continue;
}
}
for (const tag of tags) {
const id = tag[1];
const rootIndex = events.findIndex((ev) => ev.id === id);
if (rootIndex !== -1) {
const rootEvent = events[rootIndex];
if (rootEvent.replies?.length) {
const childIndex = rootEvent.replies.findIndex(
(ev) => ev.id === id,
);
if (childIndex !== -1) {
const childEvent = rootEvent.replies[rootIndex];
if (childEvent.replies?.length) {
childEvent.replies.push(event);
} else {
childEvent.replies = [event];
}
// Add current event to queue
removeQueues.add(event.id);
}
replies.add(event.id);
} else {
rootEvent.replies = [event];
// Add current event to queue
removeQueues.add(event.id);
}
}
break;
}
}
return events.filter((ev) => !replies.has(ev.id));
return events.filter((ev) => !removeQueues.has(ev.id));
}
return events;
@@ -165,6 +189,26 @@ export class LumeEvent {
}
}
public async listenEventReply() {
const query = await commands.listenEventReply(this.id);
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
}
public async unlistenEventReply() {
const query = await commands.unlisten(this.id);
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
}
public async zap(amount: number, message: string) {
const query = await commands.zapEvent(this.id, amount.toString(), message);
@@ -226,4 +270,27 @@ export class LumeEvent {
throw new Error(query.error);
}
}
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) {
const nostrEvent: NostrEvent = JSON.parse(raw);
if (parsed) {
nostrEvent.meta = parsed;
} else {
nostrEvent.meta = null;
}
return new LumeEvent(nostrEvent);
}
}
+84 -84
View File
@@ -1,5 +1,6 @@
import type { LumeColumn, Metadata, NostrEvent, Relay } from "@lume/types";
import { resolveResource } from "@tauri-apps/api/path";
import { getCurrent } from "@tauri-apps/api/window";
import { open } from "@tauri-apps/plugin-dialog";
import { readFile, readTextFile } from "@tauri-apps/plugin-fs";
import { relaunch } from "@tauri-apps/plugin-process";
@@ -7,26 +8,26 @@ import { nip19 } from "nostr-tools";
import { type Result, type RichEvent, commands } from "./commands";
import { LumeEvent } from "./event";
export class NostrQuery {
static #toLumeEvents(richEvents: RichEvent[]) {
const events = richEvents.map((item) => {
const nostrEvent: NostrEvent = JSON.parse(item.raw);
function toLumeEvents(richEvents: RichEvent[]) {
const events = richEvents.map((item) => {
const nostrEvent: NostrEvent = JSON.parse(item.raw);
if (item.parsed) {
nostrEvent.meta = item.parsed;
} else {
nostrEvent.meta = null;
}
if (item.parsed) {
nostrEvent.meta = item.parsed;
} else {
nostrEvent.meta = null;
}
const lumeEvent = new LumeEvent(nostrEvent);
const lumeEvent = new LumeEvent(nostrEvent);
return lumeEvent;
});
return lumeEvent;
});
return events;
}
return events;
}
static async upload(filePath?: string) {
export const NostrQuery = {
upload: async (filePath?: string) => {
const allowExts = [
"png",
"jpeg",
@@ -79,9 +80,8 @@ export class NostrQuery {
} catch (e) {
throw new Error(String(e));
}
}
static async getNotifications() {
},
getNotifications: async () => {
const query = await commands.getNotifications();
if (query.status === "ok") {
@@ -93,9 +93,8 @@ export class NostrQuery {
console.error(query.error);
return [];
}
}
static async getProfile(pubkey: string) {
},
getProfile: async (pubkey: string) => {
const normalize = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, "");
const query = await commands.getProfile(normalize);
@@ -105,9 +104,8 @@ export class NostrQuery {
} else {
return null;
}
}
static async getEvent(id: string, hint?: string) {
},
getEvent: async (id: string, hint?: string) => {
// Validate ID
const normalizeId: string = id
.replace("nostr:", "")
@@ -149,9 +147,8 @@ export class NostrQuery {
console.log("[getEvent]: ", query.error);
return null;
}
}
static async getRepostEvent(event: LumeEvent) {
},
getRepostEvent: async (event: LumeEvent) => {
try {
const embed: NostrEvent = JSON.parse(event.content);
const query = await commands.getEventMeta(embed.content);
@@ -180,70 +177,74 @@ export class NostrQuery {
return null;
}
}
}
static async getUserEvents(pubkey: string, asOf?: number) {
},
getUserEvents: async (pubkey: string, asOf?: number) => {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const query = await commands.getEventsBy(pubkey, until);
if (query.status === "ok") {
const data = NostrQuery.#toLumeEvents(query.data);
const data = toLumeEvents(query.data);
return data;
} else {
return [];
}
}
static async getLocalEvents(asOf?: number) {
},
getLocalEvents: async (asOf?: number) => {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const query = await commands.getLocalEvents(until);
if (query.status === "ok") {
const data = NostrQuery.#toLumeEvents(query.data);
const data = toLumeEvents(query.data);
return data;
} else {
return [];
}
}
},
listenLocalEvent: async () => {
const label = getCurrent().label;
const query = await commands.listenLocalEvent(label);
static async getGroupEvents(pubkeys: string[], asOf?: number) {
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
},
getGroupEvents: async (pubkeys: string[], asOf?: number) => {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const query = await commands.getGroupEvents(pubkeys, until);
if (query.status === "ok") {
const data = NostrQuery.#toLumeEvents(query.data);
const data = toLumeEvents(query.data);
return data;
} else {
return [];
}
}
static async getGlobalEvents(asOf?: number) {
},
getGlobalEvents: async (asOf?: number) => {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const query = await commands.getGlobalEvents(until);
if (query.status === "ok") {
const data = NostrQuery.#toLumeEvents(query.data);
const data = toLumeEvents(query.data);
return data;
} else {
return [];
}
}
static async getHashtagEvents(hashtags: string[], asOf?: number) {
},
getHashtagEvents: async (hashtags: string[], asOf?: number) => {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const nostrTags = hashtags.map((tag) => tag.replace("#", ""));
const query = await commands.getHashtagEvents(nostrTags, until);
if (query.status === "ok") {
const data = NostrQuery.#toLumeEvents(query.data);
const data = toLumeEvents(query.data);
return data;
} else {
return [];
}
}
static async verifyNip05(pubkey: string, nip05?: string) {
},
verifyNip05: async (pubkey: string, nip05?: string) => {
const query = await commands.verifyNip05(pubkey, nip05);
if (query.status === "ok") {
@@ -251,9 +252,8 @@ export class NostrQuery {
} else {
return false;
}
}
static async getNstore(key: string) {
},
getNstore: async (key: string) => {
const query = await commands.getNstore(key);
if (query.status === "ok") {
@@ -262,9 +262,8 @@ export class NostrQuery {
} else {
return null;
}
}
static async setNstore(key: string, value: string) {
},
setNstore: async (key: string, value: string) => {
const query = await commands.setNstore(key, value);
if (query.status === "ok") {
@@ -272,9 +271,8 @@ export class NostrQuery {
} else {
throw new Error(query.error);
}
}
static async getUserSettings() {
},
getUserSettings: async () => {
const query = await commands.getSettings();
if (query.status === "ok") {
@@ -282,9 +280,8 @@ export class NostrQuery {
} else {
return query.error;
}
}
static async setUserSettings(newSettings: string) {
},
setUserSettings: async (newSettings: string) => {
const query = await commands.setNewSettings(newSettings);
if (query.status === "ok") {
@@ -292,9 +289,8 @@ export class NostrQuery {
} else {
return query.error;
}
}
static async getColumns() {
},
getColumns: async () => {
const key = "lume:columns";
const systemPath = "resources/system_columns.json";
const resourcePath = await resolveResource(systemPath);
@@ -319,9 +315,8 @@ export class NostrQuery {
} catch {
return systemColumns;
}
}
static async setColumns(columns: LumeColumn[]) {
},
setColumns: async (columns: LumeColumn[]) => {
const key = "lume:columns";
const content = JSON.stringify(columns);
const query = await commands.setNstore(key, content);
@@ -331,9 +326,8 @@ export class NostrQuery {
} else {
throw new Error(query.error);
}
}
static async getRelays() {
},
getRelays: async () => {
const query = await commands.getRelays();
if (query.status === "ok") {
@@ -341,9 +335,8 @@ export class NostrQuery {
} else {
throw new Error(query.error);
}
}
static async connectRelay(url: string) {
},
connectRelay: async (url: string) => {
const relayUrl = new URL(url);
if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") {
@@ -355,9 +348,8 @@ export class NostrQuery {
throw new Error(query.error);
}
}
}
static async removeRelay(url: string) {
},
removeRelay: async (url: string) => {
const relayUrl = new URL(url);
if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") {
@@ -369,9 +361,8 @@ export class NostrQuery {
throw new Error(query.error);
}
}
}
static async getBootstrapRelays() {
},
getBootstrapRelays: async () => {
const query = await commands.getBootstrapRelays();
if (query.status === "ok") {
@@ -389,9 +380,8 @@ export class NostrQuery {
} else {
return [];
}
}
static async saveBootstrapRelays(relays: Relay[]) {
},
saveBootstrapRelays: async (relays: Relay[]) => {
const text = relays
.map((relay) => Object.values(relay).join(","))
.join("\n");
@@ -402,5 +392,15 @@ export class NostrQuery {
} else {
throw new Error(query.error);
}
}
}
},
unlisten: async (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);
}
},
};
+31 -25
View File
@@ -2,21 +2,20 @@ import type { NostrEvent } from "@lume/types";
import { commands } from "./commands";
import type { LumeEvent } from "./event";
export class LumeWindow {
static async openMainWindow() {
export const LumeWindow = {
openMainWindow: async () => {
const query = await commands.openMainWindow();
return query;
}
static async openEvent(event: NostrEvent | LumeEvent) {
},
openEvent: async (event: NostrEvent | LumeEvent) => {
const eTags = event.tags.filter((tag) => tag[0] === "e" || tag[0] === "q");
const root: string =
eTags.find((el) => el[3] === "root")?.[1] ?? eTags[0]?.[1];
const reply: string =
eTags.find((el) => el[3] === "reply")?.[1] ?? eTags[1]?.[1];
const label = `event-${event.id}`;
const url = `/events/${root ?? reply ?? event.id}`;
const label = `event-${root ?? reply ?? event.id}`;
const query = await commands.openWindow({
label,
@@ -26,6 +25,7 @@ export class LumeWindow {
height: 800,
maximizable: true,
minimizable: true,
hidden_title: false,
});
if (query.status === "ok") {
@@ -33,9 +33,8 @@ export class LumeWindow {
} else {
throw new Error(query.error);
}
}
static async openProfile(pubkey: string) {
},
openProfile: async (pubkey: string) => {
const label = `user-${pubkey}`;
const query = await commands.openWindow({
label,
@@ -45,6 +44,7 @@ export class LumeWindow {
height: 800,
maximizable: true,
minimizable: true,
hidden_title: true,
});
if (query.status === "ok") {
@@ -52,9 +52,8 @@ export class LumeWindow {
} else {
throw new Error(query.error);
}
}
static async openEditor(reply_to?: string, quote?: string) {
},
openEditor: async (reply_to?: string, quote?: string) => {
let url: string;
if (reply_to) {
@@ -76,8 +75,9 @@ export class LumeWindow {
title: "Editor",
width: 560,
height: 340,
maximizable: true,
maximizable: false,
minimizable: false,
hidden_title: true,
});
if (query.status === "ok") {
@@ -85,9 +85,8 @@ export class LumeWindow {
} else {
throw new Error(query.error);
}
}
static async openZap(id: string) {
},
openZap: async (id: string) => {
const wallet = await commands.loadWallet();
if (wallet.status === "ok") {
@@ -99,13 +98,13 @@ export class LumeWindow {
height: 460,
maximizable: false,
minimizable: false,
hidden_title: true,
});
} else {
await LumeWindow.openSettings("bitcoin-connect");
}
}
static async openSettings(path?: string) {
},
openSettings: async (path?: string) => {
const label = "settings";
const query = await commands.openWindow({
label,
@@ -115,6 +114,7 @@ export class LumeWindow {
height: 500,
maximizable: false,
minimizable: false,
hidden_title: true,
});
if (query.status === "ok") {
@@ -122,18 +122,24 @@ export class LumeWindow {
} else {
throw new Error(query.error);
}
}
},
openSearch: async (searchType: "notes" | "users", searchQuery: string) => {
const url = `/search/${searchType}?query=${searchQuery}`;
const label = `search-${searchQuery
.toLowerCase()
.replace(/[^\w ]+/g, "")
.replace(/ +/g, "_")
.replace(/_+/g, "_")}`;
static async openSearch() {
const label = "search";
const query = await commands.openWindow({
label,
url: "/search",
url,
title: "Search",
width: 400,
height: 600,
maximizable: false,
minimizable: false,
hidden_title: true,
});
if (query.status === "ok") {
@@ -141,5 +147,5 @@ export class LumeWindow {
} else {
throw new Error(query.error);
}
}
}
},
};
+2 -1
View File
@@ -15,6 +15,7 @@
"@tailwindcss/typography": "^0.5.13",
"tailwind-gradient-mask-image": "^1.2.0",
"tailwind-scrollbar": "^3.1.0",
"tailwindcss": "^3.4.4"
"tailwindcss": "^3.4.4",
"tailwindcss-content-visibility": "^0.2.0"
}
}
+1 -1
View File
@@ -49,7 +49,7 @@ const config = {
require("@tailwindcss/forms"),
require("@tailwindcss/typography"),
require("tailwind-gradient-mask-image"),
require("tailwind-scrollbar")({ nocompatible: true }),
require("tailwindcss-content-visibility"),
],
};
+2 -1
View File
@@ -78,10 +78,11 @@ export interface LumeColumn {
}
export interface ColumnEvent {
type: "reset" | "add" | "remove" | "update" | "left" | "right" | "set_title";
type: "reset" | "add" | "remove" | "update" | "move" | "set_title";
label?: string;
title?: string;
column?: LumeColumn;
direction?: "left" | "right";
}
export interface Relays {
+1 -1
View File
@@ -9,6 +9,6 @@
"access": "public"
},
"devDependencies": {
"typescript": "^5.4.5"
"typescript": "^5.5.2"
}
}
+1 -1
View File
@@ -16,6 +16,6 @@
"@lume/types": "workspace:^",
"@types/react": "^18.3.3",
"tailwindcss": "^3.4.4",
"typescript": "^5.4.5"
"typescript": "^5.5.2"
}
}
-84
View File
@@ -1,84 +0,0 @@
import { ArrowLeftIcon, ArrowRightIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import { useSnapCarousel } from "react-snap-carousel";
interface CarouselProps<T> {
readonly items: T[];
readonly renderItem: (
props: CarouselRenderItemProps<T>,
) => React.ReactElement<CarouselItemProps>;
}
interface CarouselRenderItemProps<T> {
readonly item: T;
readonly index: number;
readonly isSnapPoint: boolean;
}
export const Carousel = <T,>({ items, renderItem }: CarouselProps<T>) => {
const { scrollRef, pages, activePageIndex, prev, next, snapPointIndexes } =
useSnapCarousel();
return (
<div className="relative group">
<ul
ref={scrollRef}
className="relative flex overflow-auto snap-x scrollbar-none"
>
{items.map((item, index) =>
renderItem({
item,
index,
isSnapPoint: snapPointIndexes.has(index),
}),
)}
</ul>
<div
aria-hidden
className="absolute z-10 items-center justify-between hidden w-full px-5 transform -translate-x-1/2 -translate-y-1/2 group-hover:flex left-1/2 top-1/2"
>
<button
type="button"
className={cn(
"size-11 rounded-full bg-black/50 backdrop-blur-sm flex items-center justify-center text-white",
activePageIndex <= 0 ? "opacity-50" : "",
)}
onClick={() => prev()}
>
<ArrowLeftIcon className="size-6" />
</button>
<button
type="button"
className={cn(
"size-11 rounded-full bg-black/50 backdrop-blur-sm flex items-center justify-center text-white",
activePageIndex <= 0 ? "opacity-50" : "",
)}
onClick={() => next()}
>
<ArrowRightIcon className="size-6" />
</button>
</div>
<div className="absolute flex items-center justify-center w-12 h-6 text-sm font-medium text-white bg-black rounded-full top-3 right-3 mix-blend-multiply bg-opacity-20 backdrop-blur-sm">
{activePageIndex + 1} / {pages.length}
</div>
</div>
);
};
interface CarouselItemProps {
readonly isSnapPoint: boolean;
readonly children?: React.ReactNode;
}
export const CarouselItem = ({ isSnapPoint, children }: CarouselItemProps) => {
return (
<li
className={cn(
"w-[240px] h-[320px] shrink-0 pl-3 last:pr-2",
isSnapPoint ? "" : "snap-start",
)}
>
{children}
</li>
);
};
-1
View File
@@ -1,4 +1,3 @@
export * from "./container";
export * from "./box";
export * from "./spinner";
export * from "./carousel";
-1
View File
@@ -1,5 +1,4 @@
export * from "./src/constants";
export * from "./src/delay";
export * from "./src/formater";
export * from "./src/editor";
export * from "./src/cn";
+1 -1
View File
@@ -24,6 +24,6 @@
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"tailwind-merge": "^2.3.0",
"typescript": "^5.4.5"
"typescript": "^5.5.2"
}
}
-1
View File
@@ -1 +0,0 @@
export const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
+4400 -3781
View File
File diff suppressed because it is too large Load Diff
+423 -327
View File
File diff suppressed because it is too large Load Diff
+11 -8
View File
@@ -15,14 +15,14 @@ nostr-sdk = { version = "0.32", features = ["sqlite"] }
tokio = { version = "1", features = ["full"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
monitor = { git = "https://github.com/ahkohd/tauri-toolkit", branch = "v2" }
tauri = { version = "2.0.0-beta", features = [
"unstable",
"tray-icon",
"macos-private-api",
"native-tls-vendored",
"protocol-asset",
"unstable",
"tray-icon",
"macos-private-api",
"native-tls-vendored",
"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-dialog = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-fs = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
@@ -33,8 +33,9 @@ tauri-plugin-process = { git = "https://github.com/tauri-apps/plugins-workspace"
tauri-plugin-shell = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-updater = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-upload = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
tauri-specta = { version = "^2.0.0-rc.11", features = ["typescript"] }
tauri-specta = { git = "https://github.com/reyamir/tauri-specta", features = [
"typescript",
] }
tauri-plugin-theme = "0.4.1"
tauri-plugin-decorum = "0.1.0"
specta = "^2.0.0-rc.12"
@@ -50,6 +51,8 @@ regex = "1.10.4"
cocoa = "0.25.0"
objc = "0.2.7"
rand = "0.8.5"
monitor = { git = "https://github.com/ahkohd/tauri-toolkit", branch = "v2" }
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
[profile.release]
codegen-units = 1
+51
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/*"
}
]
}
]
}
+89 -87
View File
@@ -1,89 +1,91 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "desktop-capability",
"description": "Capability for the desktop",
"platforms": ["linux", "macOS", "windows"],
"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-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/*"
}
]
}
]
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "desktop-capability",
"description": "Capability for the desktop",
"platforms": [
"linux",
"macOS",
"windows"
],
"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/*"
}
]
}
]
}
File diff suppressed because one or more lines are too long
+1 -1
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-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"]}}
+347 -66
View File
@@ -37,7 +37,7 @@
],
"definitions": {
"Capability": {
"description": "a grouping and boundary mechanism developers can use to separate windows or plugins functionality from each other at runtime.\n\nIf a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create trust groups and reduce impact of vulnerabilities in certain plugins or windows. Windows can be added to a capability by exact name or glob patterns like *, admin-* or main-window.",
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows fine grained access to the Tauri core, application, or plugin commands. If a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"path:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, \"platforms\": [\"macOS\",\"windows\"] } ```",
"type": "object",
"required": [
"identifier",
@@ -45,16 +45,16 @@
],
"properties": {
"identifier": {
"description": "Identifier of the capability.",
"description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`",
"type": "string"
},
"description": {
"description": "Description of the capability.",
"description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.",
"default": "",
"type": "string"
},
"remote": {
"description": "Configure remote URLs that can use the capability permissions.",
"description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```",
"anyOf": [
{
"$ref": "#/definitions/CapabilityRemote"
@@ -70,28 +70,29 @@
"type": "boolean"
},
"windows": {
"description": "List of windows that uses this capability. Can be a glob pattern.\n\nOn multiwebview windows, prefer [`Self::webviews`] for a fine grained access control.",
"description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nOn multiwebview windows, prefer [`Self::webviews`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`",
"type": "array",
"items": {
"type": "string"
}
},
"webviews": {
"description": "List of webviews that uses this capability. Can be a glob pattern.\n\nThis is only required when using on multiwebview contexts, by default all child webviews of a window that matches [`Self::windows`] are linked.",
"description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThis is only required when using on multiwebview contexts, by default all child webviews of a window that matches [`Self::windows`] are linked.\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`",
"type": "array",
"items": {
"type": "string"
}
},
"permissions": {
"description": "List of permissions attached to this capability. Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.",
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"path:default\", \"event:default\", \"window:default\", \"app:default\", \"image:default\", \"resources:default\", \"menu:default\", \"tray:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ```",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionEntry"
}
},
"uniqueItems": true
},
"platforms": {
"description": "Target platforms this capability applies. By default all platforms are affected by this capability.",
"description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`",
"type": [
"array",
"null"
@@ -110,7 +111,7 @@
],
"properties": {
"urls": {
"description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n# Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api",
"description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api",
"type": "array",
"items": {
"type": "string"
@@ -142,7 +143,7 @@
"identifier": {
"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",
"enum": [
"fs:default"
@@ -1373,6 +1374,13 @@
"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.",
"type": "string",
@@ -1562,6 +1570,13 @@
"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.",
"type": "string",
@@ -1605,7 +1620,7 @@
]
},
{
"description": "fs:scope-app-recursive -> This scope recursive access to the complete `$APP` folder, including sub directories and files.",
"description": "fs:scope-app-recursive -> This scope permits recursive access to the complete `$APP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-app-recursive"
@@ -1626,7 +1641,7 @@
]
},
{
"description": "fs:scope-appcache-recursive -> This scope recursive access to the complete `$APPCACHE` folder, including sub directories and files.",
"description": "fs:scope-appcache-recursive -> This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appcache-recursive"
@@ -1647,7 +1662,7 @@
]
},
{
"description": "fs:scope-appconfig-recursive -> This scope recursive access to the complete `$APPCONFIG` folder, including sub directories and files.",
"description": "fs:scope-appconfig-recursive -> This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appconfig-recursive"
@@ -1668,7 +1683,7 @@
]
},
{
"description": "fs:scope-appdata-recursive -> This scope recursive access to the complete `$APPDATA` folder, including sub directories and files.",
"description": "fs:scope-appdata-recursive -> This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appdata-recursive"
@@ -1689,7 +1704,7 @@
]
},
{
"description": "fs:scope-applocaldata-recursive -> This scope recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.",
"description": "fs:scope-applocaldata-recursive -> This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-applocaldata-recursive"
@@ -1710,7 +1725,7 @@
]
},
{
"description": "fs:scope-applog-recursive -> This scope recursive access to the complete `$APPLOG` folder, including sub directories and files.",
"description": "fs:scope-applog-recursive -> This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-applog-recursive"
@@ -1731,7 +1746,7 @@
]
},
{
"description": "fs:scope-audio-recursive -> This scope recursive access to the complete `$AUDIO` folder, including sub directories and files.",
"description": "fs:scope-audio-recursive -> This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-audio-recursive"
@@ -1752,7 +1767,7 @@
]
},
{
"description": "fs:scope-cache-recursive -> This scope recursive access to the complete `$CACHE` folder, including sub directories and files.",
"description": "fs:scope-cache-recursive -> This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-cache-recursive"
@@ -1773,7 +1788,7 @@
]
},
{
"description": "fs:scope-config-recursive -> This scope recursive access to the complete `$CONFIG` folder, including sub directories and files.",
"description": "fs:scope-config-recursive -> This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-config-recursive"
@@ -1794,7 +1809,7 @@
]
},
{
"description": "fs:scope-data-recursive -> This scope recursive access to the complete `$DATA` folder, including sub directories and files.",
"description": "fs:scope-data-recursive -> This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-data-recursive"
@@ -1815,7 +1830,7 @@
]
},
{
"description": "fs:scope-desktop-recursive -> This scope recursive access to the complete `$DESKTOP` folder, including sub directories and files.",
"description": "fs:scope-desktop-recursive -> This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-desktop-recursive"
@@ -1836,7 +1851,7 @@
]
},
{
"description": "fs:scope-document-recursive -> This scope recursive access to the complete `$DOCUMENT` folder, including sub directories and files.",
"description": "fs:scope-document-recursive -> This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-document-recursive"
@@ -1857,7 +1872,7 @@
]
},
{
"description": "fs:scope-download-recursive -> This scope recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.",
"description": "fs:scope-download-recursive -> This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-download-recursive"
@@ -1878,7 +1893,7 @@
]
},
{
"description": "fs:scope-exe-recursive -> This scope recursive access to the complete `$EXE` folder, including sub directories and files.",
"description": "fs:scope-exe-recursive -> This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-exe-recursive"
@@ -1899,7 +1914,7 @@
]
},
{
"description": "fs:scope-font-recursive -> This scope recursive access to the complete `$FONT` folder, including sub directories and files.",
"description": "fs:scope-font-recursive -> This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-font-recursive"
@@ -1920,7 +1935,7 @@
]
},
{
"description": "fs:scope-home-recursive -> This scope recursive access to the complete `$HOME` folder, including sub directories and files.",
"description": "fs:scope-home-recursive -> This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-home-recursive"
@@ -1941,7 +1956,7 @@
]
},
{
"description": "fs:scope-localdata-recursive -> This scope recursive access to the complete `$LOCALDATA` folder, including sub directories and files.",
"description": "fs:scope-localdata-recursive -> This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-localdata-recursive"
@@ -1962,7 +1977,7 @@
]
},
{
"description": "fs:scope-log-recursive -> This scope recursive access to the complete `$LOG` folder, including sub directories and files.",
"description": "fs:scope-log-recursive -> This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-log-recursive"
@@ -1983,7 +1998,7 @@
]
},
{
"description": "fs:scope-picture-recursive -> This scope recursive access to the complete `$PICTURE` folder, including sub directories and files.",
"description": "fs:scope-picture-recursive -> This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-picture-recursive"
@@ -2004,7 +2019,7 @@
]
},
{
"description": "fs:scope-public-recursive -> This scope recursive access to the complete `$PUBLIC` folder, including sub directories and files.",
"description": "fs:scope-public-recursive -> This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-public-recursive"
@@ -2025,7 +2040,7 @@
]
},
{
"description": "fs:scope-resource-recursive -> This scope recursive access to the complete `$RESOURCE` folder, including sub directories and files.",
"description": "fs:scope-resource-recursive -> This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-resource-recursive"
@@ -2046,7 +2061,7 @@
]
},
{
"description": "fs:scope-runtime-recursive -> This scope recursive access to the complete `$RUNTIME` folder, including sub directories and files.",
"description": "fs:scope-runtime-recursive -> This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-runtime-recursive"
@@ -2067,7 +2082,7 @@
]
},
{
"description": "fs:scope-temp-recursive -> This scope recursive access to the complete `$TEMP` folder, including sub directories and files.",
"description": "fs:scope-temp-recursive -> This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-temp-recursive"
@@ -2088,7 +2103,7 @@
]
},
{
"description": "fs:scope-template-recursive -> This scope recursive access to the complete `$TEMPLATE` folder, including sub directories and files.",
"description": "fs:scope-template-recursive -> This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-template-recursive"
@@ -2109,7 +2124,7 @@
]
},
{
"description": "fs:scope-video-recursive -> This scope recursive access to the complete `$VIDEO` folder, including sub directories and files.",
"description": "fs:scope-video-recursive -> This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-video-recursive"
@@ -2190,7 +2205,7 @@
"identifier": {
"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",
"enum": [
"http:default"
@@ -2313,6 +2328,7 @@
"identifier": {
"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",
"enum": [
"shell:default"
@@ -2489,6 +2505,13 @@
"app:allow-app-show"
]
},
{
"description": "app:allow-default-window-icon -> Enables the default_window_icon command without any pre-configured scope.",
"type": "string",
"enum": [
"app:allow-default-window-icon"
]
},
{
"description": "app:allow-name -> Enables the name command without any pre-configured scope.",
"type": "string",
@@ -2524,6 +2547,13 @@
"app:deny-app-show"
]
},
{
"description": "app:deny-default-window-icon -> Denies the default_window_icon command without any pre-configured scope.",
"type": "string",
"enum": [
"app:deny-default-window-icon"
]
},
{
"description": "app:deny-name -> Denies the name command without any pre-configured scope.",
"type": "string",
@@ -2546,6 +2576,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",
"enum": [
"clipboard-manager:default"
@@ -2656,6 +2687,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",
"enum": [
"dialog:default"
@@ -3852,7 +3884,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",
"enum": [
"fs:default"
@@ -4026,6 +4058,13 @@
"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.",
"type": "string",
@@ -4215,6 +4254,13 @@
"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.",
"type": "string",
@@ -4258,7 +4304,7 @@
]
},
{
"description": "fs:scope-app-recursive -> This scope recursive access to the complete `$APP` folder, including sub directories and files.",
"description": "fs:scope-app-recursive -> This scope permits recursive access to the complete `$APP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-app-recursive"
@@ -4279,7 +4325,7 @@
]
},
{
"description": "fs:scope-appcache-recursive -> This scope recursive access to the complete `$APPCACHE` folder, including sub directories and files.",
"description": "fs:scope-appcache-recursive -> This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appcache-recursive"
@@ -4300,7 +4346,7 @@
]
},
{
"description": "fs:scope-appconfig-recursive -> This scope recursive access to the complete `$APPCONFIG` folder, including sub directories and files.",
"description": "fs:scope-appconfig-recursive -> This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appconfig-recursive"
@@ -4321,7 +4367,7 @@
]
},
{
"description": "fs:scope-appdata-recursive -> This scope recursive access to the complete `$APPDATA` folder, including sub directories and files.",
"description": "fs:scope-appdata-recursive -> This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appdata-recursive"
@@ -4342,7 +4388,7 @@
]
},
{
"description": "fs:scope-applocaldata-recursive -> This scope recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.",
"description": "fs:scope-applocaldata-recursive -> This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-applocaldata-recursive"
@@ -4363,7 +4409,7 @@
]
},
{
"description": "fs:scope-applog-recursive -> This scope recursive access to the complete `$APPLOG` folder, including sub directories and files.",
"description": "fs:scope-applog-recursive -> This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-applog-recursive"
@@ -4384,7 +4430,7 @@
]
},
{
"description": "fs:scope-audio-recursive -> This scope recursive access to the complete `$AUDIO` folder, including sub directories and files.",
"description": "fs:scope-audio-recursive -> This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-audio-recursive"
@@ -4405,7 +4451,7 @@
]
},
{
"description": "fs:scope-cache-recursive -> This scope recursive access to the complete `$CACHE` folder, including sub directories and files.",
"description": "fs:scope-cache-recursive -> This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-cache-recursive"
@@ -4426,7 +4472,7 @@
]
},
{
"description": "fs:scope-config-recursive -> This scope recursive access to the complete `$CONFIG` folder, including sub directories and files.",
"description": "fs:scope-config-recursive -> This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-config-recursive"
@@ -4447,7 +4493,7 @@
]
},
{
"description": "fs:scope-data-recursive -> This scope recursive access to the complete `$DATA` folder, including sub directories and files.",
"description": "fs:scope-data-recursive -> This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-data-recursive"
@@ -4468,7 +4514,7 @@
]
},
{
"description": "fs:scope-desktop-recursive -> This scope recursive access to the complete `$DESKTOP` folder, including sub directories and files.",
"description": "fs:scope-desktop-recursive -> This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-desktop-recursive"
@@ -4489,7 +4535,7 @@
]
},
{
"description": "fs:scope-document-recursive -> This scope recursive access to the complete `$DOCUMENT` folder, including sub directories and files.",
"description": "fs:scope-document-recursive -> This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-document-recursive"
@@ -4510,7 +4556,7 @@
]
},
{
"description": "fs:scope-download-recursive -> This scope recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.",
"description": "fs:scope-download-recursive -> This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-download-recursive"
@@ -4531,7 +4577,7 @@
]
},
{
"description": "fs:scope-exe-recursive -> This scope recursive access to the complete `$EXE` folder, including sub directories and files.",
"description": "fs:scope-exe-recursive -> This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-exe-recursive"
@@ -4552,7 +4598,7 @@
]
},
{
"description": "fs:scope-font-recursive -> This scope recursive access to the complete `$FONT` folder, including sub directories and files.",
"description": "fs:scope-font-recursive -> This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-font-recursive"
@@ -4573,7 +4619,7 @@
]
},
{
"description": "fs:scope-home-recursive -> This scope recursive access to the complete `$HOME` folder, including sub directories and files.",
"description": "fs:scope-home-recursive -> This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-home-recursive"
@@ -4594,7 +4640,7 @@
]
},
{
"description": "fs:scope-localdata-recursive -> This scope recursive access to the complete `$LOCALDATA` folder, including sub directories and files.",
"description": "fs:scope-localdata-recursive -> This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-localdata-recursive"
@@ -4615,7 +4661,7 @@
]
},
{
"description": "fs:scope-log-recursive -> This scope recursive access to the complete `$LOG` folder, including sub directories and files.",
"description": "fs:scope-log-recursive -> This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-log-recursive"
@@ -4636,7 +4682,7 @@
]
},
{
"description": "fs:scope-picture-recursive -> This scope recursive access to the complete `$PICTURE` folder, including sub directories and files.",
"description": "fs:scope-picture-recursive -> This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-picture-recursive"
@@ -4657,7 +4703,7 @@
]
},
{
"description": "fs:scope-public-recursive -> This scope recursive access to the complete `$PUBLIC` folder, including sub directories and files.",
"description": "fs:scope-public-recursive -> This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-public-recursive"
@@ -4678,7 +4724,7 @@
]
},
{
"description": "fs:scope-resource-recursive -> This scope recursive access to the complete `$RESOURCE` folder, including sub directories and files.",
"description": "fs:scope-resource-recursive -> This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-resource-recursive"
@@ -4699,7 +4745,7 @@
]
},
{
"description": "fs:scope-runtime-recursive -> This scope recursive access to the complete `$RUNTIME` folder, including sub directories and files.",
"description": "fs:scope-runtime-recursive -> This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-runtime-recursive"
@@ -4720,7 +4766,7 @@
]
},
{
"description": "fs:scope-temp-recursive -> This scope recursive access to the complete `$TEMP` folder, including sub directories and files.",
"description": "fs:scope-temp-recursive -> This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-temp-recursive"
@@ -4741,7 +4787,7 @@
]
},
{
"description": "fs:scope-template-recursive -> This scope recursive access to the complete `$TEMPLATE` folder, including sub directories and files.",
"description": "fs:scope-template-recursive -> This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-template-recursive"
@@ -4762,7 +4808,7 @@
]
},
{
"description": "fs:scope-video-recursive -> This scope recursive access to the complete `$VIDEO` folder, including sub directories and files.",
"description": "fs:scope-video-recursive -> This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-video-recursive"
@@ -4783,7 +4829,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",
"enum": [
"http:default"
@@ -5238,12 +5284,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",
"enum": [
"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.",
"type": "string",
@@ -5251,6 +5346,13 @@
"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.",
"type": "string",
@@ -5258,6 +5360,34 @@
"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.",
"type": "string",
"enum": [
"notification:allow-register-action-types"
]
},
{
"description": "notification:allow-register-listener -> Enables the register_listener command without any pre-configured scope.",
"type": "string",
"enum": [
"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.",
"type": "string",
@@ -5265,6 +5395,62 @@
"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.",
"type": "string",
@@ -5272,6 +5458,13 @@
"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.",
"type": "string",
@@ -5279,6 +5472,34 @@
"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.",
"type": "string",
"enum": [
"notification:deny-register-action-types"
]
},
{
"description": "notification:deny-register-listener -> Denies the register_listener command without any pre-configured scope.",
"type": "string",
"enum": [
"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.",
"type": "string",
@@ -5287,6 +5508,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",
"enum": [
"os:default"
@@ -5524,6 +5753,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",
"enum": [
"process:default"
@@ -5579,6 +5809,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",
"enum": [
"shell:default"
@@ -5850,7 +6081,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",
"enum": [
"updater:default"
@@ -5913,6 +6144,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",
"enum": [
"upload:default"
@@ -7009,6 +7241,55 @@
"enum": [
"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"
]
}
]
},
+347 -66
View File
@@ -37,7 +37,7 @@
],
"definitions": {
"Capability": {
"description": "a grouping and boundary mechanism developers can use to separate windows or plugins functionality from each other at runtime.\n\nIf a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create trust groups and reduce impact of vulnerabilities in certain plugins or windows. Windows can be added to a capability by exact name or glob patterns like *, admin-* or main-window.",
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows fine grained access to the Tauri core, application, or plugin commands. If a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"path:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, \"platforms\": [\"macOS\",\"windows\"] } ```",
"type": "object",
"required": [
"identifier",
@@ -45,16 +45,16 @@
],
"properties": {
"identifier": {
"description": "Identifier of the capability.",
"description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`",
"type": "string"
},
"description": {
"description": "Description of the capability.",
"description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.",
"default": "",
"type": "string"
},
"remote": {
"description": "Configure remote URLs that can use the capability permissions.",
"description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```",
"anyOf": [
{
"$ref": "#/definitions/CapabilityRemote"
@@ -70,28 +70,29 @@
"type": "boolean"
},
"windows": {
"description": "List of windows that uses this capability. Can be a glob pattern.\n\nOn multiwebview windows, prefer [`Self::webviews`] for a fine grained access control.",
"description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nOn multiwebview windows, prefer [`Self::webviews`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`",
"type": "array",
"items": {
"type": "string"
}
},
"webviews": {
"description": "List of webviews that uses this capability. Can be a glob pattern.\n\nThis is only required when using on multiwebview contexts, by default all child webviews of a window that matches [`Self::windows`] are linked.",
"description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThis is only required when using on multiwebview contexts, by default all child webviews of a window that matches [`Self::windows`] are linked.\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`",
"type": "array",
"items": {
"type": "string"
}
},
"permissions": {
"description": "List of permissions attached to this capability. Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.",
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"path:default\", \"event:default\", \"window:default\", \"app:default\", \"image:default\", \"resources:default\", \"menu:default\", \"tray:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ```",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionEntry"
}
},
"uniqueItems": true
},
"platforms": {
"description": "Target platforms this capability applies. By default all platforms are affected by this capability.",
"description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`",
"type": [
"array",
"null"
@@ -110,7 +111,7 @@
],
"properties": {
"urls": {
"description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n# Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api",
"description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api",
"type": "array",
"items": {
"type": "string"
@@ -142,7 +143,7 @@
"identifier": {
"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",
"enum": [
"fs:default"
@@ -1373,6 +1374,13 @@
"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.",
"type": "string",
@@ -1562,6 +1570,13 @@
"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.",
"type": "string",
@@ -1605,7 +1620,7 @@
]
},
{
"description": "fs:scope-app-recursive -> This scope recursive access to the complete `$APP` folder, including sub directories and files.",
"description": "fs:scope-app-recursive -> This scope permits recursive access to the complete `$APP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-app-recursive"
@@ -1626,7 +1641,7 @@
]
},
{
"description": "fs:scope-appcache-recursive -> This scope recursive access to the complete `$APPCACHE` folder, including sub directories and files.",
"description": "fs:scope-appcache-recursive -> This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appcache-recursive"
@@ -1647,7 +1662,7 @@
]
},
{
"description": "fs:scope-appconfig-recursive -> This scope recursive access to the complete `$APPCONFIG` folder, including sub directories and files.",
"description": "fs:scope-appconfig-recursive -> This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appconfig-recursive"
@@ -1668,7 +1683,7 @@
]
},
{
"description": "fs:scope-appdata-recursive -> This scope recursive access to the complete `$APPDATA` folder, including sub directories and files.",
"description": "fs:scope-appdata-recursive -> This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appdata-recursive"
@@ -1689,7 +1704,7 @@
]
},
{
"description": "fs:scope-applocaldata-recursive -> This scope recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.",
"description": "fs:scope-applocaldata-recursive -> This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-applocaldata-recursive"
@@ -1710,7 +1725,7 @@
]
},
{
"description": "fs:scope-applog-recursive -> This scope recursive access to the complete `$APPLOG` folder, including sub directories and files.",
"description": "fs:scope-applog-recursive -> This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-applog-recursive"
@@ -1731,7 +1746,7 @@
]
},
{
"description": "fs:scope-audio-recursive -> This scope recursive access to the complete `$AUDIO` folder, including sub directories and files.",
"description": "fs:scope-audio-recursive -> This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-audio-recursive"
@@ -1752,7 +1767,7 @@
]
},
{
"description": "fs:scope-cache-recursive -> This scope recursive access to the complete `$CACHE` folder, including sub directories and files.",
"description": "fs:scope-cache-recursive -> This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-cache-recursive"
@@ -1773,7 +1788,7 @@
]
},
{
"description": "fs:scope-config-recursive -> This scope recursive access to the complete `$CONFIG` folder, including sub directories and files.",
"description": "fs:scope-config-recursive -> This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-config-recursive"
@@ -1794,7 +1809,7 @@
]
},
{
"description": "fs:scope-data-recursive -> This scope recursive access to the complete `$DATA` folder, including sub directories and files.",
"description": "fs:scope-data-recursive -> This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-data-recursive"
@@ -1815,7 +1830,7 @@
]
},
{
"description": "fs:scope-desktop-recursive -> This scope recursive access to the complete `$DESKTOP` folder, including sub directories and files.",
"description": "fs:scope-desktop-recursive -> This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-desktop-recursive"
@@ -1836,7 +1851,7 @@
]
},
{
"description": "fs:scope-document-recursive -> This scope recursive access to the complete `$DOCUMENT` folder, including sub directories and files.",
"description": "fs:scope-document-recursive -> This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-document-recursive"
@@ -1857,7 +1872,7 @@
]
},
{
"description": "fs:scope-download-recursive -> This scope recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.",
"description": "fs:scope-download-recursive -> This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-download-recursive"
@@ -1878,7 +1893,7 @@
]
},
{
"description": "fs:scope-exe-recursive -> This scope recursive access to the complete `$EXE` folder, including sub directories and files.",
"description": "fs:scope-exe-recursive -> This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-exe-recursive"
@@ -1899,7 +1914,7 @@
]
},
{
"description": "fs:scope-font-recursive -> This scope recursive access to the complete `$FONT` folder, including sub directories and files.",
"description": "fs:scope-font-recursive -> This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-font-recursive"
@@ -1920,7 +1935,7 @@
]
},
{
"description": "fs:scope-home-recursive -> This scope recursive access to the complete `$HOME` folder, including sub directories and files.",
"description": "fs:scope-home-recursive -> This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-home-recursive"
@@ -1941,7 +1956,7 @@
]
},
{
"description": "fs:scope-localdata-recursive -> This scope recursive access to the complete `$LOCALDATA` folder, including sub directories and files.",
"description": "fs:scope-localdata-recursive -> This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-localdata-recursive"
@@ -1962,7 +1977,7 @@
]
},
{
"description": "fs:scope-log-recursive -> This scope recursive access to the complete `$LOG` folder, including sub directories and files.",
"description": "fs:scope-log-recursive -> This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-log-recursive"
@@ -1983,7 +1998,7 @@
]
},
{
"description": "fs:scope-picture-recursive -> This scope recursive access to the complete `$PICTURE` folder, including sub directories and files.",
"description": "fs:scope-picture-recursive -> This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-picture-recursive"
@@ -2004,7 +2019,7 @@
]
},
{
"description": "fs:scope-public-recursive -> This scope recursive access to the complete `$PUBLIC` folder, including sub directories and files.",
"description": "fs:scope-public-recursive -> This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-public-recursive"
@@ -2025,7 +2040,7 @@
]
},
{
"description": "fs:scope-resource-recursive -> This scope recursive access to the complete `$RESOURCE` folder, including sub directories and files.",
"description": "fs:scope-resource-recursive -> This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-resource-recursive"
@@ -2046,7 +2061,7 @@
]
},
{
"description": "fs:scope-runtime-recursive -> This scope recursive access to the complete `$RUNTIME` folder, including sub directories and files.",
"description": "fs:scope-runtime-recursive -> This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-runtime-recursive"
@@ -2067,7 +2082,7 @@
]
},
{
"description": "fs:scope-temp-recursive -> This scope recursive access to the complete `$TEMP` folder, including sub directories and files.",
"description": "fs:scope-temp-recursive -> This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-temp-recursive"
@@ -2088,7 +2103,7 @@
]
},
{
"description": "fs:scope-template-recursive -> This scope recursive access to the complete `$TEMPLATE` folder, including sub directories and files.",
"description": "fs:scope-template-recursive -> This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-template-recursive"
@@ -2109,7 +2124,7 @@
]
},
{
"description": "fs:scope-video-recursive -> This scope recursive access to the complete `$VIDEO` folder, including sub directories and files.",
"description": "fs:scope-video-recursive -> This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-video-recursive"
@@ -2190,7 +2205,7 @@
"identifier": {
"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",
"enum": [
"http:default"
@@ -2313,6 +2328,7 @@
"identifier": {
"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",
"enum": [
"shell:default"
@@ -2489,6 +2505,13 @@
"app:allow-app-show"
]
},
{
"description": "app:allow-default-window-icon -> Enables the default_window_icon command without any pre-configured scope.",
"type": "string",
"enum": [
"app:allow-default-window-icon"
]
},
{
"description": "app:allow-name -> Enables the name command without any pre-configured scope.",
"type": "string",
@@ -2524,6 +2547,13 @@
"app:deny-app-show"
]
},
{
"description": "app:deny-default-window-icon -> Denies the default_window_icon command without any pre-configured scope.",
"type": "string",
"enum": [
"app:deny-default-window-icon"
]
},
{
"description": "app:deny-name -> Denies the name command without any pre-configured scope.",
"type": "string",
@@ -2546,6 +2576,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",
"enum": [
"clipboard-manager:default"
@@ -2656,6 +2687,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",
"enum": [
"dialog:default"
@@ -3852,7 +3884,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",
"enum": [
"fs:default"
@@ -4026,6 +4058,13 @@
"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.",
"type": "string",
@@ -4215,6 +4254,13 @@
"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.",
"type": "string",
@@ -4258,7 +4304,7 @@
]
},
{
"description": "fs:scope-app-recursive -> This scope recursive access to the complete `$APP` folder, including sub directories and files.",
"description": "fs:scope-app-recursive -> This scope permits recursive access to the complete `$APP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-app-recursive"
@@ -4279,7 +4325,7 @@
]
},
{
"description": "fs:scope-appcache-recursive -> This scope recursive access to the complete `$APPCACHE` folder, including sub directories and files.",
"description": "fs:scope-appcache-recursive -> This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appcache-recursive"
@@ -4300,7 +4346,7 @@
]
},
{
"description": "fs:scope-appconfig-recursive -> This scope recursive access to the complete `$APPCONFIG` folder, including sub directories and files.",
"description": "fs:scope-appconfig-recursive -> This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appconfig-recursive"
@@ -4321,7 +4367,7 @@
]
},
{
"description": "fs:scope-appdata-recursive -> This scope recursive access to the complete `$APPDATA` folder, including sub directories and files.",
"description": "fs:scope-appdata-recursive -> This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appdata-recursive"
@@ -4342,7 +4388,7 @@
]
},
{
"description": "fs:scope-applocaldata-recursive -> This scope recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.",
"description": "fs:scope-applocaldata-recursive -> This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-applocaldata-recursive"
@@ -4363,7 +4409,7 @@
]
},
{
"description": "fs:scope-applog-recursive -> This scope recursive access to the complete `$APPLOG` folder, including sub directories and files.",
"description": "fs:scope-applog-recursive -> This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-applog-recursive"
@@ -4384,7 +4430,7 @@
]
},
{
"description": "fs:scope-audio-recursive -> This scope recursive access to the complete `$AUDIO` folder, including sub directories and files.",
"description": "fs:scope-audio-recursive -> This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-audio-recursive"
@@ -4405,7 +4451,7 @@
]
},
{
"description": "fs:scope-cache-recursive -> This scope recursive access to the complete `$CACHE` folder, including sub directories and files.",
"description": "fs:scope-cache-recursive -> This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-cache-recursive"
@@ -4426,7 +4472,7 @@
]
},
{
"description": "fs:scope-config-recursive -> This scope recursive access to the complete `$CONFIG` folder, including sub directories and files.",
"description": "fs:scope-config-recursive -> This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-config-recursive"
@@ -4447,7 +4493,7 @@
]
},
{
"description": "fs:scope-data-recursive -> This scope recursive access to the complete `$DATA` folder, including sub directories and files.",
"description": "fs:scope-data-recursive -> This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-data-recursive"
@@ -4468,7 +4514,7 @@
]
},
{
"description": "fs:scope-desktop-recursive -> This scope recursive access to the complete `$DESKTOP` folder, including sub directories and files.",
"description": "fs:scope-desktop-recursive -> This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-desktop-recursive"
@@ -4489,7 +4535,7 @@
]
},
{
"description": "fs:scope-document-recursive -> This scope recursive access to the complete `$DOCUMENT` folder, including sub directories and files.",
"description": "fs:scope-document-recursive -> This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-document-recursive"
@@ -4510,7 +4556,7 @@
]
},
{
"description": "fs:scope-download-recursive -> This scope recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.",
"description": "fs:scope-download-recursive -> This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-download-recursive"
@@ -4531,7 +4577,7 @@
]
},
{
"description": "fs:scope-exe-recursive -> This scope recursive access to the complete `$EXE` folder, including sub directories and files.",
"description": "fs:scope-exe-recursive -> This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-exe-recursive"
@@ -4552,7 +4598,7 @@
]
},
{
"description": "fs:scope-font-recursive -> This scope recursive access to the complete `$FONT` folder, including sub directories and files.",
"description": "fs:scope-font-recursive -> This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-font-recursive"
@@ -4573,7 +4619,7 @@
]
},
{
"description": "fs:scope-home-recursive -> This scope recursive access to the complete `$HOME` folder, including sub directories and files.",
"description": "fs:scope-home-recursive -> This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-home-recursive"
@@ -4594,7 +4640,7 @@
]
},
{
"description": "fs:scope-localdata-recursive -> This scope recursive access to the complete `$LOCALDATA` folder, including sub directories and files.",
"description": "fs:scope-localdata-recursive -> This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-localdata-recursive"
@@ -4615,7 +4661,7 @@
]
},
{
"description": "fs:scope-log-recursive -> This scope recursive access to the complete `$LOG` folder, including sub directories and files.",
"description": "fs:scope-log-recursive -> This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-log-recursive"
@@ -4636,7 +4682,7 @@
]
},
{
"description": "fs:scope-picture-recursive -> This scope recursive access to the complete `$PICTURE` folder, including sub directories and files.",
"description": "fs:scope-picture-recursive -> This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-picture-recursive"
@@ -4657,7 +4703,7 @@
]
},
{
"description": "fs:scope-public-recursive -> This scope recursive access to the complete `$PUBLIC` folder, including sub directories and files.",
"description": "fs:scope-public-recursive -> This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-public-recursive"
@@ -4678,7 +4724,7 @@
]
},
{
"description": "fs:scope-resource-recursive -> This scope recursive access to the complete `$RESOURCE` folder, including sub directories and files.",
"description": "fs:scope-resource-recursive -> This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-resource-recursive"
@@ -4699,7 +4745,7 @@
]
},
{
"description": "fs:scope-runtime-recursive -> This scope recursive access to the complete `$RUNTIME` folder, including sub directories and files.",
"description": "fs:scope-runtime-recursive -> This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-runtime-recursive"
@@ -4720,7 +4766,7 @@
]
},
{
"description": "fs:scope-temp-recursive -> This scope recursive access to the complete `$TEMP` folder, including sub directories and files.",
"description": "fs:scope-temp-recursive -> This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-temp-recursive"
@@ -4741,7 +4787,7 @@
]
},
{
"description": "fs:scope-template-recursive -> This scope recursive access to the complete `$TEMPLATE` folder, including sub directories and files.",
"description": "fs:scope-template-recursive -> This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-template-recursive"
@@ -4762,7 +4808,7 @@
]
},
{
"description": "fs:scope-video-recursive -> This scope recursive access to the complete `$VIDEO` folder, including sub directories and files.",
"description": "fs:scope-video-recursive -> This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-video-recursive"
@@ -4783,7 +4829,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",
"enum": [
"http:default"
@@ -5238,12 +5284,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",
"enum": [
"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.",
"type": "string",
@@ -5251,6 +5346,13 @@
"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.",
"type": "string",
@@ -5258,6 +5360,34 @@
"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.",
"type": "string",
"enum": [
"notification:allow-register-action-types"
]
},
{
"description": "notification:allow-register-listener -> Enables the register_listener command without any pre-configured scope.",
"type": "string",
"enum": [
"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.",
"type": "string",
@@ -5265,6 +5395,62 @@
"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.",
"type": "string",
@@ -5272,6 +5458,13 @@
"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.",
"type": "string",
@@ -5279,6 +5472,34 @@
"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.",
"type": "string",
"enum": [
"notification:deny-register-action-types"
]
},
{
"description": "notification:deny-register-listener -> Denies the register_listener command without any pre-configured scope.",
"type": "string",
"enum": [
"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.",
"type": "string",
@@ -5287,6 +5508,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",
"enum": [
"os:default"
@@ -5524,6 +5753,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",
"enum": [
"process:default"
@@ -5579,6 +5809,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",
"enum": [
"shell:default"
@@ -5850,7 +6081,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",
"enum": [
"updater:default"
@@ -5913,6 +6144,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",
"enum": [
"upload:default"
@@ -7009,6 +7241,55 @@
"enum": [
"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"
]
}
]
},
+35 -50
View File
@@ -1,52 +1,37 @@
[
{
"label": "lZfXLFgPPR4NNrgjlWDxn",
"name": "Newsfeed",
"content": "/newsfeed",
"logo": "",
"cover": "/newsfeed.png",
"coverRetina": "/newsfeed@2x.png",
"author": "Lume",
"description": "Keep up to date with the people you're following."
},
{
"label": "rRtguZwIpd5G8Wt54OTb7",
"name": "Topic",
"content": "/topic",
"logo": "",
"cover": "/foryou.png",
"coverRetina": "/foryou@2x.png",
"author": "Lume",
"description": "Keep up to date with content based on your interests."
},
{
"label": "fve9fk2fVyFWORPBkjd79",
"name": "Group",
"content": "/group",
"logo": "",
"cover": "/group.png",
"coverRetina": "/group@2x.png",
"author": "Lume",
"description": "Focus feeds for people you like."
},
{
"label": "gxtcIbgD8YNPbeI5o92I8",
"name": "Trending",
"content": "/trending/notes",
"logo": "",
"cover": "/trending.png",
"coverRetina": "/trending@2x.png",
"author": "Lume",
"description": "What is trending on Nostr?."
},
{
"label": "GLFm44za8rhJDP04LMr3M",
"name": "Global",
"content": "/global",
"logo": "",
"cover": "/global.png",
"coverRetina": "/global@2x.png",
"author": "Lume",
"description": "All events from connected relays."
}
{
"label": "lZfXLFgPPR4NNrgjlWDxn",
"name": "Local Feeds",
"content": "/newsfeed",
"cover": "/newsfeed.png",
"coverRetina": "/newsfeed@2x.png"
},
{
"label": "GLFm44za8rhJDP04LMr3M",
"name": "Global Feeds",
"content": "/global",
"cover": "/global.png",
"coverRetina": "/global@2x.png"
},
{
"label": "fve9fk2fVyFWORPBkjd79",
"name": "Group Feeds",
"content": "/group",
"cover": "/group.png",
"coverRetina": "/group@2x.png"
},
{
"label": "rRtguZwIpd5G8Wt54OTb7",
"name": "Topic",
"content": "/topic",
"cover": "/foryou.png",
"coverRetina": "/foryou@2x.png"
},
{
"label": "gxtcIbgD8YNPbeI5o92I8",
"name": "Trending",
"content": "/trending/notes",
"cover": "/trending.png",
"coverRetina": "/trending@2x.png"
}
]
@@ -1,11 +1,10 @@
use std::ffi::CString;
use cocoa::appkit::NSWindowCollectionBehavior;
use tauri::Manager;
use tauri::{AppHandle, Manager, WebviewWindow};
use tauri_nspanel::{
block::ConcreteBlock,
cocoa::{
appkit::{NSMainMenuWindowLevel, NSView, NSWindow},
appkit::{NSMainMenuWindowLevel, NSView, NSWindow, NSWindowCollectionBehavior},
base::{id, nil},
foundation::{NSPoint, NSRect},
},
@@ -28,8 +27,11 @@ pub fn swizzle_to_menubar_panel(app_handle: &tauri::AppHandle) {
let handle = app_handle.clone();
panel_delegate.set_listener(Box::new(move |delegate_name: String| {
if delegate_name.as_str() == "window_did_resign_key" {
let _ = handle.emit("menubar_panel_did_resign_key", ());
match delegate_name.as_str() {
"window_did_resign_key" => {
let _ = handle.emit("menubar_panel_did_resign_key", ());
}
_ => (),
}
}));
@@ -46,12 +48,14 @@ pub fn swizzle_to_menubar_panel(app_handle: &tauri::AppHandle) {
panel.set_delegate(panel_delegate);
}
pub fn setup_menubar_panel_listeners(app_handle: &tauri::AppHandle) {
pub fn setup_menubar_panel_listeners(app_handle: &AppHandle) {
fn hide_menubar_panel(app_handle: &tauri::AppHandle) {
if check_menubar_frontmost() {
return;
}
let panel = app_handle.get_webview_panel("panel").unwrap();
panel.order_out(None);
}
@@ -78,19 +82,16 @@ pub fn setup_menubar_panel_listeners(app_handle: &tauri::AppHandle) {
);
}
pub fn update_menubar_appearance(app_handle: &tauri::AppHandle) {
let window = app_handle.get_window("panel").unwrap();
set_corner_radius(&window, 13.0);
}
pub fn set_corner_radius(window: &tauri::Window, radius: f64) {
pub fn set_corner_radius(window: &WebviewWindow, radius: f64) {
let win: id = window.ns_window().unwrap() as _;
unsafe {
let view: id = win.contentView();
view.wantsLayer();
let layer: id = view.layer();
let _: () = msg_send![layer, setCornerRadius: radius];
}
}
@@ -137,7 +138,6 @@ pub fn position_menubar_panel(app_handle: &tauri::AppHandle, padding_top: f64) {
fn register_workspace_listener(name: String, callback: Box<dyn Fn()>) {
let workspace: id = unsafe { msg_send![class!(NSWorkspace), sharedWorkspace] };
let notification_center: id = unsafe { msg_send![workspace, notificationCenter] };
let block = ConcreteBlock::new(move |_notif: id| {
@@ -159,7 +159,6 @@ fn register_workspace_listener(name: String, callback: Box<dyn Fn()>) {
fn app_pid() -> i32 {
let process_info: id = unsafe { msg_send![class!(NSProcessInfo), processInfo] };
let pid: i32 = unsafe { msg_send![process_info, processIdentifier] };
pid
-49
View File
@@ -1,49 +0,0 @@
use std::process::Command;
#[tauri::command]
#[specta::specta]
pub async fn show_in_folder(path: String) {
#[cfg(target_os = "windows")]
{
Command::new("explorer")
.args(["/select,", &path]) // The comma after select is not a typo
.spawn()
.unwrap();
}
#[cfg(target_os = "linux")]
{
use std::fs::metadata;
use std::path::PathBuf;
if path.contains(",") {
// see https://gitlab.freedesktop.org/dbus/dbus/-/issues/76
let new_path = match metadata(&path).unwrap().is_dir() {
true => path,
false => {
let mut path2 = PathBuf::from(path);
path2.pop();
path2.into_os_string().into_string().unwrap()
}
};
Command::new("xdg-open").arg(&new_path).spawn().unwrap();
} else {
Command::new("dbus-send")
.args([
"--session",
"--dest=org.freedesktop.FileManager1",
"--type=method_call",
"/org/freedesktop/FileManager1",
"org.freedesktop.FileManager1.ShowItems",
format!("array:string:file://{path}").as_str(),
"string:\"\"",
])
.spawn()
.unwrap();
}
}
#[cfg(target_os = "macos")]
{
Command::new("open").args(["-R", &path]).spawn().unwrap();
}
}
+2 -1
View File
@@ -1,2 +1,3 @@
pub mod folder;
pub mod fns;
pub mod tray;
pub mod window;
+65
View File
@@ -0,0 +1,65 @@
use std::path::PathBuf;
use tauri::window::{Effect, EffectsBuilder};
use tauri::{
tray::{MouseButtonState, TrayIconEvent},
WebviewWindowBuilder,
};
use tauri::{AppHandle, Manager, WebviewUrl};
use tauri_nspanel::ManagerExt;
use super::fns::{
position_menubar_panel, set_corner_radius, setup_menubar_panel_listeners,
swizzle_to_menubar_panel,
};
pub fn create_tray_panel(account: &str, app: &AppHandle) {
let tray = app.tray_by_id("main").unwrap();
tray.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click { button_state, .. } = event {
if button_state == MouseButtonState::Up {
let app = tray.app_handle();
let panel = app.get_webview_panel("panel").unwrap();
match panel.is_visible() {
true => panel.order_out(None),
false => {
position_menubar_panel(app, 0.0);
panel.show();
}
}
}
}
});
if let Some(window) = app.get_webview_window("panel") {
let _ = window.destroy();
};
let mut url = "/panel/".to_owned();
url.push_str(account);
let window = WebviewWindowBuilder::new(app, "panel", WebviewUrl::App(PathBuf::from(url)))
.title("Panel")
.inner_size(350.0, 500.0)
.fullscreen(false)
.resizable(false)
.visible(false)
.decorations(false)
.transparent(true)
.build()
.unwrap();
let _ = window.set_effects(
EffectsBuilder::new()
.effect(Effect::Popover)
.state(tauri::window::EffectState::FollowsWindowActiveState)
.build(),
);
set_corner_radius(&window, 13.0);
// Convert window to panel
swizzle_to_menubar_panel(app);
setup_menubar_panel_listeners(app);
}
+74 -42
View File
@@ -5,12 +5,12 @@ use std::str::FromStr;
use cocoa::{appkit::NSApp, base::nil, foundation::NSString};
use serde::{Deserialize, Serialize};
use specta::Type;
use tauri::{LogicalPosition, LogicalSize, Manager, State, WebviewUrl};
use tauri::utils::config::WindowEffectsConfig;
use tauri::window::Effect;
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
use tauri::utils::config::WindowEffectsConfig;
use tauri::WebviewWindowBuilder;
use tauri::window::Effect;
use tauri::{LogicalPosition, LogicalSize, Manager, State, WebviewUrl};
use tauri_plugin_decorum::WebviewWindowExt;
use url::Url;
@@ -25,39 +25,45 @@ pub struct Window {
height: f64,
maximizable: bool,
minimizable: bool,
hidden_title: bool,
}
#[derive(Serialize, Deserialize, Type)]
pub struct Column {
label: String,
url: String,
x: f32,
y: f32,
width: f32,
height: f32,
}
#[tauri::command]
#[specta::specta]
pub fn create_column(
label: &str,
x: f32,
y: f32,
width: f32,
height: f32,
url: &str,
column: Column,
app_handle: tauri::AppHandle,
state: State<'_, Nostr>,
) -> Result<String, String> {
let settings = state.settings.lock().unwrap().clone();
match app_handle.get_window("main") {
Some(main_window) => match app_handle.get_webview(label) {
Some(_) => Ok(label.into()),
Some(main_window) => match app_handle.get_webview(&column.label) {
Some(_) => Ok(column.label),
None => {
let path = PathBuf::from(url);
let path = PathBuf::from(column.url);
let webview_url = WebviewUrl::App(path);
let builder = match settings.proxy {
Some(url) => {
let proxy = Url::from_str(&url).unwrap();
tauri::webview::WebviewBuilder::new(label, webview_url)
tauri::webview::WebviewBuilder::new(column.label, webview_url)
.user_agent("Lume/4.0")
.zoom_hotkeys_enabled(true)
.enable_clipboard_access()
.transparent(true)
.proxy_url(proxy)
}
None => tauri::webview::WebviewBuilder::new(label, webview_url)
None => tauri::webview::WebviewBuilder::new(column.label, webview_url)
.user_agent("Lume/4.0")
.zoom_hotkeys_enabled(true)
.enable_clipboard_access()
@@ -65,8 +71,8 @@ pub fn create_column(
};
match main_window.add_child(
builder,
LogicalPosition::new(x, y),
LogicalSize::new(width, height),
LogicalPosition::new(column.x, column.y),
LogicalSize::new(column.width, column.height),
) {
Ok(webview) => Ok(webview.label().into()),
Err(_) => Err("Create webview failed".into()),
@@ -79,7 +85,7 @@ pub fn create_column(
#[tauri::command]
#[specta::specta]
pub fn close_column(label: &str, app_handle: tauri::AppHandle) -> Result<bool, ()> {
pub fn close_column(label: &str, app_handle: tauri::AppHandle) -> Result<bool, String> {
match app_handle.get_webview(label) {
Some(webview) => {
if webview.close().is_ok() {
@@ -88,7 +94,7 @@ pub fn close_column(label: &str, app_handle: tauri::AppHandle) -> Result<bool, (
Ok(false)
}
}
None => Ok(true),
None => Err("Column not found.".into()),
}
}
@@ -132,6 +138,21 @@ pub fn resize_column(
}
}
#[tauri::command]
#[specta::specta]
pub fn reload_column(label: &str, app_handle: tauri::AppHandle) -> Result<(), String> {
match app_handle.get_webview(label) {
Some(webview) => {
if webview.eval("window.location.reload()").is_ok() {
Ok(())
} else {
Err("Reload column failed".into())
}
}
None => Err("Webview not found".into()),
}
}
#[tauri::command]
#[specta::specta]
pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), String> {
@@ -152,14 +173,13 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
.title(&window.title)
.min_inner_size(window.width, window.height)
.inner_size(window.width, window.height)
.hidden_title(true)
.hidden_title(window.hidden_title)
.title_bar_style(TitleBarStyle::Overlay)
.transparent(true)
.minimizable(window.minimizable)
.maximizable(window.maximizable)
.effects(WindowEffectsConfig {
state: None,
effects: vec![Effect::WindowBackground],
effects: vec![Effect::UnderWindowBackground],
radius: None,
color: None,
})
@@ -171,7 +191,6 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
.title(title)
.min_inner_size(width, height)
.inner_size(width, height)
.transparent(true)
.effects(WindowEffectsConfig {
state: None,
effects: vec![Effect::Mica],
@@ -189,14 +208,45 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
.build()
.unwrap();
#[cfg(target_os = "windows")]
// Create a custom titlebar for Windows
// Set decoration
window.create_overlay_titlebar().unwrap();
// Make main window transparent
#[cfg(target_os = "macos")]
window.make_transparent().unwrap();
}
Ok(())
}
#[tauri::command]
#[specta::specta]
pub fn open_main_window(app: tauri::AppHandle) {
if let Some(window) = app.get_window("main") {
if window.is_visible().unwrap_or_default() {
let _ = window.set_focus();
} else {
let _ = window.show();
let _ = window.set_focus();
};
} else {
let window = WebviewWindowBuilder::from_config(&app, app.config().app.windows.first().unwrap())
.unwrap()
.build()
.unwrap();
// Make main window transparent
#[cfg(target_os = "macos")]
window.make_transparent().unwrap();
}
}
#[tauri::command]
#[specta::specta]
pub fn force_quit() {
std::process::exit(0)
}
#[tauri::command]
#[specta::specta]
pub fn set_badge(count: i32) {
@@ -211,21 +261,3 @@ pub fn set_badge(count: i32) {
let _: cocoa::base::id = msg_send![dock_tile, setBadgeLabel: label];
}
}
#[tauri::command]
#[specta::specta]
pub fn open_main_window(app: tauri::AppHandle) {
if let Some(window) = app.get_window("main") {
if window.is_visible().unwrap_or_default() {
let _ = window.set_focus();
} else {
let _ = window.show();
let _ = window.set_focus();
};
} else {
let _ = WebviewWindowBuilder::from_config(&app, app.config().app.windows.first().unwrap())
.unwrap()
.build()
.unwrap();
}
}
+50 -71
View File
@@ -9,31 +9,20 @@ extern crate cocoa;
#[macro_use]
extern crate objc;
use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize};
use specta::Type;
use std::sync::Mutex;
use std::time::Duration;
use std::{
fs,
io::{self, BufRead},
str::FromStr,
};
use std::sync::Mutex;
use std::time::Duration;
use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize};
use specta::Type;
use tauri::{Manager, path::BaseDirectory};
#[cfg(target_os = "macos")]
use tauri::tray::{MouseButtonState, TrayIconEvent};
use tauri_nspanel::ManagerExt;
use tauri::{path::BaseDirectory, Manager};
use tauri_plugin_decorum::WebviewWindowExt;
#[cfg(target_os = "macos")]
use crate::fns::{
position_menubar_panel, setup_menubar_panel_listeners, swizzle_to_menubar_panel,
update_menubar_appearance,
};
pub mod commands;
pub mod fns;
pub mod nostr;
#[derive(Serialize)]
@@ -54,6 +43,7 @@ pub struct Settings {
display_zap_button: bool,
display_repost_button: bool,
display_media: bool,
vibrancy: bool,
}
impl Default for Settings {
@@ -67,10 +57,15 @@ impl Default for Settings {
display_zap_button: true,
display_repost_button: true,
display_media: true,
vibrancy: true,
}
}
}
pub const FETCH_LIMIT: usize = 20;
pub const NEWSFEED_NEG_LIMIT: usize = 256;
pub const NOTIFICATION_NEG_LIMIT: usize = 64;
fn main() {
let mut ctx = tauri::generate_context!();
@@ -112,8 +107,10 @@ fn main() {
nostr::event::get_event,
nostr::event::get_event_from,
nostr::event::get_replies,
nostr::event::listen_event_reply,
nostr::event::get_events_by,
nostr::event::get_local_events,
nostr::event::listen_local_event,
nostr::event::get_group_events,
nostr::event::get_global_events,
nostr::event::get_hashtag_events,
@@ -122,13 +119,15 @@ fn main() {
nostr::event::repost,
nostr::event::event_to_bech32,
nostr::event::user_to_bech32,
commands::folder::show_in_folder,
nostr::event::unlisten,
commands::window::create_column,
commands::window::close_column,
commands::window::reposition_column,
commands::window::resize_column,
commands::window::reload_column,
commands::window::open_window,
commands::window::open_main_window,
commands::window::force_quit,
commands::window::set_badge
]);
@@ -142,45 +141,18 @@ fn main() {
.setup(|app| {
let main_window = app.get_webview_window("main").unwrap();
// Create a custom titlebar for Windows
// Set custom decoration for Windows
#[cfg(target_os = "windows")]
main_window.create_overlay_titlebar().unwrap();
// Make main window transparent
#[cfg(target_os = "macos")]
main_window.make_transparent().unwrap();
// Set a custom inset to the traffic lights
#[cfg(target_os = "macos")]
main_window.set_traffic_lights_inset(8.0, 16.0).unwrap();
// Create panel
#[cfg(target_os = "macos")]
swizzle_to_menubar_panel(app.handle());
#[cfg(target_os = "macos")]
update_menubar_appearance(app.handle());
#[cfg(target_os = "macos")]
setup_menubar_panel_listeners(app.handle());
// Setup tray icon
#[cfg(target_os = "macos")]
let tray = app.tray_by_id("tray_panel").unwrap();
// Handle tray icon event
#[cfg(target_os = "macos")]
tray.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click { button_state, .. } = event {
if button_state == MouseButtonState::Up {
let app = tray.app_handle();
let panel = app.get_webview_panel("panel").unwrap();
match panel.is_visible() {
true => panel.order_out(None),
false => {
position_menubar_panel(app, 0.0);
panel.show();
}
}
}
}
});
// Create data folder if not exist
let home_dir = app.path().home_dir().unwrap();
let _ = fs::create_dir_all(home_dir.join("Lume/"));
@@ -192,7 +164,8 @@ fn main() {
// Config
let opts = Options::new()
.automatic_authentication(true)
.connection_timeout(Some(Duration::from_secs(5)));
.connection_timeout(Some(Duration::from_secs(5)))
.timeout(Duration::from_secs(30));
// Setup nostr client
let client = match database {
@@ -201,29 +174,30 @@ fn main() {
};
// Get bootstrap relays
let relays_path = app
if let Ok(path) = app
.path()
.resolve("resources/relays.txt", BaseDirectory::Resource)
.expect("Bootstrap relays not found.");
let file = std::fs::File::open(&relays_path).unwrap();
let lines = io::BufReader::new(file).lines();
{
let file = std::fs::File::open(&path).unwrap();
let lines = io::BufReader::new(file).lines();
// Add bootstrap relays to relay pool
for line in lines.map_while(Result::ok) {
if let Some((relay, option)) = line.split_once(',') {
match RelayMetadata::from_str(option) {
Ok(meta) => {
println!("connecting to bootstrap relay...: {} - {}", relay, meta);
let opts = if meta == RelayMetadata::Read {
RelayOptions::new().read(true).write(false)
} else {
RelayOptions::new().write(true).read(false)
};
let _ = client.add_relay_with_opts(relay, opts).await;
}
Err(_) => {
println!("connecting to bootstrap relay...: {}", relay);
let _ = client.add_relay(relay).await;
// Add bootstrap relays to relay pool
for line in lines.map_while(Result::ok) {
if let Some((relay, option)) = line.split_once(',') {
match RelayMetadata::from_str(option) {
Ok(meta) => {
println!("connecting to bootstrap relay...: {} - {}", relay, meta);
let opts = if meta == RelayMetadata::Read {
RelayOptions::new().read(true).write(false)
} else {
RelayOptions::new().write(true).read(false)
};
let _ = client.add_relay_with_opts(relay, opts).await;
}
Err(_) => {
println!("connecting to bootstrap relay...: {}", relay);
let _ = client.add_relay(relay).await;
}
}
}
}
@@ -255,6 +229,11 @@ fn main() {
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_upload::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(
tauri_plugin_window_state::Builder::new()
.with_denylist(&["panel"])
.build(),
)
.invoke_handler(invoke_handler)
.build(ctx)
.expect("error while running tauri application")
+133 -63
View File
@@ -6,10 +6,10 @@ use serde::Serialize;
use specta::Type;
use tauri::State;
use crate::Nostr;
use crate::nostr::utils::{create_event_tags, dedup_event, Meta, parse_event};
use crate::nostr::utils::{create_event_tags, dedup_event, parse_event, Meta};
use crate::{Nostr, FETCH_LIMIT};
#[derive(Debug, Serialize, Type)]
#[derive(Debug, Clone, Serialize, Type)]
pub struct RichEvent {
pub raw: String,
pub parsed: Option<Meta>,
@@ -69,7 +69,11 @@ pub async fn get_event_from(
state: State<'_, Nostr>,
) -> Result<RichEvent, String> {
let client = &state.client;
let settings = state.settings.lock().unwrap().clone();
let settings = state
.settings
.lock()
.map_err(|err| err.to_string())?
.clone();
let event_id = match Nip19::from_bech32(id) {
Ok(val) => match val {
@@ -142,33 +146,57 @@ pub async fn get_event_from(
pub async fn get_replies(id: &str, state: State<'_, Nostr>) -> Result<Vec<RichEvent>, String> {
let client = &state.client;
match EventId::from_hex(id) {
Ok(event_id) => {
let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id);
let event_id = match EventId::from_hex(id) {
Ok(id) => id,
Err(err) => return Err(err.to_string()),
};
match client.get_events_of(vec![filter], None).await {
Ok(events) => {
let futures = events.into_iter().map(|ev| async move {
let raw = ev.as_json();
let parsed = if ev.kind == Kind::TextNote {
Some(parse_event(&ev.content).await)
} else {
None
};
let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id);
RichEvent { raw, parsed }
});
let rich_events = join_all(futures).await;
match client.get_events_of(vec![filter], None).await {
Ok(events) => {
let futures = events.into_iter().map(|ev| async move {
let raw = ev.as_json();
let parsed = if ev.kind == Kind::TextNote {
Some(parse_event(&ev.content).await)
} else {
None
};
Ok(rich_events)
}
Err(err) => Err(err.to_string()),
}
RichEvent { raw, parsed }
});
let rich_events = join_all(futures).await;
Ok(rich_events)
}
Err(_) => Err("Event ID is not valid".into()),
Err(err) => Err(err.to_string()),
}
}
#[tauri::command]
#[specta::specta]
pub async fn listen_event_reply(id: &str, state: State<'_, Nostr>) -> Result<(), String> {
let client = &state.client;
let mut label = "event-".to_owned();
label.push_str(id);
let sub_id = SubscriptionId::new(label);
let event_id = match EventId::from_hex(id) {
Ok(id) => id,
Err(err) => return Err(err.to_string()),
};
let filter = Filter::new()
.kinds(vec![Kind::TextNote])
.event(event_id)
.since(Timestamp::now());
// Subscribe
client.subscribe_with_id(sub_id, vec![filter], None).await;
Ok(())
}
#[tauri::command]
#[specta::specta]
pub async fn get_events_by(
@@ -181,13 +209,13 @@ pub async fn get_events_by(
match PublicKey::from_str(public_key) {
Ok(author) => {
let until = match as_of {
Some(until) => Timestamp::from_str(until).unwrap(),
Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?,
None => Timestamp::now(),
};
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.author(author)
.limit(20)
.limit(FETCH_LIMIT)
.until(until);
match client.get_events_of(vec![filter], None).await {
@@ -220,10 +248,14 @@ pub async fn get_local_events(
state: State<'_, Nostr>,
) -> Result<Vec<RichEvent>, String> {
let client = &state.client;
let contact_list = state.contact_list.lock().unwrap().clone();
let contact_list = state
.contact_list
.lock()
.map_err(|err| err.to_string())?
.clone();
let as_of = match until {
Some(until) => Timestamp::from_str(until).unwrap(),
Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?,
None => Timestamp::now(),
};
@@ -231,17 +263,13 @@ pub async fn get_local_events(
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(20)
.limit(64)
.until(as_of)
.authors(authors);
match client
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
.await
{
match client.database().query(vec![filter], Order::Desc).await {
Ok(events) => {
let dedup = dedup_event(&events);
let futures = dedup.into_iter().map(|ev| async move {
let raw = ev.as_json();
let parsed = if ev.kind == Kind::TextNote {
@@ -252,7 +280,6 @@ pub async fn get_local_events(
RichEvent { raw, parsed }
});
let rich_events = join_all(futures).await;
Ok(rich_events)
@@ -261,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]
#[specta::specta]
pub async fn get_group_events(
@@ -271,7 +323,7 @@ pub async fn get_group_events(
let client = &state.client;
let as_of = match until {
Some(until) => Timestamp::from_str(until).unwrap(),
Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?,
None => Timestamp::now(),
};
@@ -279,16 +331,16 @@ pub async fn get_group_events(
.into_iter()
.map(|p| {
if p.starts_with("npub1") {
PublicKey::from_bech32(p).unwrap()
PublicKey::from_bech32(p).map_err(|err| err.to_string())
} else {
PublicKey::from_hex(p).unwrap()
PublicKey::from_hex(p).map_err(|err| err.to_string())
}
})
.collect();
.collect::<Result<Vec<_>, _>>()?;
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(20)
.limit(FETCH_LIMIT)
.until(as_of)
.authors(authors);
@@ -326,13 +378,13 @@ pub async fn get_global_events(
) -> Result<Vec<RichEvent>, String> {
let client = &state.client;
let as_of = match until {
Some(until) => Timestamp::from_str(until).unwrap(),
Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?,
None => Timestamp::now(),
};
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(20)
.limit(FETCH_LIMIT)
.until(as_of);
match client
@@ -368,12 +420,12 @@ pub async fn get_hashtag_events(
) -> Result<Vec<RichEvent>, String> {
let client = &state.client;
let as_of = match until {
Some(until) => Timestamp::from_str(until).unwrap(),
Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?,
None => Timestamp::now(),
};
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(20)
.limit(FETCH_LIMIT)
.until(as_of)
.hashtags(hashtags);
@@ -427,7 +479,7 @@ pub async fn publish(
};
// Get public key
let public_key = signer.public_key().await.unwrap();
let public_key = signer.public_key().await.map_err(|err| err.to_string())?;
// Create unsigned event
let unsigned_event = match difficulty {
@@ -438,7 +490,7 @@ pub async fn publish(
// Publish
match signer.sign_event(unsigned_event).await {
Ok(event) => match client.send_event(event).await {
Ok(event_id) => Ok(event_id.to_bech32().unwrap()),
Ok(event_id) => Ok(event_id.to_bech32().map_err(|err| err.to_string())?),
Err(err) => Err(err.to_string()),
},
Err(err) => Err(err.to_string()),
@@ -470,12 +522,15 @@ pub async fn reply(
{
Ok(events) => {
if let Some(event) = events.into_iter().next() {
let relay_hint =
if let Some(relays) = database.event_seen_on_relays(event.id).await.unwrap() {
relays.into_iter().next().map(UncheckedUrl::new)
} else {
None
};
let relay_hint = if let Some(relays) = database
.event_seen_on_relays(event.id)
.await
.map_err(|err| err.to_string())?
{
relays.into_iter().next().map(UncheckedUrl::new)
} else {
None
};
let t = TagStandard::Event {
event_id: event.id,
relay_url: relay_hint,
@@ -502,12 +557,15 @@ pub async fn reply(
.await
{
if let Some(event) = events.into_iter().next() {
let relay_hint =
if let Some(relays) = database.event_seen_on_relays(event.id).await.unwrap() {
relays.into_iter().next().map(UncheckedUrl::new)
} else {
None
};
let relay_hint = if let Some(relays) = database
.event_seen_on_relays(event.id)
.await
.map_err(|err| err.to_string())?
{
relays.into_iter().next().map(UncheckedUrl::new)
} else {
None
};
let t = TagStandard::Event {
event_id: event.id,
relay_url: relay_hint,
@@ -521,7 +579,7 @@ pub async fn reply(
};
match client.publish_text_note(content, tags).await {
Ok(event_id) => Ok(event_id.to_bech32().unwrap()),
Ok(event_id) => Ok(event_id.to_bech32().map_err(|err| err.to_string())?),
Err(err) => Err(err.to_string()),
}
}
@@ -530,7 +588,7 @@ pub async fn reply(
#[specta::specta]
pub async fn repost(raw: &str, state: State<'_, Nostr>) -> Result<String, String> {
let client = &state.client;
let event = Event::from_json(raw).unwrap();
let event = Event::from_json(raw).map_err(|err| err.to_string())?;
match client.repost(&event, None).await {
Ok(event_id) => Ok(event_id.to_string()),
@@ -552,7 +610,7 @@ pub async fn event_to_bech32(id: &str, state: State<'_, Nostr>) -> Result<String
.database()
.event_seen_on_relays(event_id)
.await
.unwrap();
.map_err(|err| err.to_string())?;
match seens {
Some(set) => {
@@ -598,9 +656,9 @@ pub async fn user_to_bech32(user: &str, state: State<'_, Nostr>) -> Result<Strin
.into_iter()
.map(|i| i.0.to_string())
.collect::<Vec<_>>();
let profile = Nip19Profile::new(public_key, relays).unwrap();
let profile = Nip19Profile::new(public_key, relays).map_err(|err| err.to_string())?;
Ok(profile.to_bech32().unwrap())
Ok(profile.to_bech32().map_err(|err| err.to_string())?)
}
None => match public_key.to_bech32() {
Ok(pk) => Ok(pk),
@@ -613,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(())
}
+75
View File
@@ -0,0 +1,75 @@
use crate::Settings;
use nostr_sdk::prelude::*;
pub async fn init_nip65(client: &Client) {
let signer = client.signer().await.unwrap();
let public_key = signer.public_key().await.unwrap();
if let Ok(events) = client
.get_events_of(
vec![Filter::new()
.author(public_key)
.kind(Kind::RelayList)
.limit(1)],
None,
)
.await
{
if let Some(event) = events.first() {
let relay_list = nip65::extract_relay_list(event);
for item in relay_list.into_iter() {
let relay_url = item.0.to_string();
let opts = match item.1 {
Some(val) => {
if val == &RelayMetadata::Read {
RelayOptions::new().read(true).write(false)
} else {
RelayOptions::new().write(true).read(false)
}
}
None => RelayOptions::default(),
};
// Add relay to relay pool
let _ = client
.add_relay_with_opts(&relay_url, opts)
.await
.unwrap_or_default();
// Connect relay
client.connect_relay(relay_url).await.unwrap_or_default();
println!("connecting to relay: {} - {:?}", item.0, item.1);
}
}
};
}
pub async fn get_user_settings(client: &Client) -> Result<Settings, String> {
let ident = "lume:settings";
let signer = client.signer().await.unwrap();
let public_key = signer.public_key().await.unwrap();
let filter = Filter::new()
.author(public_key)
.kind(Kind::ApplicationSpecificData)
.identifier(ident)
.limit(1);
if let Ok(events) = client.get_events_of(vec![filter], None).await {
if let Some(event) = events.first() {
let content = event.content();
if let Ok(decrypted) = signer.nip44_decrypt(public_key, content).await {
match serde_json::from_str(&decrypted) {
Ok(parsed) => parsed,
Err(_) => Err("Could not parse settings payload".into()),
}
} else {
Err("Decrypt settings failed.".into())
}
} else {
Err("Settings not found.".into())
}
} else {
Err("Settings not found.".into())
}
}
+220 -219
View File
@@ -1,14 +1,17 @@
use std::time::Duration;
use keyring::Entry;
use keyring_search::{Limit, List, Search};
use nostr_sdk::prelude::*;
use serde::Serialize;
use specta::Type;
use std::time::Duration;
use tauri::{EventTarget, Manager, State};
use tauri_plugin_notification::NotificationExt;
use crate::{Nostr, Settings};
use crate::commands::tray::create_tray_panel;
use crate::nostr::event::RichEvent;
use crate::nostr::internal::{get_user_settings, init_nip65};
use crate::nostr::utils::parse_event;
use crate::{Nostr, NEWSFEED_NEG_LIMIT, NOTIFICATION_NEG_LIMIT};
#[derive(Serialize, Type)]
pub struct Account {
@@ -44,7 +47,7 @@ pub fn get_accounts() -> Result<Vec<String>, String> {
#[tauri::command]
#[specta::specta]
pub fn create_account() -> Result<Account, ()> {
pub fn create_account() -> Result<Account, String> {
let keys = Keys::generate();
let public_key = keys.public_key();
let secret_key = keys.secret_key().unwrap();
@@ -57,6 +60,19 @@ pub fn create_account() -> Result<Account, ()> {
Ok(result)
}
#[tauri::command]
#[specta::specta]
pub fn get_private_key(npub: &str) -> Result<String, String> {
let keyring = Entry::new(npub, "nostr_secret").unwrap();
if let Ok(nsec) = keyring.get_password() {
let secret_key = SecretKey::from_bech32(nsec).unwrap();
Ok(secret_key.to_bech32().unwrap())
} else {
Err("Key not found".into())
}
}
#[tauri::command]
#[specta::specta]
pub async fn save_account(
@@ -94,6 +110,56 @@ pub async fn save_account(
}
}
#[tauri::command]
#[specta::specta]
pub async fn connect_remote_account(uri: &str, state: State<'_, Nostr>) -> Result<String, String> {
let client = &state.client;
match NostrConnectURI::parse(uri) {
Ok(bunker_uri) => {
let app_keys = Keys::generate();
let app_secret = app_keys.secret_key().unwrap().to_string();
// Get remote user
let remote_user = bunker_uri.signer_public_key().unwrap();
let remote_npub = remote_user.to_bech32().unwrap();
match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(120), None).await {
Ok(signer) => {
let keyring = Entry::new(&remote_npub, "nostr_secret").unwrap();
let _ = keyring.set_password(&app_secret);
// Update signer
let _ = client.set_signer(Some(signer.into())).await;
Ok(remote_npub)
}
Err(err) => Err(err.to_string()),
}
}
Err(err) => Err(err.to_string()),
}
}
#[tauri::command]
#[specta::specta]
pub async fn get_encrypted_key(npub: &str, password: &str) -> Result<String, String> {
let keyring = Entry::new(npub, "nostr_secret").unwrap();
if let Ok(nsec) = keyring.get_password() {
let secret_key = SecretKey::from_bech32(nsec).unwrap();
let new_key = EncryptedSecretKey::new(&secret_key, password, 16, KeySecurity::Medium);
if let Ok(key) = new_key {
Ok(key.to_bech32().unwrap())
} else {
Err("Encrypt key failed".into())
}
} else {
Err("Key not found".into())
}
}
#[tauri::command]
#[specta::specta]
pub async fn load_account(
@@ -102,178 +168,138 @@ pub async fn load_account(
state: State<'_, Nostr>,
app: tauri::AppHandle,
) -> Result<bool, String> {
let handle = app.clone();
let client = &state.client;
let keyring = Entry::new(npub, "nostr_secret").unwrap();
if let Ok(password) = keyring.get_password() {
match bunker {
Some(uri) => {
let app_keys = Keys::parse(password).expect("Secret Key is modified, please check again.");
let password = match keyring.get_password() {
Ok(pw) => pw,
Err(_) => return Err("Cancelled".into()),
};
match NostrConnectURI::parse(uri) {
Ok(bunker_uri) => {
match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(30), None).await {
Ok(signer) => client.set_signer(Some(signer.into())).await,
Err(err) => return Err(err.to_string()),
}
match bunker {
Some(uri) => {
let app_keys = Keys::parse(password).expect("Secret Key is modified, please check again.");
match NostrConnectURI::parse(uri) {
Ok(bunker_uri) => {
match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(30), None).await {
Ok(signer) => client.set_signer(Some(signer.into())).await,
Err(err) => return Err(err.to_string()),
}
Err(err) => return Err(err.to_string()),
}
}
None => {
let keys = Keys::parse(password).expect("Secret Key is modified, please check again.");
let signer = NostrSigner::Keys(keys);
// Update signer
client.set_signer(Some(signer)).await;
Err(err) => return Err(err.to_string()),
}
}
None => {
let keys = Keys::parse(password).expect("Secret Key is modified, please check again.");
let signer = NostrSigner::Keys(keys);
// Update signer
client.set_signer(Some(signer)).await;
}
}
// Connect to user's relay (NIP-65)
init_nip65(client).await;
// Create tray (macOS)
#[cfg(target_os = "macos")]
create_tray_panel(npub, &handle);
// Get user's contact list
if let Ok(contacts) = client.get_contact_list(None).await {
*state.contact_list.lock().unwrap() = contacts
};
// Get user's settings
if let Ok(settings) = get_user_settings(client).await {
*state.settings.lock().unwrap() = settings
};
tauri::async_runtime::spawn(async move {
let window = handle.get_window("main").unwrap();
let state = window.state::<Nostr>();
let client = &state.client;
let contact_list = state.contact_list.lock().unwrap().clone();
// Verify signer
let signer = client.signer().await.unwrap();
let public_key = signer.public_key().await.unwrap();
// Connect to user's relay (NIP-65)
if let Ok(events) = client
.get_events_of(
vec![Filter::new()
.author(public_key)
.kind(Kind::RelayList)
.limit(1)],
Some(Duration::from_secs(10)),
)
.await
{
if let Some(event) = events.first() {
let relay_list = nip65::extract_relay_list(event);
for item in relay_list.into_iter() {
let relay_url = item.0.to_string();
let opts = match item.1 {
Some(val) => {
if val == &RelayMetadata::Read {
RelayOptions::new().read(true).write(false)
} else {
RelayOptions::new().write(true).read(false)
}
}
None => RelayOptions::default(),
};
if !contact_list.is_empty() {
let authors: Vec<PublicKey> = contact_list.into_iter().map(|f| f.public_key).collect();
// Add relay to relay pool
let _ = client
.add_relay_with_opts(&relay_url, opts)
.await
.unwrap_or_default();
// Connect relay
client.connect_relay(relay_url).await.unwrap_or_default();
println!("connecting to relay: {} - {:?}", item.0, item.1);
match client
.reconcile(
Filter::new()
.authors(authors)
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(NEWSFEED_NEG_LIMIT),
NegentropyOptions::default(),
)
.await
{
Ok(_) => {
if handle.emit_to(EventTarget::Any, "synced", true).is_err() {
println!("Emit event failed.")
}
}
Err(_) => println!("Sync newsfeed failed."),
}
};
// Get user's contact list
let contacts = client
.get_contact_list(Some(Duration::from_secs(10)))
match client
.reconcile(
Filter::new()
.pubkey(public_key)
.kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Reaction,
Kind::ZapReceipt,
])
.limit(NOTIFICATION_NEG_LIMIT),
NegentropyOptions::default(),
)
.await
.unwrap();
// Update state
*state.contact_list.lock().unwrap() = contacts;
// Get user's settings
let handle = app.clone();
// Spawn a thread to handle it
tauri::async_runtime::spawn(async move {
let window = handle.get_window("main").unwrap();
let state = window.state::<Nostr>();
let client = &state.client;
let ident = "lume:settings";
let filter = Filter::new()
.author(public_key)
.kind(Kind::ApplicationSpecificData)
.identifier(ident)
.limit(1);
if let Ok(events) = client
.get_events_of(vec![filter], Some(Duration::from_secs(5)))
.await
{
if let Some(event) = events.first() {
let content = event.content();
if let Ok(decrypted) = signer.nip44_decrypt(public_key, content).await {
let parsed: Settings =
serde_json::from_str(&decrypted).expect("Could not parse settings payload");
*state.settings.lock().unwrap() = parsed;
}
{
Ok(_) => {
if handle.emit_to(EventTarget::Any, "synced", true).is_err() {
println!("Emit event failed.")
}
}
});
Err(_) => println!("Sync notification failed."),
};
// Run sync service
let handle = app.clone();
// Spawn a thread to handle it
tauri::async_runtime::spawn(async move {
let window = handle.get_window("main").unwrap();
let state = window.state::<Nostr>();
let client = &state.client;
let subscription_id = SubscriptionId::new("notification");
let subscription = Filter::new()
.pubkey(public_key)
.kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Reaction,
Kind::ZapReceipt,
])
.since(Timestamp::now());
let filter = Filter::new()
.pubkey(public_key)
.kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Reaction,
Kind::ZapReceipt,
])
.limit(500);
// Subscribing for new notification...
client
.subscribe_with_id(subscription_id, vec![subscription], None)
.await;
match client.reconcile(filter, NegentropyOptions::default()).await {
Ok(_) => println!("Sync notification done."),
Err(_) => println!("Sync notification failed."),
}
});
// Run notification service
// Spawn a thread to handle it
tauri::async_runtime::spawn(async move {
println!("Starting notification service...");
let window = app.get_window("main").unwrap();
let state = window.state::<Nostr>();
let client = &state.client;
// Create a subscription for notification
let notification_id = SubscriptionId::new("notification");
let filter = Filter::new()
.pubkey(public_key)
.kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Reaction,
Kind::ZapReceipt,
])
.since(Timestamp::now());
// Subscribe
client
.subscribe_with_id(notification_id.clone(), vec![filter], None)
.await;
// Handle notifications
let _ = client
.handle_notifications(|notification| async {
if let RelayPoolNotification::Event {
// Handle notifications
client
.handle_notifications(|notification| async {
if let RelayPoolNotification::Message { message, .. } = notification {
if let RelayMessage::Event {
subscription_id,
event,
..
} = notification
} = message
{
if subscription_id == notification_id {
println!("new notification: {}", event.as_json());
let id = subscription_id.to_string();
if id.starts_with("notification") {
if app
.emit_to(
EventTarget::window("panel"),
@@ -336,78 +362,53 @@ pub async fn load_account(
}
_ => {}
}
} else if id.starts_with("event-") {
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_reply",
RichEvent { raw, parsed },
)
.is_err()
{
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 {
println!("new event: {}", event.as_json())
}
} else {
println!("new message: {}", message.as_json())
}
Ok(false)
})
.await;
});
Ok(true)
} else {
Err("Cancelled".into())
}
}
#[tauri::command]
#[specta::specta]
pub async fn connect_remote_account(uri: &str, state: State<'_, Nostr>) -> Result<String, String> {
let client = &state.client;
match NostrConnectURI::parse(uri) {
Ok(bunker_uri) => {
let app_keys = Keys::generate();
let app_secret = app_keys.secret_key().unwrap().to_string();
// Get remote user
let remote_user = bunker_uri.signer_public_key().unwrap();
let remote_npub = remote_user.to_bech32().unwrap();
match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(120), None).await {
Ok(signer) => {
let keyring = Entry::new(&remote_npub, "nostr_secret").unwrap();
let _ = keyring.set_password(&app_secret);
// Update signer
let _ = client.set_signer(Some(signer.into())).await;
Ok(remote_npub)
}
Err(err) => Err(err.to_string()),
}
}
Err(err) => Err(err.to_string()),
}
}
Ok(false)
})
.await
});
#[tauri::command(async)]
#[specta::specta]
pub fn get_encrypted_key(npub: &str, password: &str) -> Result<String, String> {
let keyring = Entry::new(npub, "nostr_secret").unwrap();
if let Ok(nsec) = keyring.get_password() {
let secret_key = SecretKey::from_bech32(nsec).unwrap();
let new_key = EncryptedSecretKey::new(&secret_key, password, 16, KeySecurity::Medium);
if let Ok(key) = new_key {
Ok(key.to_bech32().unwrap())
} else {
Err("Encrypt key failed".into())
}
} else {
Err("Key not found".into())
}
}
#[tauri::command(async)]
#[specta::specta]
pub fn get_private_key(npub: &str) -> Result<String, String> {
let keyring = Entry::new(npub, "nostr_secret").unwrap();
if let Ok(nsec) = keyring.get_password() {
let secret_key = SecretKey::from_bech32(nsec).unwrap();
Ok(secret_key.to_bech32().unwrap())
} else {
Err("Key not found".into())
}
Ok(true)
}
+7 -5
View File
@@ -187,13 +187,15 @@ pub async fn is_contact_list_empty(state: State<'_, Nostr>) -> Result<bool, ()>
#[tauri::command]
#[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 public_key = PublicKey::from_str(hex).unwrap();
match contact_list.iter().position(|x| x.public_key == public_key) {
Some(_) => Ok(true),
None => Ok(false),
match PublicKey::from_str(hex) {
Ok(public_key) => match contact_list.iter().position(|x| x.public_key == public_key) {
Some(_) => Ok(true),
None => Ok(false),
},
Err(err) => Err(err.to_string()),
}
}
+1
View File
@@ -1,4 +1,5 @@
pub mod event;
mod internal;
pub mod keys;
pub mod metadata;
pub mod relay;

Some files were not shown because too many files have changed in this diff Show More