Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5255fa503 | ||
|
|
954a17b541 | ||
|
|
a55b31b0e6 | ||
|
|
bdf3ffd7bf | ||
|
|
07ce253f5b | ||
|
|
f3db010c74 | ||
|
|
dcf2791fe5 | ||
|
|
8fcf3551d8 | ||
|
|
2d987849d8 | ||
|
|
3b99926f3b | ||
|
|
113d69a4df | ||
|
|
5d12ba7216 | ||
|
|
72b59020b4 | ||
|
|
4c323b9daa | ||
|
|
72da83d648 | ||
|
|
783a4538a4 | ||
|
|
15e62cad11 | ||
|
|
c52b20ca80 | ||
|
|
04706a6d7c | ||
|
|
0755cbeb6c | ||
|
|
8eb01c8bbf | ||
|
|
ed4f89ff66 | ||
|
|
d9fe647f8e | ||
|
|
843c2d52e7 | ||
|
|
017a3676a4 | ||
|
|
fcb70c0e9a | ||
|
|
0fec21b9ce | ||
|
|
968b1ada94 | ||
|
|
5c9b599b1e | ||
|
|
717c3e17df | ||
|
|
a4540a0802 | ||
|
|
31bacc2646 |
@@ -18,10 +18,10 @@ jobs:
|
||||
args: "--target x86_64-apple-darwin"
|
||||
- platform: "macos-latest" # for Intel based macs.
|
||||
args: "--target universal-apple-darwin"
|
||||
#- platform: 'ubuntu-22.04'
|
||||
# args: ''
|
||||
#- platform: 'windows-latest'
|
||||
# args: '--target x86_64-pc-windows-msvc'
|
||||
- platform: 'ubuntu-22.04'
|
||||
args: ''
|
||||
- platform: 'windows-latest'
|
||||
args: '--target x86_64-pc-windows-msvc'
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
+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>
|
||||
|
||||
+20
-21
@@ -14,27 +14,27 @@
|
||||
"@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",
|
||||
"embla-carousel-react": "^8.1.5",
|
||||
"i18next": "^23.11.5",
|
||||
"@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.51.9",
|
||||
"@tanstack/react-query": "^5.51.9",
|
||||
"@tanstack/react-router": "^1.45.4",
|
||||
"embla-carousel-react": "^8.1.6",
|
||||
"i18next": "^23.12.1",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"minidenticons": "^4.2.1",
|
||||
"nanoid": "^5.0.7",
|
||||
"nostr-tools": "^2.7.0",
|
||||
"nostr-tools": "^2.7.1",
|
||||
"react": "^18.3.1",
|
||||
"react-currency-input-field": "^3.8.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.52.0",
|
||||
"react-i18next": "^14.1.2",
|
||||
"react-hook-form": "^7.52.1",
|
||||
"react-i18next": "^14.1.3",
|
||||
"react-string-replace": "^1.1.1",
|
||||
"slate": "^0.103.0",
|
||||
"slate-react": "^0.105.0",
|
||||
@@ -45,17 +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.45.4",
|
||||
"@tanstack/router-vite-plugin": "^1.45.3",
|
||||
"@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",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.3.1",
|
||||
"vite-plugin-top-level-await": "^1.4.1",
|
||||
"postcss": "^8.4.39",
|
||||
"tailwindcss": "^3.4.6",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.3.4",
|
||||
"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 { getCurrent } from "@tauri-apps/api/webviewWindow";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
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,28 +84,29 @@ 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);
|
||||
|
||||
const saveNewTitle = async () => {
|
||||
const mainWindow = getCurrent();
|
||||
const mainWindow = getCurrentWindow();
|
||||
await mainWindow.emit("columns", { type: "set_title", label, title });
|
||||
|
||||
// update search params
|
||||
@@ -114,10 +117,59 @@ 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 getCurrentWindow().emit("columns", {
|
||||
type: "move",
|
||||
label,
|
||||
direction: "left",
|
||||
});
|
||||
},
|
||||
}),
|
||||
MenuItem.new({
|
||||
text: "Move right",
|
||||
action: async () => {
|
||||
await getCurrentWindow().emit("columns", {
|
||||
type: "move",
|
||||
label,
|
||||
direction: "right",
|
||||
});
|
||||
},
|
||||
}),
|
||||
PredefinedMenuItem.new({ item: "Separator" }),
|
||||
MenuItem.new({
|
||||
text: "Close",
|
||||
action: async () => {
|
||||
await getCurrentWindow().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 +178,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 +201,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,11 +1,11 @@
|
||||
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";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { getCurrent } from "@tauri-apps/api/window";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import useEmblaCarousel from "embla-carousel-react";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
@@ -30,31 +30,31 @@ function Screen() {
|
||||
});
|
||||
|
||||
const scrollPrev = useCallback(() => {
|
||||
if (emblaApi) emblaApi.scrollPrev(true);
|
||||
if (emblaApi) emblaApi.scrollPrev();
|
||||
}, [emblaApi]);
|
||||
|
||||
const scrollNext = useCallback(() => {
|
||||
if (emblaApi) emblaApi.scrollNext(true);
|
||||
if (emblaApi) emblaApi.scrollNext();
|
||||
}, [emblaApi]);
|
||||
|
||||
const emitScrollEvent = useCallback(() => {
|
||||
getCurrent().emit("child-webview", { scroll: true });
|
||||
getCurrentWindow().emit("child_webview", { scroll: true });
|
||||
}, []);
|
||||
|
||||
const emitResizeEvent = useCallback(() => {
|
||||
getCurrent().emit("child-webview", { resize: true, direction: "x" });
|
||||
getCurrentWindow().emit("child_webview", { resize: true, direction: "x" });
|
||||
}, []);
|
||||
|
||||
const openLumeStore = useDebouncedCallback(async () => {
|
||||
await getCurrent().emit("columns", {
|
||||
const openLumeStore = useCallback(async () => {
|
||||
await getCurrentWindow().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);
|
||||
|
||||
@@ -84,10 +101,10 @@ function Screen() {
|
||||
|
||||
switch (event.code) {
|
||||
case "ArrowLeft":
|
||||
if (emblaApi) emblaApi.scrollPrev(true);
|
||||
if (emblaApi) emblaApi.scrollPrev();
|
||||
break;
|
||||
case "ArrowRight":
|
||||
if (emblaApi) emblaApi.scrollNext(true);
|
||||
if (emblaApi) emblaApi.scrollNext();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -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 { getCurrent } from "@tauri-apps/api/window";
|
||||
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
||||
import { getCurrentWindow } 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 getCurrentWindow().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 getCurrentWindow().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,25 +1,16 @@
|
||||
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>()({
|
||||
component: () => <Outlet />,
|
||||
pendingComponent: Pending,
|
||||
wrapInSuspense: true,
|
||||
});
|
||||
|
||||
function Pending() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Container } from "@lume/ui";
|
||||
import { Container } from "@lume/ui";
|
||||
import { Outlet, createLazyFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createLazyFileRoute("/auth")({
|
||||
@@ -8,9 +8,9 @@ export const Route = createLazyFileRoute("/auth")({
|
||||
function Screen() {
|
||||
return (
|
||||
<Container withDrag>
|
||||
<Box className="px-3 pt-3">
|
||||
<div className="max-w-sm mx-auto size-full">
|
||||
<Outlet />
|
||||
</Box>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>("");
|
||||
@@ -58,85 +55,83 @@ function Screen() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center w-full h-full gap-6 px-5 mx-auto xl:max-w-xl">
|
||||
<div className="flex flex-col items-center justify-center size-full gap-4">
|
||||
<div className="text-center">
|
||||
<h3 className="text-xl font-semibold">Let's set up your profile.</h3>
|
||||
</div>
|
||||
<div>
|
||||
<div className="relative rounded-full size-24 bg-gradient-to-tr from-orange-100 via-red-50 to-blue-200">
|
||||
{picture ? (
|
||||
<img
|
||||
src={picture}
|
||||
alt="avatar"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="absolute inset-0 z-10 object-cover w-full h-full rounded-full"
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="w-full mb-0">
|
||||
<div className="flex flex-col gap-3 w-full p-3 overflow-hidden bg-white rounded-xl shadow-primary dark:bg-white/10 dark:ring-1 ring-white/15">
|
||||
<div className="self-center relative rounded-full size-20 bg-neutral-200 dark:bg-white/70 my-3">
|
||||
{picture ? (
|
||||
<img
|
||||
src={picture}
|
||||
alt="avatar"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="absolute inset-0 z-10 object-cover w-full h-full rounded-full"
|
||||
/>
|
||||
) : null}
|
||||
<AvatarUploader
|
||||
setPicture={setPicture}
|
||||
className="absolute inset-0 z-20 flex items-center justify-center w-full h-full text-white rounded-full dark:text-black bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
|
||||
>
|
||||
<PlusIcon className="size-8" />
|
||||
</AvatarUploader>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="display_name" className="font-medium">
|
||||
Display Name *
|
||||
</label>
|
||||
<input
|
||||
type={"text"}
|
||||
{...register("display_name", { required: true, minLength: 1 })}
|
||||
placeholder="e.g. Alice in Nostrland"
|
||||
spellCheck={false}
|
||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
) : null}
|
||||
<AvatarUploader
|
||||
setPicture={setPicture}
|
||||
className="absolute inset-0 z-20 flex items-center justify-center w-full h-full text-white rounded-full dark:text-black bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
|
||||
>
|
||||
<PlusIcon className="size-8" />
|
||||
</AvatarUploader>
|
||||
</div>
|
||||
</div>
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="flex flex-col w-full gap-3"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="display_name" className="font-medium">
|
||||
{t("user.displayName")} *
|
||||
</label>
|
||||
<input
|
||||
type={"text"}
|
||||
{...register("display_name", { required: true, minLength: 1 })}
|
||||
placeholder="e.g. Alice in Nostrland"
|
||||
spellCheck={false}
|
||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="name" className="font-medium">
|
||||
{t("user.name")}
|
||||
</label>
|
||||
<input
|
||||
type={"text"}
|
||||
{...register("name")}
|
||||
placeholder="e.g. alice"
|
||||
spellCheck={false}
|
||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="about" className="font-medium">
|
||||
{t("user.bio")}
|
||||
</label>
|
||||
<textarea
|
||||
{...register("about")}
|
||||
placeholder="e.g. Artist, anime-lover, and k-pop fan"
|
||||
spellCheck={false}
|
||||
className="relative h-24 w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-2 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="website" className="font-medium">
|
||||
{t("user.website")}
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
{...register("website")}
|
||||
placeholder="e.g. https://alice.me"
|
||||
spellCheck={false}
|
||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-500 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="name" className="font-medium">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type={"text"}
|
||||
{...register("name")}
|
||||
placeholder="e.g. alice"
|
||||
spellCheck={false}
|
||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="about" className="font-medium">
|
||||
Bio
|
||||
</label>
|
||||
<textarea
|
||||
{...register("about")}
|
||||
placeholder="e.g. Artist, anime-lover, and k-pop fan"
|
||||
spellCheck={false}
|
||||
className="relative h-24 w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-2 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="website" className="font-medium">
|
||||
Website
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
{...register("website")}
|
||||
placeholder="e.g. https://alice.me"
|
||||
spellCheck={false}
|
||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-500 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
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"
|
||||
disabled={loading}
|
||||
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"
|
||||
>
|
||||
{loading ? <Spinner /> : t("global.continue")}
|
||||
{loading ? <Spinner /> : "Continue"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -38,47 +38,49 @@ function Screen() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center w-full h-full gap-6 px-5 mx-auto xl:max-w-xl">
|
||||
<div className="flex flex-col items-center justify-center size-full gap-4">
|
||||
<div className="text-center">
|
||||
<h3 className="text-xl font-semibold">Continue with Private Key</h3>
|
||||
</div>
|
||||
<div className="flex flex-col w-full gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="key"
|
||||
className="font-medium text-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
Private Key
|
||||
</label>
|
||||
<input
|
||||
name="key"
|
||||
type="text"
|
||||
placeholder="nsec or ncryptsec..."
|
||||
value={key}
|
||||
onChange={(e) => setKey(e.target.value)}
|
||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="font-medium text-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
Password (Optional)
|
||||
</label>
|
||||
<input
|
||||
name="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex flex-col gap-3 w-full p-3 overflow-hidden bg-white rounded-xl shadow-primary dark:bg-white/10 dark:ring-1 ring-white/15">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="key"
|
||||
className="font-medium text-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
Private Key
|
||||
</label>
|
||||
<input
|
||||
name="key"
|
||||
type="text"
|
||||
placeholder="nsec or ncryptsec..."
|
||||
value={key}
|
||||
onChange={(e) => setKey(e.target.value)}
|
||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="font-medium text-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
Password (Optional)
|
||||
</label>
|
||||
<input
|
||||
name="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
disabled={loading}
|
||||
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"
|
||||
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"
|
||||
>
|
||||
{loading ? <Spinner /> : "Login"}
|
||||
</button>
|
||||
|
||||
@@ -37,12 +37,12 @@ function Screen() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center w-full h-full gap-6 px-5 mx-auto xl:max-w-xl">
|
||||
<div className="flex flex-col items-center justify-center size-full gap-4">
|
||||
<div className="text-center">
|
||||
<h3 className="text-xl font-semibold">Continue with Nostr Connect</h3>
|
||||
</div>
|
||||
<div className="flex flex-col w-full gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex flex-col gap-1 w-full p-3 overflow-hidden bg-white rounded-xl shadow-primary dark:bg-white/10 dark:ring-1 ring-white/15">
|
||||
<label
|
||||
htmlFor="uri"
|
||||
className="font-medium text-neutral-900 dark:text-neutral-100"
|
||||
@@ -63,7 +63,7 @@ function Screen() {
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
disabled={loading}
|
||||
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"
|
||||
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"
|
||||
>
|
||||
{loading ? <Spinner /> : "Login"}
|
||||
</button>
|
||||
|
||||
@@ -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-4">
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl 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>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { NostrQuery } from "@lume/system";
|
||||
import { Spinner } from "@lume/ui";
|
||||
import { insertImage, isImagePath } from "@lume/utils";
|
||||
import type { UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { getCurrent } from "@tauri-apps/api/window";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSlateStatic } from "slate-react";
|
||||
@@ -32,7 +32,7 @@ export function MediaButton() {
|
||||
let unlisten: UnlistenFn = undefined;
|
||||
|
||||
async function listenFileDrop() {
|
||||
const window = getCurrent();
|
||||
const window = getCurrentWindow();
|
||||
if (!unlisten) {
|
||||
unlisten = await window.listen("tauri://file-drop", async (event) => {
|
||||
// @ts-ignore, lfg !!!
|
||||
|
||||
@@ -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,146 @@
|
||||
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 { getCurrentWindow } 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 = getCurrentWindow().listen<Payload>(
|
||||
"new_reply",
|
||||
(data) => {
|
||||
const event = LumeEvent.from(data.payload.raw, data.payload.parsed);
|
||||
setReplies((prev) => [event, ...prev]);
|
||||
},
|
||||
);
|
||||
|
||||
const unlistenWindow = getCurrentWindow().onCloseRequested(async () => {
|
||||
await event.unlistenEventReply();
|
||||
await getCurrentWindow().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 { getCurrentWindow } 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 = getCurrentWindow().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,68 @@ 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 { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
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")({
|
||||
beforeLoad: async ({ context }) => {
|
||||
console.log(context);
|
||||
},
|
||||
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 () => {
|
||||
console.log(queryClient);
|
||||
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 +86,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 +102,7 @@ function Screen() {
|
||||
PredefinedMenuItem.new({ item: "Separator" }),
|
||||
MenuItem.new({
|
||||
text: "Quit",
|
||||
action: async () => await exit(0),
|
||||
action: async () => await invoke("force_quit"),
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -131,45 +114,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 = getCurrentWindow().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 +147,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 +156,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 +177,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 +201,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 +223,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 +238,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 +251,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 +269,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 +306,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 +323,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 +339,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">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, init } from "@getalby/bitcoin-connect-react";
|
||||
import { NostrAccount } from "@lume/system";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { getCurrent } from "@tauri-apps/api/webviewWindow";
|
||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
|
||||
export const Route = createFileRoute("/settings/bitcoin-connect")({
|
||||
beforeLoad: () => {
|
||||
@@ -17,7 +17,7 @@ export const Route = createFileRoute("/settings/bitcoin-connect")({
|
||||
function Screen() {
|
||||
const setNwcUri = async (uri: string) => {
|
||||
const cmd = await NostrAccount.setWallet(uri);
|
||||
if (cmd) getCurrent().close();
|
||||
if (cmd) getCurrentWebviewWindow().close();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -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 { getCurrentWindow } 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 = getCurrentWindow();
|
||||
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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { User } from "@/components/user";
|
||||
import { NostrQuery } from "@lume/system";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { getCurrent } from "@tauri-apps/api/webviewWindow";
|
||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useState } from "react";
|
||||
import CurrencyInput from "react-currency-input-field";
|
||||
@@ -35,7 +35,7 @@ function Screen() {
|
||||
if (val) {
|
||||
setIsCompleted(true);
|
||||
// close current window
|
||||
await getCurrent().close();
|
||||
await getCurrentWebviewWindow().close();
|
||||
}
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import { TanStackRouterVite } from "@tanstack/router-vite-plugin";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import { defineConfig } from "vite";
|
||||
import topLevelAwait from "vite-plugin-top-level-await";
|
||||
import viteTsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
viteTsconfigPaths(),
|
||||
topLevelAwait({
|
||||
promiseExportName: "__tla",
|
||||
promiseImportName: (i) => `__tla_${i}`,
|
||||
}),
|
||||
TanStackRouterVite(),
|
||||
],
|
||||
plugins: [react(), viteTsconfigPaths(), TanStackRouterVite()],
|
||||
build: {
|
||||
outDir: "../../dist",
|
||||
},
|
||||
|
||||
@@ -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.6",
|
||||
"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"
|
||||
"tailwindcss": "^3.4.6",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.13"
|
||||
|
||||
+28
-25
@@ -1,28 +1,31 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.4.1/schema.json",
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"files": {
|
||||
"ignore": ["apps/desktop2/src/router.gen.ts"]
|
||||
"$schema": "https://biomejs.dev/schemas/1.4.1/schema.json",
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"style": {
|
||||
"noNonNullAssertion": "warn",
|
||||
"noUselessElse": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"useExhaustiveDependencies": "off"
|
||||
},
|
||||
"a11y": {
|
||||
"noSvgWithoutTitle": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noStaticOnlyClass": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
"files": {
|
||||
"ignore": [
|
||||
"apps/desktop2/src/router.gen.ts",
|
||||
"packages/system/src/commands.ts"
|
||||
]
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"style": {
|
||||
"noNonNullAssertion": "warn",
|
||||
"noUselessElse": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"useExhaustiveDependencies": "off"
|
||||
},
|
||||
"a11y": {
|
||||
"noSvgWithoutTitle": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noStaticOnlyClass": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+33
-33
@@ -1,35 +1,35 @@
|
||||
{
|
||||
"name": "lume",
|
||||
"private": true,
|
||||
"version": "4.0.0",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"web:dev": "turbo run dev --filter web",
|
||||
"desktop:dev": "turbo run dev --filter desktop2",
|
||||
"desktop:build": "turbo run build --filter desktop2",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.8.1",
|
||||
"@tauri-apps/cli": "2.0.0-beta.20",
|
||||
"turbo": "^1.13.4"
|
||||
},
|
||||
"packageManager": "pnpm@8.9.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "2.0.0-beta.13",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.1.0-beta.3",
|
||||
"@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-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"
|
||||
}
|
||||
"name": "lume",
|
||||
"private": true,
|
||||
"version": "4.0.0",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"web:dev": "turbo run dev --filter web",
|
||||
"desktop:dev": "turbo run dev --filter desktop2",
|
||||
"desktop:build": "turbo run build --filter desktop2",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.8.3",
|
||||
"@tauri-apps/cli": "2.0.0-beta.22",
|
||||
"turbo": "^1.13.4"
|
||||
},
|
||||
"packageManager": "pnpm@8.9.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "2.0.0-beta.15",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.1.0-beta.5",
|
||||
"@tauri-apps/plugin-dialog": "2.0.0-beta.7",
|
||||
"@tauri-apps/plugin-fs": "2.0.0-beta.7",
|
||||
"@tauri-apps/plugin-http": "2.0.0-beta.8",
|
||||
"@tauri-apps/plugin-os": "2.0.0-beta.7",
|
||||
"@tauri-apps/plugin-process": "2.0.0-beta.7",
|
||||
"@tauri-apps/plugin-shell": "2.0.0-beta.8",
|
||||
"@tauri-apps/plugin-updater": "2.0.0-beta.7",
|
||||
"@tauri-apps/plugin-upload": "2.0.0-beta.8",
|
||||
"@tauri-apps/plugin-window-state": "2.0.0-beta.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
"devDependencies": {
|
||||
"@lume/tsconfig": "workspace:*",
|
||||
"@types/react": "^18.3.3",
|
||||
"typescript": "^5.4.5"
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
"main": "./src/index.ts",
|
||||
"dependencies": {
|
||||
"@lume/utils": "workspace:^",
|
||||
"@tanstack/query-persist-client-core": "^5.45.0",
|
||||
"@tanstack/react-query": "^5.45.0",
|
||||
"nostr-tools": "^2.7.0",
|
||||
"@tanstack/query-persist-client-core": "^5.51.9",
|
||||
"@tanstack/react-query": "^5.51.9",
|
||||
"nostr-tools": "^2.7.1",
|
||||
"react": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lume/tsconfig": "workspace:^",
|
||||
"@lume/types": "workspace:^",
|
||||
"@types/react": "^18.3.3",
|
||||
"typescript": "^5.4.5"
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
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") {
|
||||
return query.data;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static async loadAccount(npub: string) {
|
||||
return query;
|
||||
},
|
||||
loadAccount: async (npub: string) => {
|
||||
const bunker: string = localStorage.getItem(`${npub}_bunker`);
|
||||
let query: Result<boolean, string>;
|
||||
|
||||
@@ -24,16 +17,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 +30,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 +48,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 +57,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 +73,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 +82,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 +91,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 +100,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 +109,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 +118,8 @@ export class NostrAccount {
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static async getContactList() {
|
||||
},
|
||||
getContactList: async () => {
|
||||
const query = await commands.getContactList();
|
||||
|
||||
if (query.status === "ok") {
|
||||
@@ -147,9 +127,8 @@ export class NostrAccount {
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static async isContactListEmpty() {
|
||||
},
|
||||
isContactListEmpty: async () => {
|
||||
const query = await commands.isContactListEmpty();
|
||||
|
||||
if (query.status === "ok") {
|
||||
@@ -157,9 +136,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 +145,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 +154,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 +163,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) {
|
||||
@@ -44,15 +44,10 @@ try {
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getAccounts() : Promise<Result<string[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_accounts") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
async getAccounts() : Promise<string[]> {
|
||||
return await TAURI_INVOKE("get_accounts");
|
||||
},
|
||||
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 +143,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 +287,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 +311,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 +383,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 +423,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 +442,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 +461,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 { getCurrentWindow } 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 = getCurrentWindow().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 : getCurrentWindow().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.6",
|
||||
"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.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"@lume/tsconfig": "workspace:^",
|
||||
"@lume/types": "workspace:^",
|
||||
"@types/react": "^18.3.3",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "^5.4.5"
|
||||
"tailwindcss": "^3.4.6",
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.11",
|
||||
"light-bolt11-decoder": "^3.1.1",
|
||||
"nostr-tools": "^2.7.0",
|
||||
"nostr-tools": "^2.7.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"slate": "^0.103.0",
|
||||
@@ -23,7 +23,7 @@
|
||||
"@lume/types": "workspace:^",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"typescript": "^5.4.5"
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
||||
Generated
+4515
-3987
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
tab_spaces=2
|
||||
Generated
+596
-446
File diff suppressed because it is too large
Load Diff
+20
-12
@@ -11,18 +11,18 @@ rust-version = "1.70"
|
||||
tauri-build = { version = "2.0.0-beta", features = [] }
|
||||
|
||||
[dependencies]
|
||||
nostr-sdk = { version = "0.32", features = ["sqlite"] }
|
||||
nostr-sdk = { version = "0.33", 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,23 +33,31 @@ 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", branch = "feat/tauri-v2", features = [
|
||||
"typescript",
|
||||
] }
|
||||
tauri-plugin-theme = "0.4.1"
|
||||
tauri-plugin-decorum = "0.1.0"
|
||||
tauri-plugin-decorum = { git = "https://github.com/reyamir/tauri-plugin-decorum", branch = "feat/tauri-v2" }
|
||||
specta = "^2.0.0-rc.12"
|
||||
keyring = "2"
|
||||
keyring-search = "0.2.0"
|
||||
reqwest = "0.12.4"
|
||||
url = "2.5.0"
|
||||
futures = "0.3.30"
|
||||
linkify = "0.10.0"
|
||||
regex = "1.10.4"
|
||||
keyring = { version = "3", features = [
|
||||
"apple-native",
|
||||
"windows-native",
|
||||
"sync-secret-service",
|
||||
] }
|
||||
keyring-search = "1.2.0"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
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" }
|
||||
border = { git = "https://github.com/ahkohd/tauri-toolkit", 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"
|
||||
@@ -6506,6 +6738,13 @@
|
||||
"window:allow-set-title"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "window:allow-set-title-bar-style -> Enables the set_title_bar_style command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"window:allow-set-title-bar-style"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "window:allow-set-visible-on-all-workspaces -> Enables the set_visible_on_all_workspaces command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -6947,6 +7186,13 @@
|
||||
"window:deny-set-title"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "window:deny-set-title-bar-style -> Denies the set_title_bar_style command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"window:deny-set-title-bar-style"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "window:deny-set-visible-on-all-workspaces -> Denies the set_visible_on_all_workspaces command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -7009,6 +7255,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"
|
||||
@@ -6506,6 +6738,13 @@
|
||||
"window:allow-set-title"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "window:allow-set-title-bar-style -> Enables the set_title_bar_style command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"window:allow-set-title-bar-style"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "window:allow-set-visible-on-all-workspaces -> Enables the set_visible_on_all_workspaces command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -6947,6 +7186,13 @@
|
||||
"window:deny-set-title"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "window:deny-set-title-bar-style -> Denies the set_title_bar_style command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"window:deny-set-title-bar-style"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "window:deny-set-visible-on-all-workspaces -> Denies the set_visible_on_all_workspaces command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -7009,6 +7255,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"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
use std::ffi::CString;
|
||||
|
||||
use tauri::{AppHandle, Emitter, Listener, Manager, WebviewWindow};
|
||||
use tauri_nspanel::{
|
||||
block::ConcreteBlock,
|
||||
cocoa::{
|
||||
appkit::{NSMainMenuWindowLevel, NSView, NSWindow, NSWindowCollectionBehavior},
|
||||
base::{id, nil},
|
||||
foundation::{NSPoint, NSRect},
|
||||
},
|
||||
objc::{class, msg_send, runtime::NO, sel, sel_impl},
|
||||
panel_delegate, ManagerExt, WebviewWindowExt,
|
||||
};
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSWindowStyleMaskNonActivatingPanel: i32 = 1 << 7;
|
||||
|
||||
pub fn swizzle_to_menubar_panel(app_handle: &tauri::AppHandle) {
|
||||
let panel_delegate = panel_delegate!(SpotlightPanelDelegate {
|
||||
window_did_resign_key
|
||||
});
|
||||
|
||||
let window = app_handle.get_webview_window("panel").unwrap();
|
||||
|
||||
let panel = window.to_panel().unwrap();
|
||||
|
||||
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", ());
|
||||
}
|
||||
}));
|
||||
|
||||
panel.set_level(NSMainMenuWindowLevel + 1);
|
||||
|
||||
panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel);
|
||||
|
||||
panel.set_collection_behaviour(
|
||||
NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces
|
||||
| NSWindowCollectionBehavior::NSWindowCollectionBehaviorStationary
|
||||
| NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary,
|
||||
);
|
||||
|
||||
panel.set_delegate(panel_delegate);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
let handle = app_handle.clone();
|
||||
|
||||
app_handle.listen_any("menubar_panel_did_resign_key", move |_| {
|
||||
hide_menubar_panel(&handle);
|
||||
});
|
||||
|
||||
let handle = app_handle.clone();
|
||||
|
||||
let callback = Box::new(move || {
|
||||
hide_menubar_panel(&handle);
|
||||
});
|
||||
|
||||
register_workspace_listener(
|
||||
"NSWorkspaceDidActivateApplicationNotification".into(),
|
||||
callback.clone(),
|
||||
);
|
||||
|
||||
register_workspace_listener(
|
||||
"NSWorkspaceActiveSpaceDidChangeNotification".into(),
|
||||
callback,
|
||||
);
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
pub fn position_menubar_panel(app_handle: &tauri::AppHandle, padding_top: f64) {
|
||||
let window = app_handle.get_webview_window("panel").unwrap();
|
||||
|
||||
let monitor = monitor::get_monitor_with_cursor().unwrap();
|
||||
|
||||
let scale_factor = monitor.scale_factor();
|
||||
|
||||
let visible_area = monitor.visible_area();
|
||||
|
||||
let monitor_pos = visible_area.position().to_logical::<f64>(scale_factor);
|
||||
|
||||
let monitor_size = visible_area.size().to_logical::<f64>(scale_factor);
|
||||
|
||||
let mouse_location: NSPoint = unsafe { msg_send![class!(NSEvent), mouseLocation] };
|
||||
|
||||
let handle: id = window.ns_window().unwrap() as _;
|
||||
|
||||
let mut win_frame: NSRect = unsafe { msg_send![handle, frame] };
|
||||
|
||||
win_frame.origin.y = (monitor_pos.y + monitor_size.height) - win_frame.size.height;
|
||||
|
||||
win_frame.origin.y -= padding_top;
|
||||
|
||||
win_frame.origin.x = {
|
||||
let top_right = mouse_location.x + (win_frame.size.width / 2.0);
|
||||
|
||||
let is_offscreen = top_right > monitor_pos.x + monitor_size.width;
|
||||
|
||||
if !is_offscreen {
|
||||
mouse_location.x - (win_frame.size.width / 2.0)
|
||||
} else {
|
||||
let diff = top_right - (monitor_pos.x + monitor_size.width);
|
||||
|
||||
mouse_location.x - (win_frame.size.width / 2.0) - diff
|
||||
}
|
||||
};
|
||||
|
||||
let _: () = unsafe { msg_send![handle, setFrame: win_frame display: NO] };
|
||||
}
|
||||
|
||||
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| {
|
||||
callback();
|
||||
});
|
||||
|
||||
let block = block.copy();
|
||||
|
||||
let name: id =
|
||||
unsafe { msg_send![class!(NSString), stringWithCString: CString::new(name).unwrap()] };
|
||||
|
||||
unsafe {
|
||||
let _: () = msg_send![
|
||||
notification_center,
|
||||
addObserverForName: name object: nil queue: nil usingBlock: block
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
fn app_pid() -> i32 {
|
||||
let process_info: id = unsafe { msg_send![class!(NSProcessInfo), processInfo] };
|
||||
let pid: i32 = unsafe { msg_send![process_info, processIdentifier] };
|
||||
|
||||
pid
|
||||
}
|
||||
|
||||
fn get_frontmost_app_pid() -> i32 {
|
||||
let workspace: id = unsafe { msg_send![class!(NSWorkspace), sharedWorkspace] };
|
||||
let frontmost_application: id = unsafe { msg_send![workspace, frontmostApplication] };
|
||||
let pid: i32 = unsafe { msg_send![frontmost_application, processIdentifier] };
|
||||
|
||||
pid
|
||||
}
|
||||
|
||||
pub fn check_menubar_frontmost() -> bool {
|
||||
get_frontmost_app_pid() == app_pid()
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user