Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72da83d648 | ||
|
|
783a4538a4 | ||
|
|
15e62cad11 | ||
|
|
c52b20ca80 | ||
|
|
04706a6d7c | ||
|
|
0755cbeb6c | ||
|
|
8eb01c8bbf | ||
|
|
ed4f89ff66 | ||
|
|
d9fe647f8e | ||
|
|
843c2d52e7 | ||
|
|
017a3676a4 | ||
|
|
fcb70c0e9a | ||
|
|
0fec21b9ce | ||
|
|
968b1ada94 | ||
|
|
5c9b599b1e | ||
|
|
717c3e17df | ||
|
|
a4540a0802 | ||
|
|
31bacc2646 |
+11
-11
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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" />
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
"devDependencies": {
|
||||
"@lume/tsconfig": "workspace:*",
|
||||
"@types/react": "^18.3.3",
|
||||
"typescript": "^5.4.5"
|
||||
"typescript": "^5.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
Vendored
+2
-1
@@ -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 {
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
"access": "public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.5"
|
||||
"typescript": "^5.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
"@lume/types": "workspace:^",
|
||||
"@types/react": "^18.3.3",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "^5.4.5"
|
||||
"typescript": "^5.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
export * from "./container";
|
||||
export * from "./box";
|
||||
export * from "./spinner";
|
||||
export * from "./carousel";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from "./src/constants";
|
||||
export * from "./src/delay";
|
||||
export * from "./src/formater";
|
||||
export * from "./src/editor";
|
||||
export * from "./src/cn";
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
export const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
||||
Generated
+4400
-3781
File diff suppressed because it is too large
Load Diff
Generated
+423
-327
File diff suppressed because it is too large
Load Diff
+11
-8
@@ -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
|
||||
|
||||
@@ -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/*"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 @@
|
||||
{"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"]}}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod folder;
|
||||
pub mod fns;
|
||||
pub mod tray;
|
||||
pub mod window;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,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
Reference in New Issue
Block a user