Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6996e30889 | ||
|
|
7ba793fad8 | ||
|
|
f11f836518 | ||
|
|
04fe0fcec8 | ||
|
|
799835a629 | ||
|
|
4e7da4108b | ||
|
|
7c7b082b3a | ||
|
|
38d6c51921 | ||
|
|
1738cbdd97 | ||
|
|
2e885b76a1 | ||
|
|
f94680e487 | ||
|
|
c682a58842 | ||
|
|
921cf871ee | ||
|
|
d5b1593aca | ||
|
|
6676b4e2a4 | ||
|
|
5f30ddcfca | ||
|
|
41d0de539d | ||
|
|
e254ee3203 | ||
|
|
6d42360549 | ||
|
|
70c5143445 | ||
|
|
41b66b18f5 | ||
|
|
dda0720ed4 |
@@ -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'
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 201 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 104 KiB |
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,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];
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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/*"
|
||||||
]
|
}
|
||||||
}
|
]
|
||||||
]
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"]}}
|
||||||
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.9 KiB |
@@ -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()
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -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(())
|
|
||||||
}
|
|
||||||
@@ -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}}"
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||