Compare commits

..
32 Commits
Author SHA1 Message Date
reya a5255fa503 chore: bump version 2024-07-31 12:51:41 +07:00
reya 954a17b541 fix: build on linux 2024-07-31 12:51:17 +07:00
reya a55b31b0e6 feat: add keyring support for linux and windows 2024-07-31 10:59:54 +07:00
reya bdf3ffd7bf fix: tray panel is missing 2024-07-19 13:58:32 +07:00
reya 07ce253f5b fix: child webview is not reposition after scroll 2024-07-19 13:10:29 +07:00
reya f3db010c74 chore: update tauri config 2024-07-19 10:01:20 +07:00
reya dcf2791fe5 fix: build 2024-07-19 09:28:03 +07:00
reya 8fcf3551d8 chore: bump version 2024-07-19 08:33:46 +07:00
reya 2d987849d8 feat: use native border in macos 2024-07-19 08:33:16 +07:00
reya 3b99926f3b feat: adapt latest changes in tauri v2 2024-07-19 08:25:36 +07:00
reya 113d69a4df chore: update deps 2024-07-18 18:57:28 +07:00
reya 5d12ba7216 fix: some screen too large 2024-07-03 14:36:54 +07:00
reya 72b59020b4 fix: build on linux and windows 2024-07-03 14:09:07 +07:00
XIAO YUandGitHub 4c323b9daa chore: Refactor code to use HashSet for account search results (#222) 2024-07-03 07:33:16 +07:00
reya 72da83d648 feat: add force quit command 2024-07-02 17:24:55 +07:00
reya 783a4538a4 Revert "chore: update ci"
This reverts commit 04706a6d7c.
2024-07-02 15:54:00 +07:00
reya 15e62cad11 fix: temporary include devtools in release build 2024-07-02 14:58:00 +07:00
reya c52b20ca80 chore: bump version 2024-07-02 14:36:16 +07:00
reya 04706a6d7c chore: update ci 2024-07-02 14:07:12 +07:00
reya 0755cbeb6c chore: bump version 2024-07-02 13:03:45 +07:00
雨宮蓮andGitHub 8eb01c8bbf Improve column management (#221)
* wip: redesign store

* feat: update trending column

* feat: add more functions
2024-07-02 12:51:50 +07:00
reya ed4f89ff66 feat: add option to toggle window transparent 2024-07-02 08:49:52 +07:00
reya d9fe647f8e feat: few improvements 2024-07-01 14:41:33 +07:00
reya 843c2d52e7 refactor: tray panel 2024-07-01 13:04:32 +07:00
reya 017a3676a4 feat: optimize spawn thread 2024-06-30 21:06:04 +07:00
reya fcb70c0e9a chore: update deps 2024-06-30 14:37:26 +07:00
雨宮蓮andGitHub 0fec21b9ce Some improments and Negentropy (#219)
* feat: adjust default window size

* feat: save window state

* feat: add window state plugin

* feat: add search

* feat: use negentropy for newsfeed

* feat: live feeds

* feat: add search user
2024-06-30 14:26:02 +07:00
XIAO YUandGitHub 968b1ada94 refactor: improve relay management code structure (#220) 2024-06-29 07:41:16 +07:00
reya 5c9b599b1e feat: use memo for some components 2024-06-26 17:49:36 +07:00
雨宮蓮andGitHub 717c3e17df Event Subscriptions (#218)
* feat: improve create column command

* refactor: thread

* feat: add window virtualized to event screen

* chore: update deps

* fix: window decoration

* feat: improve mention ntoe

* feat: add subscription to event screen
2024-06-26 14:51:50 +07:00
XIAO YUandGitHub a4540a0802 refactor: improve error handling in event.rs (#217) 2024-06-26 07:57:53 +07:00
reya 31bacc2646 chore: remove unused code 2024-06-24 11:09:32 +07:00
117 changed files with 10290 additions and 8639 deletions
+4 -4
View File
@@ -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
View File
@@ -1,14 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lume Desktop</title>
</head>
<body
class="relative h-screen w-screen cursor-default select-none overflow-hidden font-sans text-black antialiased dark:text-white"
>
<div id="root" class="h-full w-full"></div>
<script type="module" src="/src/app.tsx"></script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lume Desktop</title>
</head>
<body
class="relative h-screen w-screen cursor-default select-none overflow-hidden font-sans text-black antialiased dark:text-white"
>
<div id="root" class="h-full w-full"></div>
<script type="module" src="/src/app.tsx"></script>
</body>
</html>
+20 -21
View File
@@ -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"
}
}
+6 -17
View File
@@ -1,30 +1,19 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { RouterProvider, createRouter } from "@tanstack/react-router";
import React, { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import { I18nextProvider } from "react-i18next";
import "./app.css";
import { type } from "@tauri-apps/plugin-os";
import i18n from "./locale";
import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import { routeTree } from "./router.gen"; // auto generated file
import "./app.css";
const queryClient = new QueryClient();
const os = await type();
// Set up a Router instance
const platform = type();
const router = createRouter({
routeTree,
context: {
queryClient,
platform: os,
},
context: { queryClient, platform },
Wrap: ({ children }) => {
return (
<I18nextProvider i18n={i18n} defaultNS={"translation"}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</I18nextProvider>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
},
});
-41
View File
@@ -1,41 +0,0 @@
import { User } from "@/components/user";
import { NostrAccount } from "@lume/system";
import { getBitcoinDisplayValues } from "@lume/utils";
import { useEffect, useMemo, useState } from "react";
export function Balance({ account }: { account: string }) {
const [balance, setBalance] = useState(0);
const value = useMemo(() => getBitcoinDisplayValues(balance), [balance]);
useEffect(() => {
async function getBalance() {
const val = await NostrAccount.getBalance();
setBalance(val);
}
getBalance();
}, []);
return (
<div
data-tauri-drag-region
className="flex h-16 items-center justify-end px-3"
>
<div className="flex items-center gap-2">
<div className="text-end">
<div className="text-sm leading-tight text-neutral-700 dark:text-neutral-300">
Your balance
</div>
<div className="font-medium leading-tight">
{value.bitcoinFormatted}
</div>
</div>
<User.Provider pubkey={account}>
<User.Root>
<User.Avatar className="size-9 rounded-full" />
</User.Root>
</User.Provider>
</div>
</div>
);
}
+82 -30
View File
@@ -1,17 +1,17 @@
import { CancelIcon, CheckIcon } from "@lume/icons";
import { CheckIcon, HorizontalDotsIcon } from "@lume/icons";
import type { LumeColumn } from "@lume/types";
import { cn } from "@lume/utils";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { 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>
);
}
+1 -1
View File
@@ -42,7 +42,7 @@ export function NoteUser({ className }: { className?: string }) {
onClick={(e) => showContextMenu(e)}
className="shrink-0"
>
<User.Avatar className="object-cover rounded-full size-8 outline outline-1 -outline-offset-1 outline-black/15" />
<User.Avatar className="rounded-full size-8" />
</button>
<div className="flex items-center w-full gap-3">
<div className="flex items-center gap-1">
+6 -5
View File
@@ -1,9 +1,10 @@
import { QuoteIcon } from "@lume/icons";
import { Note } from "@/components/note";
import { cn } from "@lume/utils";
import { QuoteIcon } from "@lume/icons";
import type { LumeEvent } from "@lume/system";
import { cn } from "@lume/utils";
import { memo } from "react";
export function Quote({
export const Quote = memo(function Quote({
event,
className,
}: {
@@ -14,7 +15,7 @@ export function Quote({
<Note.Provider event={event}>
<Note.Root
className={cn(
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50",
"bg-white dark:bg-black/20 rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50",
className,
)}
>
@@ -40,4 +41,4 @@ export function Quote({
</Note.Root>
</Note.Provider>
);
}
});
+7 -6
View File
@@ -1,11 +1,12 @@
import { Spinner } from "@lume/ui";
import { Note } from "@/components/note";
import { User } from "@/components/user";
import { type LumeEvent, NostrQuery } from "@lume/system";
import { Spinner } from "@lume/ui";
import { cn } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
import { type LumeEvent, NostrQuery } from "@lume/system";
import { memo } from "react";
export function RepostNote({
export const RepostNote = memo(function RepostNote({
event,
className,
}: {
@@ -32,7 +33,7 @@ export function RepostNote({
return (
<Note.Root
className={cn(
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50",
"bg-white dark:bg-black/20 rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50",
className,
)}
>
@@ -68,7 +69,7 @@ export function RepostNote({
<div className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
Reposted by
</div>
<User.Avatar className="object-cover rounded-full size-6 shrink-0 ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
<User.Avatar className="rounded-full size-6" />
</User.Root>
</User.Provider>
</div>
@@ -78,4 +79,4 @@ export function RepostNote({
)}
</Note.Root>
);
}
});
+5 -4
View File
@@ -1,8 +1,9 @@
import { cn } from "@lume/utils";
import { Note } from "@/components/note";
import type { LumeEvent } from "@lume/system";
import { cn } from "@lume/utils";
import { memo } from "react";
export function TextNote({
export const TextNote = memo(function TextNote({
event,
className,
}: {
@@ -13,7 +14,7 @@ export function TextNote({
<Note.Provider event={event}>
<Note.Root
className={cn(
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50",
"bg-white dark:bg-black/20 rounded-xl shadow-primary dark:ring-1 dark:ring-white/5",
className,
)}
>
@@ -31,4 +32,4 @@ export function TextNote({
</Note.Root>
</Note.Provider>
);
}
});
+23 -12
View File
@@ -2,7 +2,6 @@ import { cn } from "@lume/utils";
import * as Avatar from "@radix-ui/react-avatar";
import { useRouteContext } from "@tanstack/react-router";
import { minidenticon } from "minidenticons";
import { nanoid } from "nanoid";
import { useMemo } from "react";
import { useUserContext } from "./provider";
@@ -22,22 +21,29 @@ export function UserAvatar({ className }: { className?: string }) {
}
}, [user.profile?.picture]);
const fallbackAvatar = useMemo(
const fallback = useMemo(
() =>
`data:image/svg+xml;utf8,${encodeURIComponent(
minidenticon(user.pubkey || nanoid(), 90, 50),
minidenticon(user.pubkey, 60, 50),
)}`,
[user.pubkey],
);
if (settings && !settings.display_avatar) {
return (
<Avatar.Root className="shrink-0">
<Avatar.Root
className={cn(
"shrink-0 block overflow-hidden bg-neutral-200 dark:bg-neutral-800",
className,
)}
>
<Avatar.Fallback delayMs={120}>
<img
src={fallbackAvatar}
src={fallback}
alt={user.pubkey}
className={cn("bg-black dark:bg-white", className)}
loading="lazy"
decoding="async"
className="size-full bg-black dark:bg-white outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
/>
</Avatar.Fallback>
</Avatar.Root>
@@ -45,19 +51,24 @@ export function UserAvatar({ className }: { className?: string }) {
}
return (
<Avatar.Root className="shrink-0">
<Avatar.Root
className={cn(
"shrink-0 block overflow-hidden bg-neutral-200 dark:bg-neutral-800",
className,
)}
>
<Avatar.Image
src={picture}
alt={user.pubkey}
loading="eager"
loading="lazy"
decoding="async"
className={cn("outline-[.5px] outline-black/5 object-cover", className)}
className="w-full aspect-square object-cover outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
/>
<Avatar.Fallback delayMs={120}>
<Avatar.Fallback>
<img
src={fallbackAvatar}
src={fallback}
alt={user.pubkey}
className={cn("bg-black dark:bg-white", className)}
className="size-full bg-black dark:bg-white outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
/>
</Avatar.Fallback>
</Avatar.Root>
+58 -36
View File
@@ -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>
);
+160 -137
View File
@@ -1,91 +1,119 @@
import { User } from "@/components/user";
import { ComposeFilledIcon, HorizontalDotsIcon, PlusIcon } from "@lume/icons";
import { LumeWindow, NostrAccount } from "@lume/system";
import {
ChevronDownIcon,
ComposeFilledIcon,
PlusIcon,
SearchIcon,
} from "@lume/icons";
import { LumeWindow, NostrAccount, NostrQuery } from "@lume/system";
import { cn } from "@lume/utils";
import * as Popover from "@radix-ui/react-popover";
import { Outlet, createFileRoute } from "@tanstack/react-router";
import { Link } from "@tanstack/react-router";
import { Menu, MenuItem } from "@tauri-apps/api/menu";
import { 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>
);
}
});
+2 -11
View File
@@ -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() {
+3 -3
View File
@@ -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>
+35 -33
View File
@@ -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>
+32 -13
View File
@@ -28,6 +28,13 @@ function Screen() {
const onSubmit = async (data: { url: string; purpose: string }) => {
try {
if (!data.url.startsWith("wss://") || !data.url.startsWith("ws://")) {
return await message("Relay must be starts with wss:// or ws://", {
title: "Bootstrap Relays",
kind: "info",
});
}
const relay: Relay = { url: data.url, purpose: data.purpose };
setRelays((prev) => [...prev, relay]);
reset();
@@ -50,12 +57,21 @@ function Screen() {
}, [bootstrapRelays]);
return (
<div className="flex flex-col items-center justify-center w-screen h-screen">
<div className="w-full max-w-sm mx-auto lg:max-w-lg">
<div className="text-center h-11">
<h1 className="font-semibold">Customize Bootstrap Relays</h1>
<div
data-tauri-drag-region
className="relative flex flex-col items-center justify-between w-full h-full"
>
<div
data-tauri-drag-region
className="absolute top-0 left-0 h-14 w-full"
/>
<div className="flex items-end justify-center flex-1 w-full px-4 pb-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>
);
}
+5 -5
View File
@@ -96,7 +96,7 @@ function Screen() {
/>
</div>
<div className="flex flex-col items-center w-full gap-3">
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] flex flex-col gap-3 bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl">
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] flex flex-col gap-3 bg-black/5 dark:bg-white/5 rounded-xl">
<div className="flex gap-2">
<input
name="npub"
@@ -122,11 +122,11 @@ function Screen() {
key={item}
type="button"
onClick={() => toggleUser(item)}
className="inline-flex items-center justify-between px-3 py-2 bg-white rounded-lg dark:bg-black/20 backdrop-blur-lg shadow-primary dark:ring-1 ring-neutral-800/50"
className="inline-flex items-center justify-between px-3 py-2 bg-white rounded-lg dark:bg-black/20 shadow-primary dark:ring-1 ring-neutral-800/50"
>
<User.Provider pubkey={item}>
<User.Root className="flex items-center gap-2.5">
<User.Avatar className="object-cover rounded-full size-8" />
<User.Avatar className="rounded-full size-8" />
<div className="flex items-center gap-1">
<User.Name className="text-sm font-medium" />
</div>
@@ -153,11 +153,11 @@ function Screen() {
key={item}
type="button"
onClick={() => toggleUser(item)}
className="inline-flex items-center justify-between px-3 py-2 bg-white rounded-lg dark:bg-black/20 backdrop-blur-lg shadow-primary dark:ring-1 ring-neutral-800/50"
className="inline-flex items-center justify-between px-3 py-2 bg-white rounded-lg dark:bg-black/20 shadow-primary dark:ring-1 ring-neutral-800/50"
>
<User.Provider pubkey={item}>
<User.Root className="flex items-center gap-2.5">
<User.Avatar className="object-cover rounded-full size-8" />
<User.Avatar className="rounded-full size-8" />
<div className="flex items-center gap-1">
<User.Name className="text-sm font-medium" />
</div>
@@ -49,7 +49,7 @@ function Screen() {
};
return (
<div className="overflow-y-auto scrollbar-none p-2 shrink-0 h-[450px] bg-white dark:bg-white/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<div className="overflow-y-auto scrollbar-none p-2 shrink-0 h-[450px] bg-white dark:bg-white/20 rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<div className="flex flex-col justify-between h-full">
<div className="flex-1 flex flex-col gap-1.5 justify-center px-5">
<p className="font-semibold text-neutral-500">
@@ -68,7 +68,7 @@ function Screen() {
return (
<div className="flex flex-col items-center w-full gap-3">
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl">
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] bg-black/5 dark:bg-white/5 rounded-xl">
<Suspense
fallback={
<div className="flex flex-col items-center justify-center w-full h-20 gap-1">
@@ -88,14 +88,14 @@ function Screen() {
users.profiles.map((item: { pubkey: string }) => (
<div
key={item.pubkey}
className="w-full p-2 mb-2 overflow-hidden bg-white rounded-lg h-max dark:bg-black/20 backdrop-blur-lg shadow-primary dark:ring-1 ring-neutral-800/50"
className="w-full p-2 mb-2 overflow-hidden bg-white rounded-lg h-max dark:bg-black/20shadow-primary dark:ring-1 ring-neutral-800/50"
>
<User.Provider pubkey={item.pubkey}>
<User.Root>
<div className="flex flex-col w-full h-full gap-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<User.Avatar className="object-cover rounded-full size-7 shrink-0" />
<User.Avatar className="rounded-full size-7" />
<User.Name className="text-sm leadning-tight max-w-[15rem] truncate font-semibold" />
</div>
<button
+2 -2
View File
@@ -75,14 +75,14 @@ function Screen() {
<span className="text-sm font-medium">Added: {topics.length}</span>
</div>
<div className="flex flex-col items-center w-full gap-3">
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl">
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] bg-black/5 dark:bg-white/5 rounded-xl">
<div className="flex flex-col gap-3">
{TOPICS.map((topic) => (
<button
key={topic.title}
type="button"
onClick={() => toggleTopic(topic)}
className="flex items-center justify-between px-3 bg-white border border-transparent rounded-lg h-11 dark:bg-black/20 backdrop-blur-lg hover:border-blue-500 shadow-primary dark:ring-1 ring-neutral-800/50"
className="flex items-center justify-between px-3 bg-white border border-transparent rounded-lg h-11 dark:bg-black/20 hover:border-blue-500 shadow-primary dark:ring-1 ring-neutral-800/50"
>
<div className="inline-flex items-center gap-1">
<div>{topic.icon}</div>
@@ -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 !!!
+7 -7
View File
@@ -1,7 +1,12 @@
import { Note } from "@/components/note";
import { MentionNote } from "@/components/note/mentions/note";
import { User } from "@/components/user";
import { ComposeFilledIcon } from "@lume/icons";
import { LumeEvent, useEvent } from "@lume/system";
import { Spinner } from "@lume/ui";
import { cn, insertImage, insertNostrEvent, isImageUrl } from "@lume/utils";
import { createFileRoute } from "@tanstack/react-router";
import { nip19 } from "nostr-tools";
import { useEffect, useState } from "react";
import { type Descendant, Node, Transforms, createEditor } from "slate";
import {
@@ -14,13 +19,8 @@ import {
withReact,
} from "slate-react";
import { MediaButton } from "./-components/media";
import { LumeEvent, useEvent } from "@lume/system";
import { WarningButton } from "./-components/warning";
import { MentionNote } from "@/components/note/mentions/note";
import { PowButton } from "./-components/pow";
import { User } from "@/components/user";
import { Note } from "@/components/note";
import { nip19 } from "nostr-tools";
import { WarningButton } from "./-components/warning";
type EditorSearch = {
reply_to: string;
@@ -250,7 +250,7 @@ function ChildNote({ id }: { id: string }) {
<Note.Root className="flex items-center gap-2">
<User.Provider pubkey={data.pubkey}>
<User.Root className="shrink-0">
<User.Avatar className="rounded-full size-8 shrink-0" />
<User.Avatar className="rounded-full size-8" />
</User.Root>
</User.Provider>
<div className="content-break line-clamp-1">{data.content}</div>
@@ -1,82 +0,0 @@
import { Note } from "@/components/note";
import { type LumeEvent, NostrQuery } from "@lume/system";
import { Box, Container, Spinner } from "@lume/ui";
import { createFileRoute } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { WindowVirtualizer } from "virtua";
import { Reply } from "./-components/reply";
export const Route = createFileRoute("/events/$eventId")({
beforeLoad: async () => {
const settings = await NostrQuery.getUserSettings();
return { settings };
},
loader: async ({ params }) => {
const event = await NostrQuery.getEvent(params.eventId);
return event;
},
component: Screen,
});
function Screen() {
const event = Route.useLoaderData();
const [reload, setReload] = useState(false);
const [replies, setReplies] = useState<LumeEvent[]>(null);
useEffect(() => {
let mounted = true;
if (event) {
event.getAllReplies().then((data) => {
if (mounted) setReplies(data);
});
}
return () => {
mounted = false;
};
}, [event]);
return (
<Container withDrag>
<Box className="scrollbar-none">
<WindowVirtualizer>
<Note.Provider event={event}>
<Note.Root>
<div className="flex items-center justify-between px-3 h-14">
<Note.User />
<Note.Menu />
</div>
<Note.ContentLarge className="px-3" />
<div className="flex items-center justify-end gap-2 px-3 mt-4 h-11">
<Note.Reply large />
<Note.Repost large />
<Note.Zap large />
</div>
</Note.Root>
</Note.Provider>
<div className="flex flex-col">
<div className="flex items-center px-3 text-sm font-semibold border-t h-11 text-neutral-700 dark:text-neutral-300 border-neutral-100 dark:border-neutral-900">
Replies ({replies?.length ?? 0})
</div>
{!replies ? (
<Spinner />
) : !replies.length ? (
<div className="flex items-center justify-center w-full">
<div className="flex flex-col items-center justify-center gap-2 py-6">
<h3 className="text-3xl">👋</h3>
<p className="leading-none text-neutral-600 dark:text-neutral-400">
Be the first to Reply!
</p>
</div>
</div>
) : (
replies.map((event) => <Reply key={event.id} event={event} />)
)}
</div>
</WindowVirtualizer>
</Box>
</Container>
);
}
+146
View File
@@ -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>
);
}
+2 -2
View File
@@ -81,11 +81,11 @@ export function Screen() {
<ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
<Virtualizer scrollRef={ref}>
{isFetching && !isLoading && !isFetchingNextPage ? (
<div className="flex items-center justify-center w-full mb-3 h-11 bg-black/10 dark:bg-white/10 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<div className="flex items-center justify-center w-full mb-3 h-12 bg-black/5 dark:bg-white/5 rounded-xl">
<div className="flex items-center justify-center gap-2">
<Spinner className="size-5" />
<span className="text-sm font-medium">
Fetching new notes...
Getting new notes...
</span>
</div>
</div>
+2 -2
View File
@@ -95,11 +95,11 @@ export function Screen() {
<ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
<Virtualizer scrollRef={ref}>
{isFetching && !isLoading && !isFetchingNextPage ? (
<div className="flex items-center justify-center w-full mb-3 h-11 bg-black/10 dark:bg-white/10 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<div className="flex items-center justify-center w-full mb-3 h-12 bg-black/5 dark:bg-white/5 rounded-xl">
<div className="flex items-center justify-center gap-2">
<Spinner className="size-5" />
<span className="text-sm font-medium">
Fetching new notes...
Getting new notes...
</span>
</div>
</div>
+7 -3
View File
@@ -67,8 +67,12 @@ function Screen() {
return (
<div
data-tauri-drag-region
className="flex flex-col items-center justify-between w-full h-full"
className="relative flex flex-col items-center justify-between w-full h-full"
>
<div
data-tauri-drag-region
className="absolute top-0 left-0 h-14 w-full"
/>
<div className="flex items-end justify-center flex-1 w-full px-4 pb-10">
<div className="text-center">
<h2 className="mb-1 text-lg text-neutral-700 dark:text-neutral-300">
@@ -78,7 +82,7 @@ function Screen() {
</div>
</div>
<div className="flex flex-col items-center flex-1 w-full gap-3">
<div className="flex flex-col w-full max-w-sm mx-auto overflow-hidden bg-white divide-y divide-neutral-100 dark:divide-white/5 rounded-xl shadow-primary backdrop-blur-lg dark:bg-white/10 dark:ring-1 ring-white/15">
<div className="flex flex-col w-full max-w-sm mx-auto overflow-hidden bg-white divide-y divide-neutral-100 dark:divide-white/5 rounded-xl shadow-primary dark:bg-white/10 dark:ring-1 ring-white/15">
{context.accounts.map((account) => (
<div
key={account}
@@ -88,7 +92,7 @@ function Screen() {
>
<User.Provider pubkey={account}>
<User.Root className="flex items-center gap-2.5 p-3">
<User.Avatar className="object-cover rounded-full size-10 shrink-0" />
<User.Avatar className="rounded-full size-10" />
<div className="inline-flex flex-col items-start">
<User.Name className="max-w-[6rem] truncate font-medium leading-tight" />
<span className="text-sm text-neutral-700 dark:text-neutral-300">
+1 -1
View File
@@ -12,7 +12,7 @@ function Screen() {
className="flex flex-col items-center justify-center w-screen h-screen"
>
<div className="w-full max-w-xs mx-auto lg:max-w-md">
<div className="flex flex-col w-full gap-2 px-2 bg-white rounded-xl shadow-primary backdrop-blur-lg dark:bg-white/20 dark:ring-1 ring-neutral-800/50">
<div className="flex flex-col w-full gap-2 px-2 bg-white rounded-xl shadow-primary dark:bg-white/20 dark:ring-1 ring-neutral-800/50">
<div className="flex items-center h-20 border-b border-neutral-100 dark:border-white/5">
<Link
to="/auth/create-profile"
+87 -9
View File
@@ -2,16 +2,23 @@ import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon } from "@lume/icons";
import { type LumeEvent, NostrAccount, NostrQuery } from "@lume/system";
import { type ColumnRouteSearch, Kind } from "@lume/types";
import { ArrowRightCircleIcon, ArrowUpIcon } from "@lume/icons";
import { LumeEvent, NostrAccount, NostrQuery } from "@lume/system";
import { type ColumnRouteSearch, Kind, type Meta } from "@lume/types";
import { Spinner } from "@lume/ui";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useInfiniteQuery } from "@tanstack/react-query";
import { type InfiniteData, useInfiniteQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { useCallback, useRef } from "react";
import { listen } from "@tauri-apps/api/event";
import { 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>
);
}
-52
View File
@@ -1,52 +0,0 @@
import { ZapIcon } from "@lume/icons";
import { NostrAccount } from "@lume/system";
import { Container } from "@lume/ui";
import { createLazyFileRoute } from "@tanstack/react-router";
import { useState } from "react";
export const Route = createLazyFileRoute("/nwc")({
component: Screen,
});
function Screen() {
const [uri, setUri] = useState("");
const [isDone, setIsDone] = useState(false);
const save = async () => {
const nwc = await NostrAccount.setWallet(uri);
setIsDone(nwc);
};
return (
<Container withDrag>
<div className="flex-1 w-full h-full px-5">
<div className="flex flex-col gap-2">
<div>
<h3 className="text-2xl font-light">
Connect <span className="font-semibold">bitcoin wallet</span> to
start zapping to your favorite content and creator.
</h3>
</div>
</div>
<div className="flex flex-col gap-2 mt-10">
<div className="flex flex-col gap-1.5">
<label>Paste a Nostr Wallet Connect connection string</label>
<textarea
value={uri}
onChange={(e) => setUri(e.target.value)}
placeholder="nostrconnect://"
className="w-full h-24 px-3 bg-transparent rounded-lg border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/>
</div>
<button
type="button"
onClick={save}
className="inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-lg bg-blue-500 px-5 font-medium text-white hover:bg-blue-600"
>
Save & Connect
</button>
</div>
</div>
</Container>
);
}
+4 -4
View File
@@ -22,7 +22,7 @@ function Screen() {
</p>
</div>
<div className="px-3 flex flex-col gap-3">
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10 backdrop-blur-lg">
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10">
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
01.
</div>
@@ -45,7 +45,7 @@ function Screen() {
</video>
</div>
</div>
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10 backdrop-blur-lg">
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10">
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
02.
</div>
@@ -68,7 +68,7 @@ function Screen() {
</video>
</div>
</div>
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10 backdrop-blur-lg">
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10">
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
03.
</div>
@@ -91,7 +91,7 @@ function Screen() {
</video>
</div>
</div>
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10 backdrop-blur-lg">
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10">
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
04.
</div>
@@ -1,13 +1,9 @@
import { Note } from "@/components/note";
import { User } from "@/components/user";
import {
HorizontalDotsIcon,
InfoIcon,
RepostIcon,
SearchIcon,
} from "@lume/icons";
import { HorizontalDotsIcon, InfoIcon, RepostIcon } from "@lume/icons";
import { type LumeEvent, LumeWindow, NostrQuery, useEvent } from "@lume/system";
import { Kind } from "@lume/types";
import { Spinner } from "@lume/ui";
import {
checkForAppUpdates,
decodeZapInvoice,
@@ -15,77 +11,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" />
+96
View File
@@ -0,0 +1,96 @@
import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { LumeEvent, NostrQuery } from "@lume/system";
import { Kind, type NostrEvent } from "@lume/types";
import { Spinner } from "@lume/ui";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { fetch } from "@tauri-apps/plugin-http";
import { useCallback, useRef } from "react";
import { Virtualizer } from "virtua";
type Search = {
query: string;
};
export const Route = createFileRoute("/search/notes")({
validateSearch: (search: Record<string, string>): Search => {
return {
query: search.query,
};
},
beforeLoad: async () => {
const settings = await NostrQuery.getUserSettings();
return { settings };
},
component: Screen,
});
function Screen() {
const { query } = Route.useSearch();
const { isLoading, data } = useQuery({
queryKey: ["search", query],
queryFn: async () => {
try {
const res = await fetch(
`https://api.nostr.wine/search?query=${query}&kind=1&limit=50`,
);
const content = await res.json();
const events = content.data as NostrEvent[];
const lumeEvents = await Promise.all(
events.map(async (item): Promise<LumeEvent> => {
const event = await LumeEvent.build(item);
return event;
}),
);
return lumeEvents.sort((a, b) => b.created_at - a.created_at);
} catch (e) {
throw new Error(e);
}
},
refetchOnWindowFocus: false,
});
const ref = useRef<HTMLDivElement>(null);
const renderItem = useCallback(
(event: LumeEvent) => {
if (!event) return;
switch (event.kind) {
case Kind.Repost:
return <RepostNote key={event.id} event={event} className="mb-3" />;
default: {
if (event.isConversation) {
return (
<Conversation key={event.id} className="mb-3" event={event} />
);
}
if (event.isQuote) {
return <Quote key={event.id} event={event} className="mb-3" />;
}
return <TextNote key={event.id} event={event} className="mb-3" />;
}
}
},
[data],
);
return (
<ScrollArea.Viewport ref={ref} className="h-full p-3">
<Virtualizer scrollRef={ref}>
{isLoading ? (
<div className="flex items-center justify-center w-full h-11 gap-2">
<Spinner className="size-5" />
<span className="text-sm font-medium">Searching...</span>
</div>
) : (
data.map((item) => renderItem(item))
)}
</Virtualizer>
</ScrollArea.Viewport>
);
}
+32 -136
View File
@@ -1,148 +1,44 @@
import { Note } from "@/components/note";
import { User } from "@/components/user";
import { SearchIcon } from "@lume/icons";
import { LumeEvent, LumeWindow } from "@lume/system";
import { Kind, type NostrEvent } from "@lume/types";
import { Spinner } from "@lume/ui";
import { createFileRoute } from "@tanstack/react-router";
import { message } from "@tauri-apps/plugin-dialog";
import { useEffect, useState } from "react";
import { useDebounce } from "use-debounce";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { Outlet, createFileRoute } from "@tanstack/react-router";
type Search = {
query: string;
};
export const Route = createFileRoute("/search")({
validateSearch: (search: Record<string, string>): Search => {
return {
query: search.query,
};
},
component: Screen,
});
function Screen() {
const [loading, setLoading] = useState(false);
const [events, setEvents] = useState<LumeEvent[]>([]);
const [search, setSearch] = useState("");
const [searchValue] = useDebounce(search, 500);
const searchEvents = async () => {
try {
setLoading(true);
const query = `https://api.nostr.wine/search?query=${searchValue}&kind=0,1`;
const res = await fetch(query);
const content = await res.json();
const events = content.data as NostrEvent[];
const lumeEvents = events.map((ev) => new LumeEvent(ev));
const sorted = lumeEvents.sort((a, b) => b.created_at - a.created_at);
setLoading(false);
setEvents(sorted);
} catch (e) {
setLoading(false);
await message(String(e), {
title: "Search",
kind: "error",
});
}
};
useEffect(() => {
if (searchValue.length >= 3 && searchValue.length < 500) {
searchEvents();
}
}, [searchValue]);
const { query } = Route.useSearch();
return (
<div data-tauri-drag-region className="flex flex-col w-full h-full">
<div className="relative flex flex-col h-24 border-b shrink-0 border-black/5 dark:border-white/5">
<div data-tauri-drag-region className="w-full h-4 shrink-0" />
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") searchEvents();
}}
placeholder="Search anything..."
className="w-full h-20 px-3 pt-10 text-lg bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-neutral-500 dark:placeholder:text-neutral-600"
/>
</div>
<div className="flex-1 p-3 overflow-y-auto scrollbar-none">
{loading ? (
<div className="flex items-center justify-center w-full h-full">
<Spinner />
</div>
) : events.length ? (
<div className="flex flex-col gap-5">
<div className="flex flex-col gap-1.5">
<div className="text-sm font-medium text-neutral-700 dark:text-neutral-300 shrink-0">
Users
</div>
<div className="flex flex-col flex-1 gap-1">
{events
.filter((ev) => ev.kind === Kind.Metadata)
.map((event) => (
<SearchUser key={event.pubkey} event={event} />
))}
</div>
</div>
<div className="flex flex-col gap-1.5">
<div className="text-sm font-medium text-neutral-700 dark:text-neutral-300 shrink-0">
Notes
</div>
<div className="flex flex-col flex-1 gap-3">
{events
.filter((ev) => ev.kind === Kind.Text)
.map((event) => (
<SearchNote key={event.id} event={event} />
))}
</div>
</div>
</div>
) : null}
{!loading && !events.length ? (
<div className="flex flex-col items-center justify-center h-full gap-3">
<div className="inline-flex items-center justify-center rounded-full size-16 bg-black/10 dark:bg-white/10">
<SearchIcon className="size-6" />
</div>
Try searching for people, notes, or keywords
</div>
) : null}
<div className="flex flex-col h-full">
<div
data-tauri-drag-region
className="shrink-0 flex items-end gap-1 h-20 px-3 pb-3 w-full border-b border-black/10 dark:border-white/10"
>
Search result for: <span className="font-semibold">{query}</span>
</div>
</div>
);
}
function SearchUser({ event }: { event: LumeEvent }) {
return (
<button
key={event.id}
type="button"
onClick={() => LumeWindow.openProfile(event.pubkey)}
className="col-span-1 p-2 rounded-lg hover:bg-black/10 dark:hover:bg-white/10"
>
<User.Provider pubkey={event.pubkey} embedProfile={event.content}>
<User.Root className="flex items-center gap-2">
<User.Avatar className="rounded-full size-9 shrink-0" />
<div className="inline-flex items-center gap-1.5">
<User.Name className="font-semibold" />
<User.NIP05 />
</div>
</User.Root>
</User.Provider>
</button>
);
}
function SearchNote({ event }: { event: LumeEvent }) {
return (
<div className="bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<Note.Provider event={event}>
<Note.Root>
<div className="flex items-center justify-between px-3 h-14">
<Note.User />
<Note.Menu />
</div>
<Note.Content className="px-3" quote={false} mention={false} />
<div className="flex items-center gap-4 px-3 mt-3 h-14">
<Note.Open />
</div>
</Note.Root>
</Note.Provider>
<ScrollArea.Root
type={"scroll"}
scrollHideDelay={300}
className="overflow-hidden size-full flex-1"
>
<Outlet />
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
</div>
);
}
+101
View File
@@ -0,0 +1,101 @@
import { User } from "@/components/user";
import { LumeWindow, NostrQuery } from "@lume/system";
import type { NostrEvent } from "@lume/types";
import { Spinner } from "@lume/ui";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { fetch } from "@tauri-apps/plugin-http";
import { useRef } from "react";
import { Virtualizer } from "virtua";
type Search = {
query: string;
};
type UserItem = {
pubkey: string;
profile: string;
};
export const Route = createFileRoute("/search/users")({
validateSearch: (search: Record<string, string>): Search => {
return {
query: search.query,
};
},
beforeLoad: async () => {
const settings = await NostrQuery.getUserSettings();
return { settings };
},
component: Screen,
});
function Screen() {
const { query } = Route.useSearch();
const { isLoading, data } = useQuery({
queryKey: ["search", query],
queryFn: async () => {
try {
const res = await fetch(
`https://api.nostr.wine/search?query=${query}&kind=0&limit=100`,
);
const content = await res.json();
const events = content.data as NostrEvent[];
const users: UserItem[] = events.map((ev) => ({
pubkey: ev.pubkey,
profile: ev.content,
}));
return users;
} catch (e) {
throw new Error(e);
}
},
refetchOnWindowFocus: false,
});
const ref = useRef<HTMLDivElement>(null);
return (
<ScrollArea.Viewport ref={ref} className="h-full px-3 pt-3">
<Virtualizer scrollRef={ref}>
{isLoading ? (
<div className="flex items-center justify-center w-full h-11 gap-2">
<Spinner className="size-5" />
<span className="text-sm font-medium">Searching...</span>
</div>
) : (
data.map((item) => (
<div
key={item.pubkey}
className="w-full p-3 mb-2 overflow-hidden bg-white rounded-lg h-max dark:bg-black/20 shadow-primary dark:ring-1 ring-neutral-800/50"
>
<User.Provider pubkey={item.pubkey} embedProfile={item.profile}>
<User.Root className="flex flex-col w-full h-full gap-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<User.Avatar className="rounded-full size-7" />
<div className="inline-flex items-center gap-1">
<User.Name className="text-sm leadning-tight max-w-[15rem] truncate font-semibold" />
<User.NIP05 />
</div>
</div>
<button
type="button"
onClick={() => LumeWindow.openProfile(item.pubkey)}
className="inline-flex items-center justify-center w-16 text-sm font-medium rounded-md h-7 bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
>
View
</button>
</div>
<User.About className="select-text line-clamp-3 max-w-none text-neutral-800 dark:text-neutral-400" />
</User.Root>
</User.Provider>
</div>
))
)}
</Virtualizer>
</ScrollArea.Viewport>
);
}
+3 -12
View File
@@ -8,15 +8,12 @@ import {
import { cn } from "@lume/utils";
import { Link } from "@tanstack/react-router";
import { Outlet, createFileRoute } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
export const Route = createFileRoute("/settings")({
component: Screen,
});
function Screen() {
const { t } = useTranslation();
return (
<div className="flex flex-col w-full h-full">
<div
@@ -36,9 +33,7 @@ function Screen() {
)}
>
<SettingsIcon className="size-5 shrink-0" />
<p className="text-sm font-medium">
{t("settings.general.title")}
</p>
<p className="text-sm font-medium">General</p>
</div>
);
}}
@@ -55,9 +50,7 @@ function Screen() {
)}
>
<UserIcon className="size-5 shrink-0" />
<p className="text-sm font-medium">
{t("settings.user.title")}
</p>
<p className="text-sm font-medium">User</p>
</div>
);
}}
@@ -108,9 +101,7 @@ function Screen() {
)}
>
<SecureIcon className="size-5 shrink-0" />
<p className="text-sm font-medium">
{t("settings.backup.title")}
</p>
<p className="text-sm font-medium">Backup</p>
</div>
);
}}
+1 -1
View File
@@ -51,7 +51,7 @@ function Account({ account }: { account: string }) {
<div className="flex items-center justify-between gap-2 py-3">
<User.Provider pubkey={account}>
<User.Root className="flex items-center gap-2">
<User.Avatar className="object-cover rounded-full size-8" />
<User.Avatar className="rounded-full size-8" />
<div className="flex flex-col">
<User.Name className="text-sm leading-tight" />
<span className="text-sm leading-tight text-black/50 dark:text-white/50">
@@ -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>
);
}
+91 -35
View File
@@ -1,48 +1,104 @@
import { GlobalIcon, LaurelIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import { Link } from "@tanstack/react-router";
import { Outlet, createFileRoute } from "@tanstack/react-router";
import { CommunityIcon, LaurelIcon } from "@lume/icons";
import type { LumeColumn } from "@lume/types";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { createFileRoute } from "@tanstack/react-router";
import { resolveResource } from "@tauri-apps/api/path";
import { 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>
);
}
+2 -2
View File
@@ -105,11 +105,11 @@ export function Screen() {
<ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
<Virtualizer scrollRef={ref}>
{isFetching && !isLoading && !isFetchingNextPage ? (
<div className="flex items-center justify-center w-full mb-3 h-11 bg-black/10 dark:bg-white/10 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<div className="flex items-center justify-center w-full mb-3 h-12 bg-black/5 dark:bg-white/5 rounded-xl">
<div className="flex items-center justify-center gap-2">
<Spinner className="size-5" />
<span className="text-sm font-medium">
Fetching new notes...
Getting new notes...
</span>
</div>
</div>
+26 -8
View File
@@ -1,11 +1,14 @@
import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { LumeEvent } from "@lume/system";
import type { NostrEvent } from "@lume/types";
import { Kind, type NostrEvent } from "@lume/types";
import { Spinner } from "@lume/ui";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { Await, createFileRoute } from "@tanstack/react-router";
import { defer } from "@tanstack/react-router";
import { Suspense, useRef } from "react";
import { Suspense, useCallback, useRef } from "react";
import { Virtualizer } from "virtua";
export const Route = createFileRoute("/trending/notes")({
@@ -21,7 +24,9 @@ export const Route = createFileRoute("/trending/notes")({
const events: NostrEvent[] = res.notes.map(
(item: { event: NostrEvent }) => item.event,
);
const lumeEvents = events.map((ev) => new LumeEvent(ev));
const lumeEvents = Promise.all(
events.map(async (ev) => await LumeEvent.build(ev)),
);
return lumeEvents;
}),
),
@@ -35,7 +40,24 @@ export const Route = createFileRoute("/trending/notes")({
export function Screen() {
const { data } = Route.useLoaderData();
const ref = useRef<HTMLDivElement>(null);
const renderItem = useCallback((event: LumeEvent) => {
if (!event) return;
switch (event.kind) {
case Kind.Repost:
return <RepostNote key={event.id} event={event} className="mb-3" />;
default: {
if (event.isConversation) {
return <Conversation key={event.id} className="mb-3" event={event} />;
}
if (event.isQuote) {
return <Quote key={event.id} event={event} className="mb-3" />;
}
return <TextNote key={event.id} event={event} className="mb-3" />;
}
}
}, []);
return (
<ScrollArea.Root
@@ -60,11 +82,7 @@ export function Screen() {
}
>
<Await promise={data}>
{(notes) =>
notes.map((event) => (
<TextNote key={event.id} event={event} className="mb-3" />
))
}
{(notes) => notes.map((event) => renderItem(event))}
</Await>
</Suspense>
</Virtualizer>
+7 -7
View File
@@ -25,14 +25,14 @@ function Screen() {
return (
<div className="flex flex-col h-full">
<div className="inline-flex items-center w-full gap-1 px-3 h-11 shrink-0">
<div className="inline-flex items-center w-full h-full gap-1">
<div className="shrink-0 h-11 flex items-center w-full gap-1 px-3">
<div className="flex w-full h-full gap-1">
<Link to="/trending/notes" search={search}>
{({ isActive }) => (
<div
className={cn(
"inline-flex h-7 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium",
isActive ? "bg-neutral-50 dark:bg-white/10" : "opacity-50",
"inline-flex h-8 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium",
isActive ? "bg-black/10 dark:bg-white/10" : "opacity-50",
)}
>
<ArticleIcon className="size-4" />
@@ -44,8 +44,8 @@ function Screen() {
{({ isActive }) => (
<div
className={cn(
"inline-flex h-7 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium",
isActive ? "bg-neutral-50 dark:bg-white/10" : "opacity-50",
"inline-flex h-8 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium",
isActive ? "bg-black/10 dark:bg-white/10" : "opacity-50",
)}
>
<GroupFeedsIcon className="size-4" />
@@ -55,7 +55,7 @@ function Screen() {
</Link>
</div>
</div>
<div className="flex-1 w-full h-full p-2 overflow-y-auto scrollbar-none">
<div className="flex-1 w-full h-full overflow-y-auto scrollbar-none">
<Outlet />
</div>
</div>
+3 -3
View File
@@ -1,5 +1,5 @@
import { Spinner } from "@lume/ui";
import { User } from "@/components/user";
import { Spinner } from "@lume/ui";
import { Await, defer } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
import { Suspense } from "react";
@@ -45,14 +45,14 @@ export function Screen() {
users.profiles.map((item: { pubkey: string }) => (
<div
key={item.pubkey}
className="h-max w-full overflow-hidden mb-3 p-2 bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl"
className="h-max w-full overflow-hidden mb-3 p-2 bg-black/5 dark:bg-white/5 rounded-xl"
>
<User.Provider pubkey={item.pubkey}>
<User.Root>
<div className="flex h-full w-full flex-col gap-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
<User.Avatar className="size-10 shrink-0 rounded-full object-cover" />
<User.Avatar className="size-10 rounded-full" />
<User.Name className="leadning-tight max-w-[15rem] truncate font-semibold" />
</div>
<User.Button className="inline-flex h-8 w-20 items-center justify-center rounded-lg bg-black/10 text-sm font-medium hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20" />
+1 -1
View File
@@ -50,7 +50,7 @@ function Screen() {
return (
<Container withDrag>
<Box className="px-0 scrollbar-none bg-black/5 dark:bg-white/5 backdrop-blur-sm">
<Box className="px-0 scrollbar-none bg-black/5 dark:bg-white/5">
<WindowVirtualizer>
<User.Provider pubkey={pubkey}>
<User.Root>
+2 -2
View File
@@ -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 -10
View File
@@ -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",
},
+3 -3
View File
@@ -13,12 +13,12 @@
"@astrojs/check": "^0.5.10",
"@astrojs/tailwind": "^5.1.0",
"@fontsource/alice": "^5.0.13",
"astro": "^4.10.2",
"astro": "^4.11.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
View File
@@ -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
View File
@@ -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"
}
}
+1 -1
View File
@@ -9,6 +9,6 @@
"devDependencies": {
"@lume/tsconfig": "workspace:*",
"@types/react": "^18.3.3",
"typescript": "^5.4.5"
"typescript": "^5.5.3"
}
}
+4 -4
View File
@@ -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"
}
}
+35 -59
View File
@@ -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);
}
}
}
},
};
+49 -21
View File
@@ -4,7 +4,7 @@
/** user-defined commands **/
export const commands = {
async getRelays() : Promise<Result<Relays, null>> {
async getRelays() : Promise<Result<Relays, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_relays") };
} catch (e) {
@@ -12,7 +12,7 @@ try {
else return { status: "error", error: e as any };
}
},
async connectRelay(relay: string) : Promise<Result<boolean, null>> {
async connectRelay(relay: string) : Promise<Result<boolean, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("connect_relay", { relay }) };
} catch (e) {
@@ -20,7 +20,7 @@ try {
else return { status: "error", error: e as any };
}
},
async removeRelay(relay: string) : Promise<Result<boolean, null>> {
async removeRelay(relay: string) : Promise<Result<boolean, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("remove_relay", { relay }) };
} catch (e) {
@@ -28,7 +28,7 @@ try {
else return { status: "error", error: e as any };
}
},
async getBootstrapRelays() : Promise<Result<string[], null>> {
async getBootstrapRelays() : Promise<Result<string[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_bootstrap_relays") };
} catch (e) {
@@ -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
View File
@@ -1,10 +1,4 @@
import type {
EventTag,
EventWithReplies,
Kind,
Meta,
NostrEvent,
} from "@lume/types";
import type { EventTag, Kind, Meta, NostrEvent } from "@lume/types";
import { type Result, commands } from "./commands";
export class LumeEvent {
@@ -110,52 +104,82 @@ export class LumeEvent {
}
}
public async getAllReplies() {
public async getEventReplies() {
const query = await commands.getReplies(this.id);
if (query.status === "ok") {
const events = query.data.map((item) => {
const nostrEvent: NostrEvent = JSON.parse(item.raw);
const events = query.data
// Create Lume Events
.map((item) => LumeEvent.from(item.raw, item.parsed))
// Filter quote event
.filter(
(ev) =>
!ev.tags.filter((t) => t[0] === "q" || t[3] === "mention").length,
);
if (item.parsed) {
nostrEvent.meta = item.parsed;
} else {
nostrEvent.meta = null;
}
const lumeEvent = new LumeEvent(nostrEvent);
return lumeEvent;
});
if (events.length > 0) {
const replies = new Set();
if (events.length > 1) {
const removeQueues = new Set();
for (const event of events) {
const tags = event.tags.filter(
(el) => el[0] === "e" && el[1] !== this.id && el[3] !== "mention",
(t) => t[0] === "e" && t[1] !== this.id,
);
if (tags.length > 0) {
for (const tag of tags) {
const rootIndex = events.findIndex((el) => el.id === tag[1]);
if (tags.length === 1) {
const index = events.findIndex((ev) => ev.id === tags[0][1]);
if (rootIndex !== -1) {
const rootEvent = events[rootIndex];
if (index !== -1) {
const rootEvent = events[index];
if (rootEvent?.replies) {
rootEvent.replies.push(event);
} else {
rootEvent.replies = [event];
if (rootEvent.replies?.length) {
rootEvent.replies.push(event);
} else {
rootEvent.replies = [event];
}
// Add current event to queue
removeQueues.add(event.id);
continue;
}
}
for (const tag of tags) {
const id = tag[1];
const rootIndex = events.findIndex((ev) => ev.id === id);
if (rootIndex !== -1) {
const rootEvent = events[rootIndex];
if (rootEvent.replies?.length) {
const childIndex = rootEvent.replies.findIndex(
(ev) => ev.id === id,
);
if (childIndex !== -1) {
const childEvent = rootEvent.replies[rootIndex];
if (childEvent.replies?.length) {
childEvent.replies.push(event);
} else {
childEvent.replies = [event];
}
// Add current event to queue
removeQueues.add(event.id);
}
replies.add(event.id);
} else {
rootEvent.replies = [event];
// Add current event to queue
removeQueues.add(event.id);
}
}
break;
}
}
return events.filter((ev) => !replies.has(ev.id));
return events.filter((ev) => !removeQueues.has(ev.id));
}
return events;
@@ -165,6 +189,26 @@ export class LumeEvent {
}
}
public async listenEventReply() {
const query = await commands.listenEventReply(this.id);
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
}
public async unlistenEventReply() {
const query = await commands.unlisten(this.id);
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
}
public async zap(amount: number, message: string) {
const query = await commands.zapEvent(this.id, amount.toString(), message);
@@ -226,4 +270,27 @@ export class LumeEvent {
throw new Error(query.error);
}
}
static async build(event: NostrEvent) {
const query = await commands.getEventMeta(event.content);
if (query.status === "ok") {
event.meta = query.data;
return new LumeEvent(event);
} else {
return new LumeEvent(event);
}
}
static from(raw: string, parsed?: Meta) {
const nostrEvent: NostrEvent = JSON.parse(raw);
if (parsed) {
nostrEvent.meta = parsed;
} else {
nostrEvent.meta = null;
}
return new LumeEvent(nostrEvent);
}
}
+84 -84
View File
@@ -1,5 +1,6 @@
import type { LumeColumn, Metadata, NostrEvent, Relay } from "@lume/types";
import { resolveResource } from "@tauri-apps/api/path";
import { 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);
}
},
};
+31 -25
View File
@@ -2,21 +2,20 @@ import type { NostrEvent } from "@lume/types";
import { commands } from "./commands";
import type { LumeEvent } from "./event";
export class LumeWindow {
static async openMainWindow() {
export const LumeWindow = {
openMainWindow: async () => {
const query = await commands.openMainWindow();
return query;
}
static async openEvent(event: NostrEvent | LumeEvent) {
},
openEvent: async (event: NostrEvent | LumeEvent) => {
const eTags = event.tags.filter((tag) => tag[0] === "e" || tag[0] === "q");
const root: string =
eTags.find((el) => el[3] === "root")?.[1] ?? eTags[0]?.[1];
const reply: string =
eTags.find((el) => el[3] === "reply")?.[1] ?? eTags[1]?.[1];
const label = `event-${event.id}`;
const url = `/events/${root ?? reply ?? event.id}`;
const label = `event-${root ?? reply ?? event.id}`;
const query = await commands.openWindow({
label,
@@ -26,6 +25,7 @@ export class LumeWindow {
height: 800,
maximizable: true,
minimizable: true,
hidden_title: false,
});
if (query.status === "ok") {
@@ -33,9 +33,8 @@ export class LumeWindow {
} else {
throw new Error(query.error);
}
}
static async openProfile(pubkey: string) {
},
openProfile: async (pubkey: string) => {
const label = `user-${pubkey}`;
const query = await commands.openWindow({
label,
@@ -45,6 +44,7 @@ export class LumeWindow {
height: 800,
maximizable: true,
minimizable: true,
hidden_title: true,
});
if (query.status === "ok") {
@@ -52,9 +52,8 @@ export class LumeWindow {
} else {
throw new Error(query.error);
}
}
static async openEditor(reply_to?: string, quote?: string) {
},
openEditor: async (reply_to?: string, quote?: string) => {
let url: string;
if (reply_to) {
@@ -76,8 +75,9 @@ export class LumeWindow {
title: "Editor",
width: 560,
height: 340,
maximizable: true,
maximizable: false,
minimizable: false,
hidden_title: true,
});
if (query.status === "ok") {
@@ -85,9 +85,8 @@ export class LumeWindow {
} else {
throw new Error(query.error);
}
}
static async openZap(id: string) {
},
openZap: async (id: string) => {
const wallet = await commands.loadWallet();
if (wallet.status === "ok") {
@@ -99,13 +98,13 @@ export class LumeWindow {
height: 460,
maximizable: false,
minimizable: false,
hidden_title: true,
});
} else {
await LumeWindow.openSettings("bitcoin-connect");
}
}
static async openSettings(path?: string) {
},
openSettings: async (path?: string) => {
const label = "settings";
const query = await commands.openWindow({
label,
@@ -115,6 +114,7 @@ export class LumeWindow {
height: 500,
maximizable: false,
minimizable: false,
hidden_title: true,
});
if (query.status === "ok") {
@@ -122,18 +122,24 @@ export class LumeWindow {
} else {
throw new Error(query.error);
}
}
},
openSearch: async (searchType: "notes" | "users", searchQuery: string) => {
const url = `/search/${searchType}?query=${searchQuery}`;
const label = `search-${searchQuery
.toLowerCase()
.replace(/[^\w ]+/g, "")
.replace(/ +/g, "_")
.replace(/_+/g, "_")}`;
static async openSearch() {
const label = "search";
const query = await commands.openWindow({
label,
url: "/search",
url,
title: "Search",
width: 400,
height: 600,
maximizable: false,
minimizable: false,
hidden_title: true,
});
if (query.status === "ok") {
@@ -141,5 +147,5 @@ export class LumeWindow {
} else {
throw new Error(query.error);
}
}
}
},
};
+2 -1
View File
@@ -15,6 +15,7 @@
"@tailwindcss/typography": "^0.5.13",
"tailwind-gradient-mask-image": "^1.2.0",
"tailwind-scrollbar": "^3.1.0",
"tailwindcss": "^3.4.4"
"tailwindcss": "^3.4.6",
"tailwindcss-content-visibility": "^0.2.0"
}
}
+1 -1
View File
@@ -49,7 +49,7 @@ const config = {
require("@tailwindcss/forms"),
require("@tailwindcss/typography"),
require("tailwind-gradient-mask-image"),
require("tailwind-scrollbar")({ nocompatible: true }),
require("tailwindcss-content-visibility"),
],
};
+2 -1
View File
@@ -78,10 +78,11 @@ export interface LumeColumn {
}
export interface ColumnEvent {
type: "reset" | "add" | "remove" | "update" | "left" | "right" | "set_title";
type: "reset" | "add" | "remove" | "update" | "move" | "set_title";
label?: string;
title?: string;
column?: LumeColumn;
direction?: "left" | "right";
}
export interface Relays {
+1 -1
View File
@@ -9,6 +9,6 @@
"access": "public"
},
"devDependencies": {
"typescript": "^5.4.5"
"typescript": "^5.5.3"
}
}
+2 -2
View File
@@ -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"
}
}
-84
View File
@@ -1,84 +0,0 @@
import { ArrowLeftIcon, ArrowRightIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import { useSnapCarousel } from "react-snap-carousel";
interface CarouselProps<T> {
readonly items: T[];
readonly renderItem: (
props: CarouselRenderItemProps<T>,
) => React.ReactElement<CarouselItemProps>;
}
interface CarouselRenderItemProps<T> {
readonly item: T;
readonly index: number;
readonly isSnapPoint: boolean;
}
export const Carousel = <T,>({ items, renderItem }: CarouselProps<T>) => {
const { scrollRef, pages, activePageIndex, prev, next, snapPointIndexes } =
useSnapCarousel();
return (
<div className="relative group">
<ul
ref={scrollRef}
className="relative flex overflow-auto snap-x scrollbar-none"
>
{items.map((item, index) =>
renderItem({
item,
index,
isSnapPoint: snapPointIndexes.has(index),
}),
)}
</ul>
<div
aria-hidden
className="absolute z-10 items-center justify-between hidden w-full px-5 transform -translate-x-1/2 -translate-y-1/2 group-hover:flex left-1/2 top-1/2"
>
<button
type="button"
className={cn(
"size-11 rounded-full bg-black/50 backdrop-blur-sm flex items-center justify-center text-white",
activePageIndex <= 0 ? "opacity-50" : "",
)}
onClick={() => prev()}
>
<ArrowLeftIcon className="size-6" />
</button>
<button
type="button"
className={cn(
"size-11 rounded-full bg-black/50 backdrop-blur-sm flex items-center justify-center text-white",
activePageIndex <= 0 ? "opacity-50" : "",
)}
onClick={() => next()}
>
<ArrowRightIcon className="size-6" />
</button>
</div>
<div className="absolute flex items-center justify-center w-12 h-6 text-sm font-medium text-white bg-black rounded-full top-3 right-3 mix-blend-multiply bg-opacity-20 backdrop-blur-sm">
{activePageIndex + 1} / {pages.length}
</div>
</div>
);
};
interface CarouselItemProps {
readonly isSnapPoint: boolean;
readonly children?: React.ReactNode;
}
export const CarouselItem = ({ isSnapPoint, children }: CarouselItemProps) => {
return (
<li
className={cn(
"w-[240px] h-[320px] shrink-0 pl-3 last:pr-2",
isSnapPoint ? "" : "snap-start",
)}
>
{children}
</li>
);
};
-1
View File
@@ -1,4 +1,3 @@
export * from "./container";
export * from "./box";
export * from "./spinner";
export * from "./carousel";
-1
View File
@@ -1,5 +1,4 @@
export * from "./src/constants";
export * from "./src/delay";
export * from "./src/formater";
export * from "./src/editor";
export * from "./src/cn";
+3 -3
View File
@@ -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
View File
@@ -1 +0,0 @@
export const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
+4515 -3987
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -1 +0,0 @@
tab_spaces=2
+596 -446
View File
File diff suppressed because it is too large Load Diff
+20 -12
View File
@@ -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
+51
View File
@@ -0,0 +1,51 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "desktop-capability",
"description": "Capability for the column",
"platforms": [
"linux",
"macOS",
"windows"
],
"windows": [
"column-*"
],
"permissions": [
"resources:default",
"tray:default",
"os:allow-locale",
"os:allow-os-type",
"clipboard-manager:allow-write-text",
"dialog:allow-open",
"dialog:allow-ask",
"dialog:allow-message",
"fs:allow-read-file",
"menu:default",
"menu:allow-new",
"menu:allow-popup",
"http:default",
"shell:allow-open",
{
"identifier": "http:default",
"allow": [
{
"url": "http://**/"
},
{
"url": "https://**/"
}
]
},
{
"identifier": "fs:allow-read-text-file",
"allow": [
{
"path": "$RESOURCE/locales/*"
},
{
"path": "$RESOURCE/resources/*"
}
]
}
]
}
+89 -87
View File
@@ -1,89 +1,91 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "desktop-capability",
"description": "Capability for the desktop",
"platforms": ["linux", "macOS", "windows"],
"windows": [
"main",
"panel",
"splash",
"settings",
"search",
"nwc",
"activity",
"zap-*",
"event-*",
"user-*",
"editor-*",
"column-*"
],
"permissions": [
"path:default",
"event:default",
"window:default",
"app:default",
"resources:default",
"menu:default",
"tray:default",
"notification:allow-is-permission-granted",
"notification:allow-request-permission",
"notification:default",
"os:allow-locale",
"os:allow-platform",
"os:allow-os-type",
"updater:default",
"updater:allow-check",
"updater:allow-download-and-install",
"window:allow-start-dragging",
"window:allow-create",
"window:allow-close",
"window:allow-set-focus",
"window:allow-center",
"window:allow-minimize",
"window:allow-maximize",
"window:allow-set-size",
"window:allow-set-focus",
"window:allow-start-dragging",
"decorum:allow-show-snap-overlay",
"clipboard-manager:allow-write-text",
"clipboard-manager:allow-read-text",
"webview:allow-create-webview-window",
"webview:allow-create-webview",
"webview:allow-set-webview-size",
"webview:allow-set-webview-position",
"webview:allow-webview-close",
"dialog:allow-open",
"dialog:allow-ask",
"dialog:allow-message",
"process:allow-restart",
"fs:allow-read-file",
"theme:allow-set-theme",
"theme:allow-get-theme",
"menu:allow-new",
"menu:allow-popup",
"http:default",
"shell:allow-open",
{
"identifier": "http:default",
"allow": [
{
"url": "http://**/"
},
{
"url": "https://**/"
}
]
},
{
"identifier": "fs:allow-read-text-file",
"allow": [
{
"path": "$RESOURCE/locales/*"
},
{
"path": "$RESOURCE/resources/*"
}
]
}
]
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "desktop-capability",
"description": "Capability for the desktop",
"platforms": [
"linux",
"macOS",
"windows"
],
"windows": [
"main",
"panel",
"settings",
"search-*",
"zap-*",
"event-*",
"user-*",
"editor-*"
],
"permissions": [
"path:default",
"event:default",
"window:default",
"app:default",
"resources:default",
"menu:default",
"tray:default",
"notification:allow-is-permission-granted",
"notification:allow-request-permission",
"notification:default",
"os:allow-locale",
"os:allow-platform",
"os:allow-os-type",
"updater:default",
"updater:allow-check",
"updater:allow-download-and-install",
"window:allow-start-dragging",
"window:allow-create",
"window:allow-close",
"window:allow-destroy",
"window:allow-set-focus",
"window:allow-center",
"window:allow-minimize",
"window:allow-maximize",
"window:allow-set-size",
"window:allow-set-focus",
"window:allow-start-dragging",
"decorum:allow-show-snap-overlay",
"clipboard-manager:allow-write-text",
"clipboard-manager:allow-read-text",
"webview:allow-create-webview-window",
"webview:allow-create-webview",
"webview:allow-set-webview-size",
"webview:allow-set-webview-position",
"webview:allow-webview-close",
"dialog:allow-open",
"dialog:allow-ask",
"dialog:allow-message",
"process:allow-restart",
"process:allow-exit",
"fs:allow-read-file",
"theme:allow-set-theme",
"theme:allow-get-theme",
"menu:allow-new",
"menu:allow-popup",
"http:default",
"shell:allow-open",
{
"identifier": "http:default",
"allow": [
{
"url": "http://**/"
},
{
"url": "https://**/"
}
]
},
{
"identifier": "fs:allow-read-text-file",
"allow": [
{
"path": "$RESOURCE/locales/*"
},
{
"path": "$RESOURCE/resources/*"
}
]
}
]
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","panel","splash","settings","search","nwc","activity","zap-*","event-*","user-*","editor-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","os:allow-os-type","updater:default","updater:allow-check","updater:allow-download-and-install","window:allow-start-dragging","window:allow-create","window:allow-close","window:allow-set-focus","window:allow-center","window:allow-minimize","window:allow-maximize","window:allow-set-size","window:allow-set-focus","window:allow-start-dragging","decorum:allow-show-snap-overlay","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","webview:allow-create-webview-window","webview:allow-create-webview","webview:allow-set-webview-size","webview:allow-set-webview-position","webview:allow-webview-close","dialog:allow-open","dialog:allow-ask","dialog:allow-message","process:allow-restart","fs:allow-read-file","theme:allow-set-theme","theme:allow-get-theme","menu:allow-new","menu:allow-popup","http:default","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["linux","macOS","windows"]}}
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","panel","settings","search-*","zap-*","event-*","user-*","editor-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","os:allow-os-type","updater:default","updater:allow-check","updater:allow-download-and-install","window:allow-start-dragging","window:allow-create","window:allow-close","window:allow-destroy","window:allow-set-focus","window:allow-center","window:allow-minimize","window:allow-maximize","window:allow-set-size","window:allow-set-focus","window:allow-start-dragging","decorum:allow-show-snap-overlay","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","webview:allow-create-webview-window","webview:allow-create-webview","webview:allow-set-webview-size","webview:allow-set-webview-position","webview:allow-webview-close","dialog:allow-open","dialog:allow-ask","dialog:allow-message","process:allow-restart","process:allow-exit","fs:allow-read-file","theme:allow-set-theme","theme:allow-get-theme","menu:allow-new","menu:allow-popup","http:default","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["linux","macOS","windows"]}}
+361 -66
View File
@@ -37,7 +37,7 @@
],
"definitions": {
"Capability": {
"description": "a grouping and boundary mechanism developers can use to separate windows or plugins functionality from each other at runtime.\n\nIf a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create trust groups and reduce impact of vulnerabilities in certain plugins or windows. Windows can be added to a capability by exact name or glob patterns like *, admin-* or main-window.",
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows fine grained access to the Tauri core, application, or plugin commands. If a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"path:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, \"platforms\": [\"macOS\",\"windows\"] } ```",
"type": "object",
"required": [
"identifier",
@@ -45,16 +45,16 @@
],
"properties": {
"identifier": {
"description": "Identifier of the capability.",
"description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`",
"type": "string"
},
"description": {
"description": "Description of the capability.",
"description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.",
"default": "",
"type": "string"
},
"remote": {
"description": "Configure remote URLs that can use the capability permissions.",
"description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```",
"anyOf": [
{
"$ref": "#/definitions/CapabilityRemote"
@@ -70,28 +70,29 @@
"type": "boolean"
},
"windows": {
"description": "List of windows that uses this capability. Can be a glob pattern.\n\nOn multiwebview windows, prefer [`Self::webviews`] for a fine grained access control.",
"description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nOn multiwebview windows, prefer [`Self::webviews`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`",
"type": "array",
"items": {
"type": "string"
}
},
"webviews": {
"description": "List of webviews that uses this capability. Can be a glob pattern.\n\nThis is only required when using on multiwebview contexts, by default all child webviews of a window that matches [`Self::windows`] are linked.",
"description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThis is only required when using on multiwebview contexts, by default all child webviews of a window that matches [`Self::windows`] are linked.\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`",
"type": "array",
"items": {
"type": "string"
}
},
"permissions": {
"description": "List of permissions attached to this capability. Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.",
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"path:default\", \"event:default\", \"window:default\", \"app:default\", \"image:default\", \"resources:default\", \"menu:default\", \"tray:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ```",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionEntry"
}
},
"uniqueItems": true
},
"platforms": {
"description": "Target platforms this capability applies. By default all platforms are affected by this capability.",
"description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`",
"type": [
"array",
"null"
@@ -110,7 +111,7 @@
],
"properties": {
"urls": {
"description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n# Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api",
"description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api",
"type": "array",
"items": {
"type": "string"
@@ -142,7 +143,7 @@
"identifier": {
"oneOf": [
{
"description": "fs:default -> # Tauri `fs` default permissions\n\nThis configuration file defines the default permissions granted\nto the filesystem.\n\n### Granted Permissions\n\nThis default permission set enables all read-related commands and\nallows access to the `$APP` folder and sub directories created in it.\nThe location of the `$APP` folder depends on the operating system,\nwhere the application is run.\n\nIn general the `$APP` folder needs to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\n### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n",
"description": "fs:default -> This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n",
"type": "string",
"enum": [
"fs:default"
@@ -1373,6 +1374,13 @@
"fs:allow-write-text-file"
]
},
{
"description": "fs:create-app-specific-dirs -> This permissions allows to create the application specific directories.\n",
"type": "string",
"enum": [
"fs:create-app-specific-dirs"
]
},
{
"description": "fs:deny-copy-file -> Denies the copy_file command without any pre-configured scope.",
"type": "string",
@@ -1562,6 +1570,13 @@
"fs:read-all"
]
},
{
"description": "fs:read-app-specific-dirs-recursive -> This permission allows recursive read functionality on the application\nspecific base directories. \n",
"type": "string",
"enum": [
"fs:read-app-specific-dirs-recursive"
]
},
{
"description": "fs:read-dirs -> This enables directory read and file metadata related commands without any pre-configured accessible paths.",
"type": "string",
@@ -1605,7 +1620,7 @@
]
},
{
"description": "fs:scope-app-recursive -> This scope recursive access to the complete `$APP` folder, including sub directories and files.",
"description": "fs:scope-app-recursive -> This scope permits recursive access to the complete `$APP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-app-recursive"
@@ -1626,7 +1641,7 @@
]
},
{
"description": "fs:scope-appcache-recursive -> This scope recursive access to the complete `$APPCACHE` folder, including sub directories and files.",
"description": "fs:scope-appcache-recursive -> This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appcache-recursive"
@@ -1647,7 +1662,7 @@
]
},
{
"description": "fs:scope-appconfig-recursive -> This scope recursive access to the complete `$APPCONFIG` folder, including sub directories and files.",
"description": "fs:scope-appconfig-recursive -> This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appconfig-recursive"
@@ -1668,7 +1683,7 @@
]
},
{
"description": "fs:scope-appdata-recursive -> This scope recursive access to the complete `$APPDATA` folder, including sub directories and files.",
"description": "fs:scope-appdata-recursive -> This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appdata-recursive"
@@ -1689,7 +1704,7 @@
]
},
{
"description": "fs:scope-applocaldata-recursive -> This scope recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.",
"description": "fs:scope-applocaldata-recursive -> This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-applocaldata-recursive"
@@ -1710,7 +1725,7 @@
]
},
{
"description": "fs:scope-applog-recursive -> This scope recursive access to the complete `$APPLOG` folder, including sub directories and files.",
"description": "fs:scope-applog-recursive -> This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-applog-recursive"
@@ -1731,7 +1746,7 @@
]
},
{
"description": "fs:scope-audio-recursive -> This scope recursive access to the complete `$AUDIO` folder, including sub directories and files.",
"description": "fs:scope-audio-recursive -> This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-audio-recursive"
@@ -1752,7 +1767,7 @@
]
},
{
"description": "fs:scope-cache-recursive -> This scope recursive access to the complete `$CACHE` folder, including sub directories and files.",
"description": "fs:scope-cache-recursive -> This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-cache-recursive"
@@ -1773,7 +1788,7 @@
]
},
{
"description": "fs:scope-config-recursive -> This scope recursive access to the complete `$CONFIG` folder, including sub directories and files.",
"description": "fs:scope-config-recursive -> This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-config-recursive"
@@ -1794,7 +1809,7 @@
]
},
{
"description": "fs:scope-data-recursive -> This scope recursive access to the complete `$DATA` folder, including sub directories and files.",
"description": "fs:scope-data-recursive -> This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-data-recursive"
@@ -1815,7 +1830,7 @@
]
},
{
"description": "fs:scope-desktop-recursive -> This scope recursive access to the complete `$DESKTOP` folder, including sub directories and files.",
"description": "fs:scope-desktop-recursive -> This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-desktop-recursive"
@@ -1836,7 +1851,7 @@
]
},
{
"description": "fs:scope-document-recursive -> This scope recursive access to the complete `$DOCUMENT` folder, including sub directories and files.",
"description": "fs:scope-document-recursive -> This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-document-recursive"
@@ -1857,7 +1872,7 @@
]
},
{
"description": "fs:scope-download-recursive -> This scope recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.",
"description": "fs:scope-download-recursive -> This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-download-recursive"
@@ -1878,7 +1893,7 @@
]
},
{
"description": "fs:scope-exe-recursive -> This scope recursive access to the complete `$EXE` folder, including sub directories and files.",
"description": "fs:scope-exe-recursive -> This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-exe-recursive"
@@ -1899,7 +1914,7 @@
]
},
{
"description": "fs:scope-font-recursive -> This scope recursive access to the complete `$FONT` folder, including sub directories and files.",
"description": "fs:scope-font-recursive -> This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-font-recursive"
@@ -1920,7 +1935,7 @@
]
},
{
"description": "fs:scope-home-recursive -> This scope recursive access to the complete `$HOME` folder, including sub directories and files.",
"description": "fs:scope-home-recursive -> This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-home-recursive"
@@ -1941,7 +1956,7 @@
]
},
{
"description": "fs:scope-localdata-recursive -> This scope recursive access to the complete `$LOCALDATA` folder, including sub directories and files.",
"description": "fs:scope-localdata-recursive -> This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-localdata-recursive"
@@ -1962,7 +1977,7 @@
]
},
{
"description": "fs:scope-log-recursive -> This scope recursive access to the complete `$LOG` folder, including sub directories and files.",
"description": "fs:scope-log-recursive -> This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-log-recursive"
@@ -1983,7 +1998,7 @@
]
},
{
"description": "fs:scope-picture-recursive -> This scope recursive access to the complete `$PICTURE` folder, including sub directories and files.",
"description": "fs:scope-picture-recursive -> This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-picture-recursive"
@@ -2004,7 +2019,7 @@
]
},
{
"description": "fs:scope-public-recursive -> This scope recursive access to the complete `$PUBLIC` folder, including sub directories and files.",
"description": "fs:scope-public-recursive -> This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-public-recursive"
@@ -2025,7 +2040,7 @@
]
},
{
"description": "fs:scope-resource-recursive -> This scope recursive access to the complete `$RESOURCE` folder, including sub directories and files.",
"description": "fs:scope-resource-recursive -> This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-resource-recursive"
@@ -2046,7 +2061,7 @@
]
},
{
"description": "fs:scope-runtime-recursive -> This scope recursive access to the complete `$RUNTIME` folder, including sub directories and files.",
"description": "fs:scope-runtime-recursive -> This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-runtime-recursive"
@@ -2067,7 +2082,7 @@
]
},
{
"description": "fs:scope-temp-recursive -> This scope recursive access to the complete `$TEMP` folder, including sub directories and files.",
"description": "fs:scope-temp-recursive -> This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-temp-recursive"
@@ -2088,7 +2103,7 @@
]
},
{
"description": "fs:scope-template-recursive -> This scope recursive access to the complete `$TEMPLATE` folder, including sub directories and files.",
"description": "fs:scope-template-recursive -> This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-template-recursive"
@@ -2109,7 +2124,7 @@
]
},
{
"description": "fs:scope-video-recursive -> This scope recursive access to the complete `$VIDEO` folder, including sub directories and files.",
"description": "fs:scope-video-recursive -> This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-video-recursive"
@@ -2190,7 +2205,7 @@
"identifier": {
"oneOf": [
{
"description": "http:default -> Allows all fetch operations",
"description": "http:default -> This permission set configures what kind of\nfetch operations are available from the http plugin.\n\nThis enables all fetch operations but does not\nallow explicitly any origins to be fetched. This needs to\nbe manually configured before usage.\n\n#### Granted Permissions\n\nAll fetch operations are enabled.\n\n",
"type": "string",
"enum": [
"http:default"
@@ -2313,6 +2328,7 @@
"identifier": {
"oneOf": [
{
"description": "shell:default -> This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n",
"type": "string",
"enum": [
"shell:default"
@@ -2489,6 +2505,13 @@
"app:allow-app-show"
]
},
{
"description": "app:allow-default-window-icon -> Enables the default_window_icon command without any pre-configured scope.",
"type": "string",
"enum": [
"app:allow-default-window-icon"
]
},
{
"description": "app:allow-name -> Enables the name command without any pre-configured scope.",
"type": "string",
@@ -2524,6 +2547,13 @@
"app:deny-app-show"
]
},
{
"description": "app:deny-default-window-icon -> Denies the default_window_icon command without any pre-configured scope.",
"type": "string",
"enum": [
"app:deny-default-window-icon"
]
},
{
"description": "app:deny-name -> Denies the name command without any pre-configured scope.",
"type": "string",
@@ -2546,6 +2576,7 @@
]
},
{
"description": "clipboard-manager:default -> No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n",
"type": "string",
"enum": [
"clipboard-manager:default"
@@ -2656,6 +2687,7 @@
]
},
{
"description": "dialog:default -> This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n",
"type": "string",
"enum": [
"dialog:default"
@@ -3852,7 +3884,7 @@
]
},
{
"description": "fs:default -> # Tauri `fs` default permissions\n\nThis configuration file defines the default permissions granted\nto the filesystem.\n\n### Granted Permissions\n\nThis default permission set enables all read-related commands and\nallows access to the `$APP` folder and sub directories created in it.\nThe location of the `$APP` folder depends on the operating system,\nwhere the application is run.\n\nIn general the `$APP` folder needs to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\n### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n",
"description": "fs:default -> This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n",
"type": "string",
"enum": [
"fs:default"
@@ -4026,6 +4058,13 @@
"fs:allow-write-text-file"
]
},
{
"description": "fs:create-app-specific-dirs -> This permissions allows to create the application specific directories.\n",
"type": "string",
"enum": [
"fs:create-app-specific-dirs"
]
},
{
"description": "fs:deny-copy-file -> Denies the copy_file command without any pre-configured scope.",
"type": "string",
@@ -4215,6 +4254,13 @@
"fs:read-all"
]
},
{
"description": "fs:read-app-specific-dirs-recursive -> This permission allows recursive read functionality on the application\nspecific base directories. \n",
"type": "string",
"enum": [
"fs:read-app-specific-dirs-recursive"
]
},
{
"description": "fs:read-dirs -> This enables directory read and file metadata related commands without any pre-configured accessible paths.",
"type": "string",
@@ -4258,7 +4304,7 @@
]
},
{
"description": "fs:scope-app-recursive -> This scope recursive access to the complete `$APP` folder, including sub directories and files.",
"description": "fs:scope-app-recursive -> This scope permits recursive access to the complete `$APP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-app-recursive"
@@ -4279,7 +4325,7 @@
]
},
{
"description": "fs:scope-appcache-recursive -> This scope recursive access to the complete `$APPCACHE` folder, including sub directories and files.",
"description": "fs:scope-appcache-recursive -> This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appcache-recursive"
@@ -4300,7 +4346,7 @@
]
},
{
"description": "fs:scope-appconfig-recursive -> This scope recursive access to the complete `$APPCONFIG` folder, including sub directories and files.",
"description": "fs:scope-appconfig-recursive -> This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appconfig-recursive"
@@ -4321,7 +4367,7 @@
]
},
{
"description": "fs:scope-appdata-recursive -> This scope recursive access to the complete `$APPDATA` folder, including sub directories and files.",
"description": "fs:scope-appdata-recursive -> This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appdata-recursive"
@@ -4342,7 +4388,7 @@
]
},
{
"description": "fs:scope-applocaldata-recursive -> This scope recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.",
"description": "fs:scope-applocaldata-recursive -> This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-applocaldata-recursive"
@@ -4363,7 +4409,7 @@
]
},
{
"description": "fs:scope-applog-recursive -> This scope recursive access to the complete `$APPLOG` folder, including sub directories and files.",
"description": "fs:scope-applog-recursive -> This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-applog-recursive"
@@ -4384,7 +4430,7 @@
]
},
{
"description": "fs:scope-audio-recursive -> This scope recursive access to the complete `$AUDIO` folder, including sub directories and files.",
"description": "fs:scope-audio-recursive -> This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-audio-recursive"
@@ -4405,7 +4451,7 @@
]
},
{
"description": "fs:scope-cache-recursive -> This scope recursive access to the complete `$CACHE` folder, including sub directories and files.",
"description": "fs:scope-cache-recursive -> This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-cache-recursive"
@@ -4426,7 +4472,7 @@
]
},
{
"description": "fs:scope-config-recursive -> This scope recursive access to the complete `$CONFIG` folder, including sub directories and files.",
"description": "fs:scope-config-recursive -> This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-config-recursive"
@@ -4447,7 +4493,7 @@
]
},
{
"description": "fs:scope-data-recursive -> This scope recursive access to the complete `$DATA` folder, including sub directories and files.",
"description": "fs:scope-data-recursive -> This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-data-recursive"
@@ -4468,7 +4514,7 @@
]
},
{
"description": "fs:scope-desktop-recursive -> This scope recursive access to the complete `$DESKTOP` folder, including sub directories and files.",
"description": "fs:scope-desktop-recursive -> This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-desktop-recursive"
@@ -4489,7 +4535,7 @@
]
},
{
"description": "fs:scope-document-recursive -> This scope recursive access to the complete `$DOCUMENT` folder, including sub directories and files.",
"description": "fs:scope-document-recursive -> This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-document-recursive"
@@ -4510,7 +4556,7 @@
]
},
{
"description": "fs:scope-download-recursive -> This scope recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.",
"description": "fs:scope-download-recursive -> This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-download-recursive"
@@ -4531,7 +4577,7 @@
]
},
{
"description": "fs:scope-exe-recursive -> This scope recursive access to the complete `$EXE` folder, including sub directories and files.",
"description": "fs:scope-exe-recursive -> This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-exe-recursive"
@@ -4552,7 +4598,7 @@
]
},
{
"description": "fs:scope-font-recursive -> This scope recursive access to the complete `$FONT` folder, including sub directories and files.",
"description": "fs:scope-font-recursive -> This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-font-recursive"
@@ -4573,7 +4619,7 @@
]
},
{
"description": "fs:scope-home-recursive -> This scope recursive access to the complete `$HOME` folder, including sub directories and files.",
"description": "fs:scope-home-recursive -> This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-home-recursive"
@@ -4594,7 +4640,7 @@
]
},
{
"description": "fs:scope-localdata-recursive -> This scope recursive access to the complete `$LOCALDATA` folder, including sub directories and files.",
"description": "fs:scope-localdata-recursive -> This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-localdata-recursive"
@@ -4615,7 +4661,7 @@
]
},
{
"description": "fs:scope-log-recursive -> This scope recursive access to the complete `$LOG` folder, including sub directories and files.",
"description": "fs:scope-log-recursive -> This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-log-recursive"
@@ -4636,7 +4682,7 @@
]
},
{
"description": "fs:scope-picture-recursive -> This scope recursive access to the complete `$PICTURE` folder, including sub directories and files.",
"description": "fs:scope-picture-recursive -> This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-picture-recursive"
@@ -4657,7 +4703,7 @@
]
},
{
"description": "fs:scope-public-recursive -> This scope recursive access to the complete `$PUBLIC` folder, including sub directories and files.",
"description": "fs:scope-public-recursive -> This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-public-recursive"
@@ -4678,7 +4724,7 @@
]
},
{
"description": "fs:scope-resource-recursive -> This scope recursive access to the complete `$RESOURCE` folder, including sub directories and files.",
"description": "fs:scope-resource-recursive -> This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-resource-recursive"
@@ -4699,7 +4745,7 @@
]
},
{
"description": "fs:scope-runtime-recursive -> This scope recursive access to the complete `$RUNTIME` folder, including sub directories and files.",
"description": "fs:scope-runtime-recursive -> This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-runtime-recursive"
@@ -4720,7 +4766,7 @@
]
},
{
"description": "fs:scope-temp-recursive -> This scope recursive access to the complete `$TEMP` folder, including sub directories and files.",
"description": "fs:scope-temp-recursive -> This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-temp-recursive"
@@ -4741,7 +4787,7 @@
]
},
{
"description": "fs:scope-template-recursive -> This scope recursive access to the complete `$TEMPLATE` folder, including sub directories and files.",
"description": "fs:scope-template-recursive -> This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-template-recursive"
@@ -4762,7 +4808,7 @@
]
},
{
"description": "fs:scope-video-recursive -> This scope recursive access to the complete `$VIDEO` folder, including sub directories and files.",
"description": "fs:scope-video-recursive -> This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-video-recursive"
@@ -4783,7 +4829,7 @@
]
},
{
"description": "http:default -> Allows all fetch operations",
"description": "http:default -> This permission set configures what kind of\nfetch operations are available from the http plugin.\n\nThis enables all fetch operations but does not\nallow explicitly any origins to be fetched. This needs to\nbe manually configured before usage.\n\n#### Granted Permissions\n\nAll fetch operations are enabled.\n\n",
"type": "string",
"enum": [
"http:default"
@@ -5238,12 +5284,61 @@
]
},
{
"description": "notification:default -> Allows requesting permission, checking permission state and sending notifications",
"description": "notification:default -> This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n",
"type": "string",
"enum": [
"notification:default"
]
},
{
"description": "notification:allow-batch -> Enables the batch command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-batch"
]
},
{
"description": "notification:allow-cancel -> Enables the cancel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-cancel"
]
},
{
"description": "notification:allow-check-permissions -> Enables the check_permissions command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-check-permissions"
]
},
{
"description": "notification:allow-create-channel -> Enables the create_channel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-create-channel"
]
},
{
"description": "notification:allow-delete-channel -> Enables the delete_channel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-delete-channel"
]
},
{
"description": "notification:allow-get-active -> Enables the get_active command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-get-active"
]
},
{
"description": "notification:allow-get-pending -> Enables the get_pending command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-get-pending"
]
},
{
"description": "notification:allow-is-permission-granted -> Enables the is_permission_granted command without any pre-configured scope.",
"type": "string",
@@ -5251,6 +5346,13 @@
"notification:allow-is-permission-granted"
]
},
{
"description": "notification:allow-list-channels -> Enables the list_channels command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-list-channels"
]
},
{
"description": "notification:allow-notify -> Enables the notify command without any pre-configured scope.",
"type": "string",
@@ -5258,6 +5360,34 @@
"notification:allow-notify"
]
},
{
"description": "notification:allow-permission-state -> Enables the permission_state command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-permission-state"
]
},
{
"description": "notification:allow-register-action-types -> Enables the register_action_types command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-register-action-types"
]
},
{
"description": "notification:allow-register-listener -> Enables the register_listener command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-register-listener"
]
},
{
"description": "notification:allow-remove-active -> Enables the remove_active command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-remove-active"
]
},
{
"description": "notification:allow-request-permission -> Enables the request_permission command without any pre-configured scope.",
"type": "string",
@@ -5265,6 +5395,62 @@
"notification:allow-request-permission"
]
},
{
"description": "notification:allow-show -> Enables the show command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-show"
]
},
{
"description": "notification:deny-batch -> Denies the batch command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-batch"
]
},
{
"description": "notification:deny-cancel -> Denies the cancel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-cancel"
]
},
{
"description": "notification:deny-check-permissions -> Denies the check_permissions command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-check-permissions"
]
},
{
"description": "notification:deny-create-channel -> Denies the create_channel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-create-channel"
]
},
{
"description": "notification:deny-delete-channel -> Denies the delete_channel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-delete-channel"
]
},
{
"description": "notification:deny-get-active -> Denies the get_active command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-get-active"
]
},
{
"description": "notification:deny-get-pending -> Denies the get_pending command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-get-pending"
]
},
{
"description": "notification:deny-is-permission-granted -> Denies the is_permission_granted command without any pre-configured scope.",
"type": "string",
@@ -5272,6 +5458,13 @@
"notification:deny-is-permission-granted"
]
},
{
"description": "notification:deny-list-channels -> Denies the list_channels command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-list-channels"
]
},
{
"description": "notification:deny-notify -> Denies the notify command without any pre-configured scope.",
"type": "string",
@@ -5279,6 +5472,34 @@
"notification:deny-notify"
]
},
{
"description": "notification:deny-permission-state -> Denies the permission_state command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-permission-state"
]
},
{
"description": "notification:deny-register-action-types -> Denies the register_action_types command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-register-action-types"
]
},
{
"description": "notification:deny-register-listener -> Denies the register_listener command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-register-listener"
]
},
{
"description": "notification:deny-remove-active -> Denies the remove_active command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-remove-active"
]
},
{
"description": "notification:deny-request-permission -> Denies the request_permission command without any pre-configured scope.",
"type": "string",
@@ -5287,6 +5508,14 @@
]
},
{
"description": "notification:deny-show -> Denies the show command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-show"
]
},
{
"description": "os:default -> This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n",
"type": "string",
"enum": [
"os:default"
@@ -5524,6 +5753,7 @@
]
},
{
"description": "process:default -> This permission set configures which\nprocess feeatures are by default exposed.\n\n#### Granted Permissions\n\nThis enables to quit via `allow-exit` and restart via `allow-restart`\nthe application.\n",
"type": "string",
"enum": [
"process:default"
@@ -5579,6 +5809,7 @@
]
},
{
"description": "shell:default -> This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n",
"type": "string",
"enum": [
"shell:default"
@@ -5850,7 +6081,7 @@
]
},
{
"description": "updater:default -> Allows checking for new updates and installing them",
"description": "updater:default -> This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n",
"type": "string",
"enum": [
"updater:default"
@@ -5913,6 +6144,7 @@
]
},
{
"description": "upload:default -> This permission set configures what kind of\noperations are available from the upload plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n",
"type": "string",
"enum": [
"upload:default"
@@ -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"
]
}
]
},
+361 -66
View File
@@ -37,7 +37,7 @@
],
"definitions": {
"Capability": {
"description": "a grouping and boundary mechanism developers can use to separate windows or plugins functionality from each other at runtime.\n\nIf a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create trust groups and reduce impact of vulnerabilities in certain plugins or windows. Windows can be added to a capability by exact name or glob patterns like *, admin-* or main-window.",
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows fine grained access to the Tauri core, application, or plugin commands. If a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"path:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, \"platforms\": [\"macOS\",\"windows\"] } ```",
"type": "object",
"required": [
"identifier",
@@ -45,16 +45,16 @@
],
"properties": {
"identifier": {
"description": "Identifier of the capability.",
"description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`",
"type": "string"
},
"description": {
"description": "Description of the capability.",
"description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.",
"default": "",
"type": "string"
},
"remote": {
"description": "Configure remote URLs that can use the capability permissions.",
"description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```",
"anyOf": [
{
"$ref": "#/definitions/CapabilityRemote"
@@ -70,28 +70,29 @@
"type": "boolean"
},
"windows": {
"description": "List of windows that uses this capability. Can be a glob pattern.\n\nOn multiwebview windows, prefer [`Self::webviews`] for a fine grained access control.",
"description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nOn multiwebview windows, prefer [`Self::webviews`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`",
"type": "array",
"items": {
"type": "string"
}
},
"webviews": {
"description": "List of webviews that uses this capability. Can be a glob pattern.\n\nThis is only required when using on multiwebview contexts, by default all child webviews of a window that matches [`Self::windows`] are linked.",
"description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThis is only required when using on multiwebview contexts, by default all child webviews of a window that matches [`Self::windows`] are linked.\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`",
"type": "array",
"items": {
"type": "string"
}
},
"permissions": {
"description": "List of permissions attached to this capability. Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.",
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"path:default\", \"event:default\", \"window:default\", \"app:default\", \"image:default\", \"resources:default\", \"menu:default\", \"tray:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ```",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionEntry"
}
},
"uniqueItems": true
},
"platforms": {
"description": "Target platforms this capability applies. By default all platforms are affected by this capability.",
"description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`",
"type": [
"array",
"null"
@@ -110,7 +111,7 @@
],
"properties": {
"urls": {
"description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n# Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api",
"description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api",
"type": "array",
"items": {
"type": "string"
@@ -142,7 +143,7 @@
"identifier": {
"oneOf": [
{
"description": "fs:default -> # Tauri `fs` default permissions\n\nThis configuration file defines the default permissions granted\nto the filesystem.\n\n### Granted Permissions\n\nThis default permission set enables all read-related commands and\nallows access to the `$APP` folder and sub directories created in it.\nThe location of the `$APP` folder depends on the operating system,\nwhere the application is run.\n\nIn general the `$APP` folder needs to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\n### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n",
"description": "fs:default -> This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n",
"type": "string",
"enum": [
"fs:default"
@@ -1373,6 +1374,13 @@
"fs:allow-write-text-file"
]
},
{
"description": "fs:create-app-specific-dirs -> This permissions allows to create the application specific directories.\n",
"type": "string",
"enum": [
"fs:create-app-specific-dirs"
]
},
{
"description": "fs:deny-copy-file -> Denies the copy_file command without any pre-configured scope.",
"type": "string",
@@ -1562,6 +1570,13 @@
"fs:read-all"
]
},
{
"description": "fs:read-app-specific-dirs-recursive -> This permission allows recursive read functionality on the application\nspecific base directories. \n",
"type": "string",
"enum": [
"fs:read-app-specific-dirs-recursive"
]
},
{
"description": "fs:read-dirs -> This enables directory read and file metadata related commands without any pre-configured accessible paths.",
"type": "string",
@@ -1605,7 +1620,7 @@
]
},
{
"description": "fs:scope-app-recursive -> This scope recursive access to the complete `$APP` folder, including sub directories and files.",
"description": "fs:scope-app-recursive -> This scope permits recursive access to the complete `$APP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-app-recursive"
@@ -1626,7 +1641,7 @@
]
},
{
"description": "fs:scope-appcache-recursive -> This scope recursive access to the complete `$APPCACHE` folder, including sub directories and files.",
"description": "fs:scope-appcache-recursive -> This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appcache-recursive"
@@ -1647,7 +1662,7 @@
]
},
{
"description": "fs:scope-appconfig-recursive -> This scope recursive access to the complete `$APPCONFIG` folder, including sub directories and files.",
"description": "fs:scope-appconfig-recursive -> This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appconfig-recursive"
@@ -1668,7 +1683,7 @@
]
},
{
"description": "fs:scope-appdata-recursive -> This scope recursive access to the complete `$APPDATA` folder, including sub directories and files.",
"description": "fs:scope-appdata-recursive -> This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appdata-recursive"
@@ -1689,7 +1704,7 @@
]
},
{
"description": "fs:scope-applocaldata-recursive -> This scope recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.",
"description": "fs:scope-applocaldata-recursive -> This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-applocaldata-recursive"
@@ -1710,7 +1725,7 @@
]
},
{
"description": "fs:scope-applog-recursive -> This scope recursive access to the complete `$APPLOG` folder, including sub directories and files.",
"description": "fs:scope-applog-recursive -> This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-applog-recursive"
@@ -1731,7 +1746,7 @@
]
},
{
"description": "fs:scope-audio-recursive -> This scope recursive access to the complete `$AUDIO` folder, including sub directories and files.",
"description": "fs:scope-audio-recursive -> This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-audio-recursive"
@@ -1752,7 +1767,7 @@
]
},
{
"description": "fs:scope-cache-recursive -> This scope recursive access to the complete `$CACHE` folder, including sub directories and files.",
"description": "fs:scope-cache-recursive -> This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-cache-recursive"
@@ -1773,7 +1788,7 @@
]
},
{
"description": "fs:scope-config-recursive -> This scope recursive access to the complete `$CONFIG` folder, including sub directories and files.",
"description": "fs:scope-config-recursive -> This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-config-recursive"
@@ -1794,7 +1809,7 @@
]
},
{
"description": "fs:scope-data-recursive -> This scope recursive access to the complete `$DATA` folder, including sub directories and files.",
"description": "fs:scope-data-recursive -> This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-data-recursive"
@@ -1815,7 +1830,7 @@
]
},
{
"description": "fs:scope-desktop-recursive -> This scope recursive access to the complete `$DESKTOP` folder, including sub directories and files.",
"description": "fs:scope-desktop-recursive -> This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-desktop-recursive"
@@ -1836,7 +1851,7 @@
]
},
{
"description": "fs:scope-document-recursive -> This scope recursive access to the complete `$DOCUMENT` folder, including sub directories and files.",
"description": "fs:scope-document-recursive -> This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-document-recursive"
@@ -1857,7 +1872,7 @@
]
},
{
"description": "fs:scope-download-recursive -> This scope recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.",
"description": "fs:scope-download-recursive -> This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-download-recursive"
@@ -1878,7 +1893,7 @@
]
},
{
"description": "fs:scope-exe-recursive -> This scope recursive access to the complete `$EXE` folder, including sub directories and files.",
"description": "fs:scope-exe-recursive -> This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-exe-recursive"
@@ -1899,7 +1914,7 @@
]
},
{
"description": "fs:scope-font-recursive -> This scope recursive access to the complete `$FONT` folder, including sub directories and files.",
"description": "fs:scope-font-recursive -> This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-font-recursive"
@@ -1920,7 +1935,7 @@
]
},
{
"description": "fs:scope-home-recursive -> This scope recursive access to the complete `$HOME` folder, including sub directories and files.",
"description": "fs:scope-home-recursive -> This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-home-recursive"
@@ -1941,7 +1956,7 @@
]
},
{
"description": "fs:scope-localdata-recursive -> This scope recursive access to the complete `$LOCALDATA` folder, including sub directories and files.",
"description": "fs:scope-localdata-recursive -> This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-localdata-recursive"
@@ -1962,7 +1977,7 @@
]
},
{
"description": "fs:scope-log-recursive -> This scope recursive access to the complete `$LOG` folder, including sub directories and files.",
"description": "fs:scope-log-recursive -> This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-log-recursive"
@@ -1983,7 +1998,7 @@
]
},
{
"description": "fs:scope-picture-recursive -> This scope recursive access to the complete `$PICTURE` folder, including sub directories and files.",
"description": "fs:scope-picture-recursive -> This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-picture-recursive"
@@ -2004,7 +2019,7 @@
]
},
{
"description": "fs:scope-public-recursive -> This scope recursive access to the complete `$PUBLIC` folder, including sub directories and files.",
"description": "fs:scope-public-recursive -> This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-public-recursive"
@@ -2025,7 +2040,7 @@
]
},
{
"description": "fs:scope-resource-recursive -> This scope recursive access to the complete `$RESOURCE` folder, including sub directories and files.",
"description": "fs:scope-resource-recursive -> This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-resource-recursive"
@@ -2046,7 +2061,7 @@
]
},
{
"description": "fs:scope-runtime-recursive -> This scope recursive access to the complete `$RUNTIME` folder, including sub directories and files.",
"description": "fs:scope-runtime-recursive -> This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-runtime-recursive"
@@ -2067,7 +2082,7 @@
]
},
{
"description": "fs:scope-temp-recursive -> This scope recursive access to the complete `$TEMP` folder, including sub directories and files.",
"description": "fs:scope-temp-recursive -> This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-temp-recursive"
@@ -2088,7 +2103,7 @@
]
},
{
"description": "fs:scope-template-recursive -> This scope recursive access to the complete `$TEMPLATE` folder, including sub directories and files.",
"description": "fs:scope-template-recursive -> This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-template-recursive"
@@ -2109,7 +2124,7 @@
]
},
{
"description": "fs:scope-video-recursive -> This scope recursive access to the complete `$VIDEO` folder, including sub directories and files.",
"description": "fs:scope-video-recursive -> This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-video-recursive"
@@ -2190,7 +2205,7 @@
"identifier": {
"oneOf": [
{
"description": "http:default -> Allows all fetch operations",
"description": "http:default -> This permission set configures what kind of\nfetch operations are available from the http plugin.\n\nThis enables all fetch operations but does not\nallow explicitly any origins to be fetched. This needs to\nbe manually configured before usage.\n\n#### Granted Permissions\n\nAll fetch operations are enabled.\n\n",
"type": "string",
"enum": [
"http:default"
@@ -2313,6 +2328,7 @@
"identifier": {
"oneOf": [
{
"description": "shell:default -> This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n",
"type": "string",
"enum": [
"shell:default"
@@ -2489,6 +2505,13 @@
"app:allow-app-show"
]
},
{
"description": "app:allow-default-window-icon -> Enables the default_window_icon command without any pre-configured scope.",
"type": "string",
"enum": [
"app:allow-default-window-icon"
]
},
{
"description": "app:allow-name -> Enables the name command without any pre-configured scope.",
"type": "string",
@@ -2524,6 +2547,13 @@
"app:deny-app-show"
]
},
{
"description": "app:deny-default-window-icon -> Denies the default_window_icon command without any pre-configured scope.",
"type": "string",
"enum": [
"app:deny-default-window-icon"
]
},
{
"description": "app:deny-name -> Denies the name command without any pre-configured scope.",
"type": "string",
@@ -2546,6 +2576,7 @@
]
},
{
"description": "clipboard-manager:default -> No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n",
"type": "string",
"enum": [
"clipboard-manager:default"
@@ -2656,6 +2687,7 @@
]
},
{
"description": "dialog:default -> This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n",
"type": "string",
"enum": [
"dialog:default"
@@ -3852,7 +3884,7 @@
]
},
{
"description": "fs:default -> # Tauri `fs` default permissions\n\nThis configuration file defines the default permissions granted\nto the filesystem.\n\n### Granted Permissions\n\nThis default permission set enables all read-related commands and\nallows access to the `$APP` folder and sub directories created in it.\nThe location of the `$APP` folder depends on the operating system,\nwhere the application is run.\n\nIn general the `$APP` folder needs to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\n### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n",
"description": "fs:default -> This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n",
"type": "string",
"enum": [
"fs:default"
@@ -4026,6 +4058,13 @@
"fs:allow-write-text-file"
]
},
{
"description": "fs:create-app-specific-dirs -> This permissions allows to create the application specific directories.\n",
"type": "string",
"enum": [
"fs:create-app-specific-dirs"
]
},
{
"description": "fs:deny-copy-file -> Denies the copy_file command without any pre-configured scope.",
"type": "string",
@@ -4215,6 +4254,13 @@
"fs:read-all"
]
},
{
"description": "fs:read-app-specific-dirs-recursive -> This permission allows recursive read functionality on the application\nspecific base directories. \n",
"type": "string",
"enum": [
"fs:read-app-specific-dirs-recursive"
]
},
{
"description": "fs:read-dirs -> This enables directory read and file metadata related commands without any pre-configured accessible paths.",
"type": "string",
@@ -4258,7 +4304,7 @@
]
},
{
"description": "fs:scope-app-recursive -> This scope recursive access to the complete `$APP` folder, including sub directories and files.",
"description": "fs:scope-app-recursive -> This scope permits recursive access to the complete `$APP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-app-recursive"
@@ -4279,7 +4325,7 @@
]
},
{
"description": "fs:scope-appcache-recursive -> This scope recursive access to the complete `$APPCACHE` folder, including sub directories and files.",
"description": "fs:scope-appcache-recursive -> This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appcache-recursive"
@@ -4300,7 +4346,7 @@
]
},
{
"description": "fs:scope-appconfig-recursive -> This scope recursive access to the complete `$APPCONFIG` folder, including sub directories and files.",
"description": "fs:scope-appconfig-recursive -> This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appconfig-recursive"
@@ -4321,7 +4367,7 @@
]
},
{
"description": "fs:scope-appdata-recursive -> This scope recursive access to the complete `$APPDATA` folder, including sub directories and files.",
"description": "fs:scope-appdata-recursive -> This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-appdata-recursive"
@@ -4342,7 +4388,7 @@
]
},
{
"description": "fs:scope-applocaldata-recursive -> This scope recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.",
"description": "fs:scope-applocaldata-recursive -> This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-applocaldata-recursive"
@@ -4363,7 +4409,7 @@
]
},
{
"description": "fs:scope-applog-recursive -> This scope recursive access to the complete `$APPLOG` folder, including sub directories and files.",
"description": "fs:scope-applog-recursive -> This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-applog-recursive"
@@ -4384,7 +4430,7 @@
]
},
{
"description": "fs:scope-audio-recursive -> This scope recursive access to the complete `$AUDIO` folder, including sub directories and files.",
"description": "fs:scope-audio-recursive -> This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-audio-recursive"
@@ -4405,7 +4451,7 @@
]
},
{
"description": "fs:scope-cache-recursive -> This scope recursive access to the complete `$CACHE` folder, including sub directories and files.",
"description": "fs:scope-cache-recursive -> This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-cache-recursive"
@@ -4426,7 +4472,7 @@
]
},
{
"description": "fs:scope-config-recursive -> This scope recursive access to the complete `$CONFIG` folder, including sub directories and files.",
"description": "fs:scope-config-recursive -> This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-config-recursive"
@@ -4447,7 +4493,7 @@
]
},
{
"description": "fs:scope-data-recursive -> This scope recursive access to the complete `$DATA` folder, including sub directories and files.",
"description": "fs:scope-data-recursive -> This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-data-recursive"
@@ -4468,7 +4514,7 @@
]
},
{
"description": "fs:scope-desktop-recursive -> This scope recursive access to the complete `$DESKTOP` folder, including sub directories and files.",
"description": "fs:scope-desktop-recursive -> This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-desktop-recursive"
@@ -4489,7 +4535,7 @@
]
},
{
"description": "fs:scope-document-recursive -> This scope recursive access to the complete `$DOCUMENT` folder, including sub directories and files.",
"description": "fs:scope-document-recursive -> This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-document-recursive"
@@ -4510,7 +4556,7 @@
]
},
{
"description": "fs:scope-download-recursive -> This scope recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.",
"description": "fs:scope-download-recursive -> This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-download-recursive"
@@ -4531,7 +4577,7 @@
]
},
{
"description": "fs:scope-exe-recursive -> This scope recursive access to the complete `$EXE` folder, including sub directories and files.",
"description": "fs:scope-exe-recursive -> This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-exe-recursive"
@@ -4552,7 +4598,7 @@
]
},
{
"description": "fs:scope-font-recursive -> This scope recursive access to the complete `$FONT` folder, including sub directories and files.",
"description": "fs:scope-font-recursive -> This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-font-recursive"
@@ -4573,7 +4619,7 @@
]
},
{
"description": "fs:scope-home-recursive -> This scope recursive access to the complete `$HOME` folder, including sub directories and files.",
"description": "fs:scope-home-recursive -> This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-home-recursive"
@@ -4594,7 +4640,7 @@
]
},
{
"description": "fs:scope-localdata-recursive -> This scope recursive access to the complete `$LOCALDATA` folder, including sub directories and files.",
"description": "fs:scope-localdata-recursive -> This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-localdata-recursive"
@@ -4615,7 +4661,7 @@
]
},
{
"description": "fs:scope-log-recursive -> This scope recursive access to the complete `$LOG` folder, including sub directories and files.",
"description": "fs:scope-log-recursive -> This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-log-recursive"
@@ -4636,7 +4682,7 @@
]
},
{
"description": "fs:scope-picture-recursive -> This scope recursive access to the complete `$PICTURE` folder, including sub directories and files.",
"description": "fs:scope-picture-recursive -> This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-picture-recursive"
@@ -4657,7 +4703,7 @@
]
},
{
"description": "fs:scope-public-recursive -> This scope recursive access to the complete `$PUBLIC` folder, including sub directories and files.",
"description": "fs:scope-public-recursive -> This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-public-recursive"
@@ -4678,7 +4724,7 @@
]
},
{
"description": "fs:scope-resource-recursive -> This scope recursive access to the complete `$RESOURCE` folder, including sub directories and files.",
"description": "fs:scope-resource-recursive -> This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-resource-recursive"
@@ -4699,7 +4745,7 @@
]
},
{
"description": "fs:scope-runtime-recursive -> This scope recursive access to the complete `$RUNTIME` folder, including sub directories and files.",
"description": "fs:scope-runtime-recursive -> This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-runtime-recursive"
@@ -4720,7 +4766,7 @@
]
},
{
"description": "fs:scope-temp-recursive -> This scope recursive access to the complete `$TEMP` folder, including sub directories and files.",
"description": "fs:scope-temp-recursive -> This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-temp-recursive"
@@ -4741,7 +4787,7 @@
]
},
{
"description": "fs:scope-template-recursive -> This scope recursive access to the complete `$TEMPLATE` folder, including sub directories and files.",
"description": "fs:scope-template-recursive -> This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-template-recursive"
@@ -4762,7 +4808,7 @@
]
},
{
"description": "fs:scope-video-recursive -> This scope recursive access to the complete `$VIDEO` folder, including sub directories and files.",
"description": "fs:scope-video-recursive -> This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.",
"type": "string",
"enum": [
"fs:scope-video-recursive"
@@ -4783,7 +4829,7 @@
]
},
{
"description": "http:default -> Allows all fetch operations",
"description": "http:default -> This permission set configures what kind of\nfetch operations are available from the http plugin.\n\nThis enables all fetch operations but does not\nallow explicitly any origins to be fetched. This needs to\nbe manually configured before usage.\n\n#### Granted Permissions\n\nAll fetch operations are enabled.\n\n",
"type": "string",
"enum": [
"http:default"
@@ -5238,12 +5284,61 @@
]
},
{
"description": "notification:default -> Allows requesting permission, checking permission state and sending notifications",
"description": "notification:default -> This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n",
"type": "string",
"enum": [
"notification:default"
]
},
{
"description": "notification:allow-batch -> Enables the batch command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-batch"
]
},
{
"description": "notification:allow-cancel -> Enables the cancel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-cancel"
]
},
{
"description": "notification:allow-check-permissions -> Enables the check_permissions command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-check-permissions"
]
},
{
"description": "notification:allow-create-channel -> Enables the create_channel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-create-channel"
]
},
{
"description": "notification:allow-delete-channel -> Enables the delete_channel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-delete-channel"
]
},
{
"description": "notification:allow-get-active -> Enables the get_active command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-get-active"
]
},
{
"description": "notification:allow-get-pending -> Enables the get_pending command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-get-pending"
]
},
{
"description": "notification:allow-is-permission-granted -> Enables the is_permission_granted command without any pre-configured scope.",
"type": "string",
@@ -5251,6 +5346,13 @@
"notification:allow-is-permission-granted"
]
},
{
"description": "notification:allow-list-channels -> Enables the list_channels command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-list-channels"
]
},
{
"description": "notification:allow-notify -> Enables the notify command without any pre-configured scope.",
"type": "string",
@@ -5258,6 +5360,34 @@
"notification:allow-notify"
]
},
{
"description": "notification:allow-permission-state -> Enables the permission_state command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-permission-state"
]
},
{
"description": "notification:allow-register-action-types -> Enables the register_action_types command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-register-action-types"
]
},
{
"description": "notification:allow-register-listener -> Enables the register_listener command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-register-listener"
]
},
{
"description": "notification:allow-remove-active -> Enables the remove_active command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-remove-active"
]
},
{
"description": "notification:allow-request-permission -> Enables the request_permission command without any pre-configured scope.",
"type": "string",
@@ -5265,6 +5395,62 @@
"notification:allow-request-permission"
]
},
{
"description": "notification:allow-show -> Enables the show command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:allow-show"
]
},
{
"description": "notification:deny-batch -> Denies the batch command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-batch"
]
},
{
"description": "notification:deny-cancel -> Denies the cancel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-cancel"
]
},
{
"description": "notification:deny-check-permissions -> Denies the check_permissions command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-check-permissions"
]
},
{
"description": "notification:deny-create-channel -> Denies the create_channel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-create-channel"
]
},
{
"description": "notification:deny-delete-channel -> Denies the delete_channel command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-delete-channel"
]
},
{
"description": "notification:deny-get-active -> Denies the get_active command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-get-active"
]
},
{
"description": "notification:deny-get-pending -> Denies the get_pending command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-get-pending"
]
},
{
"description": "notification:deny-is-permission-granted -> Denies the is_permission_granted command without any pre-configured scope.",
"type": "string",
@@ -5272,6 +5458,13 @@
"notification:deny-is-permission-granted"
]
},
{
"description": "notification:deny-list-channels -> Denies the list_channels command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-list-channels"
]
},
{
"description": "notification:deny-notify -> Denies the notify command without any pre-configured scope.",
"type": "string",
@@ -5279,6 +5472,34 @@
"notification:deny-notify"
]
},
{
"description": "notification:deny-permission-state -> Denies the permission_state command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-permission-state"
]
},
{
"description": "notification:deny-register-action-types -> Denies the register_action_types command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-register-action-types"
]
},
{
"description": "notification:deny-register-listener -> Denies the register_listener command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-register-listener"
]
},
{
"description": "notification:deny-remove-active -> Denies the remove_active command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-remove-active"
]
},
{
"description": "notification:deny-request-permission -> Denies the request_permission command without any pre-configured scope.",
"type": "string",
@@ -5287,6 +5508,14 @@
]
},
{
"description": "notification:deny-show -> Denies the show command without any pre-configured scope.",
"type": "string",
"enum": [
"notification:deny-show"
]
},
{
"description": "os:default -> This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n",
"type": "string",
"enum": [
"os:default"
@@ -5524,6 +5753,7 @@
]
},
{
"description": "process:default -> This permission set configures which\nprocess feeatures are by default exposed.\n\n#### Granted Permissions\n\nThis enables to quit via `allow-exit` and restart via `allow-restart`\nthe application.\n",
"type": "string",
"enum": [
"process:default"
@@ -5579,6 +5809,7 @@
]
},
{
"description": "shell:default -> This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n",
"type": "string",
"enum": [
"shell:default"
@@ -5850,7 +6081,7 @@
]
},
{
"description": "updater:default -> Allows checking for new updates and installing them",
"description": "updater:default -> This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n",
"type": "string",
"enum": [
"updater:default"
@@ -5913,6 +6144,7 @@
]
},
{
"description": "upload:default -> This permission set configures what kind of\noperations are available from the upload plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n",
"type": "string",
"enum": [
"upload:default"
@@ -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"
]
}
]
},
+35 -50
View File
@@ -1,52 +1,37 @@
[
{
"label": "lZfXLFgPPR4NNrgjlWDxn",
"name": "Newsfeed",
"content": "/newsfeed",
"logo": "",
"cover": "/newsfeed.png",
"coverRetina": "/newsfeed@2x.png",
"author": "Lume",
"description": "Keep up to date with the people you're following."
},
{
"label": "rRtguZwIpd5G8Wt54OTb7",
"name": "Topic",
"content": "/topic",
"logo": "",
"cover": "/foryou.png",
"coverRetina": "/foryou@2x.png",
"author": "Lume",
"description": "Keep up to date with content based on your interests."
},
{
"label": "fve9fk2fVyFWORPBkjd79",
"name": "Group",
"content": "/group",
"logo": "",
"cover": "/group.png",
"coverRetina": "/group@2x.png",
"author": "Lume",
"description": "Focus feeds for people you like."
},
{
"label": "gxtcIbgD8YNPbeI5o92I8",
"name": "Trending",
"content": "/trending/notes",
"logo": "",
"cover": "/trending.png",
"coverRetina": "/trending@2x.png",
"author": "Lume",
"description": "What is trending on Nostr?."
},
{
"label": "GLFm44za8rhJDP04LMr3M",
"name": "Global",
"content": "/global",
"logo": "",
"cover": "/global.png",
"coverRetina": "/global@2x.png",
"author": "Lume",
"description": "All events from connected relays."
}
{
"label": "lZfXLFgPPR4NNrgjlWDxn",
"name": "Local Feeds",
"content": "/newsfeed",
"cover": "/newsfeed.png",
"coverRetina": "/newsfeed@2x.png"
},
{
"label": "GLFm44za8rhJDP04LMr3M",
"name": "Global Feeds",
"content": "/global",
"cover": "/global.png",
"coverRetina": "/global@2x.png"
},
{
"label": "fve9fk2fVyFWORPBkjd79",
"name": "Group Feeds",
"content": "/group",
"cover": "/group.png",
"coverRetina": "/group@2x.png"
},
{
"label": "rRtguZwIpd5G8Wt54OTb7",
"name": "Topic",
"content": "/topic",
"cover": "/foryou.png",
"coverRetina": "/foryou@2x.png"
},
{
"label": "gxtcIbgD8YNPbeI5o92I8",
"name": "Trending",
"content": "/trending/notes",
"cover": "/trending.png",
"coverRetina": "/trending@2x.png"
}
]
+174
View File
@@ -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