Compare commits

...
22 Commits
Author SHA1 Message Date
reya 6996e30889 chore: update github ci 2024-06-07 11:26:20 +07:00
reya 7ba793fad8 chore: bump version 2024-06-07 10:03:04 +07:00
reya f11f836518 chore: update tray icon 2024-06-07 09:56:57 +07:00
reya 04fe0fcec8 feat: respect the relay hint 2024-06-07 09:07:33 +07:00
799835a629 Notification Panel (#200)
* feat: add tauri nspanel

* feat: add notification panel

* feat: move notification service to backend

* feat: add sync notification job

* feat: enable panel to join all spaces including fullscreen (#203)

* feat: fetch notification

* feat: listen for new notification

* feat: finish panel

---------

Co-authored-by: Victor Aremu <me@victorare.mu>
2024-06-06 14:32:30 +07:00
XIAO YUandGitHub 4e7da4108b feat: Add get user following function (#202)
* feat: Add get user following function

* refactor: Refactor get_following function to use state and string public key

* feat: Update get_following function to use timeout duration

* feat: Fix connect_remote_account function to return remote_npub without conversion

* feat: Refactor get_following function to handle public key parsing errors

* Refactor get_followers function to handle public key parsing errors and use timeout duration
2024-06-05 13:24:32 +07:00
reya 7c7b082b3a fix: memory leak in image component 2024-06-03 07:32:34 +07:00
reya 38d6c51921 feat: use nostrdb for unix and rocksdb for windows 2024-06-02 08:16:59 +07:00
reya 1738cbdd97 chore: upgrade tauri 2024-06-01 14:54:35 +07:00
reya 2e885b76a1 feat: improve text wrap 2024-06-01 08:27:22 +07:00
reya f94680e487 fix: column overlapped after change account 2024-05-31 15:25:47 +07:00
XIAO YUandGitHub c682a58842 chore: Remove unused modules and update metadata.rs (#199) 2024-05-31 12:53:35 +07:00
reya 921cf871ee chore: bump version 2024-05-31 08:54:53 +07:00
reya d5b1593aca feat: improve nostr connect flow 2024-05-31 08:54:17 +07:00
reya 6676b4e2a4 Revert "chore: Update dependencies and add thiserror crate (#196)"
This reverts commit e254ee3203.
2024-05-30 15:40:44 +07:00
reya 5f30ddcfca Merge branch 'main' of github.com:lumehq/lume 2024-05-30 15:23:11 +07:00
reya 41d0de539d feat: revamp nostr connect flow 2024-05-30 15:21:33 +07:00
XIAO YUandGitHub e254ee3203 chore: Update dependencies and add thiserror crate (#196) 2024-05-30 07:12:46 +07:00
reya 6d42360549 chore: bump version 2024-05-29 14:47:22 +07:00
reya 70c5143445 feat: redirect to index after add account 2024-05-29 14:41:33 +07:00
reya 41b66b18f5 feat: lazy load onboarding videos 2024-05-29 14:11:30 +07:00
雨宮蓮andGitHub dda0720ed4 chore: update deps (#195) 2024-05-29 12:54:58 +07:00
59 changed files with 3438 additions and 3313 deletions
+2
View File
@@ -16,6 +16,8 @@ jobs:
args: "--target aarch64-apple-darwin" args: "--target aarch64-apple-darwin"
- platform: "macos-latest" # for Intel based macs. - platform: "macos-latest" # for Intel based macs.
args: "--target x86_64-apple-darwin" args: "--target x86_64-apple-darwin"
- platform: "macos-latest" # for Intel based macs.
args: "--target universal-apple-darwin"
#- platform: 'ubuntu-22.04' #- platform: 'ubuntu-22.04'
# args: '' # args: ''
#- platform: 'windows-latest' #- platform: 'windows-latest'
+16 -16
View File
@@ -9,8 +9,8 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@lume/system": "workspace:^",
"@lume/icons": "workspace:^", "@lume/icons": "workspace:^",
"@lume/system": "workspace:^",
"@lume/ui": "workspace:^", "@lume/ui": "workspace:^",
"@lume/utils": "workspace:^", "@lume/utils": "workspace:^",
"@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-avatar": "^1.0.4",
@@ -21,43 +21,43 @@
"@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/query-sync-storage-persister": "^5.36.0", "@tanstack/query-sync-storage-persister": "^5.40.0",
"@tanstack/react-query": "^5.36.0", "@tanstack/react-query": "^5.40.0",
"@tanstack/react-query-persist-client": "^5.36.0", "@tanstack/react-query-persist-client": "^5.40.0",
"@tanstack/react-router": "1.32.5", "@tanstack/react-router": "^1.34.9",
"i18next": "^23.11.4", "i18next": "^23.11.5",
"i18next-resources-to-backend": "^1.2.1", "i18next-resources-to-backend": "^1.2.1",
"minidenticons": "^4.2.1", "minidenticons": "^4.2.1",
"nanoid": "^5.0.7", "nanoid": "^5.0.7",
"nostr-tools": "^2.5.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-currency-input-field": "^3.8.0", "react-currency-input-field": "^3.8.0",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-hook-form": "^7.51.4", "react-hook-form": "^7.51.5",
"react-hotkeys-hook": "^4.5.0", "react-hotkeys-hook": "^4.5.0",
"react-i18next": "^14.1.1", "react-i18next": "^14.1.2",
"react-string-replace": "^1.1.1", "react-string-replace": "^1.1.1",
"slate": "^0.103.0", "slate": "^0.103.0",
"slate-react": "^0.102.0", "slate-react": "^0.104.0",
"sonner": "^1.4.41", "sonner": "^1.4.41",
"use-debounce": "^10.0.0", "use-debounce": "^10.0.1",
"virtua": "^0.31.0" "virtua": "^0.31.0"
}, },
"devDependencies": { "devDependencies": {
"@lume/tailwindcss": "workspace:^", "@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^", "@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^", "@lume/types": "workspace:^",
"@tanstack/router-devtools": "^1.32.5", "@tanstack/router-devtools": "^1.34.9",
"@tanstack/router-vite-plugin": "^1.32.2", "@tanstack/router-vite-plugin": "^1.34.8",
"@types/react": "^18.3.2", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.6.0", "@vitejs/plugin-react-swc": "^3.7.0",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"postcss": "^8.4.38", "postcss": "^8.4.38",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"vite": "^5.2.11", "vite": "^5.2.12",
"vite-plugin-top-level-await": "^1.4.1", "vite-plugin-top-level-await": "^1.4.1",
"vite-tsconfig-paths": "^4.3.2" "vite-tsconfig-paths": "^4.3.2"
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

+14 -4
View File
@@ -3,7 +3,7 @@ import type { LumeColumn } from "@lume/types";
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { getCurrent } from "@tauri-apps/api/webviewWindow"; import { getCurrent } from "@tauri-apps/api/webviewWindow";
import { useEffect, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
export function Column({ export function Column({
column, column,
@@ -17,7 +17,10 @@ export function Column({
isResize: boolean; isResize: boolean;
}) { }) {
const container = useRef<HTMLDivElement>(null); const container = useRef<HTMLDivElement>(null);
const webviewLabel = `column-${account}_${column.label}`; const webviewLabel = useMemo(
() => `column-${account}_${column.label}`,
[account],
);
const [isCreated, setIsCreated] = useState(false); const [isCreated, setIsCreated] = useState(false);
@@ -48,6 +51,8 @@ export function Column({
}, [isScroll]); }, [isScroll]);
useEffect(() => { useEffect(() => {
if (!container?.current) return;
const rect = container.current.getBoundingClientRect(); const rect = container.current.getBoundingClientRect();
const url = `${column.content}?account=${account}&label=${column.label}&name=${column.name}`; const url = `${column.content}?account=${account}&label=${column.label}&name=${column.name}`;
@@ -59,11 +64,16 @@ export function Column({
width: rect.width, width: rect.width,
height: rect.height, height: rect.height,
url, url,
}).then(() => setIsCreated(true)); }).then(() => {
console.log("created: ", webviewLabel);
setIsCreated(true);
});
// close webview when unmounted // close webview when unmounted
return () => { return () => {
invoke("close_column", { label: webviewLabel }); invoke("close_column", { label: webviewLabel }).then(() => {
console.log("closed: ", webviewLabel);
});
}; };
}, [account]); }, [account]);
@@ -102,7 +102,7 @@ export function NoteContent({
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div <div
className={cn( className={cn(
"select-text text-[15px] text-balance break-words overflow-hidden", "select-text text-[15px] text-pretty content-break overflow-hidden",
event.content.length > 500 ? "max-h-[300px] gradient-mask-b-0" : "", event.content.length > 500 ? "max-h-[300px] gradient-mask-b-0" : "",
className, className,
)} )}
@@ -145,7 +145,7 @@ export function NoteContentLarge({
return ( return (
<div className={cn("select-text", className)}> <div className={cn("select-text", className)}>
<div className="text-[15px] text-balance content-break leading-normal"> <div className="text-[15px] text-pretty content-break leading-normal">
{content} {content}
</div> </div>
</div> </div>
@@ -2,7 +2,7 @@ export function Hashtag({ tag }: { tag: string }) {
return ( return (
<button <button
type="button" type="button"
className="break-all cursor-default leading-normal group" className="break-all cursor-default leading-normal group text-start"
> >
<span className="text-blue-500">#</span> <span className="text-blue-500">#</span>
<span className="underline-offset-1 underline decoration-2 decoration-blue-200 dark:decoration-blue-800 group-hover:decoration-blue-500"> <span className="underline-offset-1 underline decoration-2 decoration-blue-200 dark:decoration-blue-800 group-hover:decoration-blue-500">
@@ -48,7 +48,7 @@ export function MentionNote({
</User.Provider> </User.Provider>
<div <div
className={cn( className={cn(
"px-3 select-text content-break whitespace-normal text-balance leading-normal", "px-3 select-text whitespace-normal text-pretty content-break leading-normal",
data.content.length > 100 ? "max-h-[150px] gradient-mask-b-0" : "", data.content.length > 100 ? "max-h-[150px] gradient-mask-b-0" : "",
)} )}
> >
@@ -1,61 +1,44 @@
import { CheckCircleIcon, DownloadIcon } from "@lume/icons";
import { downloadDir } from "@tauri-apps/api/path";
import { WebviewWindow } from "@tauri-apps/api/webviewWindow"; import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
import { download } from "@tauri-apps/plugin-upload";
import { type SyntheticEvent, useState } from "react";
export function ImagePreview({ url }: { url: string }) { export function ImagePreview({ url }: { url: string }) {
const [downloaded, setDownloaded] = useState(false); const open = async (url: string) => {
const name = new URL(url).pathname
.split("/")
.pop()
.replace(/[^a-zA-Z ]/g, "");
const label = `viewer-${name}`;
const window = WebviewWindow.getByLabel(label);
const downloadImage = async (e: { stopPropagation: () => void }) => { if (!window) {
try { const newWindow = new WebviewWindow(label, {
e.stopPropagation(); url,
title: "Image Viewer",
width: 800,
height: 800,
titleBarStyle: "overlay",
});
const downloadDirPath = await downloadDir(); return newWindow;
const filename = url.substring(url.lastIndexOf("/") + 1);
await download(url, `${downloadDirPath}/${filename}`);
setDownloaded(true);
} catch (e) {
console.error(e);
} }
};
const open = async () => { return await window.setFocus();
const name = new URL(url).pathname.split("/").pop();
return new WebviewWindow("image-viewer", {
url,
title: name,
});
};
const fallback = (event: SyntheticEvent<HTMLImageElement, Event>) => {
event.currentTarget.src = "/fallback-image.jpg";
}; };
return ( return (
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> <div className="group relative my-1">
<div onClick={() => open()} className="group relative my-1">
<img <img
src={url} src={url}
alt={url} alt={url}
loading="lazy" loading="lazy"
decoding="async" decoding="async"
style={{ contentVisibility: "auto" }} style={{ contentVisibility: "auto" }}
onError={fallback}
className="max-h-[600px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15" className="max-h-[600px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
onClick={() => open(url)}
onError={({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = "/404.jpg";
}}
/> />
<button
type="button"
onClick={(e) => downloadImage(e)}
className="absolute right-2 top-2 z-20 hidden size-8 items-center justify-center rounded-md bg-white/10 text-white/70 backdrop-blur-2xl hover:bg-blue-500 hover:text-white group-hover:inline-flex"
>
{downloaded ? (
<CheckCircleIcon className="size-4" />
) : (
<DownloadIcon className="size-4" />
)}
</button>
</div> </div>
); );
} }
@@ -36,6 +36,10 @@ export function Images({ urls }: { urls: string[] }) {
style={{ contentVisibility: "auto" }} style={{ contentVisibility: "auto" }}
className="max-h-[400px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15" className="max-h-[400px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
onClick={() => open(urls[0])} onClick={() => open(urls[0])}
onError={({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = "/404.jpg";
}}
/> />
</div> </div>
); );
@@ -54,6 +58,10 @@ export function Images({ urls }: { urls: string[] }) {
style={{ contentVisibility: "auto" }} style={{ contentVisibility: "auto" }}
className="w-full h-full object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15" className="w-full h-full object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
onClick={() => open(item)} onClick={() => open(item)}
onError={({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = "/404.jpg";
}}
/> />
</CarouselItem> </CarouselItem>
)} )}
+1 -1
View File
@@ -23,7 +23,7 @@ export function UserAvatar({ className }: { className?: string }) {
alt={user.pubkey} alt={user.pubkey}
loading="eager" loading="eager"
decoding="async" decoding="async"
className={cn("outline-[.5px] outline-black/5", className)} className={cn("outline-[.5px] outline-black/5 object-cover", className)}
/> />
<Avatar.Fallback delayMs={120}> <Avatar.Fallback delayMs={120}>
<img <img
+17 -7
View File
@@ -13,9 +13,11 @@ import { VList, type VListHandle } from "virtua";
export const Route = createFileRoute("/$account/home")({ export const Route = createFileRoute("/$account/home")({
loader: async () => { loader: async () => {
const columns = NostrQuery.getColumns(); const columns = await NostrQuery.getColumns();
return columns; return columns;
}, },
gcTime: 0,
shouldReload: false,
component: Screen, component: Screen,
}); });
@@ -24,11 +26,16 @@ function Screen() {
const initialColumnList = Route.useLoaderData(); const initialColumnList = Route.useLoaderData();
const vlistRef = useRef<VListHandle>(null); const vlistRef = useRef<VListHandle>(null);
const [columns, setColumns] = useState<LumeColumn[]>([]);
const [selectedIndex, setSelectedIndex] = useState(-1); const [selectedIndex, setSelectedIndex] = useState(-1);
const [columns, setColumns] = useState([]);
const [isScroll, setIsScroll] = useState(false); const [isScroll, setIsScroll] = useState(false);
const [isResize, setIsResize] = useState(false); const [isResize, setIsResize] = useState(false);
const reset = () => {
setColumns(null);
setSelectedIndex(-1);
};
const goLeft = () => { const goLeft = () => {
const prevIndex = Math.max(selectedIndex - 1, 0); const prevIndex = Math.max(selectedIndex - 1, 0);
setSelectedIndex(prevIndex); setSelectedIndex(prevIndex);
@@ -64,7 +71,7 @@ function Screen() {
// scroll to the newest column // scroll to the newest column
vlistRef.current.scrollToIndex(newCols.length - 1, { vlistRef.current.scrollToIndex(newCols.length - 1, {
align: "end", align: "center",
}); });
}, 150); }, 150);
@@ -103,12 +110,15 @@ function Screen() {
}, [initialColumnList]); }, [initialColumnList]);
useEffect(() => { useEffect(() => {
// save state // save current state
NostrQuery.setColumns(columns); if (columns?.length) {
NostrQuery.setColumns(columns).then(() => console.log("saved"));
}
}, [columns]); }, [columns]);
useEffect(() => { useEffect(() => {
const unlistenColEvent = listen<EventColumns>("columns", (data) => { const unlistenColEvent = listen<EventColumns>("columns", (data) => {
if (data.payload.type === "reset") reset();
if (data.payload.type === "add") add(data.payload.column); if (data.payload.type === "add") add(data.payload.column);
if (data.payload.type === "remove") remove(data.payload.label); if (data.payload.type === "remove") remove(data.payload.label);
if (data.payload.type === "set_title") if (data.payload.type === "set_title")
@@ -137,9 +147,9 @@ function Screen() {
onScrollEnd={() => setIsScroll(false)} onScrollEnd={() => setIsScroll(false)}
className="scrollbar-none h-full w-full overflow-x-auto focus:outline-none" className="scrollbar-none h-full w-full overflow-x-auto focus:outline-none"
> >
{columns.map((column) => ( {columns?.map((column) => (
<Column <Column
key={column.label} key={account + column.label}
column={column} column={column}
account={account} account={account}
isScroll={isScroll} isScroll={isScroll}
+18 -84
View File
@@ -1,36 +1,23 @@
import { import {
BellIcon,
ComposeFilledIcon, ComposeFilledIcon,
HorizontalDotsIcon, HorizontalDotsIcon,
PlusIcon, PlusIcon,
SearchIcon, SearchIcon,
} from "@lume/icons"; } from "@lume/icons";
import { type NostrEvent, Kind } from "@lume/types";
import { User } from "@/components/user"; import { User } from "@/components/user";
import { import { cn } from "@lume/utils";
cn,
decodeZapInvoice,
displayNpub,
sendNativeNotification,
} from "@lume/utils";
import { Outlet, createFileRoute } from "@tanstack/react-router"; import { Outlet, createFileRoute } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core";
import { getCurrent } from "@tauri-apps/api/window"; import { getCurrent } from "@tauri-apps/api/window";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import * as Popover from "@radix-ui/react-popover"; import * as Popover from "@radix-ui/react-popover";
import { LumeWindow, NostrAccount, NostrQuery } from "@lume/system"; import { LumeWindow, NostrAccount } from "@lume/system";
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
type AccountSearch = {
accounts?: string[];
};
export const Route = createFileRoute("/$account")({ export const Route = createFileRoute("/$account")({
validateSearch: (search: Record<string, unknown>): AccountSearch => { beforeLoad: async () => {
return { const accounts = await NostrAccount.getAccounts();
accounts: (search?.accounts as string[]) || [], return { accounts };
};
}, },
component: Screen, component: Screen,
}); });
@@ -50,7 +37,7 @@ function Screen() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Accounts /> <Accounts />
<Link <Link
to="/landing/" to="/landing"
className="inline-flex size-8 shrink-0 items-center justify-center rounded-full bg-black/10 text-neutral-800 hover:bg-black/20 dark:bg-white/10 dark:text-neutral-200 dark:hover:bg-white/20" className="inline-flex size-8 shrink-0 items-center justify-center rounded-full bg-black/10 text-neutral-800 hover:bg-black/20 dark:bg-white/10 dark:text-neutral-200 dark:hover:bg-white/20"
> >
<PlusIcon className="size-5" /> <PlusIcon className="size-5" />
@@ -65,7 +52,6 @@ function Screen() {
<ComposeFilledIcon className="size-4" /> <ComposeFilledIcon className="size-4" />
New Post New Post
</button> </button>
<Bell />
<button <button
type="button" type="button"
onClick={() => LumeWindow.openSearch()} onClick={() => LumeWindow.openSearch()}
@@ -84,7 +70,7 @@ function Screen() {
} }
function Accounts() { function Accounts() {
const { accounts } = Route.useSearch(); const { accounts } = Route.useRouteContext();
const { account } = Route.useParams(); const { account } = Route.useParams();
const [windowWidth, setWindowWidth] = useState<number>(null); const [windowWidth, setWindowWidth] = useState<number>(null);
@@ -108,11 +94,20 @@ function Accounts() {
return await LumeWindow.openProfile(account); return await LumeWindow.openProfile(account);
} }
// change current account and update signer // Change current account and update signer
const select = await NostrAccount.loadAccount(npub); const select = await NostrAccount.loadAccount(npub);
if (select) { if (select) {
return navigate({ to: "/$account/home", params: { account: npub } }); // 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 { } else {
toast.warning("Something wrong."); toast.warning("Something wrong.");
} }
@@ -191,64 +186,3 @@ function Accounts() {
</div> </div>
); );
} }
function Bell() {
const { account } = Route.useParams();
const [count, setCount] = useState(0);
useEffect(() => {
const unlisten = getCurrent().listen<string>(
"activity",
async (payload) => {
setCount((prevCount) => prevCount + 1);
await invoke("set_badge", { count });
const event: NostrEvent = JSON.parse(payload.payload);
const user = await NostrQuery.getProfile(event.pubkey);
const userName =
user.display_name || user.name || displayNpub(event.pubkey, 16);
switch (event.kind) {
case Kind.Text: {
sendNativeNotification("Mentioned you in a note", userName);
break;
}
case Kind.Repost: {
sendNativeNotification("Reposted your note", userName);
break;
}
case Kind.ZapReceipt: {
const amount = decodeZapInvoice(event.tags);
sendNativeNotification(
`Zapped ₿ ${amount.bitcoinFormatted}`,
userName,
);
break;
}
default:
break;
}
},
);
return () => {
unlisten.then((f) => f());
};
}, []);
return (
<button
type="button"
onClick={() => {
setCount(0);
LumeWindow.openActivity(account);
}}
className="relative inline-flex size-8 items-center justify-center rounded-full text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10"
>
<BellIcon className="size-5" />
{count > 0 ? (
<span className="absolute right-0 top-0 block size-2 rounded-full bg-teal-500 ring-1 ring-black/5" />
) : null}
</button>
);
}
@@ -1,61 +0,0 @@
import { Spinner } from "@lume/ui";
import { Note } from "@/components/note";
import { Await, createFileRoute, defer } from "@tanstack/react-router";
import { Suspense } from "react";
import { Virtualizer } from "virtua";
import { NostrQuery } from "@lume/system";
export const Route = createFileRoute("/activity/$account/texts")({
loader: async ({ params }) => {
return { data: defer(NostrQuery.getUserActivities(params.account, "1")) };
},
component: Screen,
});
function Screen() {
const { data } = Route.useLoaderData();
return (
<Virtualizer overscan={3}>
<Suspense
fallback={
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
<button
type="button"
className="inline-flex items-center gap-2 text-sm font-medium"
disabled
>
<Spinner className="size-5" />
Loading...
</button>
</div>
}
>
<Await promise={data}>
{(events) =>
events.map((event) => (
<div
key={event.id}
className="flex flex-col gap-2 mb-3 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="px-3 h-14 flex items-center justify-between">
<Note.User />
<Note.Menu />
</div>
<Note.Activity className="px-3" />
<Note.Content className="px-3" quote={false} clean />
<div className="mt-3 flex items-center gap-4 h-14 px-3">
<Note.Open />
</div>
</Note.Root>
</Note.Provider>
</div>
))
}
</Await>
</Suspense>
</Virtualizer>
);
}
@@ -1,50 +0,0 @@
import { Box, Container } from "@lume/ui";
import { cn } from "@lume/utils";
import { Link, Outlet } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/activity/$account")({
component: Screen,
});
function Screen() {
const { account } = Route.useParams();
return (
<Container withDrag>
<Box className="scrollbar-none shadow-none bg-black/5 dark:bg-white/5 backdrop-blur-sm flex flex-col overflow-y-auto">
<div className="h-14 shrink-0 flex w-full items-center gap-1 px-3">
<div className="inline-flex h-full w-full items-center gap-1">
<Link to="/activity/$account/texts" params={{ account }}>
{({ 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",
)}
>
Notes
</div>
)}
</Link>
<Link to="/activity/$account/zaps" params={{ account }}>
{({ 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",
)}
>
Zaps
</div>
)}
</Link>
</div>
</div>
<div className="px-2 flex-1 overflow-y-auto w-full h-full scrollbar-none">
<Outlet />
</div>
</Box>
</Container>
);
}
@@ -1,67 +0,0 @@
import { User } from "@/components/user";
import { NostrQuery } from "@lume/system";
import { Spinner } from "@lume/ui";
import { decodeZapInvoice } from "@lume/utils";
import { Await, createFileRoute, defer } from "@tanstack/react-router";
import { Suspense } from "react";
import { Virtualizer } from "virtua";
export const Route = createFileRoute("/activity/$account/zaps")({
loader: async ({ params }) => {
return {
data: defer(NostrQuery.getUserActivities(params.account, "9735")),
};
},
component: Screen,
});
function Screen() {
const { data } = Route.useLoaderData();
return (
<Virtualizer overscan={3}>
<Suspense
fallback={
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
<button
type="button"
className="inline-flex items-center gap-2 text-sm font-medium"
disabled
>
<Spinner className="size-5" />
Loading...
</button>
</div>
}
>
<Await promise={data}>
{(events) =>
events.map((event) => (
<div
key={event.id}
className="flex flex-col gap-2 mb-3 bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50"
>
<User.Provider pubkey={event.pubkey}>
<User.Root className="flex flex-col">
<div className="text-lg h-20 font-medium leading-tight flex w-full items-center justify-center">
{decodeZapInvoice(event.tags).bitcoinFormatted}
</div>
<div className="h-11 border-t border-neutral-100 dark:border-neutral-900 flex items-center gap-1 px-2">
<div className="inline-flex items-center gap-2">
<User.Avatar className="size-7 rounded-full shrink-0" />
<User.Name className="text-sm font-medium" />
</div>
<div className="text-sm text-neutral-700 dark:text-neutral-300">
zapped you
</div>
</div>
</User.Root>
</User.Provider>
</div>
))
}
</Await>
</Suspense>
</Virtualizer>
);
}
@@ -9,7 +9,7 @@ import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
export const Route = createFileRoute("/auth/new/$account/backup")({ export const Route = createFileRoute("/auth/$account/backup")({
component: Screen, component: Screen,
}); });
@@ -3,7 +3,6 @@ import { NostrQuery } from "@lume/system";
import { Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import * as Switch from "@radix-ui/react-switch"; import * as Switch from "@radix-ui/react-switch";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { requestPermission } from "@tauri-apps/plugin-notification";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -18,28 +17,14 @@ export const Route = createFileRoute("/auth/$account/settings")({
}); });
function Screen() { function Screen() {
const navigate = Route.useNavigate();
const { account } = Route.useParams();
const { settings } = Route.useRouteContext(); const { settings } = Route.useRouteContext();
const { account } = Route.useParams();
const { t } = useTranslation(); const { t } = useTranslation();
const [newSettings, setNewSettings] = useState(settings); const [newSettings, setNewSettings] = useState(settings);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const toggleNofitication = async () => { const navigate = Route.useNavigate();
await requestPermission();
setNewSettings((prev) => ({
...prev,
notification: !newSettings.notification,
}));
};
const toggleAutoUpdate = () => {
setNewSettings((prev) => ({
...prev,
autoUpdate: !newSettings.autoUpdate,
}));
};
const toggleEnhancedPrivacy = () => { const toggleEnhancedPrivacy = () => {
setNewSettings((prev) => ({ setNewSettings((prev) => ({
@@ -100,22 +85,6 @@ function Screen() {
</div> </div>
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5">
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-white/10">
<div className="flex-1">
<h3 className="font-semibold">Push Notification</h3>
<p className="text-sm text-neutral-700 dark:text-neutral-300">
Enabling push notifications will allow you to receive
notifications from Lume.
</p>
</div>
<Switch.Root
checked={newSettings.notification}
onClick={() => toggleNofitication()}
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-white/20"
>
<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 className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-white/10"> <div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-white/10">
<div className="flex-1"> <div className="flex-1">
<h3 className="font-semibold">Enhanced Privacy</h3> <h3 className="font-semibold">Enhanced Privacy</h3>
@@ -132,21 +101,6 @@ function Screen() {
<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.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> </Switch.Root>
</div> </div>
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-white/10">
<div className="flex-1">
<h3 className="font-semibold">Auto Update</h3>
<p className="text-sm text-neutral-700 dark:text-neutral-300">
Automatically download and install new version.
</p>
</div>
<Switch.Root
checked={newSettings.autoUpdate}
onClick={() => toggleAutoUpdate()}
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-white/20"
>
<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 className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-white/10"> <div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-white/10">
<div className="flex-1"> <div className="flex-1">
<h3 className="font-semibold">Zap</h3> <h3 className="font-semibold">Zap</h3>
@@ -186,7 +140,7 @@ function Screen() {
disabled={loading} disabled={loading}
className="mb-1 inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50" className="mb-1 inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
> >
{t("global.continue")} {loading ? <Spinner /> : t("global.continue")}
</button> </button>
</div> </div>
</div> </div>
@@ -45,7 +45,7 @@ function Screen() {
if (eventId) { if (eventId) {
navigate({ navigate({
to: "/auth/new/$account/backup", to: "/auth/$account/backup",
params: { account: account.npub }, params: { account: account.npub },
replace: true, replace: true,
}); });
@@ -23,12 +23,12 @@ function Screen() {
try { try {
setLoading(true); setLoading(true);
const npub = await NostrAccount.connectRemoteAccount(uri); const remoteAccount = await NostrAccount.connectRemoteAccount(uri);
if (npub) { if (remoteAccount?.length) {
navigate({ return navigate({
to: "/auth/$account/settings", to: "/auth/$account/settings",
params: { account: npub }, params: { account: remoteAccount },
replace: true, replace: true,
}); });
} }
+4 -4
View File
@@ -41,9 +41,6 @@ function Screen() {
return navigate({ return navigate({
to: "/$account/home", to: "/$account/home",
params: { account: npub }, params: { account: npub },
search: {
accounts: context.accounts,
},
replace: true, replace: true,
}); });
} }
@@ -60,7 +57,10 @@ function Screen() {
}); });
return ( return (
<div className="relative flex h-full w-full items-center justify-center"> <div
data-tauri-drag-region
className="relative flex h-full w-full items-center justify-center"
>
<div className="relative z-20 flex flex-col items-center gap-16"> <div className="relative z-20 flex flex-col items-center gap-16">
<div className="text-center"> <div className="text-center">
<h2 className="text-xl text-neutral-700 dark:text-neutral-300"> <h2 className="text-xl text-neutral-700 dark:text-neutral-300">
+8
View File
@@ -34,6 +34,8 @@ function Screen() {
className="h-auto w-full aspect-square rounded-lg shadow-md transform" className="h-auto w-full aspect-square rounded-lg shadow-md transform"
controls controls
muted muted
preload="none"
poster="/poster_1.jpeg"
> >
<source <source
src="https://video.nostr.build/692f71e2be47ecfc29edcbdaa198cc5979bfb9c900f05d78682895dd546d8d4f.mp4" src="https://video.nostr.build/692f71e2be47ecfc29edcbdaa198cc5979bfb9c900f05d78682895dd546d8d4f.mp4"
@@ -55,6 +57,8 @@ function Screen() {
className="h-auto w-full aspect-square rounded-lg shadow-md transform" className="h-auto w-full aspect-square rounded-lg shadow-md transform"
controls controls
muted muted
preload="none"
poster="/poster_2.jpeg"
> >
<source <source
src="https://video.nostr.build/d33962520506d86acfb4b55a7b265821e10ae637f5ec830a173b7e6092b16ec8.mp4" src="https://video.nostr.build/d33962520506d86acfb4b55a7b265821e10ae637f5ec830a173b7e6092b16ec8.mp4"
@@ -76,6 +80,8 @@ function Screen() {
className="h-auto w-full aspect-square rounded-lg shadow-md transform" className="h-auto w-full aspect-square rounded-lg shadow-md transform"
controls controls
muted muted
preload="none"
poster="/poster_3.jpeg"
> >
<source <source
src="https://video.nostr.build/927abbfde2097e470ac751181b1db456b7e4b9149550408efff1a966a7ffb9a8.mp4" src="https://video.nostr.build/927abbfde2097e470ac751181b1db456b7e4b9149550408efff1a966a7ffb9a8.mp4"
@@ -97,6 +103,8 @@ function Screen() {
className="h-auto w-full rounded-lg shadow-md transform" className="h-auto w-full rounded-lg shadow-md transform"
controls controls
muted muted
preload="none"
poster="/poster_4.jpeg"
> >
<source <source
src="https://video.nostr.build/513de4824b6abaf7e9698c1dad2f68096574356848c0c200bc8cb8074df29410.mp4" src="https://video.nostr.build/513de4824b6abaf7e9698c1dad2f68096574356848c0c200bc8cb8074df29410.mp4"
+314
View File
@@ -0,0 +1,314 @@
import { Note } from "@/components/note";
import { User } from "@/components/user";
import { LumeWindow, NostrQuery, useEvent } from "@lume/system";
import { Kind, NostrEvent } from "@lume/types";
import { createFileRoute } from "@tanstack/react-router";
import { getCurrent } from "@tauri-apps/api/window";
import { useEffect, useMemo, useState } from "react";
import * as Tabs from "@radix-ui/react-tabs";
import { InfoIcon, RepostIcon, SettingsIcon } from "@lume/icons";
import { decodeZapInvoice, formatCreatedAt } from "@lume/utils";
interface EmitAccount {
account: string;
}
export const Route = createFileRoute("/panel")({
component: Screen,
});
function Screen() {
const [account, setAccount] = useState<string>(null);
const [events, setEvents] = useState<NostrEvent[]>([]);
const texts = useMemo(
() => events.filter((ev) => ev.kind === Kind.Text),
[events],
);
const zaps = useMemo(() => {
const groups = new Map<string, NostrEvent[]>();
const list = events.filter((ev) => ev.kind === Kind.ZapReceipt);
for (const event of list) {
const rootId = event.tags.filter((tag) => tag[0] === "e")[0]?.[1];
if (rootId) {
if (groups.has(rootId)) {
groups.get(rootId).push(event);
} else {
groups.set(rootId, [event]);
}
}
}
return groups;
}, [events]);
const reactions = useMemo(() => {
const groups = new Map<string, NostrEvent[]>();
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]);
}
}
}
return groups;
}, [events]);
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 event: NostrEvent = JSON.parse(data.payload as string);
setEvents((prev) => [event, ...prev]);
});
return () => {
unlistenLoad.then((f) => f());
unlistenNewEvent.then((f) => f());
};
}, []);
if (!account) {
return (
<div className="w-full h-full flex items-center justify-center text-sm">
Please log in.
</div>
);
}
return (
<div className="w-full h-full flex flex-col">
<div className="h-11 shrink-0 flex items-center justify-between border-b border-black/5 px-4">
<div>
<h1 className="text-sm font-semibold">Notifications</h1>
</div>
<div className="inline-flex items-center gap-2">
<User.Provider pubkey={account}>
<User.Root>
<User.Avatar className="size-7 rounded-full" />
</User.Root>
</User.Provider>
<button
type="button"
onClick={() => LumeWindow.openSettings()}
className="size-7 inline-flex items-center justify-center bg-black/5 dark:bg-white/5 rounded-full"
>
<SettingsIcon className="size-4" />
</button>
</div>
</div>
<Tabs.Root
defaultValue="replies"
className="flex-1 overflow-y-auto overflow-x-hidden scrollbar-none"
>
<Tabs.List className="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 data-[state=active]:border-black/30 dark:data-[state=active] data-[state=inactive]:opacity-50"
value="replies"
>
Replies
</Tabs.Trigger>
<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 data-[state=active]:border-black/30 dark:data-[state=active] data-[state=inactive]:opacity-50"
value="reactions"
>
Reactions
</Tabs.Trigger>
<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 data-[state=active]:border-black/30 dark:data-[state=active] data-[state=inactive]:opacity-50"
value="zaps"
>
Zaps
</Tabs.Trigger>
</Tabs.List>
<div className="p-2">
<Tabs.Content value="replies" className="flex flex-col gap-2">
{texts.map((event) => (
<TextNote event={event} />
))}
</Tabs.Content>
<Tabs.Content value="reactions" className="flex flex-col gap-2">
{[...reactions.entries()].map(([root, events]) => (
<div
key={root}
className="shrink-0 flex flex-col gap-1 rounded-lg p-2 backdrop-blur-md bg-black/10 dark:bg-white/10"
>
<div className="min-w-0 flex-1 flex flex-col gap-2">
<div className="pb-2 border-b border-black/5 dark:border-white/5 flex items-center gap-2">
<RootNote id={root} />
</div>
<div className="flex flex-wrap items-center gap-3">
{events.map((event) => (
<User.Provider 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.Avatar className="flex-1 size-7 rounded-full" />
<div className="flex-1 size-7 rounded-full inline-flex items-center justify-center text-xs truncate">
{event.kind === Kind.Reaction ? (
event.content === "+" ? (
"👍"
) : (
event.content
)
) : (
<RepostIcon className="size-4 dark:text-teal-600 text-teal-400" />
)}
</div>
</User.Root>
</User.Provider>
))}
</div>
</div>
</div>
))}
</Tabs.Content>
<Tabs.Content value="zaps" className="flex flex-col gap-2">
{[...zaps.entries()].map(([root, events]) => (
<div
key={root}
className="shrink-0 flex flex-col gap-1 rounded-lg p-2 backdrop-blur-md bg-black/10 dark:bg-white/10"
>
<div className="min-w-0 flex-1 flex flex-col gap-2">
<div className="pb-2 border-b border-black/5 dark:border-white/5 flex items-center gap-2">
<RootNote id={root} />
</div>
<div className="flex flex-wrap items-center gap-3">
{events.map((event) => (
<User.Provider
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 size-7 rounded-full" />
<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>
</User.Root>
</User.Provider>
))}
</div>
</div>
</div>
))}
</Tabs.Content>
</div>
</Tabs.Root>
</div>
);
}
function RootNote({ id }: { id: string }) {
const { isLoading, isError, data } = useEvent(id);
if (isLoading) {
return (
<div className="pb-2 mb-2 flex items-center">
<div className="size-8 shrink-0 rounded-full bg-black/20 dark:bg-white/20 animate-pulse" />
<div className="animate-pulse rounded-md h-4 w-2/3 bg-black/20 dark:bg-white/20" />
</div>
);
}
if (isError || !data) {
return (
<div className="flex items-center gap-2">
<div className="size-8 shrink-0 rounded-full bg-red-500 text-white inline-flex items-center justify-center">
<InfoIcon className="size-5" />
</div>
<p className="text-red-500 text-sm">
Event not found with your current relay set
</p>
</div>
);
}
return (
<Note.Provider event={data}>
<Note.Root className="flex items-center gap-2">
<User.Provider pubkey={data.pubkey}>
<User.Root className="shrink-0">
<User.Avatar className="size-8 shrink-0 rounded-full" />
</User.Root>
</User.Provider>
<div className="line-clamp-1">{data.content}</div>
</Note.Root>
</Note.Provider>
);
}
function TextNote({ event }: { event: NostrEvent }) {
const pTags = event.tags
.filter((tag) => tag[0] === "p")
.map((tag) => tag[1])
.slice(0, 3);
return (
<button
type="button"
key={event.id}
onClick={() => LumeWindow.openEvent(event)}
>
<Note.Provider event={event}>
<Note.Root className="shrink-0 flex flex-col gap-1 rounded-lg p-2 backdrop-blur-md bg-black/10 dark:bg-white/10">
<User.Provider pubkey={event.pubkey}>
<User.Root className="inline-flex items-center gap-2">
<User.Avatar className="size-9 shrink-0 rounded-full" />
<div className="flex-1 flex flex-col">
<div className="w-full flex items-baseline justify-between">
<User.Name className="leading-tight text-sm font-semibold" />
<span className="leading-tight text-sm text-black/50 dark:text-white/50">
{formatCreatedAt(event.created_at)}
</span>
</div>
<div className="inline-flex items-baseline gap-1 text-xs">
<span className="leading-tight text-black/50 dark:text-white/50">
Reply to:
</span>
<div className="inline-flex items-baseline gap-1">
{pTags.map((replyTo) => (
<User.Provider pubkey={replyTo}>
<User.Root>
<User.Name className="leading-tight font-medium" />
</User.Root>
</User.Provider>
))}
</div>
</div>
</div>
</User.Root>
</User.Provider>
<div className="flex gap-2">
<div className="w-9 shrink-0" />
<div className="line-clamp-1">{event.content}</div>
</div>
</Note.Root>
</Note.Provider>
</button>
);
}
+1 -1
View File
@@ -13,7 +13,7 @@
"@astrojs/check": "^0.5.10", "@astrojs/check": "^0.5.10",
"@astrojs/tailwind": "^5.1.0", "@astrojs/tailwind": "^5.1.0",
"@fontsource/alice": "^5.0.13", "@fontsource/alice": "^5.0.13",
"astro": "^4.8.3", "astro": "^4.9.2",
"astro-seo-meta": "^4.1.1", "astro-seo-meta": "^4.1.1",
"astro-seo-schema": "^4.0.2", "astro-seo-schema": "^4.0.2",
"schema-dts": "^1.1.2", "schema-dts": "^1.1.2",
+33 -34
View File
@@ -1,36 +1,35 @@
{ {
"name": "lume", "name": "lume",
"private": true, "private": true,
"version": "4.0.0", "version": "4.0.0",
"scripts": { "scripts": {
"build": "turbo run build", "build": "turbo run build",
"dev": "turbo run dev", "dev": "turbo run dev",
"web:dev": "turbo run dev --filter web", "web:dev": "turbo run dev --filter web",
"desktop:dev": "turbo run dev --filter desktop2", "desktop:dev": "turbo run dev --filter desktop2",
"desktop:build": "turbo run build --filter desktop2", "desktop:build": "turbo run build --filter desktop2",
"tauri": "tauri" "tauri": "tauri"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.7.3", "@biomejs/biome": "1.7.3",
"@tauri-apps/cli": "2.0.0-beta.12", "@tauri-apps/cli": "2.0.0-beta.20",
"turbo": "^1.13.3" "turbo": "^1.13.3"
}, },
"packageManager": "pnpm@8.9.0", "packageManager": "pnpm@8.9.0",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.0.0-beta.7", "@tauri-apps/api": "2.0.0-beta.13",
"@tauri-apps/plugin-autostart": "2.0.0-beta.3", "@tauri-apps/plugin-clipboard-manager": "2.1.0-beta.3",
"@tauri-apps/plugin-clipboard-manager": "2.1.0-beta.1", "@tauri-apps/plugin-dialog": "2.0.0-beta.5",
"@tauri-apps/plugin-dialog": "2.0.0-beta.3", "@tauri-apps/plugin-fs": "2.0.0-beta.5",
"@tauri-apps/plugin-fs": "2.0.0-beta.3", "@tauri-apps/plugin-http": "2.0.0-beta.5",
"@tauri-apps/plugin-http": "2.0.0-beta.3", "@tauri-apps/plugin-notification": "2.0.0-beta.5",
"@tauri-apps/plugin-notification": "2.0.0-beta.3", "@tauri-apps/plugin-os": "2.0.0-beta.5",
"@tauri-apps/plugin-os": "2.0.0-beta.3", "@tauri-apps/plugin-process": "2.0.0-beta.5",
"@tauri-apps/plugin-process": "2.0.0-beta.3", "@tauri-apps/plugin-shell": "2.0.0-beta.6",
"@tauri-apps/plugin-shell": "2.0.0-beta.4", "@tauri-apps/plugin-updater": "2.0.0-beta.5",
"@tauri-apps/plugin-updater": "2.0.0-beta.3", "@tauri-apps/plugin-upload": "2.0.0-beta.6"
"@tauri-apps/plugin-upload": "2.0.0-beta.4" }
}
} }
+1 -1
View File
@@ -8,7 +8,7 @@
}, },
"devDependencies": { "devDependencies": {
"@lume/tsconfig": "workspace:*", "@lume/tsconfig": "workspace:*",
"@types/react": "^18.3.2", "@types/react": "^18.3.3",
"typescript": "^5.4.5" "typescript": "^5.4.5"
} }
} }
+2 -2
View File
@@ -5,13 +5,13 @@
"main": "./src/index.ts", "main": "./src/index.ts",
"dependencies": { "dependencies": {
"@lume/utils": "workspace:^", "@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.36.0", "@tanstack/react-query": "^5.40.0",
"react": "^18.3.1" "react": "^18.3.1"
}, },
"devDependencies": { "devDependencies": {
"@lume/tsconfig": "workspace:^", "@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^", "@lume/types": "workspace:^",
"@types/react": "^18.3.2", "@types/react": "^18.3.3",
"typescript": "^5.4.5" "typescript": "^5.4.5"
} }
} }
+23 -12
View File
@@ -1,5 +1,6 @@
import { Metadata } from "@lume/types"; import { Metadata } from "@lume/types";
import { commands } from "./commands"; import { Result, commands } from "./commands";
import { Window } from "@tauri-apps/api/window";
export class NostrAccount { export class NostrAccount {
static async getAccounts() { static async getAccounts() {
@@ -13,9 +14,19 @@ export class NostrAccount {
} }
static async loadAccount(npub: string) { static async loadAccount(npub: string) {
const query = await commands.loadAccount(npub); const bunker: string = localStorage.getItem(`${npub}_bunker`);
let query: Result<boolean, string>;
if (bunker?.length && bunker?.startsWith("bunker://")) {
query = await commands.loadAccount(npub, bunker);
} else {
query = await commands.loadAccount(npub, null);
}
if (query.status === "ok") { if (query.status === "ok") {
const panel = Window.getByLabel("panel");
panel.emit("load-notification", { account: npub }); // trigger load notification
return query.data; return query.data;
} else { } else {
throw new Error(query.error); throw new Error(query.error);
@@ -62,19 +73,19 @@ export class NostrAccount {
} }
static async connectRemoteAccount(uri: string) { static async connectRemoteAccount(uri: string) {
const remoteKey = uri.replace("bunker://", "").split("?")[0]; const connect = await commands.connectRemoteAccount(uri);
const npub = await commands.toNpub(remoteKey);
if (npub.status === "ok") { if (connect.status === "ok") {
const connect = await commands.nostrConnect(npub.data, uri); const npub = connect.data;
const parsed = new URL(uri);
parsed.searchParams.delete("secret");
if (connect.status === "ok") { // save connection string
return connect.data; localStorage.setItem(`${npub}_bunker`, parsed.toString());
} else {
throw new Error(connect.error); return npub;
}
} else { } else {
throw new Error(npub.error); throw new Error(connect.error);
} }
} }
+394 -579
View File
@@ -1,598 +1,413 @@
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
export const commands = { // This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
async getRelays(): Promise<Result<Relays, null>> {
try { /** user-defined commands **/
return { status: "ok", data: await TAURI_INVOKE("get_relays") };
} catch (e) { export const commands = {
if (e instanceof Error) throw e; async getRelays() : Promise<Result<Relays, null>> {
else return { status: "error", error: e as any }; try {
} return { status: "ok", data: await TAURI_INVOKE("get_relays") };
}, } catch (e) {
async connectRelay(relay: string): Promise<Result<boolean, null>> { if(e instanceof Error) throw e;
try { else return { status: "error", error: e as any };
return { }
status: "ok", },
data: await TAURI_INVOKE("connect_relay", { relay }), async connectRelay(relay: string) : Promise<Result<boolean, null>> {
}; try {
} catch (e) { return { status: "ok", data: await TAURI_INVOKE("connect_relay", { relay }) };
if (e instanceof Error) throw e; } catch (e) {
else return { status: "error", error: e as any }; if(e instanceof Error) throw e;
} else return { status: "error", error: e as any };
}, }
async removeRelay(relay: string): Promise<Result<boolean, null>> { },
try { async removeRelay(relay: string) : Promise<Result<boolean, null>> {
return { try {
status: "ok", return { status: "ok", data: await TAURI_INVOKE("remove_relay", { relay }) };
data: await TAURI_INVOKE("remove_relay", { relay }), } catch (e) {
}; if(e instanceof Error) throw e;
} catch (e) { else return { status: "error", error: e as any };
if (e instanceof Error) throw e; }
else return { status: "error", error: e as any }; },
} async getAccounts() : Promise<Result<string[], string>> {
}, try {
async getAccounts(): Promise<Result<string[], string>> { return { status: "ok", data: await TAURI_INVOKE("get_accounts") };
try { } catch (e) {
return { status: "ok", data: await TAURI_INVOKE("get_accounts") }; if(e instanceof Error) throw e;
} catch (e) { else return { status: "error", error: e as any };
if (e instanceof Error) throw e; }
else return { status: "error", error: e as any }; },
} async createAccount() : Promise<Result<Account, null>> {
}, try {
async createAccount(): Promise<Result<Account, null>> { return { status: "ok", data: await TAURI_INVOKE("create_account") };
try { } catch (e) {
return { status: "ok", data: await TAURI_INVOKE("create_account") }; if(e instanceof Error) throw e;
} catch (e) { else return { status: "error", error: e as any };
if (e instanceof Error) throw e; }
else return { status: "error", error: e as any }; },
} async saveAccount(nsec: string, password: string) : Promise<Result<string, string>> {
}, try {
async saveAccount( return { status: "ok", data: await TAURI_INVOKE("save_account", { nsec, password }) };
nsec: string, } catch (e) {
password: string, if(e instanceof Error) throw e;
): Promise<Result<string, string>> { else return { status: "error", error: e as any };
try { }
return { },
status: "ok", async getEncryptedKey(npub: string, password: string) : Promise<Result<string, string>> {
data: await TAURI_INVOKE("save_account", { nsec, password }), try {
}; return { status: "ok", data: await TAURI_INVOKE("get_encrypted_key", { npub, password }) };
} catch (e) { } catch (e) {
if (e instanceof Error) throw e; if(e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async getEncryptedKey( async connectRemoteAccount(uri: string) : Promise<Result<string, string>> {
npub: string, try {
password: string, return { status: "ok", data: await TAURI_INVOKE("connect_remote_account", { uri }) };
): Promise<Result<string, string>> { } catch (e) {
try { if(e instanceof Error) throw e;
return { else return { status: "error", error: e as any };
status: "ok", }
data: await TAURI_INVOKE("get_encrypted_key", { npub, password }), },
}; async loadAccount(npub: string, bunker: string | null) : Promise<Result<boolean, string>> {
} catch (e) { try {
if (e instanceof Error) throw e; return { status: "ok", data: await TAURI_INVOKE("load_account", { npub, bunker }) };
else return { status: "error", error: e as any }; } catch (e) {
} if(e instanceof Error) throw e;
}, else return { status: "error", error: e as any };
async nostrConnect( }
npub: string, },
uri: string, async eventToBech32(id: string, relays: string[]) : Promise<Result<string, null>> {
): Promise<Result<string, string>> { try {
try { return { status: "ok", data: await TAURI_INVOKE("event_to_bech32", { id, relays }) };
return { } catch (e) {
status: "ok", if(e instanceof Error) throw e;
data: await TAURI_INVOKE("nostr_connect", { npub, uri }), else return { status: "error", error: e as any };
}; }
} catch (e) { },
if (e instanceof Error) throw e; async userToBech32(key: string, relays: string[]) : Promise<Result<string, null>> {
else return { status: "error", error: e as any }; try {
} return { status: "ok", data: await TAURI_INVOKE("user_to_bech32", { key, relays }) };
}, } catch (e) {
async loadAccount(npub: string): Promise<Result<boolean, string>> { if(e instanceof Error) throw e;
try { else return { status: "error", error: e as any };
return { }
status: "ok", },
data: await TAURI_INVOKE("load_account", { npub }), async verifyNip05(key: string, nip05: string) : Promise<Result<boolean, string>> {
}; try {
} catch (e) { return { status: "ok", data: await TAURI_INVOKE("verify_nip05", { key, nip05 }) };
if (e instanceof Error) throw e; } catch (e) {
else return { status: "error", error: e as any }; if(e instanceof Error) throw e;
} else return { status: "error", error: e as any };
}, }
async eventToBech32( },
id: string, async getCurrentUserProfile() : Promise<Result<string, string>> {
relays: string[], try {
): Promise<Result<string, null>> { return { status: "ok", data: await TAURI_INVOKE("get_current_user_profile") };
try { } catch (e) {
return { if(e instanceof Error) throw e;
status: "ok", else return { status: "error", error: e as any };
data: await TAURI_INVOKE("event_to_bech32", { id, relays }), }
}; },
} catch (e) { async getProfile(id: string) : Promise<Result<string, string>> {
if (e instanceof Error) throw e; try {
else return { status: "error", error: e as any }; return { status: "ok", data: await TAURI_INVOKE("get_profile", { id }) };
} } catch (e) {
}, if(e instanceof Error) throw e;
async userToBech32( else return { status: "error", error: e as any };
key: string, }
relays: string[], },
): Promise<Result<string, null>> { async getContactList() : Promise<Result<string[], string>> {
try { try {
return { return { status: "ok", data: await TAURI_INVOKE("get_contact_list") };
status: "ok", } catch (e) {
data: await TAURI_INVOKE("user_to_bech32", { key, relays }), if(e instanceof Error) throw e;
}; else return { status: "error", error: e as any };
} catch (e) { }
if (e instanceof Error) throw e; },
else return { status: "error", error: e as any }; async setContactList(pubkeys: string[]) : Promise<Result<boolean, string>> {
} try {
}, return { status: "ok", data: await TAURI_INVOKE("set_contact_list", { pubkeys }) };
async toNpub(hex: string): Promise<Result<string, null>> { } catch (e) {
try { if(e instanceof Error) throw e;
return { status: "ok", data: await TAURI_INVOKE("to_npub", { hex }) }; else return { status: "error", error: e as any };
} catch (e) { }
if (e instanceof Error) throw e; },
else return { status: "error", error: e as any }; async createProfile(name: string, displayName: string, about: string, picture: string, banner: string, nip05: string, lud16: string, website: string) : Promise<Result<string, string>> {
} try {
}, return { status: "ok", data: await TAURI_INVOKE("create_profile", { name, displayName, about, picture, banner, nip05, lud16, website }) };
async verifyNip05( } catch (e) {
key: string, if(e instanceof Error) throw e;
nip05: string, else return { status: "error", error: e as any };
): Promise<Result<boolean, string>> { }
try { },
return { async follow(id: string, alias: string | null) : Promise<Result<string, string>> {
status: "ok", try {
data: await TAURI_INVOKE("verify_nip05", { key, nip05 }), return { status: "ok", data: await TAURI_INVOKE("follow", { id, alias }) };
}; } catch (e) {
} catch (e) { if(e instanceof Error) throw e;
if (e instanceof Error) throw e; else return { status: "error", error: e as any };
else return { status: "error", error: e as any }; }
} },
}, async unfollow(id: string) : Promise<Result<string, string>> {
async getActivities( try {
account: string, return { status: "ok", data: await TAURI_INVOKE("unfollow", { id }) };
kind: string, } catch (e) {
): Promise<Result<string[], string>> { if(e instanceof Error) throw e;
try { else return { status: "error", error: e as any };
return { }
status: "ok", },
data: await TAURI_INVOKE("get_activities", { account, kind }), async getNstore(key: string) : Promise<Result<string, string>> {
}; try {
} catch (e) { return { status: "ok", data: await TAURI_INVOKE("get_nstore", { key }) };
if (e instanceof Error) throw e; } catch (e) {
else return { status: "error", error: e as any }; if(e instanceof Error) throw e;
} else return { status: "error", error: e as any };
}, }
async getCurrentUserProfile(): Promise<Result<string, string>> { },
try { async setNstore(key: string, content: string) : Promise<Result<string, string>> {
return { try {
status: "ok", return { status: "ok", data: await TAURI_INVOKE("set_nstore", { key, content }) };
data: await TAURI_INVOKE("get_current_user_profile"), } catch (e) {
}; if(e instanceof Error) throw e;
} catch (e) { else return { status: "error", error: e as any };
if (e instanceof Error) throw e; }
else return { status: "error", error: e as any }; },
} async setNwc(uri: string) : Promise<Result<boolean, string>> {
}, try {
async getProfile(id: string): Promise<Result<string, string>> { return { status: "ok", data: await TAURI_INVOKE("set_nwc", { uri }) };
try { } catch (e) {
return { status: "ok", data: await TAURI_INVOKE("get_profile", { id }) }; if(e instanceof Error) throw e;
} catch (e) { else return { status: "error", error: e as any };
if (e instanceof Error) throw e; }
else return { status: "error", error: e as any }; },
} async loadNwc() : Promise<Result<boolean, string>> {
}, try {
async getContactList(): Promise<Result<string[], string>> { return { status: "ok", data: await TAURI_INVOKE("load_nwc") };
try { } catch (e) {
return { status: "ok", data: await TAURI_INVOKE("get_contact_list") }; if(e instanceof Error) throw e;
} catch (e) { else return { status: "error", error: e as any };
if (e instanceof Error) throw e; }
else return { status: "error", error: e as any }; },
} async getBalance() : Promise<Result<string, string>> {
}, try {
async setContactList(pubkeys: string[]): Promise<Result<boolean, string>> { return { status: "ok", data: await TAURI_INVOKE("get_balance") };
try { } catch (e) {
return { if(e instanceof Error) throw e;
status: "ok", else return { status: "error", error: e as any };
data: await TAURI_INVOKE("set_contact_list", { pubkeys }), }
}; },
} catch (e) { async zapProfile(id: string, amount: string, message: string) : Promise<Result<boolean, string>> {
if (e instanceof Error) throw e; try {
else return { status: "error", error: e as any }; return { status: "ok", data: await TAURI_INVOKE("zap_profile", { id, amount, message }) };
} } catch (e) {
}, if(e instanceof Error) throw e;
async createProfile( else return { status: "error", error: e as any };
name: string, }
displayName: string, },
about: string, async zapEvent(id: string, amount: string, message: string) : Promise<Result<boolean, string>> {
picture: string, try {
banner: string, return { status: "ok", data: await TAURI_INVOKE("zap_event", { id, amount, message }) };
nip05: string, } catch (e) {
lud16: string, if(e instanceof Error) throw e;
website: string, else return { status: "error", error: e as any };
): Promise<Result<string, string>> { }
try { },
return { async friendToFriend(npub: string) : Promise<Result<boolean, string>> {
status: "ok", try {
data: await TAURI_INVOKE("create_profile", { return { status: "ok", data: await TAURI_INVOKE("friend_to_friend", { npub }) };
name, } catch (e) {
displayName, if(e instanceof Error) throw e;
about, else return { status: "error", error: e as any };
picture, }
banner, },
nip05, async getNotifications() : Promise<Result<string[], string>> {
lud16, try {
website, return { status: "ok", data: await TAURI_INVOKE("get_notifications") };
}), } catch (e) {
}; if(e instanceof Error) throw e;
} catch (e) { else return { status: "error", error: e as any };
if (e instanceof Error) throw e; }
else return { status: "error", error: e as any }; },
} async getEvent(id: string) : Promise<Result<string, string>> {
}, try {
async follow( return { status: "ok", data: await TAURI_INVOKE("get_event", { id }) };
id: string, } catch (e) {
alias: string | null, if(e instanceof Error) throw e;
): Promise<Result<string, string>> { else return { status: "error", error: e as any };
try { }
return { },
status: "ok", async getReplies(id: string) : Promise<Result<string[], string>> {
data: await TAURI_INVOKE("follow", { id, alias }), try {
}; return { status: "ok", data: await TAURI_INVOKE("get_replies", { id }) };
} catch (e) { } catch (e) {
if (e instanceof Error) throw e; if(e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async unfollow(id: string): Promise<Result<string, string>> { async getEventsBy(publicKey: string, asOf: string | null) : Promise<Result<string[], string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("unfollow", { id }) }; return { status: "ok", data: await TAURI_INVOKE("get_events_by", { publicKey, asOf }) };
} catch (e) { } catch (e) {
if (e instanceof Error) throw e; if(e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async getNstore(key: string): Promise<Result<string, string>> { async getLocalEvents(pubkeys: string[], until: string | null) : Promise<Result<string[], string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("get_nstore", { key }) }; return { status: "ok", data: await TAURI_INVOKE("get_local_events", { pubkeys, until }) };
} catch (e) { } catch (e) {
if (e instanceof Error) throw e; if(e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async setNstore( async getGlobalEvents(until: string | null) : Promise<Result<string[], string>> {
key: string, try {
content: string, return { status: "ok", data: await TAURI_INVOKE("get_global_events", { until }) };
): Promise<Result<string, string>> { } catch (e) {
try { if(e instanceof Error) throw e;
return { else return { status: "error", error: e as any };
status: "ok", }
data: await TAURI_INVOKE("set_nstore", { key, content }), },
}; async getHashtagEvents(hashtags: string[], until: string | null) : Promise<Result<string[], string>> {
} catch (e) { try {
if (e instanceof Error) throw e; return { status: "ok", data: await TAURI_INVOKE("get_hashtag_events", { hashtags, until }) };
else return { status: "error", error: e as any }; } catch (e) {
} if(e instanceof Error) throw e;
}, else return { status: "error", error: e as any };
async setNwc(uri: string): Promise<Result<boolean, string>> { }
try { },
return { status: "ok", data: await TAURI_INVOKE("set_nwc", { uri }) }; async publish(content: string, tags: string[][]) : Promise<Result<string, string>> {
} catch (e) { try {
if (e instanceof Error) throw e; return { status: "ok", data: await TAURI_INVOKE("publish", { content, tags }) };
else return { status: "error", error: e as any }; } catch (e) {
} if(e instanceof Error) throw e;
}, else return { status: "error", error: e as any };
async loadNwc(): Promise<Result<boolean, string>> { }
try { },
return { status: "ok", data: await TAURI_INVOKE("load_nwc") }; async repost(raw: string) : Promise<Result<string, string>> {
} catch (e) { try {
if (e instanceof Error) throw e; return { status: "ok", data: await TAURI_INVOKE("repost", { raw }) };
else return { status: "error", error: e as any }; } catch (e) {
} if(e instanceof Error) throw e;
}, else return { status: "error", error: e as any };
async getBalance(): Promise<Result<string, string>> { }
try { },
return { status: "ok", data: await TAURI_INVOKE("get_balance") }; async showInFolder(path: string) : Promise<void> {
} catch (e) { await TAURI_INVOKE("show_in_folder", { path });
if (e instanceof Error) throw e; },
else return { status: "error", error: e as any }; async createColumn(label: string, x: number, y: number, width: number, height: number, url: string) : Promise<Result<string, string>> {
} try {
}, return { status: "ok", data: await TAURI_INVOKE("create_column", { label, x, y, width, height, url }) };
async zapProfile( } catch (e) {
id: string, if(e instanceof Error) throw e;
amount: string, else return { status: "error", error: e as any };
message: string, }
): Promise<Result<boolean, string>> { },
try { async closeColumn(label: string) : Promise<Result<boolean, null>> {
return { try {
status: "ok", return { status: "ok", data: await TAURI_INVOKE("close_column", { label }) };
data: await TAURI_INVOKE("zap_profile", { id, amount, message }), } catch (e) {
}; if(e instanceof Error) throw e;
} catch (e) { else return { status: "error", error: e as any };
if (e instanceof Error) throw e; }
else return { status: "error", error: e as any }; },
} async repositionColumn(label: string, x: number, y: number) : Promise<Result<null, string>> {
}, try {
async zapEvent( return { status: "ok", data: await TAURI_INVOKE("reposition_column", { label, x, y }) };
id: string, } catch (e) {
amount: string, if(e instanceof Error) throw e;
message: string, else return { status: "error", error: e as any };
): Promise<Result<boolean, string>> { }
try { },
return { async resizeColumn(label: string, width: number, height: number) : Promise<Result<null, string>> {
status: "ok", try {
data: await TAURI_INVOKE("zap_event", { id, amount, message }), return { status: "ok", data: await TAURI_INVOKE("resize_column", { label, width, height }) };
}; } catch (e) {
} catch (e) { if(e instanceof Error) throw e;
if (e instanceof Error) throw e; else return { status: "error", error: e as any };
else return { status: "error", error: e as any }; }
} },
}, async openWindow(label: string, title: string, url: string, width: number, height: number) : Promise<Result<null, string>> {
async friendToFriend(npub: string): Promise<Result<boolean, string>> { try {
try { return { status: "ok", data: await TAURI_INVOKE("open_window", { label, title, url, width, height }) };
return { } catch (e) {
status: "ok", if(e instanceof Error) throw e;
data: await TAURI_INVOKE("friend_to_friend", { npub }), else return { status: "error", error: e as any };
}; }
} catch (e) { },
if (e instanceof Error) throw e; async setBadge(count: number) : Promise<void> {
else return { status: "error", error: e as any }; await TAURI_INVOKE("set_badge", { count });
} }
}, }
async getEvent(id: string): Promise<Result<string, string>> {
try { /** user-defined events **/
return { status: "ok", data: await TAURI_INVOKE("get_event", { id }) };
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; /** user-defined statics **/
}
},
async getReplies(id: string): Promise<Result<string[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_replies", { 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<string[], string>> {
try {
return {
status: "ok",
data: await TAURI_INVOKE("get_events_by", { publicKey, asOf }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getLocalEvents(
pubkeys: string[],
until: string | null,
): Promise<Result<string[], string>> {
try {
return {
status: "ok",
data: await TAURI_INVOKE("get_local_events", { pubkeys, until }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getGlobalEvents(
until: string | null,
): Promise<Result<string[], string>> {
try {
return {
status: "ok",
data: await TAURI_INVOKE("get_global_events", { until }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getHashtagEvents(
hashtags: string[],
until: string | null,
): Promise<Result<string[], string>> {
try {
return {
status: "ok",
data: await TAURI_INVOKE("get_hashtag_events", { hashtags, until }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async publish(
content: string,
tags: string[][],
): Promise<Result<string, string>> {
try {
return {
status: "ok",
data: await TAURI_INVOKE("publish", { content, tags }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async repost(raw: string): Promise<Result<string, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("repost", { raw }) };
} catch (e) {
if (e instanceof Error) throw e;
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>> {
try {
return {
status: "ok",
data: await TAURI_INVOKE("create_column", {
label,
x,
y,
width,
height,
url,
}),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async closeColumn(label: string): Promise<Result<boolean, null>> {
try {
return {
status: "ok",
data: await TAURI_INVOKE("close_column", { label }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async repositionColumn(
label: string,
x: number,
y: number,
): Promise<Result<null, string>> {
try {
return {
status: "ok",
data: await TAURI_INVOKE("reposition_column", { label, x, y }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async resizeColumn(
label: string,
width: number,
height: number,
): Promise<Result<null, string>> {
try {
return {
status: "ok",
data: await TAURI_INVOKE("resize_column", { label, width, height }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async openWindow(
label: string,
title: string,
url: string,
width: number,
height: number,
): Promise<Result<null, string>> {
try {
return {
status: "ok",
data: await TAURI_INVOKE("open_window", {
label,
title,
url,
width,
height,
}),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async setBadge(count: number): Promise<void> {
await TAURI_INVOKE("set_badge", { count });
},
};
/** user-defined types **/ /** user-defined types **/
export type Account = { npub: string; nsec: string }; export type Account = { npub: string; nsec: string }
export type Relays = { export type Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null }
connected: string[];
read: string[] | null;
write: string[] | null;
both: string[] | null;
};
/** tauri-specta globals **/ /** tauri-specta globals **/
import { invoke as TAURI_INVOKE } from "@tauri-apps/api/core"; import { invoke as TAURI_INVOKE } from "@tauri-apps/api/core";
import * as TAURI_API_EVENT from "@tauri-apps/api/event"; import * as TAURI_API_EVENT from "@tauri-apps/api/event";
import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow";
type __EventObj__<T> = { type __EventObj__<T> = {
listen: ( listen: (
cb: TAURI_API_EVENT.EventCallback<T>, cb: TAURI_API_EVENT.EventCallback<T>
) => ReturnType<typeof TAURI_API_EVENT.listen<T>>; ) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
once: ( once: (
cb: TAURI_API_EVENT.EventCallback<T>, cb: TAURI_API_EVENT.EventCallback<T>
) => ReturnType<typeof TAURI_API_EVENT.once<T>>; ) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
emit: T extends null emit: T extends null
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit> ? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>; : (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
}; };
export type Result<T, E> = export type Result<T, E> =
| { status: "ok"; data: T } | { status: "ok"; data: T }
| { status: "error"; error: E }; | { status: "error"; error: E };
function __makeEvents__<T extends Record<string, any>>( function __makeEvents__<T extends Record<string, any>>(
mappings: Record<keyof T, string>, mappings: Record<keyof T, string>
) { ) {
return new Proxy( return new Proxy(
{} as unknown as { {} as unknown as {
[K in keyof T]: __EventObj__<T[K]> & { [K in keyof T]: __EventObj__<T[K]> & {
(handle: __WebviewWindow__): __EventObj__<T[K]>; (handle: __WebviewWindow__): __EventObj__<T[K]>;
}; };
}, },
{ {
get: (_, event) => { get: (_, event) => {
const name = mappings[event as keyof T]; const name = mappings[event as keyof T];
return new Proxy((() => {}) as any, { return new Proxy((() => {}) as any, {
apply: (_, __, [window]: [__WebviewWindow__]) => ({ apply: (_, __, [window]: [__WebviewWindow__]) => ({
listen: (arg: any) => window.listen(name, arg), listen: (arg: any) => window.listen(name, arg),
once: (arg: any) => window.once(name, arg), once: (arg: any) => window.once(name, arg),
emit: (arg: any) => window.emit(name, arg), emit: (arg: any) => window.emit(name, arg),
}), }),
get: (_, command: keyof __EventObj__<any>) => { get: (_, command: keyof __EventObj__<any>) => {
switch (command) { switch (command) {
case "listen": case "listen":
return (arg: any) => TAURI_API_EVENT.listen(name, arg); return (arg: any) => TAURI_API_EVENT.listen(name, arg);
case "once": case "once":
return (arg: any) => TAURI_API_EVENT.once(name, arg); return (arg: any) => TAURI_API_EVENT.once(name, arg);
case "emit": case "emit":
return (arg: any) => TAURI_API_EVENT.emit(name, arg); return (arg: any) => TAURI_API_EVENT.emit(name, arg);
} }
}, },
}); });
}, },
}, }
); );
} }
+22 -6
View File
@@ -22,7 +22,7 @@ export class LumeEvent {
return this.tags.filter((tag) => tag[0] === "p").map((tag) => tag[1]); return this.tags.filter((tag) => tag[0] === "p").map((tag) => tag[1]);
} }
static getEventThread(tags: string[][], gossip?: boolean) { static getEventThread(tags: string[][], gossip = true) {
let root: string = null; let root: string = null;
let reply: string = null; let reply: string = null;
@@ -30,14 +30,22 @@ export class LumeEvent {
const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention"); const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention");
if (gossip) { if (gossip) {
const relays = tags.filter((el) => el[0] === "e" && el[2]?.length); const relays = tags
.filter((el) => el[0] === "e" && el[2]?.length)
.map((tag) => tag[2]);
if (relays.length >= 1) { if (relays.length >= 1) {
for (const relay of relays) { for (const relay of relays) {
if (relay[2]?.length) try {
commands if (relay.length) {
.connectRelay(relay[2]) const url = new URL(relay);
.then(() => console.log("[gossip]: ", relay[2])); commands
.connectRelay(url.toString())
.then(() => console.log("[relay hint]: ", url));
}
} catch (e) {
console.log("[relay hint] error: ", relay);
}
} }
} }
} }
@@ -82,6 +90,14 @@ export class LumeEvent {
for (const tag of tags) { for (const tag of tags) {
const rootIndex = events.findIndex((el) => el.id === tag[1]); const rootIndex = events.findIndex((el) => el.id === tag[1]);
// Relay Hint
if (tag[2]?.length) {
const url = new URL(tag[2]);
commands
.connectRelay(url.toString())
.then(() => console.log("[relay hint]: ", url));
}
if (rootIndex !== -1) { if (rootIndex !== -1) {
const rootEvent = events[rootIndex]; const rootEvent = events[rootIndex];
+28 -29
View File
@@ -68,6 +68,18 @@ export class NostrQuery {
} }
} }
static async getNotifications() {
const query = await commands.getNotifications();
if (query.status === "ok") {
const events = query.data.map((item) => JSON.parse(item) as NostrEvent);
return events;
} else {
console.error(query.error);
return [];
}
}
static async getProfile(pubkey: string) { static async getProfile(pubkey: string) {
const normalize = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, ""); const normalize = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, "");
const query = await commands.getProfile(normalize); const query = await commands.getProfile(normalize);
@@ -104,20 +116,6 @@ export class NostrQuery {
} }
} }
static async getUserActivities(
account: string,
kind: "1" | "6" | "9735" = "1",
) {
const query = await commands.getActivities(account, kind);
if (query.status === "ok") {
const events = query.data.map((item) => JSON.parse(item) as NostrEvent);
return events;
} else {
return [];
}
}
static async getLocalEvents(pubkeys: string[], asOf?: number) { static async getLocalEvents(pubkeys: string[], asOf?: number) {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined; const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const query = await commands.getLocalEvents(pubkeys, until); const query = await commands.getLocalEvents(pubkeys, until);
@@ -236,31 +234,32 @@ export class NostrQuery {
} }
static async getColumns() { static async getColumns() {
const systemPath = "resources/system_columns.json";
const resourcePath = await resolveResource(systemPath);
const resourceFile = await readTextFile(resourcePath);
const systemColumns: LumeColumn[] = JSON.parse(resourceFile);
const query = await commands.getNstore(NSTORE_KEYS.columns); const query = await commands.getNstore(NSTORE_KEYS.columns);
if (query.status === "ok") { try {
const columns: LumeColumn[] = query.data ? JSON.parse(query.data) : []; if (query.status === "ok") {
const columns: LumeColumn[] = JSON.parse(query.data);
if (columns.length < 1) { if (!columns?.length) {
const systemPath = "resources/system_columns.json"; return systemColumns;
const resourcePath = await resolveResource(systemPath); }
const resourceFile = await readTextFile(resourcePath);
const systemColumns: LumeColumn[] = JSON.parse(resourceFile);
return columns;
} else {
return systemColumns; return systemColumns;
} }
} catch {
return columns; return systemColumns;
} else {
return [];
} }
} }
static async setColumns(columns: LumeColumn[]) { static async setColumns(columns: LumeColumn[]) {
const query = await commands.setNstore( const content = JSON.stringify(columns);
NSTORE_KEYS.columns, const query = await commands.setNstore(NSTORE_KEYS.columns, content);
JSON.stringify(columns),
);
if (query.status === "ok") { if (query.status === "ok") {
return query.data; return query.data;
+1 -1
View File
@@ -92,7 +92,7 @@ export class LumeWindow {
const query = await commands.openWindow( const query = await commands.openWindow(
label, label,
"Settings", "Settings",
"/settings", "/settings/general",
800, 800,
500, 500,
); );
+1 -1
View File
@@ -110,7 +110,7 @@ export interface LumeColumn {
} }
export interface EventColumns { export interface EventColumns {
type: "add" | "remove" | "update" | "left" | "right" | "set_title"; type: "reset" | "add" | "remove" | "update" | "left" | "right" | "set_title";
label?: string; label?: string;
title?: string; title?: string;
column?: LumeColumn; column?: LumeColumn;
+1 -1
View File
@@ -14,7 +14,7 @@
"@lume/tailwindcss": "workspace:^", "@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^", "@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^", "@lume/types": "workspace:^",
"@types/react": "^18.3.2", "@types/react": "^18.3.3",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
"typescript": "^5.4.5" "typescript": "^5.4.5"
} }
+4 -4
View File
@@ -8,21 +8,21 @@
"access": "public" "access": "public"
}, },
"dependencies": { "dependencies": {
"@tanstack/react-query": "^5.36.0", "@tanstack/react-query": "^5.40.0",
"bitcoin-units": "^1.0.0", "bitcoin-units": "^1.0.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"light-bolt11-decoder": "^3.1.1", "light-bolt11-decoder": "^3.1.1",
"nostr-tools": "^2.5.2", "nostr-tools": "^2.7.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"slate": "^0.103.0", "slate": "^0.103.0",
"slate-react": "^0.102.0" "slate-react": "^0.104.0"
}, },
"devDependencies": { "devDependencies": {
"@lume/tsconfig": "workspace:^", "@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^", "@lume/types": "workspace:^",
"@types/react": "^18.3.2", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",
"typescript": "^5.4.5" "typescript": "^5.4.5"
+1031 -997
View File
File diff suppressed because it is too large Load Diff
+558 -538
View File
File diff suppressed because it is too large Load Diff
+16 -16
View File
@@ -5,7 +5,7 @@ description = "nostr client"
authors = ["npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445"] authors = ["npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445"]
repository = "https://github.com/lumehq/lume" repository = "https://github.com/lumehq/lume"
edition = "2021" edition = "2021"
rust-version = "1.68" rust-version = "1.70"
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.0.0-beta", features = [] } tauri-build = { version = "2.0.0-beta", features = [] }
@@ -15,6 +15,7 @@ nostr-sdk = { version = "0.31", features = ["sqlite"] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
monitor = { git = "https://github.com/ahkohd/tauri-toolkit", branch = "v2" }
tauri = { version = "2.0.0-beta", features = [ tauri = { version = "2.0.0-beta", features = [
"unstable", "unstable",
"tray-icon", "tray-icon",
@@ -22,24 +23,23 @@ tauri = { version = "2.0.0-beta", features = [
"native-tls-vendored", "native-tls-vendored",
"protocol-asset", "protocol-asset",
] } ] }
tauri-plugin-clipboard-manager = "2.1.0-beta" tauri-plugin-clipboard-manager = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-dialog = "2.0.0-beta" tauri-plugin-dialog = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-fs = "2.0.0-beta" tauri-plugin-fs = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-http = "2.0.0-beta" tauri-plugin-http = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-notification = "2.0.0-beta" tauri-plugin-notification = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-os = "2.0.0-beta" tauri-plugin-os = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-process = "2.0.0-beta" tauri-plugin-process = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-shell = "2.0.0-beta" tauri-plugin-shell = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-updater = "2.0.0-beta" tauri-plugin-updater = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-upload = "2.0.0-beta" tauri-plugin-upload = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-window-state = "2.0.0-beta" tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
tauri-specta = { version = "^2.0.0-rc.11", features = ["typescript"] }
tauri-plugin-theme = "0.4.1"
tauri-plugin-decorum = "0.1.0" tauri-plugin-decorum = "0.1.0"
webpage = { version = "2.0", features = ["serde"] } specta = "^2.0.0-rc.12"
keyring = "2" keyring = "2"
keyring-search = "0.2.0" keyring-search = "0.2.0"
specta = "=2.0.0-rc.12"
tauri-specta = { version = "=2.0.0-rc.10", features = ["typescript"] }
tauri-plugin-theme = "0.4.1"
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.25.0" cocoa = "0.25.0"
+84 -83
View File
@@ -1,85 +1,86 @@
{ {
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "desktop-capability", "identifier": "desktop-capability",
"description": "Capability for the desktop", "description": "Capability for the desktop",
"platforms": ["linux", "macOS", "windows"], "platforms": ["linux", "macOS", "windows"],
"windows": [ "windows": [
"main", "main",
"splash", "panel",
"settings", "splash",
"search", "settings",
"nwc", "search",
"activity", "nwc",
"zap-*", "activity",
"event-*", "zap-*",
"user-*", "event-*",
"editor-*", "user-*",
"column-*" "editor-*",
], "column-*"
"permissions": [ ],
"path:default", "permissions": [
"event:default", "path:default",
"window:default", "event:default",
"app:default", "window:default",
"resources:default", "app:default",
"menu:default", "resources:default",
"tray:default", "menu:default",
"notification:allow-is-permission-granted", "tray:default",
"notification:allow-request-permission", "notification:allow-is-permission-granted",
"notification:default", "notification:allow-request-permission",
"os:allow-locale", "notification:default",
"os:allow-platform", "os:allow-locale",
"os:allow-os-type", "os:allow-platform",
"updater:default", "os:allow-os-type",
"updater:allow-check", "updater:default",
"updater:allow-download-and-install", "updater:allow-check",
"window:allow-start-dragging", "updater:allow-download-and-install",
"window:allow-create", "window:allow-start-dragging",
"window:allow-close", "window:allow-create",
"window:allow-set-focus", "window:allow-close",
"window:allow-center", "window:allow-set-focus",
"window:allow-minimize", "window:allow-center",
"window:allow-maximize", "window:allow-minimize",
"window:allow-set-size", "window:allow-maximize",
"window:allow-set-focus", "window:allow-set-size",
"window:allow-start-dragging", "window:allow-set-focus",
"decorum:allow-show-snap-overlay", "window:allow-start-dragging",
"clipboard-manager:allow-write-text", "decorum:allow-show-snap-overlay",
"clipboard-manager:allow-read-text", "clipboard-manager:allow-write-text",
"webview:allow-create-webview-window", "clipboard-manager:allow-read-text",
"webview:allow-create-webview", "webview:allow-create-webview-window",
"webview:allow-set-webview-size", "webview:allow-create-webview",
"webview:allow-set-webview-position", "webview:allow-set-webview-size",
"webview:allow-webview-close", "webview:allow-set-webview-position",
"dialog:allow-open", "webview:allow-webview-close",
"dialog:allow-ask", "dialog:allow-open",
"dialog:allow-message", "dialog:allow-ask",
"process:allow-restart", "dialog:allow-message",
"fs:allow-read-file", "process:allow-restart",
"theme:allow-set-theme", "fs:allow-read-file",
"theme:allow-get-theme", "theme:allow-set-theme",
"shell:allow-open", "theme:allow-get-theme",
{ "shell:allow-open",
"identifier": "http:default", {
"allow": [ "identifier": "http:default",
{ "allow": [
"url": "http://**/" {
}, "url": "http://**/"
{ },
"url": "https://**/" {
} "url": "https://**/"
] }
}, ]
{ },
"identifier": "fs:allow-read-text-file", {
"allow": [ "identifier": "fs:allow-read-text-file",
{ "allow": [
"path": "$RESOURCE/locales/*" {
}, "path": "$RESOURCE/locales/*"
{ },
"path": "$RESOURCE/resources/*" {
} "path": "$RESOURCE/resources/*"
] }
} ]
] }
]
} }
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","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","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","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","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"]}}
+42 -48
View File
@@ -5863,6 +5863,13 @@
"updater:allow-check" "updater:allow-check"
] ]
}, },
{
"description": "updater:allow-download -> Enables the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-download"
]
},
{ {
"description": "updater:allow-download-and-install -> Enables the download_and_install command without any pre-configured scope.", "description": "updater:allow-download-and-install -> Enables the download_and_install command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5870,6 +5877,13 @@
"updater:allow-download-and-install" "updater:allow-download-and-install"
] ]
}, },
{
"description": "updater:allow-install -> Enables the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-install"
]
},
{ {
"description": "updater:deny-check -> Denies the check command without any pre-configured scope.", "description": "updater:deny-check -> Denies the check command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5877,6 +5891,13 @@
"updater:deny-check" "updater:deny-check"
] ]
}, },
{
"description": "updater:deny-download -> Denies the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-download"
]
},
{ {
"description": "updater:deny-download-and-install -> Denies the download_and_install command without any pre-configured scope.", "description": "updater:deny-download-and-install -> Denies the download_and_install command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5884,6 +5905,13 @@
"updater:deny-download-and-install" "updater:deny-download-and-install"
] ]
}, },
{
"description": "updater:deny-install -> Denies the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-install"
]
},
{ {
"type": "string", "type": "string",
"enum": [ "enum": [
@@ -6261,6 +6289,13 @@
"window:allow-minimize" "window:allow-minimize"
] ]
}, },
{
"description": "window:allow-monitor-from-point -> Enables the monitor_from_point command without any pre-configured scope.",
"type": "string",
"enum": [
"window:allow-monitor-from-point"
]
},
{ {
"description": "window:allow-outer-position -> Enables the outer_position command without any pre-configured scope.", "description": "window:allow-outer-position -> Enables the outer_position command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -6695,6 +6730,13 @@
"window:deny-minimize" "window:deny-minimize"
] ]
}, },
{
"description": "window:deny-monitor-from-point -> Denies the monitor_from_point command without any pre-configured scope.",
"type": "string",
"enum": [
"window:deny-monitor-from-point"
]
},
{ {
"description": "window:deny-outer-position -> Denies the outer_position command without any pre-configured scope.", "description": "window:deny-outer-position -> Denies the outer_position command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -6967,54 +7009,6 @@
"enum": [ "enum": [
"window:deny-unminimize" "window:deny-unminimize"
] ]
},
{
"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"
]
} }
] ]
}, },
+42 -48
View File
@@ -5863,6 +5863,13 @@
"updater:allow-check" "updater:allow-check"
] ]
}, },
{
"description": "updater:allow-download -> Enables the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-download"
]
},
{ {
"description": "updater:allow-download-and-install -> Enables the download_and_install command without any pre-configured scope.", "description": "updater:allow-download-and-install -> Enables the download_and_install command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5870,6 +5877,13 @@
"updater:allow-download-and-install" "updater:allow-download-and-install"
] ]
}, },
{
"description": "updater:allow-install -> Enables the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-install"
]
},
{ {
"description": "updater:deny-check -> Denies the check command without any pre-configured scope.", "description": "updater:deny-check -> Denies the check command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5877,6 +5891,13 @@
"updater:deny-check" "updater:deny-check"
] ]
}, },
{
"description": "updater:deny-download -> Denies the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-download"
]
},
{ {
"description": "updater:deny-download-and-install -> Denies the download_and_install command without any pre-configured scope.", "description": "updater:deny-download-and-install -> Denies the download_and_install command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5884,6 +5905,13 @@
"updater:deny-download-and-install" "updater:deny-download-and-install"
] ]
}, },
{
"description": "updater:deny-install -> Denies the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-install"
]
},
{ {
"type": "string", "type": "string",
"enum": [ "enum": [
@@ -6261,6 +6289,13 @@
"window:allow-minimize" "window:allow-minimize"
] ]
}, },
{
"description": "window:allow-monitor-from-point -> Enables the monitor_from_point command without any pre-configured scope.",
"type": "string",
"enum": [
"window:allow-monitor-from-point"
]
},
{ {
"description": "window:allow-outer-position -> Enables the outer_position command without any pre-configured scope.", "description": "window:allow-outer-position -> Enables the outer_position command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -6695,6 +6730,13 @@
"window:deny-minimize" "window:deny-minimize"
] ]
}, },
{
"description": "window:deny-monitor-from-point -> Denies the monitor_from_point command without any pre-configured scope.",
"type": "string",
"enum": [
"window:deny-monitor-from-point"
]
},
{ {
"description": "window:deny-outer-position -> Denies the outer_position command without any pre-configured scope.", "description": "window:deny-outer-position -> Denies the outer_position command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -6967,54 +7009,6 @@
"enum": [ "enum": [
"window:deny-unminimize" "window:deny-unminimize"
] ]
},
{
"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"
]
} }
] ]
}, },
Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

+180
View File
@@ -0,0 +1,180 @@
use cocoa::appkit::NSWindowCollectionBehavior;
use std::ffi::CString;
use tauri::Manager;
use tauri_nspanel::{
block::ConcreteBlock,
cocoa::{
appkit::{NSMainMenuWindowLevel, NSView, NSWindow},
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 window = app_handle.get_webview_window("panel").unwrap();
let panel = window.to_panel().unwrap();
let handle = app_handle.to_owned();
let delegate = panel_delegate!(MyPanelDelegate {
window_did_become_key,
window_did_resign_key
});
delegate.set_listener(Box::new(move |delegate_name: String| {
match delegate_name.as_str() {
"window_did_become_key" => {
let app_name = handle.package_info().name.to_owned();
println!("[info]: {:?} panel becomes key window!", app_name);
}
"window_did_resign_key" => {
println!("[info]: panel resigned from key window!");
}
_ => (),
}
}));
panel.set_level(NSMainMenuWindowLevel + 1);
panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel);
panel.set_collection_behaviour(
NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces
| NSWindowCollectionBehavior::NSWindowCollectionBehaviorStationary
| NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary,
);
panel.set_delegate(delegate);
}
pub fn setup_menubar_panel_listeners(app_handle: &tauri::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 update_menubar_appearance(app_handle: &tauri::AppHandle) {
let window = app_handle.get_window("panel").unwrap();
set_corner_radius(&window, 13.0);
}
pub fn set_corner_radius(window: &tauri::Window, radius: f64) {
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()
}
+49 -13
View File
@@ -4,8 +4,8 @@
)] )]
pub mod commands; pub mod commands;
pub mod fns;
pub mod nostr; pub mod nostr;
pub mod tray;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
extern crate cocoa; extern crate cocoa;
@@ -17,8 +17,18 @@ extern crate objc;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use std::fs; use std::fs;
use tauri::Manager; use tauri::Manager;
use tauri_nspanel::ManagerExt;
use tauri_plugin_decorum::WebviewWindowExt; use tauri_plugin_decorum::WebviewWindowExt;
#[cfg(target_os = "macos")]
use crate::fns::{
position_menubar_panel, setup_menubar_panel_listeners, swizzle_to_menubar_panel,
update_menubar_appearance,
};
#[cfg(target_os = "macos")]
use tauri::tray::{MouseButtonState, TrayIconEvent};
pub struct Nostr { pub struct Nostr {
client: Client, client: Client,
} }
@@ -34,13 +44,11 @@ fn main() {
nostr::keys::create_account, nostr::keys::create_account,
nostr::keys::save_account, nostr::keys::save_account,
nostr::keys::get_encrypted_key, nostr::keys::get_encrypted_key,
nostr::keys::nostr_connect, nostr::keys::connect_remote_account,
nostr::keys::load_account, nostr::keys::load_account,
nostr::keys::event_to_bech32, nostr::keys::event_to_bech32,
nostr::keys::user_to_bech32, nostr::keys::user_to_bech32,
nostr::keys::to_npub,
nostr::keys::verify_nip05, nostr::keys::verify_nip05,
nostr::metadata::get_activities,
nostr::metadata::get_current_user_profile, nostr::metadata::get_current_user_profile,
nostr::metadata::get_profile, nostr::metadata::get_profile,
nostr::metadata::get_contact_list, nostr::metadata::get_contact_list,
@@ -56,6 +64,7 @@ fn main() {
nostr::metadata::zap_profile, nostr::metadata::zap_profile,
nostr::metadata::zap_event, nostr::metadata::zap_event,
nostr::metadata::friend_to_friend, nostr::metadata::friend_to_friend,
nostr::metadata::get_notifications,
nostr::event::get_event, nostr::event::get_event,
nostr::event::get_replies, nostr::event::get_replies,
nostr::event::get_events_by, nostr::event::get_events_by,
@@ -91,20 +100,46 @@ fn main() {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
main_window.set_traffic_lights_inset(8.0, 16.0).unwrap(); main_window.set_traffic_lights_inset(8.0, 16.0).unwrap();
// Setup app tray // Create panel
let handle = app.handle().clone(); #[cfg(target_os = "macos")]
tray::create_tray(app.handle()).unwrap(); swizzle_to_menubar_panel(&app.handle());
#[cfg(target_os = "macos")]
update_menubar_appearance(&app.handle());
#[cfg(target_os = "macos")]
setup_menubar_panel_listeners(&app.handle());
// Setup tray icon
#[cfg(target_os = "macos")]
let tray = app.tray_by_id("tray_panel").unwrap();
// Handle tray icon event
#[cfg(target_os = "macos")]
tray.on_tray_icon_event(|tray, event| match event {
TrayIconEvent::Click { button_state, .. } => {
if button_state == MouseButtonState::Up {
let app = tray.app_handle();
let panel = app.get_webview_panel("panel").unwrap();
match panel.is_visible() {
true => panel.order_out(None),
false => {
position_menubar_panel(&app, 0.0);
panel.show();
}
}
}
}
_ => {}
});
// Create data folder if not exist // Create data folder if not exist
let home_dir = app.path().home_dir().unwrap(); let home_dir = app.path().home_dir().unwrap();
fs::create_dir_all(home_dir.join("Lume/")).unwrap(); let _ = fs::create_dir_all(home_dir.join("Lume/"));
tauri::async_runtime::block_on(async move { tauri::async_runtime::block_on(async move {
// Create nostr database connection
let sqlite = SQLiteDatabase::open(home_dir.join("Lume/lume.db")).await;
// Create nostr connection // Create nostr connection
let client = match sqlite { let database = SQLiteDatabase::open(home_dir.join("Lume/lume.db")).await;
let client = match database {
Ok(db) => ClientBuilder::default().database(db).build(), Ok(db) => ClientBuilder::default().database(db).build(),
Err(_) => ClientBuilder::default().build(), Err(_) => ClientBuilder::default().build(),
}; };
@@ -130,11 +165,12 @@ fn main() {
client.connect().await; client.connect().await;
// Update global state // Update global state
handle.manage(Nostr { client }) app.handle().manage(Nostr { client })
}); });
Ok(()) Ok(())
}) })
.plugin(tauri_nspanel::init())
.plugin(tauri_plugin_theme::init(ctx.config_mut())) .plugin(tauri_plugin_theme::init(ctx.config_mut()))
.plugin(tauri_plugin_decorum::init()) .plugin(tauri_plugin_decorum::init())
.plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_clipboard_manager::init())
+5 -6
View File
@@ -13,9 +13,8 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<String, Stri
Nip19::Event(event) => { Nip19::Event(event) => {
let relays = event.relays; let relays = event.relays;
for relay in relays.into_iter() { for relay in relays.into_iter() {
let url = Url::from_str(&relay).unwrap(); let _ = client.add_relay(&relay).await.unwrap_or_default();
let _ = client.add_relay(&url).await.unwrap_or_default(); client.connect_relay(&relay).await.unwrap_or_default();
client.connect_relay(&url).await.unwrap_or_default();
} }
Some(event.event_id) Some(event.event_id)
} }
@@ -122,11 +121,11 @@ pub async fn get_local_events(
let filter = Filter::new() let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost]) .kinds(vec![Kind::TextNote, Kind::Repost])
.limit(20) .limit(20)
.authors(authors) .until(as_of)
.until(as_of); .authors(authors);
match client match client
.get_events_of(vec![filter], Some(Duration::from_secs(8))) .get_events_of(vec![filter], Some(Duration::from_secs(10)))
.await .await
{ {
Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()), Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()),
+197 -127
View File
@@ -6,7 +6,8 @@ use serde::Serialize;
use specta::Type; use specta::Type;
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use tauri::{Manager, State}; use tauri::{EventTarget, Manager, State};
use tauri_plugin_notification::NotificationExt;
#[derive(Serialize, Type)] #[derive(Serialize, Type)]
pub struct Account { pub struct Account {
@@ -102,158 +103,236 @@ pub async fn save_account(
#[specta::specta] #[specta::specta]
pub async fn load_account( pub async fn load_account(
npub: &str, npub: &str,
bunker: Option<&str>,
state: State<'_, Nostr>, state: State<'_, Nostr>,
app: tauri::AppHandle, app: tauri::AppHandle,
) -> Result<bool, String> { ) -> Result<bool, String> {
let handle = app.clone();
let client = &state.client; let client = &state.client;
let keyring = Entry::new(npub, "nostr_secret").unwrap(); let keyring = Entry::new(npub, "nostr_secret").unwrap();
match keyring.get_password() { if let Ok(password) = keyring.get_password() {
Ok(password) => { match bunker {
if password.starts_with("bunker://") { Some(uri) => {
let local_keyring = Entry::new(npub, "bunker_local_account").unwrap(); let app_keys = Keys::parse(password).expect("Secret Key is modified, please check again.");
match local_keyring.get_password() { match NostrConnectURI::parse(uri) {
Ok(local_password) => { Ok(bunker_uri) => {
let secret_key = SecretKey::from_bech32(local_password).unwrap(); match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(30), None).await {
let app_keys = Keys::new(secret_key); Ok(signer) => client.set_signer(Some(signer.into())).await,
let bunker_uri = NostrConnectURI::parse(password).unwrap(); Err(err) => return Err(err.to_string()),
let signer = Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(60), None) }
.await
.unwrap();
// Update signer
client.set_signer(Some(signer.into())).await;
} }
Err(_) => todo!(), Err(err) => return Err(err.to_string()),
} }
} else { }
let secret_key = SecretKey::from_bech32(password).expect("Get secret key failed"); None => {
let keys = Keys::new(secret_key); let keys = Keys::parse(password).expect("Secret Key is modified, please check again.");
let signer = NostrSigner::Keys(keys); let signer = NostrSigner::Keys(keys);
// Update signer // Update signer
client.set_signer(Some(signer)).await; client.set_signer(Some(signer)).await;
} }
}
// Verify signer // Verify signer
let signer = client.signer().await.unwrap(); let signer = client.signer().await.unwrap();
let public_key = signer.public_key().await.unwrap(); let public_key = signer.public_key().await.unwrap();
let filter = Filter::new() let filter = Filter::new()
.author(public_key) .author(public_key)
.kind(Kind::RelayList) .kind(Kind::RelayList)
.limit(1); .limit(1);
// Connect to user's relay (NIP-65) // Connect to user's relay (NIP-65)
// #TODO: Let rust-nostr handle it // #TODO: Let rust-nostr handle it
match client if let Ok(events) = client
.get_events_of(vec![filter], Some(Duration::from_secs(10))) .get_events_of(vec![filter], Some(Duration::from_secs(10)))
.await .await
{ {
Ok(events) => { if let Some(event) = events.first() {
if let Some(event) = events.first() { let relay_list = nip65::extract_relay_list(event);
let relay_list = nip65::extract_relay_list(event); for item in relay_list.into_iter() {
for item in relay_list.into_iter() { println!("connecting to relay: {} - {:?}", item.0, item.1);
println!("connecting to relay: {} - {:?}", item.0, item.1);
let relay_url = item.0.to_string(); let relay_url = item.0.to_string();
let opts = match item.1 { let opts = match item.1 {
Some(val) => { Some(val) => {
if val == &RelayMetadata::Read { if val == &RelayMetadata::Read {
RelayOptions::new().read(true).write(false) RelayOptions::new().read(true).write(false)
} else { } else {
RelayOptions::new().write(true).read(false) RelayOptions::new().write(true).read(false)
}
}
None => RelayOptions::new(),
};
// Add relay to relay pool
let _ = client
.add_relay_with_opts(relay_url.clone(), opts)
.await
.unwrap_or_default();
// Connect relay
client.connect_relay(relay_url).await.unwrap_or_default();
}
}
}
Err(_) => todo!(),
};
// Run notification service
tauri::async_runtime::spawn(async move {
let window = app.get_window("main").unwrap();
let state = window.state::<Nostr>();
let client = &state.client;
let subscription = Filter::new()
.pubkey(public_key)
.kinds(vec![Kind::TextNote, Kind::Repost, Kind::ZapReceipt])
.since(Timestamp::now());
let activity_id = SubscriptionId::new("activity");
// Create a subscription for activity
client
.subscribe_with_id(activity_id.clone(), vec![subscription], None)
.await;
// Handle notifications
let _ = client
.handle_notifications(|notification| async {
if let RelayPoolNotification::Event {
subscription_id,
event,
..
} = notification
{
if subscription_id == activity_id {
let _ = app.emit("activity", event.as_json());
} }
} }
Ok(false) None => RelayOptions::new(),
}) };
.await;
});
Ok(true) // Add relay to relay pool
} let _ = client
Err(err) => Err(err.to_string()), .add_relay_with_opts(&relay_url, opts)
.await
.unwrap_or_default();
// Connect relay
client.connect_relay(relay_url).await.unwrap_or_default();
}
}
};
// Run sync service
tauri::async_runtime::spawn(async move {
let window = handle.get_window("main").unwrap();
let state = window.state::<Nostr>();
let client = &state.client;
let filter = Filter::new()
.pubkey(public_key)
.kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Reaction,
Kind::ZapReceipt,
])
.limit(500);
match client.reconcile(filter, NegentropyOptions::default()).await {
Ok(_) => println!("Sync notification done."),
Err(_) => println!("Sync notification failed."),
}
});
// Run notification service
tauri::async_runtime::spawn(async move {
println!("Starting notification service...");
let window = app.get_window("main").unwrap();
let state = window.state::<Nostr>();
let client = &state.client;
// Create a subscription for notification
let notification_id = SubscriptionId::new("notification");
let filter = Filter::new()
.pubkey(public_key)
.kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Reaction,
Kind::ZapReceipt,
])
.since(Timestamp::now());
// Subscribe
client
.subscribe_with_id(notification_id.clone(), vec![filter], None)
.await;
// Handle notifications
let _ = client
.handle_notifications(|notification| async {
if let RelayPoolNotification::Event {
subscription_id,
event,
..
} = notification
{
if subscription_id == notification_id {
println!("new notification: {}", event.as_json());
if let Err(_) = app.emit_to(
EventTarget::window("panel"),
"notification",
event.as_json(),
) {
println!("Emit new notification failed.")
}
let handle = app.app_handle();
let author = client.metadata(event.pubkey).await.unwrap();
match event.kind() {
Kind::TextNote => {
if let Err(e) = handle
.notification()
.builder()
.body("Mentioned you in a thread.")
.title(author.display_name.unwrap_or_else(|| "Lume".to_string()))
.show()
{
println!("Failed to show notification: {:?}", e);
}
}
Kind::Repost => {
if let Err(e) = handle
.notification()
.builder()
.body("Reposted your note.")
.title(author.display_name.unwrap_or_else(|| "Lume".to_string()))
.show()
{
println!("Failed to show notification: {:?}", e);
}
}
Kind::Reaction => {
let content = event.content();
if let Err(e) = handle
.notification()
.builder()
.body(content)
.title(author.display_name.unwrap_or_else(|| "Lume".to_string()))
.show()
{
println!("Failed to show notification: {:?}", e);
}
}
Kind::ZapReceipt => {
if let Err(e) = handle
.notification()
.builder()
.body("Zapped you.")
.title(author.display_name.unwrap_or_else(|| "Lume".to_string()))
.show()
{
println!("Failed to show notification: {:?}", e);
}
}
_ => {}
}
}
}
Ok(false)
})
.await;
});
Ok(true)
} else {
Err("Key not found.".into())
} }
} }
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn nostr_connect( pub async fn connect_remote_account(uri: &str, state: State<'_, Nostr>) -> Result<String, String> {
npub: &str,
uri: &str,
state: State<'_, Nostr>,
) -> Result<String, String> {
let client = &state.client; let client = &state.client;
let local_key = Keys::generate();
match NostrConnectURI::parse(uri) { match NostrConnectURI::parse(uri) {
Ok(bunker_uri) => { Ok(bunker_uri) => {
match Nip46Signer::new( let app_keys = Keys::generate();
bunker_uri, let app_secret = app_keys.secret_key().unwrap().to_string();
local_key.clone(),
Duration::from_secs(120), // Get remote user
None, let remote_user = bunker_uri.signer_public_key().unwrap();
) let remote_npub = remote_user.to_bech32().unwrap();
.await
{ match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(120), None).await {
Ok(signer) => { Ok(signer) => {
let local_secret = local_key.secret_key().unwrap().to_bech32().unwrap(); let keyring = Entry::new(&remote_npub, "nostr_secret").unwrap();
let secret_keyring = Entry::new(&npub, "nostr_secret").unwrap(); let _ = keyring.set_password(&app_secret);
let account_keyring = Entry::new(&npub, "bunker_local_account").unwrap();
let _ = secret_keyring.set_password(uri);
let _ = account_keyring.set_password(&local_secret);
// Update signer // Update signer
let _ = client.set_signer(Some(signer.into())).await; let _ = client.set_signer(Some(signer.into())).await;
Ok(npub.into()) Ok(remote_npub)
} }
Err(err) => Err(err.to_string()), Err(err) => Err(err.to_string()),
} }
@@ -299,15 +378,6 @@ pub fn user_to_bech32(key: &str, relays: Vec<String>) -> Result<String, ()> {
Ok(profile.to_bech32().unwrap()) Ok(profile.to_bech32().unwrap())
} }
#[tauri::command]
#[specta::specta]
pub fn to_npub(hex: &str) -> Result<String, ()> {
let public_key = PublicKey::from_str(hex).unwrap();
let npub = Nip19::Pubkey(public_key);
Ok(npub.to_bech32().unwrap())
}
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn verify_nip05(key: &str, nip05: &str) -> Result<bool, String> { pub async fn verify_nip05(key: &str, nip05: &str) -> Result<bool, String> {
+139 -70
View File
@@ -1,76 +1,9 @@
use super::get_latest_event;
use crate::Nostr; use crate::Nostr;
use keyring::Entry; use keyring::Entry;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use std::{str::FromStr, time::Duration}; use std::{str::FromStr, time::Duration};
use tauri::State; use tauri::State;
use url::Url;
#[tauri::command]
#[specta::specta]
pub async fn get_activities(
account: &str,
kind: &str,
state: State<'_, Nostr>,
) -> Result<Vec<String>, String> {
let client = &state.client;
if let Ok(pubkey) = PublicKey::from_str(account) {
if let Ok(kind) = Kind::from_str(kind) {
let filter = Filter::new()
.pubkey(pubkey)
.kind(kind)
.limit(100)
.until(Timestamp::now());
match client.get_events_of(vec![filter], None).await {
Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()),
Err(err) => Err(err.to_string()),
}
} else {
Err("Kind is not valid, please check again.".into())
}
} else {
Err("Public Key is not valid, please check again.".into())
}
}
#[tauri::command]
#[specta::specta]
pub async fn friend_to_friend(npub: &str, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client;
match PublicKey::from_bech32(npub) {
Ok(author) => {
let mut contact_list: Vec<Contact> = Vec::new();
let contact_list_filter = Filter::new()
.author(author)
.kind(Kind::ContactList)
.limit(1);
if let Ok(contact_list_events) = client.get_events_of(vec![contact_list_filter], None).await {
for event in contact_list_events.into_iter() {
for tag in event.into_iter_tags() {
if let Some(TagStandard::PublicKey {
public_key,
relay_url,
alias,
uppercase: false,
}) = tag.to_standardized()
{
contact_list.push(Contact::new(public_key, relay_url, alias))
}
}
}
}
match client.set_contact_list(contact_list).await {
Ok(_) => Ok(true),
Err(err) => Err(err.to_string()),
}
}
Err(err) => Err(err.to_string()),
}
}
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
@@ -88,7 +21,7 @@ pub async fn get_current_user_profile(state: State<'_, Nostr>) -> Result<String,
.await .await
{ {
Ok(events) => { Ok(events) => {
if let Some(event) = events.first() { if let Some(event) = get_latest_event(&events) {
if let Ok(metadata) = Metadata::from_json(&event.content) { if let Ok(metadata) = Metadata::from_json(&event.content) {
Ok(metadata.as_json()) Ok(metadata.as_json())
} else { } else {
@@ -109,7 +42,14 @@ pub async fn get_profile(id: &str, state: State<'_, Nostr>) -> Result<String, St
let public_key: Option<PublicKey> = match Nip19::from_bech32(id) { let public_key: Option<PublicKey> = match Nip19::from_bech32(id) {
Ok(val) => match val { Ok(val) => match val {
Nip19::Pubkey(pubkey) => Some(pubkey), Nip19::Pubkey(pubkey) => Some(pubkey),
Nip19::Profile(profile) => Some(profile.public_key), Nip19::Profile(profile) => {
let relays = profile.relays;
for relay in relays.into_iter() {
let _ = client.add_relay(&relay).await.unwrap_or_default();
client.connect_relay(&relay).await.unwrap_or_default();
}
Some(profile.public_key)
}
_ => None, _ => None,
}, },
Err(_) => match PublicKey::from_str(id) { Err(_) => match PublicKey::from_str(id) {
@@ -464,3 +404,132 @@ pub async fn zap_event(
Err("Parse public key failed".into()) Err("Parse public key failed".into())
} }
} }
#[tauri::command]
#[specta::specta]
pub async fn friend_to_friend(npub: &str, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client;
match PublicKey::from_bech32(npub) {
Ok(author) => {
let mut contact_list: Vec<Contact> = Vec::new();
let contact_list_filter = Filter::new()
.author(author)
.kind(Kind::ContactList)
.limit(1);
if let Ok(contact_list_events) = client.get_events_of(vec![contact_list_filter], None).await {
for event in contact_list_events.into_iter() {
for tag in event.into_iter_tags() {
if let Some(TagStandard::PublicKey {
public_key,
relay_url,
alias,
uppercase: false,
}) = tag.to_standardized()
{
contact_list.push(Contact::new(public_key, relay_url, alias))
}
}
}
}
match client.set_contact_list(contact_list).await {
Ok(_) => Ok(true),
Err(err) => Err(err.to_string()),
}
}
Err(err) => Err(err.to_string()),
}
}
pub async fn get_following(
state: State<'_, Nostr>,
public_key: &str,
timeout: Option<u64>,
) -> Result<Vec<String>, String> {
let client = &state.client;
let public_key = match PublicKey::from_str(public_key) {
Ok(val) => val,
Err(err) => return Err(err.to_string()),
};
let duration = timeout.map(Duration::from_secs);
let filter = Filter::new().kind(Kind::ContactList).author(public_key);
let events = match client.get_events_of(vec![filter], duration).await {
Ok(events) => events,
Err(err) => return Err(err.to_string()),
};
let mut ret: Vec<String> = vec![];
if let Some(latest_event) = events.iter().max_by_key(|event| event.created_at()) {
ret.extend(latest_event.tags().iter().filter_map(|tag| {
if let Some(TagStandard::PublicKey {
uppercase: false, ..
}) = <nostr_sdk::Tag as Clone>::clone(tag).to_standardized()
{
tag.content().map(String::from)
} else {
None
}
}));
}
Ok(ret)
}
pub async fn get_followers(
state: State<'_, Nostr>,
public_key: &str,
timeout: Option<u64>,
) -> Result<Vec<String>, String> {
let client = &state.client;
let public_key = match PublicKey::from_str(public_key) {
Ok(val) => val,
Err(err) => return Err(err.to_string()),
};
let duration = timeout.map(Duration::from_secs);
let filter = Filter::new().kind(Kind::ContactList).custom_tag(
SingleLetterTag::lowercase(Alphabet::P),
vec![public_key.to_hex()],
);
let events = match client.get_events_of(vec![filter], duration).await {
Ok(events) => events,
Err(err) => return Err(err.to_string()),
};
let ret: Vec<String> = events
.into_iter()
.map(|event| event.author().to_hex())
.collect();
Ok(ret)
//todo: get more than 500 events
}
#[tauri::command]
#[specta::specta]
pub async fn get_notifications(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
let client = &state.client;
match client.signer().await {
Ok(signer) => {
let public_key = signer.public_key().await.unwrap();
let filter = Filter::new()
.pubkey(public_key)
.kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Reaction,
Kind::ZapReceipt,
])
.limit(200);
match client
.database()
.query(vec![filter], Order::default())
.await
{
Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()),
Err(err) => Err(err.to_string()),
}
}
Err(err) => Err(err.to_string()),
}
}
@@ -2,3 +2,5 @@ pub mod event;
pub mod keys; pub mod keys;
pub mod metadata; pub mod metadata;
pub mod relay; pub mod relay;
mod utils;
pub use utils::get_latest_event;
+5
View File
@@ -0,0 +1,5 @@
use nostr_sdk::Event;
pub fn get_latest_event(events: &[Event]) -> Option<&Event> {
events.iter().max_by_key(|event| event.created_at())
}
-180
View File
@@ -1,180 +0,0 @@
use std::path::PathBuf;
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
use tauri::{
utils::config::WindowEffectsConfig, window::Effect, Manager, Runtime, WebviewUrl,
WebviewWindowBuilder,
};
use tauri_plugin_shell::ShellExt;
pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
let version = app.package_info().version.to_string();
let tray = app.tray_by_id("main_tray").unwrap();
let menu = tauri::menu::MenuBuilder::new(app)
.item(&tauri::menu::MenuItem::with_id(app, "open", "Open Lume", true, None::<&str>).unwrap())
.item(&tauri::menu::MenuItem::with_id(app, "editor", "New Post", true, Some("cmd+n")).unwrap())
.item(&tauri::menu::MenuItem::with_id(app, "search", "Search", true, Some("cmd+k")).unwrap())
.separator()
.item(
&tauri::menu::MenuItem::with_id(
app,
"version",
format!("Version {}", version),
false,
None::<&str>,
)
.unwrap(),
)
.item(&tauri::menu::MenuItem::with_id(app, "about", "About Lume", true, None::<&str>).unwrap())
.item(
&tauri::menu::MenuItem::with_id(app, "update", "Check for Updates", true, None::<&str>)
.unwrap(),
)
.item(
&tauri::menu::MenuItem::with_id(app, "settings", "Settings...", true, Some("cmd+,")).unwrap(),
)
.separator()
.item(&tauri::menu::MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).unwrap())
.build()
.unwrap();
let _ = tray.set_menu(Some(menu));
tray.on_menu_event(move |app, event| match event.id.0.as_str() {
"open" => {
if let Some(window) = app.get_window("main") {
if window.is_visible().unwrap_or_default() {
let _ = window.set_focus();
} else {
let _ = window.show();
let _ = window.set_focus();
};
}
}
"editor" => {
if let Some(window) = app.get_window("editor-0") {
if window.is_visible().unwrap_or_default() {
let _ = window.set_focus();
} else {
let _ = window.show();
let _ = window.set_focus();
};
} else {
#[cfg(target_os = "macos")]
let _ =
WebviewWindowBuilder::new(app, "editor-0", WebviewUrl::App(PathBuf::from("editor")))
.title("Editor")
.min_inner_size(560., 340.)
.inner_size(560., 340.)
.hidden_title(true)
.title_bar_style(TitleBarStyle::Overlay)
.transparent(true)
.effects(WindowEffectsConfig {
state: None,
effects: vec![Effect::WindowBackground],
radius: None,
color: None,
})
.build()
.unwrap();
#[cfg(not(target_os = "macos"))]
let _ =
WebviewWindowBuilder::new(app, "editor-0", WebviewUrl::App(PathBuf::from("editor")))
.title("Editor")
.min_inner_size(560., 340.)
.inner_size(560., 340.)
.build()
.unwrap();
}
}
"search" => {
if let Some(window) = app.get_window("search") {
if window.is_visible().unwrap_or_default() {
let _ = window.set_focus();
} else {
let _ = window.show();
let _ = window.set_focus();
};
} else {
#[cfg(target_os = "macos")]
let _ = WebviewWindowBuilder::new(app, "search", WebviewUrl::App(PathBuf::from("search")))
.title("Search")
.inner_size(400., 600.)
.minimizable(false)
.title_bar_style(TitleBarStyle::Overlay)
.transparent(true)
.effects(WindowEffectsConfig {
state: None,
effects: vec![Effect::WindowBackground],
radius: None,
color: None,
})
.build()
.unwrap();
#[cfg(not(target_os = "macos"))]
let _ = WebviewWindowBuilder::new(app, "Search", WebviewUrl::App(PathBuf::from("search")))
.title("Search")
.inner_size(750., 470.)
.minimizable(false)
.resizable(false)
.build()
.unwrap();
}
}
"about" => {
app.shell().open("https://lume.nu", None).unwrap();
}
"update" => {
println!("todo!")
}
"settings" => {
if let Some(window) = app.get_window("settings") {
if window.is_visible().unwrap_or_default() {
let _ = window.set_focus();
} else {
let _ = window.show();
let _ = window.set_focus();
};
} else {
#[cfg(target_os = "macos")]
let _ = WebviewWindowBuilder::new(
app,
"settings",
WebviewUrl::App(PathBuf::from("settings/general")),
)
.title("Settings")
.inner_size(800., 500.)
.title_bar_style(TitleBarStyle::Overlay)
.hidden_title(true)
.resizable(false)
.minimizable(false)
.transparent(true)
.effects(WindowEffectsConfig {
state: None,
effects: vec![Effect::WindowBackground],
radius: None,
color: None,
})
.build()
.unwrap();
#[cfg(not(target_os = "macos"))]
let _ = WebviewWindowBuilder::new(
app,
"settings",
WebviewUrl::App(PathBuf::from("settings/general")),
)
.title("Settings")
.inner_size(800., 500.)
.resizable(false)
.minimizable(false)
.build()
.unwrap();
}
}
"quit" => {
app.exit(0);
}
_ => {}
});
Ok(())
}
+122 -121
View File
@@ -1,123 +1,124 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/schema.json", "$schema": "../node_modules/@tauri-apps/cli/schema.json",
"productName": "Lume", "productName": "Lume",
"version": "4.0.6", "version": "4.0.9",
"identifier": "nu.lume.Lume", "identifier": "nu.lume.Lume",
"build": { "build": {
"beforeBuildCommand": "pnpm desktop:build", "beforeBuildCommand": "pnpm desktop:build",
"beforeDevCommand": "pnpm desktop:dev", "beforeDevCommand": "pnpm desktop:dev",
"devUrl": "http://localhost:3000", "devUrl": "http://localhost:3000",
"frontendDist": "../dist" "frontendDist": "../dist"
}, },
"app": { "app": {
"macOSPrivateApi": true, "macOSPrivateApi": true,
"withGlobalTauri": true, "withGlobalTauri": true,
"trayIcon": { "trayIcon": {
"id": "main_tray", "id": "tray_panel",
"iconPath": "./icons/tray.png", "iconPath": "./icons/tray.png",
"iconAsTemplate": true "iconAsTemplate": true,
}, "menuOnLeftClick": false
"security": { },
"assetProtocol": { "security": {
"enable": true, "assetProtocol": {
"scope": [ "enable": true,
"$APPDATA/*", "scope": [
"$DATA/*", "$APPDATA/*",
"$LOCALDATA/*", "$DATA/*",
"$DESKTOP/*", "$LOCALDATA/*",
"$DOCUMENT/*", "$DESKTOP/*",
"$DOWNLOAD/*", "$DOCUMENT/*",
"$HOME/*", "$DOWNLOAD/*",
"$PICTURE/*", "$HOME/*",
"$PUBLIC/*", "$PICTURE/*",
"$VIDEO/*", "$PUBLIC/*",
"$APPCONFIG/*", "$VIDEO/*",
"$RESOURCE/*" "$APPCONFIG/*",
] "$RESOURCE/*"
} ]
} }
}, }
"bundle": { },
"licenseFile": "../LICENSE", "bundle": {
"longDescription": "nostr client for desktop", "licenseFile": "../LICENSE",
"shortDescription": "nostr client", "longDescription": "nostr client for desktop",
"targets": "all", "shortDescription": "nostr client",
"active": true, "targets": "all",
"category": "SocialNetworking", "active": true,
"resources": ["resources/*", "locales/*"], "category": "SocialNetworking",
"icon": [ "resources": ["resources/*", "locales/*"],
"icons/32x32.png", "icon": [
"icons/128x128.png", "icons/32x32.png",
"icons/128x128@2x.png", "icons/128x128.png",
"icons/icon.icns", "icons/128x128@2x.png",
"icons/icon.ico" "icons/icon.icns",
], "icons/icon.ico"
"linux": { ],
"appimage": { "linux": {
"bundleMediaFramework": true, "appimage": {
"files": {} "bundleMediaFramework": true,
}, "files": {}
"deb": { },
"files": {} "deb": {
}, "files": {}
"rpm": { },
"epoch": 0, "rpm": {
"files": {}, "epoch": 0,
"release": "1" "files": {},
} "release": "1"
}, }
"macOS": { },
"dmg": { "macOS": {
"appPosition": { "dmg": {
"x": 180, "appPosition": {
"y": 170 "x": 180,
}, "y": 170
"applicationFolderPosition": { },
"x": 480, "applicationFolderPosition": {
"y": 170 "x": 480,
}, "y": 170
"windowSize": { },
"height": 400, "windowSize": {
"width": 660 "height": 400,
} "width": 660
}, }
"files": {}, },
"minimumSystemVersion": "10.15" "files": {},
}, "minimumSystemVersion": "10.15"
"windows": { },
"allowDowngrades": true, "windows": {
"certificateThumbprint": null, "allowDowngrades": true,
"digestAlgorithm": "sha256", "certificateThumbprint": null,
"nsis": null, "digestAlgorithm": "sha256",
"timestampUrl": null, "nsis": null,
"tsp": false, "timestampUrl": null,
"webviewFixedRuntimePath": null, "tsp": false,
"webviewInstallMode": { "webviewFixedRuntimePath": null,
"silent": true, "webviewInstallMode": {
"type": "downloadBootstrapper" "silent": true,
}, "type": "downloadBootstrapper"
"wix": null },
}, "wix": null
"fileAssociations": [ },
{ "fileAssociations": [
"name": "bech32", {
"description": "Nostr Bech32", "name": "bech32",
"ext": ["npub", "nsec", "nprofile", "nevent", "naddr", "nrelay"], "description": "Nostr Bech32",
"role": "Viewer" "ext": ["npub", "nsec", "nprofile", "nevent", "naddr", "nrelay"],
} "role": "Viewer"
] }
}, ]
"plugins": { },
"updater": { "plugins": {
"active": true, "updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEU3OTdCMkM3RjU5QzE2NzkKUldSNUZwejF4N0tYNTVHYjMrU0JkL090SlEyNUVLYU5TM2hTU3RXSWtEWngrZWJ4a0pydUhXZHEK", "active": true,
"windows": { "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEU3OTdCMkM3RjU5QzE2NzkKUldSNUZwejF4N0tYNTVHYjMrU0JkL090SlEyNUVLYU5TM2hTU3RXSWtEWngrZWJ4a0pydUhXZHEK",
"installMode": "quiet" "windows": {
}, "installMode": "quiet"
"endpoints": [ },
"https://lus.reya3772.workers.dev/v1/{{target}}/{{arch}}/{{current_version}}", "endpoints": [
"https://lus.reya3772.workers.dev/{{target}}/{{current_version}}" "https://lus.reya3772.workers.dev/v1/{{target}}/{{arch}}/{{current_version}}",
] "https://lus.reya3772.workers.dev/{{target}}/{{current_version}}"
} ]
} }
}
} }
+15
View File
@@ -16,6 +16,21 @@
"windowEffects": { "windowEffects": {
"effects": ["windowBackground"] "effects": ["windowBackground"]
} }
},
{
"title": "Lume Panel",
"label": "panel",
"url": "/panel",
"width": 350,
"height": 500,
"fullscreen": false,
"resizable": false,
"decorations": false,
"transparent": true,
"visible": false,
"windowEffects": {
"effects": ["popover"]
}
} }
] ]
} }