Compare commits

...

19 Commits

Author SHA1 Message Date
reya
5d3f2264e9 chore: bump version 2024-05-22 13:26:08 +07:00
雨宮蓮
407fe40b67 refactor: account managements (#190)
* feat: add keyring-search

* feat: improve nostr connect
2024-05-22 13:24:58 +07:00
雨宮蓮
1f38eba2cc feat: immersive onboarding (#189)
* feat: change the default onboarding column

* feat: add newsfeed onboarding

* feat: add topic onboarding

* feat: add group onboarding

* chore: polish and format

* feat: rename foryou to topic

* fix: array
2024-05-22 10:44:19 +07:00
reya
9b5867f80c feat: improve tauri commands 2024-05-20 07:05:30 +07:00
reya
cac774a0c1 feat: small ui improve 2024-05-19 08:48:29 +07:00
reya
82689bf3c3 feat: add windows support for v4 2024-05-18 21:02:04 +07:00
reya
f60e438a64 feat: add custom titlebar 2024-05-18 19:07:34 +07:00
reya
ca06f2b6ed chore: clean up tauri commands 2024-05-18 14:59:34 +07:00
reya
99d9c70826 feat: improve event discovery 2024-05-18 08:16:05 +07:00
reya
60afbf090b chore: update to rust-nostr 0.31 2024-05-18 07:52:28 +07:00
reya
10ca4e6ff4 chore: bump version 2024-05-17 15:32:14 +07:00
reya
b0f387d029 fix: wrong permissions for dialog 2024-05-17 15:31:35 +07:00
reya
1a8f750640 fix: profile is not show on edit screen 2024-05-17 14:45:17 +07:00
reya
25523229a2 fix: copy to clipboard is not working properly 2024-05-16 14:24:55 +07:00
雨宮蓮
47835ed857 feat: improve ui for multi-account (#188) 2024-05-16 14:13:07 +07:00
reya
d84647bc6b chore: bump version 2024-05-15 13:58:39 +07:00
reya
7724eccd72 feat: improve tauri commands 2024-05-15 13:58:03 +07:00
reya
8ea2335225 chore: update deps 2024-05-15 10:50:35 +07:00
reya
b60d4db0df feat: improve multi-account switching 2024-05-15 10:14:21 +07:00
72 changed files with 3123 additions and 3326 deletions

View File

@@ -22,42 +22,42 @@
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/query-sync-storage-persister": "^5.32.0",
"@tanstack/react-query": "^5.32.0",
"@tanstack/react-query-persist-client": "^5.32.0",
"@tanstack/react-router": "1.29.2",
"i18next": "^23.11.3",
"@tanstack/query-sync-storage-persister": "^5.36.0",
"@tanstack/react-query": "^5.36.0",
"@tanstack/react-query-persist-client": "^5.36.0",
"@tanstack/react-router": "1.32.5",
"i18next": "^23.11.4",
"i18next-resources-to-backend": "^1.2.1",
"minidenticons": "^4.2.1",
"nanoid": "^5.0.7",
"nostr-tools": "^2.5.1",
"nostr-tools": "^2.5.2",
"react": "^18.3.1",
"react-currency-input-field": "^3.8.0",
"react-dom": "^18.3.1",
"react-hook-form": "^7.51.3",
"react-hook-form": "^7.51.4",
"react-hotkeys-hook": "^4.5.0",
"react-i18next": "^14.1.1",
"react-string-replace": "^1.1.1",
"slate": "^0.102.0",
"slate": "^0.103.0",
"slate-react": "^0.102.0",
"sonner": "^1.4.41",
"use-debounce": "^10.0.0",
"virtua": "^0.30.2"
"virtua": "^0.31.0"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@tanstack/router-devtools": "^1.31.3",
"@tanstack/router-vite-plugin": "^1.30.0",
"@types/react": "^18.3.1",
"@tanstack/router-devtools": "^1.32.5",
"@tanstack/router-vite-plugin": "^1.32.2",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.6.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
"vite": "^5.2.10",
"vite": "^5.2.11",
"vite-plugin-top-level-await": "^1.4.1",
"vite-tsconfig-paths": "^4.3.2"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -1,4 +1,4 @@
import { User } from "./user";
import { User } from "@/components/user";
import { getBitcoinDisplayValues } from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router";
import { useEffect, useMemo, useState } from "react";

View File

@@ -5,7 +5,7 @@ import { invoke } from "@tauri-apps/api/core";
import { getCurrent } from "@tauri-apps/api/webviewWindow";
import { useEffect, useRef, useState } from "react";
export function Col({
export function Column({
column,
account,
isScroll,
@@ -17,68 +17,55 @@ export function Col({
isResize: boolean;
}) {
const container = useRef<HTMLDivElement>(null);
const [webview, setWebview] = useState<string | undefined>(undefined);
const webviewLabel = `column-${account}_${column.label}`;
const [isCreated, setIsCreated] = useState(false);
const repositionWebview = async () => {
if (webview && webview.length > 1) {
const newRect = container.current.getBoundingClientRect();
await invoke("reposition_column", {
label: webview,
x: newRect.x,
y: newRect.y,
});
}
const newRect = container.current.getBoundingClientRect();
await invoke("reposition_column", {
label: webviewLabel,
x: newRect.x,
y: newRect.y,
});
};
const resizeWebview = async () => {
if (webview && webview.length > 1) {
const newRect = container.current.getBoundingClientRect();
await invoke("resize_column", {
label: webview,
width: newRect.width,
height: newRect.height,
});
}
const newRect = container.current.getBoundingClientRect();
await invoke("resize_column", {
label: webviewLabel,
width: newRect.width,
height: newRect.height,
});
};
useEffect(() => {
resizeWebview();
if (isCreated) resizeWebview();
}, [isResize]);
useEffect(() => {
if (isScroll) repositionWebview();
if (isScroll && isCreated) repositionWebview();
}, [isScroll]);
useEffect(() => {
(async () => {
if (webview && webview.length > 1) return;
const rect = container.current.getBoundingClientRect();
const url = `${column.content}?account=${account}&label=${column.label}&name=${column.name}`;
const rect = container.current.getBoundingClientRect();
const windowLabel = `column-${column.label}`;
const url = `${column.content}?account=${account}&label=${column.label}&name=${column.name}`;
// create new webview
const label: string = await invoke("create_column", {
label: windowLabel,
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
url,
});
setWebview(label);
})();
// create new webview
invoke("create_column", {
label: webviewLabel,
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
url,
}).then(() => setIsCreated(true));
// close webview when unmounted
return () => {
if (webview && webview.length > 1) {
invoke("close_column", {
label: webview,
});
}
invoke("close_column", { label: webviewLabel });
};
}, [webview]);
}, [account]);
return (
<div className="h-full w-[440px] shrink-0 p-2">

View File

@@ -1,6 +1,6 @@
import { ThreadIcon } from "@lume/icons";
import type { Event } from "@lume/types";
import { Note } from "./note";
import { Note } from "@/components/note";
import { cn } from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router";
@@ -12,7 +12,7 @@ export function Conversation({
className?: string;
}) {
const { ark } = useRouteContext({ strict: false });
const thread = ark.parse_event_thread(event.tags);
const thread = ark.get_thread(event.tags);
return (
<Note.Provider event={event}>

View File

@@ -1,6 +1,7 @@
import { useEvent } from "@lume/ark";
import { cn } from "@lume/utils";
import { Note } from ".";
import { InfoIcon } from "@lume/icons";
export function NoteChild({
eventId,
@@ -12,11 +13,25 @@ export function NoteChild({
const { isLoading, isError, data } = useEvent(eventId);
if (isLoading) {
return <div>Loading...</div>;
return (
<div className="pt-3 px-3 flex items-center gap-2">
<div className="size-8 shrink-0 rounded-full bg-neutral-200 dark:bg-neutral-800 animate-pulse" />
<div className="animate-pulse rounded-md h-4 w-2/3 bg-neutral-200 dark:bg-neutral-800" />
</div>
);
}
if (isError || !data) {
return <div>Error</div>;
return (
<div className="pt-3 px-3 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 (

View File

@@ -4,6 +4,7 @@ import { useRouteContext } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
import { cn } from "@lume/utils";
import { User } from "@/components/user";
import { Spinner } from "@lume/ui";
export function MentionNote({
eventId,
@@ -18,15 +19,15 @@ export function MentionNote({
if (isLoading) {
return (
<div className="flex w-full cursor-default items-center justify-between rounded-xl border border-black/10 p-3 dark:border-white/10">
<p>Loading...</p>
<div className="mt-2 w-full flex h-20 items-center justify-center rounded-xl border border-black/10 dark:border-white/10">
<Spinner className="size-5" />
</div>
);
}
if (isError || !data) {
return (
<div className="w-full cursor-default rounded-xl border border-black/10 p-3 dark:border-white/10">
<div className="mt-2 w-full rounded-xl border border-black/10 p-3 dark:border-white/10">
{t("note.error")}
</div>
);

View File

@@ -1,5 +1,5 @@
import type { Event } from "@lume/types";
import { Note } from "./note";
import { Note } from "@/components/note";
import { cn } from "@lume/utils";
export function Notification({

View File

@@ -1,6 +1,6 @@
import { QuoteIcon } from "@lume/icons";
import type { Event } from "@lume/types";
import { Note } from "./note";
import { Note } from "@/components/note";
import { cn } from "@lume/utils";
export function Quote({

View File

@@ -1,7 +1,7 @@
import type { Event } from "@lume/types";
import { Spinner } from "@lume/ui";
import { Note } from "./note";
import { User } from "./user";
import { Note } from "@/components/note";
import { User } from "@/components/user";
import { cn } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
import { useRouteContext } from "@tanstack/react-router";

View File

@@ -1,6 +1,6 @@
import type { Event } from "@lume/types";
import { cn } from "@lume/utils";
import { Note } from "./note";
import { Note } from "@/components/note";
export function TextNote({
event,

View File

@@ -1,4 +1,4 @@
import { Col } from "@/components/col";
import { Column } from "@/components/column";
import { Toolbar } from "@/components/toolbar";
import { ArrowLeftIcon, ArrowRightIcon } from "@lume/icons";
import type { EventColumns, LumeColumn } from "@lume/types";
@@ -13,20 +13,19 @@ import { useDebouncedCallback } from "use-debounce";
import { VList, type VListHandle } from "virtua";
export const Route = createFileRoute("/$account/home")({
beforeLoad: async ({ context }) => {
loader: async ({ context }) => {
try {
const ark = context.ark;
const resourcePath = await resolveResource(
"resources/system_columns.json",
);
const systemColumns: LumeColumn[] = JSON.parse(
await readTextFile(resourcePath),
);
const userColumns = await ark.get_columns();
const userColumns = await context.ark.get_columns();
if (userColumns.length > 0) {
return userColumns;
} else {
const systemPath = "resources/system_columns.json";
const resourcePath = await resolveResource(systemPath);
const resourceFile = await readTextFile(resourcePath);
const systemColumns: LumeColumn[] = JSON.parse(resourceFile);
return {
storedColumns: !userColumns.length ? systemColumns : userColumns,
};
return systemColumns;
}
} catch (e) {
console.error(String(e));
}
@@ -35,13 +34,14 @@ export const Route = createFileRoute("/$account/home")({
});
function Screen() {
const userSavedColumns = Route.useLoaderData();
const vlistRef = useRef<VListHandle>(null);
const { account } = Route.useParams();
const { ark, storedColumns } = Route.useRouteContext();
const { ark } = Route.useRouteContext();
const [selectedIndex, setSelectedIndex] = useState(-1);
const [columns, setColumns] = useState(storedColumns);
const [columns, setColumns] = useState([]);
const [isScroll, setIsScroll] = useState(false);
const [isResize, setIsResize] = useState(false);
@@ -114,6 +114,10 @@ function Screen() {
150,
);
useEffect(() => {
setColumns(userSavedColumns);
}, [userSavedColumns]);
useEffect(() => {
// save state
ark.set_columns(columns);
@@ -148,9 +152,10 @@ function Screen() {
onScroll={() => setIsScroll(true)}
onScrollEnd={() => setIsScroll(false)}
className="scrollbar-none h-full w-full overflow-x-auto focus:outline-none"
cache={null}
>
{columns.map((column) => (
<Col
<Column
key={column.label}
column={column}
account={account}

View File

@@ -1,5 +1,11 @@
import { BellIcon, ComposeFilledIcon, PlusIcon, SearchIcon } from "@lume/icons";
import { Event, Kind } from "@lume/types";
import {
BellIcon,
ComposeFilledIcon,
HorizontalDotsIcon,
PlusIcon,
SearchIcon,
} from "@lume/icons";
import { type Event, Kind } from "@lume/types";
import { User } from "@/components/user";
import {
cn,
@@ -10,12 +16,14 @@ import {
import { Outlet, createFileRoute } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core";
import { getCurrent } from "@tauri-apps/api/window";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import * as Popover from "@radix-ui/react-popover";
export const Route = createFileRoute("/$account")({
beforeLoad: async ({ context }) => {
const ark = context.ark;
const accounts = await ark.get_all_accounts();
const accounts = await ark.get_accounts();
return { accounts };
},
@@ -39,8 +47,8 @@ function Screen() {
<Accounts />
<button
type="button"
onClick={() => navigate({ to: "/landing" })}
className="inline-flex size-8 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"
onClick={() => navigate({ 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"
>
<PlusIcon className="size-5" />
</button>
@@ -73,43 +81,110 @@ function Screen() {
}
function Accounts() {
const navigate = Route.useNavigate();
const { ark, accounts } = Route.useRouteContext();
const { account } = Route.useParams();
const changeAccount = async (npub: string) => {
if (npub === account) return;
const [windowWidth, setWindowWidth] = useState<number>(null);
const select = await ark.load_selected_account(npub);
const navigate = Route.useNavigate();
const sortedList = useMemo(() => {
const list = accounts;
for (const [i, item] of list.entries()) {
if (item === account) {
list.splice(i, 1);
list.unshift(item);
}
}
return list;
}, [accounts]);
const changeAccount = async (npub: string) => {
if (npub === account) {
return await ark.open_profile(account);
}
// change current account and update signer
const select = await ark.load_account(npub);
if (select) {
return navigate({ to: "/$account/home", params: { account: npub } });
} else {
toast.warning("Something wrong.");
}
};
const getWindowDimensions = () => {
const { innerWidth: width, innerHeight: height } = window;
return {
width,
height,
};
};
useEffect(() => {
function handleResize() {
setWindowWidth(getWindowDimensions().width);
}
if (!windowWidth) setWindowWidth(getWindowDimensions().width);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return (
<div data-tauri-drag-region className="flex items-center gap-3">
{accounts.map((user) => (
<button key={user} type="button" onClick={() => changeAccount(user)}>
<User.Provider pubkey={user}>
<User.Root
className={cn(
"rounded-full",
user === account
? "ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950"
: "",
)}
>
<User.Avatar
{sortedList
.slice(0, windowWidth > 500 ? account.length : 2)
.map((user) => (
<button key={user} type="button" onClick={() => changeAccount(user)}>
<User.Provider pubkey={user}>
<User.Root
className={cn(
"aspect-square h-auto rounded-full object-cover",
user === account ? "w-7" : "w-8",
"shrink-0 rounded-full transition-all ease-in-out duration-150 will-change-auto",
user === account
? "ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950"
: "",
)}
/>
</User.Root>
</User.Provider>
</button>
))}
>
<User.Avatar
className={cn(
"aspect-square h-auto rounded-full object-cover transition-all ease-in-out duration-150 will-change-auto",
user === account ? "w-7" : "w-8",
)}
/>
</User.Root>
</User.Provider>
</button>
))}
{accounts.length >= 3 && windowWidth <= 700 ? (
<Popover.Root>
<Popover.Trigger className="inline-flex 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">
<HorizontalDotsIcon className="size-5" />
</Popover.Trigger>
<Popover.Portal>
<Popover.Content className="flex h-11 select-none items-center justify-center rounded-md bg-neutral-950 p-1 text-sm text-neutral-50 will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade dark:bg-neutral-50 dark:text-neutral-950">
{sortedList.slice(2).map((user) => (
<button
key={user}
type="button"
onClick={() => changeAccount(user)}
className="size-9 inline-flex items-center justify-center hover:bg-white/10 rounded-md"
>
<User.Provider pubkey={user}>
<User.Root className="rounded-full ring-1 ring-white/10">
<User.Avatar className="size-7 aspect-square h-auto rounded-full object-cover" />
</User.Root>
</User.Provider>
</button>
))}
<Popover.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
) : null}
</div>
);
}

View File

@@ -60,14 +60,21 @@ function Screen() {
className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
/>
</div>
<button
type="button"
onClick={() => submit()}
disabled={loading}
className="mt-3 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"
>
{loading ? <Spinner /> : "Login"}
</button>
<div className="flex flex-col gap-1 items-center">
<button
type="button"
onClick={() => submit()}
disabled={loading}
className="mt-3 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"
>
{loading ? <Spinner /> : "Login"}
</button>
{loading ? (
<p className="text-neutral-600 dark:text-neutral-400 text-sm text-center">
Waiting confirmation...
</p>
) : null}
</div>
</div>
</div>
);

View File

@@ -1,8 +1,8 @@
import { CheckCircleIcon } from "@lume/icons";
import { CancelIcon, CheckCircleIcon, PlusIcon } from "@lume/icons";
import type { ColumnRouteSearch } from "@lume/types";
import { Spinner } from "@lume/ui";
import { User } from "@/components/user";
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import { toast } from "sonner";
@@ -24,103 +24,173 @@ export const Route = createFileRoute("/create-group")({
function Screen() {
const contacts = Route.useLoaderData();
const router = useRouter();
const navigate = Route.useNavigate();
const { ark } = Route.useRouteContext();
const { label, redirect } = Route.useSearch();
const [title, setTitle] = useState<string>("Just a new group");
const [users, setUsers] = useState<Array<string>>([]);
const [loading, setLoading] = useState(false);
const [isDone, setIsDone] = useState(false);
const [title, setTitle] = useState("");
const [npub, setNpub] = useState("");
const [users, setUsers] = useState<string[]>([
"npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445", // reya
]);
const [isLoading, setIsLoading] = useState(false);
const toggleUser = (pubkey: string) => {
const arr = users.includes(pubkey)
? users.filter((i) => i !== pubkey)
: [...users, pubkey];
setUsers(arr);
setUsers((prev) =>
prev.includes(pubkey)
? prev.filter((i) => i !== pubkey)
: [...prev, pubkey],
);
};
const addUser = () => {
if (!npub.startsWith("npub1")) return;
if (users.includes(npub)) return;
setUsers((prev) => [...prev, npub]);
setNpub("");
};
const submit = async () => {
try {
if (isDone) return router.history.push(redirect);
setIsLoading(true);
// start loading
setLoading(true);
const key = `lume_group_${label}`;
const createGroup = await ark.set_nstore(key, JSON.stringify(users));
const groups = await ark.set_nstore(
`lume_group_${label}`,
JSON.stringify(users),
);
if (groups) {
toast.success("Group has been created successfully.");
// start loading
setIsDone(true);
setLoading(false);
if (createGroup) {
return navigate({ to: redirect });
}
} catch (e) {
setLoading(false);
setIsLoading(false);
toast.error(e);
}
};
return (
<div className="h-full overflow-y-auto scrollbar-none">
<div className="flex flex-col gap-5 p-3">
<div className="flex flex-col gap-1">
<label htmlFor="name" className="font-medium">
<div className="w-full h-full flex flex-col items-center justify-center gap-4">
<div className="text-center flex flex-col items-center justify-center">
<h1 className="text-2xl font-serif font-medium">
Focus feeds for people you like
</h1>
<p className="leading-tight text-neutral-700 dark:text-neutral-300">
Add some people for custom feeds.
</p>
</div>
<div className="w-4/5 max-w-full flex flex-col gap-3">
<div className="w-full h-9 shrink-0 flex items-center bg-black/5 dark:bg-white/5 rounded-lg">
<label
htmlFor="name"
className="w-16 border-r border-black/10 dark:border-white/10 shrink-0 text-center text-sm font-semibold"
>
Name
</label>
<input
name="name"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Nostrichs..."
className="h-10 rounded-lg bg-transparent border border-neutral-300 dark:border-neutral-700 px-3 placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
placeholder="Enter a name for this group"
className="h-full bg-transparent border-none text-sm px-3 placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
/>
</div>
<div className="flex flex-col gap-1">
<div className="inline-flex items-center justify-between">
<span className="font-medium">Pick user</span>
<span className="text-neutral-600 dark:text-neutral-400">{`${users.length} / ∞`}</span>
</div>
<div className="flex flex-col gap-2">
{contacts.map((item: string) => (
<div className="w-full flex flex-col items-center gap-3">
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] flex flex-col gap-3 bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl">
<div className="flex gap-2">
<input
name="npub"
value={npub}
onChange={(e) => setNpub(e.target.value)}
placeholder="npub1..."
className="h-9 w-full rounded-lg bg-black/10 dark:bg-white/10 border-none text-sm px-3 placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
/>
<button
key={item}
type="button"
onClick={() => toggleUser(item)}
className="inline-flex items-center justify-between px-3 py-2 rounded-lg bg-black/10 dark:bg-white/10 hover:bg-black/20 dark:hover:bg-white/20"
onClick={() => addUser()}
className="inline-flex size-9 rounded-lg items-center justify-center bg-black/20 dark:bg-white/20 shrink-0 text-white hover:bg-blue-500"
>
<User.Provider pubkey={item}>
<User.Root className="flex items-center gap-2.5">
<User.Avatar className="size-10 rounded-full object-cover" />
<div className="flex items-center gap-1">
<User.Name className="font-medium" />
<User.NIP05 />
</div>
</User.Root>
</User.Provider>
{users.includes(item) ? (
<CheckCircleIcon className="size-5 text-teal-500" />
) : null}
<PlusIcon className="size-6" />
</button>
))}
</div>
<div className="flex flex-col gap-2">
<span className="text-sm font-semibold">Added</span>
<div className="flex flex-col gap-2">
{users.length ? (
users.map((item: string) => (
<button
key={item}
type="button"
onClick={() => toggleUser(item)}
className="inline-flex items-center justify-between px-3 py-2 rounded-lg bg-white dark:bg-black/20 backdrop-blur-lg shadow-primary dark:ring-1 ring-neutral-800/50"
>
<User.Provider pubkey={item}>
<User.Root className="flex items-center gap-2.5">
<User.Avatar className="size-8 rounded-full object-cover" />
<div className="flex items-center gap-1">
<User.Name className="text-sm font-medium" />
</div>
</User.Root>
</User.Provider>
<div>
<CancelIcon className="size-4" />
</div>
</button>
))
) : (
<div className="bg-black/5 dark:bg-white/5 text-sm flex items-center justify-center h-14 rounded-lg">
Empty.
</div>
)}
</div>
</div>
<div className="flex flex-col gap-2">
<span className="text-sm font-semibold">Contacts</span>
<div className="flex flex-col gap-2">
{contacts.length ? (
contacts.map((item: string) => (
<button
key={item}
type="button"
onClick={() => toggleUser(item)}
className="inline-flex items-center justify-between px-3 py-2 rounded-lg bg-white dark:bg-black/20 backdrop-blur-lg shadow-primary dark:ring-1 ring-neutral-800/50"
>
<User.Provider pubkey={item}>
<User.Root className="flex items-center gap-2.5">
<User.Avatar className="size-8 rounded-full object-cover" />
<div className="flex items-center gap-1">
<User.Name className="text-sm font-medium" />
</div>
</User.Root>
</User.Provider>
</button>
))
) : (
<div className="bg-black/5 dark:bg-white/5 text-sm flex items-center justify-center h-14 rounded-lg">
<p>
Find more user at{" "}
<a
href="https://www.nostr.directory/"
target="_blank"
className="text-blue-600 after:content-['_↗']"
rel="noreferrer"
>
Nostr Directory
</a>
</p>
</div>
)}
</div>
</div>
</div>
</div>
</div>
<div className="fixed z-10 flex items-center justify-center w-full bottom-6">
{users.length >= 1 ? (
<button
type="button"
onClick={() => submit()}
disabled={users.length < 1}
className="inline-flex items-center justify-center px-4 font-medium text-white transform bg-blue-500 rounded-full active:translate-y-1 w-32 h-10 hover:bg-blue-600 focus:outline-none"
disabled={isLoading || users.length < 1}
className="inline-flex items-center justify-center w-36 rounded-full h-9 bg-blue-500 text-white text-sm font-medium hover:bg-blue-600 disabled:opacity-50"
>
{isDone ? "Back" : loading ? <Spinner /> : "Update"}
{isLoading ? <Spinner /> : "Confirm"}
</button>
) : null}
</div>
</div>
</div>
);

View File

@@ -0,0 +1,86 @@
import type { ColumnRouteSearch } from "@lume/types";
import { Spinner } from "@lume/ui";
import { createFileRoute } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core";
import { useState } from "react";
import { toast } from "sonner";
export const Route = createFileRoute("/create-newsfeed/f2f")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return {
account: search.account,
label: search.label,
name: search.name,
};
},
component: Screen,
});
function Screen() {
const navigate = Route.useNavigate();
const { redirect } = Route.useSearch();
const [npub, setNpub] = useState("");
const [isLoading, setIsLoading] = useState(false);
const submit = async () => {
if (!npub.startsWith("npub1"))
return toast.warning("You must enter a valid npub.");
try {
setIsLoading(true);
const sync: boolean = await invoke("friend_to_friend", { npub });
if (sync) {
return navigate({ to: redirect });
}
} catch (e) {
setIsLoading(false);
toast.error(String(e));
}
};
return (
<div className="overflow-y-auto scrollbar-none p-2 shrink-0 h-[450px] bg-white dark:bg-white/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<div className="h-full flex flex-col justify-between">
<div className="flex-1 flex flex-col gap-1.5 justify-center px-5">
<p className="font-semibold text-neutral-500">
You already have a friend on Nostr?
</p>
<p>Instead of building the timeline by yourself.</p>
<p className="font-semibold text-neutral-500">
Just enter your friend's{" "}
<span className="text-blue-500">npub.</span>
</p>
<p>
You will have the same experience as your friend. Of course, you
always can edit your network later.
</p>
</div>
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-1">
<label htmlFor="npub" className="font-medium text-sm">
NPUB
</label>
<input
name="npub"
placeholder="npub1..."
value={npub}
onChange={(e) => setNpub(e.target.value)}
spellCheck={false}
className="h-11 rounded-lg bg-transparent border border-neutral-200 dark:border-neutral-800 px-3 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:placeholder:text-neutral-400"
/>
</div>
<button
type="button"
onClick={() => submit()}
className="inline-flex items-center justify-center w-full rounded-lg h-9 bg-blue-500 text-white text-sm font-medium hover:bg-blue-600"
>
{isLoading ? <Spinner /> : "Confirm"}
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,53 @@
import { cn } from "@lume/utils";
import { Link, Outlet } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/create-newsfeed")({
component: Screen,
});
function Screen() {
return (
<div className="w-full h-full flex flex-col items-center justify-center gap-4">
<div className="text-center flex flex-col items-center justify-center">
<h1 className="text-2xl font-serif font-medium">
Build up your timeline.
</h1>
<p className="leading-tight text-neutral-700 dark:text-neutral-300">
Follow some people to keep up to date with them.
</p>
</div>
<div className="w-4/5 max-w-full flex flex-col gap-3">
<div className="w-full h-9 shrink-0 flex items-center justify-between bg-black/5 dark:bg-white/5 rounded-lg px-0.5">
<Link to="/create-newsfeed/users" className="flex-1 h-8">
{({ isActive }) => (
<div
className={cn(
"text-sm font-medium rounded-md h-full flex items-center justify-center",
isActive
? "bg-white dark:bg-white/20 shadow"
: "bg-transparent",
)}
>
Users
</div>
)}
</Link>
<Link to="/create-newsfeed/f2f" className="flex-1 h-8">
{({ isActive }) => (
<div
className={cn(
"rounded-md h-full flex items-center justify-center",
isActive ? "bg-white dark:bg-white/20" : "bg-transparent",
)}
>
Friend to Friend
</div>
)}
</Link>
</div>
<Outlet />
</div>
</div>
);
}

View File

@@ -0,0 +1,128 @@
import { createFileRoute } from "@tanstack/react-router";
import { Suspense, useState } from "react";
import { Await, defer } from "@tanstack/react-router";
import { User } from "@/components/user";
import { Spinner } from "@lume/ui";
import { toast } from "sonner";
import type { ColumnRouteSearch } from "@lume/types";
export const Route = createFileRoute("/create-newsfeed/users")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return {
account: search.account,
label: search.label,
name: search.name,
};
},
loader: async ({ abortController }) => {
try {
return {
data: defer(
fetch("https://api.nostr.band/v0/trending/profiles", {
signal: abortController.signal,
}).then((res) => res.json()),
),
};
} catch (e) {
throw new Error(String(e));
}
},
component: Screen,
});
function Screen() {
const { ark } = Route.useRouteContext();
const { data } = Route.useLoaderData();
const { redirect } = Route.useSearch();
const [isLoading, setIsLoading] = useState(false);
const [follows, setFollows] = useState<string[]>([]);
const navigate = Route.useNavigate();
const toggleFollow = (pubkey: string) => {
setFollows((prev) =>
prev.includes(pubkey)
? prev.filter((i) => i !== pubkey)
: [...prev, pubkey],
);
};
const submit = async () => {
try {
setIsLoading(true);
const newContactList = await ark.set_contact_list(follows);
if (newContactList) {
return navigate({ to: redirect });
}
} catch (e) {
setIsLoading(false);
toast.error(String(e));
}
};
return (
<div className="w-full flex flex-col items-center gap-3">
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl">
<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}>
{(users) =>
users.profiles.map((item: { pubkey: string }) => (
<div
key={item.pubkey}
className="h-max w-full overflow-hidden mb-2 p-2 bg-white dark:bg-black/20 backdrop-blur-lg rounded-lg shadow-primary dark:ring-1 ring-neutral-800/50"
>
<User.Provider pubkey={item.pubkey}>
<User.Root>
<div className="flex h-full w-full flex-col gap-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<User.Avatar className="size-7 shrink-0 rounded-full object-cover" />
<User.Name className="text-sm leadning-tight max-w-[15rem] truncate font-semibold" />
</div>
<button
type="button"
onClick={() => toggleFollow(item.pubkey)}
className="inline-flex h-7 w-20 items-center justify-center rounded-lg bg-black/10 text-sm font-medium hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
>
{follows.includes(item.pubkey)
? "Unfollow"
: "Follow"}
</button>
</div>
<User.About className="line-clamp-3 max-w-none select-text text-neutral-800 dark:text-neutral-400" />
</div>
</User.Root>
</User.Provider>
</div>
))
}
</Await>
</Suspense>
</div>
<button
type="button"
onClick={() => submit()}
disabled={isLoading || follows.length < 1}
className="inline-flex items-center justify-center w-36 rounded-full h-9 bg-blue-500 text-white text-sm font-medium hover:bg-blue-600 disabled:opacity-50"
>
{isLoading ? <Spinner /> : "Confirm"}
</button>
</div>
);
}

View File

@@ -0,0 +1,104 @@
import { CheckCircleIcon } from "@lume/icons";
import type { ColumnRouteSearch, Topic } from "@lume/types";
import { Spinner } from "@lume/ui";
import { TOPICS } from "@lume/utils";
import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import { toast } from "sonner";
export const Route = createFileRoute("/create-topic")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return {
account: search.account,
label: search.label,
name: search.name,
};
},
component: Screen,
});
function Screen() {
const { label, redirect } = Route.useSearch();
const { ark } = Route.useRouteContext();
const [topics, setTopics] = useState<Topic[]>([]);
const [isLoading, setIsLoading] = useState(false);
const navigate = Route.useNavigate();
const toggleTopic = (topic: Topic) => {
setTopics((prev) =>
prev.find((item) => item.title === topic.title)
? prev.filter((i) => i.title !== topic.title)
: [...prev, topic],
);
};
const submit = async () => {
try {
setIsLoading(true);
const key = `lume_topic_${label}`;
const createTopic = await ark.set_nstore(key, JSON.stringify(topics));
if (createTopic) {
return navigate({ to: redirect });
}
} catch (e) {
setIsLoading(false);
toast.error(String(e));
}
};
return (
<div className="w-full h-full flex flex-col items-center justify-center gap-4">
<div className="text-center flex flex-col items-center justify-center">
<h1 className="text-2xl font-serif font-medium">
What are your interests?
</h1>
<p className="leading-tight text-neutral-700 dark:text-neutral-300">
Add some topics you want to focus on.
</p>
</div>
<div className="w-4/5 max-w-full flex flex-col gap-3">
<div className="w-full h-9 shrink-0 flex items-center justify-between bg-black/5 dark:bg-white/5 rounded-lg px-3">
<span className="text-sm font-medium">Added: {topics.length}</span>
</div>
<div className="w-full flex flex-col items-center gap-3">
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl">
<div className="flex flex-col gap-3">
{TOPICS.map((topic) => (
<button
key={topic.title}
onClick={() => toggleTopic(topic)}
className="h-11 px-3 flex items-center justify-between bg-white dark:bg-black/20 backdrop-blur-lg border border-transparent hover:border-blue-500 rounded-lg shadow-primary dark:ring-1 ring-neutral-800/50"
>
<div className="inline-flex items-center gap-1">
<div>{topic.icon}</div>
<div className="text-sm font-medium">
<span>{topic.title}</span>
<span className="ml-1 italic text-neutral-400 dark:text-neutral-600 font-normal">
{topic.content.length} hashtags
</span>
</div>
</div>
{topics.find((item) => item.title === topic.title) ? (
<CheckCircleIcon className="text-teal-500 size-4" />
) : null}
</button>
))}
</div>
</div>
<button
type="button"
onClick={() => submit()}
disabled={isLoading || topics.length < 1}
className="inline-flex items-center justify-center w-36 rounded-full h-9 bg-blue-500 text-white text-sm font-medium hover:bg-blue-600 disabled:opacity-50"
>
{isLoading ? <Spinner /> : "Confirm"}
</button>
</div>
</div>
</div>
);
}

View File

@@ -25,6 +25,7 @@ import { MediaButton } from "./-components/media";
import { NsfwToggle } from "./-components/nsfw";
import { MentionButton } from "./-components/mention";
import { MentionNote } from "@/components/note/mentions/note";
import { toast } from "sonner";
type EditorSearch = {
reply_to: string;

View File

@@ -27,7 +27,7 @@ export const Route = createFileRoute("/global")({
});
export function Screen() {
const { account } = Route.useSearch();
const { label, account } = Route.useSearch();
const { ark } = Route.useRouteContext();
const {
data,
@@ -37,16 +37,13 @@ export function Screen() {
hasNextPage,
fetchNextPage,
} = useInfiniteQuery({
queryKey: ["global", account],
queryKey: [label, account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events(20, pageParam, undefined, true);
const events = await ark.get_global_events(20, pageParam);
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage?.at(-1);
return lastEvent ? lastEvent.created_at - 1 : null;
},
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
select: (data) => data?.pages.flatMap((page) => page),
refetchOnWindowFocus: false,
});

View File

@@ -1,3 +1,5 @@
import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
@@ -17,12 +19,11 @@ export const Route = createFileRoute("/group")({
},
beforeLoad: async ({ search, context }) => {
const ark = context.ark;
const groups = (await ark.get_nstore(
`lume_group_${search.label}`,
)) as string[];
const key = `lume_group_${search.label}`;
const groups = (await ark.get_nstore(key)) as string[];
const settings = await ark.get_settings();
if (!groups) {
if (!groups?.length) {
throw redirect({
to: "/create-group",
search: {
@@ -41,7 +42,7 @@ export const Route = createFileRoute("/group")({
});
export function Screen() {
const { name, account } = Route.useSearch();
const { label, account } = Route.useSearch();
const { ark, groups } = Route.useRouteContext();
const {
data,
@@ -51,16 +52,13 @@ export function Screen() {
hasNextPage,
fetchNextPage,
} = useInfiniteQuery({
queryKey: [name, account],
queryKey: [label, account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events(20, pageParam, groups);
const events = await ark.get_local_events(groups, 20, pageParam);
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage?.at(-1);
return lastEvent ? lastEvent.created_at - 1 : null;
},
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
select: (data) =>
data?.pages.flatMap((page) => page.filter((ev) => ev.kind === Kind.Text)),
refetchOnWindowFocus: false,
@@ -71,15 +69,29 @@ export function Screen() {
switch (event.kind) {
case Kind.Repost:
return <RepostNote key={event.id} event={event} />;
default:
return <TextNote key={event.id} event={event} />;
default: {
const isConversation =
event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention")
.length > 0;
const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0;
if (isConversation) {
return <Conversation key={event.id} event={event} className="mb-3" />;
}
if (isQuote) {
return <Quote key={event.id} event={event} className="mb-3" />;
}
return <TextNote key={event.id} event={event} className="mb-3" />;
}
}
};
return (
<div className="p-2 w-full h-full overflow-y-auto scrollbar-none">
{isFetching && !isLoading && !isFetchingNextPage ? (
<div className="w-full h-11 flex items-center justify-center">
<div className="mb-3 w-full h-11 flex items-center justify-center bg-black/10 dark:bg-white/10 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<div className="flex items-center justify-center gap-2">
<Spinner className="size-5" />
<span className="text-sm font-medium">Fetching new notes...</span>

View File

@@ -14,11 +14,11 @@ export const Route = createFileRoute("/")({
await checkForAppUpdates(true);
const ark = context.ark;
const accounts = await ark.get_all_accounts();
const accounts = await ark.get_accounts();
if (!accounts.length) {
throw redirect({
to: "/landing",
to: "/landing/",
replace: true,
});
}
@@ -41,7 +41,7 @@ function Screen() {
try {
setLoading(true);
const loadAccount = await ark.load_selected_account(npub);
const loadAccount = await ark.load_account(npub);
if (loadAccount) {
return navigate({
to: "/$account/home",
@@ -68,7 +68,7 @@ function Screen() {
<h2 className="mb-1 text-2xl">{currentDate}</h2>
<h2 className="text-2xl font-semibold">Welcome back!</h2>
</div>
<div className="flex items-center justify-center gap-6">
<div className="flex flex-wrap px-3 items-center justify-center gap-6">
{loading ? (
<div className="inline-flex size-6 items-center justify-center">
<Spinner className="size-6 text-white" />
@@ -89,10 +89,10 @@ function Screen() {
</User.Provider>
</button>
))}
<Link to="/landing">
<Link to="/landing/">
<div className="flex h-36 w-32 flex-col items-center justify-center gap-4 rounded-2xl p-2 text-white hover:bg-white/10 dark:hover:bg-black/10">
<div className="flex size-20 items-center justify-center rounded-full bg-white/20 dark:bg-black/20">
<PlusIcon className="size-5" />
<PlusIcon className="size-8" />
</div>
<p className="text-lg font-medium leading-tight">Add</p>
</div>

View File

@@ -1,119 +0,0 @@
import type { ColumnRouteSearch } from "@lume/types";
import { TOPICS, cn } from "@lume/utils";
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
export const Route = createFileRoute("/interests")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return {
account: search.account,
label: search.label,
name: search.name,
};
},
component: Screen,
});
function Screen() {
const { t } = useTranslation();
const { label, name, redirect } = Route.useSearch();
const { ark } = Route.useRouteContext();
const [hashtags, setHashtags] = useState<string[]>([]);
const [isDone, setIsDone] = useState(false);
const router = useRouter();
const toggleHashtag = (item: string) => {
const arr = hashtags.includes(item)
? hashtags.filter((i) => i !== item)
: [...hashtags, item];
setHashtags(arr);
};
const toggleAll = (item: string[]) => {
const sets = new Set([...hashtags, ...item]);
setHashtags([...sets]);
};
const submit = async () => {
try {
if (isDone) {
return router.history.push(redirect);
}
const eventId = await ark.set_interest(undefined, undefined, hashtags);
if (eventId) {
setIsDone(true);
toast.success("Interest has been updated successfully.");
}
} catch (e) {
toast.error(String(e));
}
};
return (
<div className="h-full flex flex-col px-2">
<div className="shrink-0 flex h-16 items-center justify-between">
<div className="flex flex-1 flex-col">
<h3 className="font-semibold">Interests</h3>
<p className="text-sm leading-tight text-neutral-700 dark:text-neutral-300">
Pick things you'd like to see.
</p>
</div>
<button
type="button"
onClick={submit}
className="inline-flex h-8 w-20 items-center justify-center rounded-full bg-blue-500 px-2 text-sm font-medium text-white hover:bg-blue-600 disabled:opacity-50"
>
{isDone ? t("global.back") : t("global.update")}
</button>
</div>
<div className="flex-1 flex flex-col gap-3 pb-2 scrollbar-none overflow-y-auto">
{TOPICS.map((topic) => (
<div
key={topic.title}
className="flex flex-col gap-4 bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50"
>
<div className="px-3 flex w-full items-center justify-between h-14 shrink-0 border-b border-neutral-100 dark:border-neutral-900">
<div className="inline-flex items-center gap-2.5">
<img
src={topic.icon}
alt={topic.title}
className="size-8 rounded-lg object-cover"
/>
<h3 className="text-lg font-semibold">{topic.title}</h3>
</div>
<button
type="button"
onClick={() => toggleAll(topic.content)}
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
{t("interests.followAll")}
</button>
</div>
<div className="px-3 pb-3 flex flex-wrap items-center gap-3">
{topic.content.map((hashtag) => (
<button
key={hashtag}
type="button"
onClick={() => toggleHashtag(hashtag)}
className={cn(
"inline-flex items-center rounded-full border border-transparent bg-neutral-100 px-2 py-1 text-sm font-medium dark:bg-neutral-900",
hashtags.includes(hashtag)
? "border-blue-500 text-blue-500"
: "",
)}
>
{hashtag}
</button>
))}
</div>
</div>
))}
</div>
</div>
);
}

View File

@@ -6,7 +6,7 @@ import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
import { type ColumnRouteSearch, type Event, Kind } from "@lume/types";
import { Spinner } from "@lume/ui";
import { useInfiniteQuery } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import { Link, redirect } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
import { Virtualizer } from "virtua";
@@ -18,18 +18,29 @@ export const Route = createFileRoute("/newsfeed")({
name: search.name,
};
},
beforeLoad: async ({ context }) => {
beforeLoad: async ({ search, context }) => {
const ark = context.ark;
const settings = await ark.get_settings();
const contacts = await ark.get_contact_list();
return { settings };
if (!contacts.length) {
throw redirect({
to: "/create-newsfeed/users",
search: {
...search,
redirect: "/newsfeed",
},
});
}
return { settings, contacts };
},
component: Screen,
});
export function Screen() {
const { label, account } = Route.useSearch();
const { ark } = Route.useRouteContext();
const { ark, contacts } = Route.useRouteContext();
const {
data,
isLoading,
@@ -41,13 +52,10 @@ export function Screen() {
queryKey: [label, account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events(20, pageParam);
const events = await ark.get_local_events(contacts, 20, pageParam);
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage?.at(-1);
return lastEvent ? lastEvent.created_at - 1 : null;
},
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
select: (data) => data?.pages.flatMap((page) => page),
refetchOnWindowFocus: false,
});
@@ -79,7 +87,7 @@ export function Screen() {
return (
<div className="p-2 w-full h-full overflow-y-auto scrollbar-none">
{isFetching && !isLoading && !isFetchingNextPage ? (
<div className="w-full h-11 flex items-center justify-center">
<div className="mb-3 w-full h-11 flex items-center justify-center bg-black/10 dark:bg-white/10 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<div className="flex items-center justify-center gap-2">
<Spinner className="size-5" />
<span className="text-sm font-medium">Fetching new notes...</span>
@@ -92,7 +100,9 @@ export function Screen() {
<span className="text-sm font-medium">Loading...</span>
</div>
) : !data.length ? (
<Empty />
<div className="flex items-center justify-center">
Yo. You're catching up on all the things happening around you.
</div>
) : (
<Virtualizer overscan={3}>
{data.map((item) => renderItem(item))}
@@ -120,35 +130,3 @@ export function Screen() {
</div>
);
}
function Empty() {
return (
<div className="flex flex-col py-10 gap-10">
<div className="text-center flex flex-col items-center justify-center">
<div className="size-24 bg-blue-100 flex flex-col items-center justify-end overflow-hidden dark:bg-blue-900 rounded-full mb-8">
<div className="w-12 h-16 bg-gradient-to-b from-blue-500 dark:from-blue-200 to-blue-50 dark:to-blue-900 rounded-t-lg" />
</div>
<p className="text-lg font-medium">Your newsfeed is empty</p>
<p className="leading-tight text-neutral-700 dark:text-neutral-300">
Here are few suggestions to get started.
</p>
</div>
<div className="flex flex-col px-3 gap-2">
<Link
to="/trending/notes"
className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3"
>
<ArrowRightIcon className="size-5" />
Show trending notes
</Link>
<Link
to="/trending/users"
className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3"
>
<ArrowRightIcon className="size-5" />
Discover trending users
</Link>
</div>
</div>
);
}

View File

@@ -1,14 +1,5 @@
import { ArrowRightIcon, CancelIcon } from "@lume/icons";
import type { ColumnRouteSearch, LumeColumn } from "@lume/types";
import { Spinner } from "@lume/ui";
import { User } from "@/components/user";
import { cn } from "@lume/utils";
import type { ColumnRouteSearch } from "@lume/types";
import { createFileRoute } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core";
import { getCurrent } from "@tauri-apps/api/window";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
export const Route = createFileRoute("/onboarding")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
@@ -22,54 +13,6 @@ export const Route = createFileRoute("/onboarding")({
});
function Screen() {
const { label } = Route.useSearch();
const {
register,
handleSubmit,
reset,
formState: { isValid, isSubmitting },
} = useForm();
const [userType, setUserType] = useState<"new" | "veteran">(null);
const install = async (column: LumeColumn) => {
const mainWindow = getCurrent();
await mainWindow.emit("columns", { type: "add", column });
};
const close = async () => {
const mainWindow = getCurrent();
await mainWindow.emit("columns", { type: "remove", label });
};
const friendToFriend = async (data: { npub: string }) => {
if (!data.npub.startsWith("npub1"))
return toast.warning(
"NPUB is invalid. NPUB must be starts with npub1...",
);
try {
const connect: boolean = await invoke("friend_to_friend", {
npub: data.npub,
});
if (connect) {
const column = {
label: "newsfeed",
name: "Newsfeed",
content: "/newsfeed",
};
const mainWindow = getCurrent();
await mainWindow.emit("columns", { type: "add", column });
// reset form
reset();
}
} catch (e) {
toast.error(String(e));
}
};
return (
<div className="h-full flex flex-col py-6 gap-6 overflow-y-auto scrollbar-none">
<div className="text-center flex flex-col items-center justify-center">
@@ -78,369 +21,92 @@ function Screen() {
Here are a few suggestions to help you get started.
</p>
</div>
<div className="px-3">
<div className="mb-6 w-full h-44">
<img
src="/lock-screen.jpg"
srcSet="/lock-screen@2x.jpg 2x"
alt="background"
className="h-full w-full object-cover rounded-xl outline outline-1 -outline-offset-1 outline-black/15"
/>
<div className="px-3 flex flex-col gap-3">
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10 backdrop-blur-lg">
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
01.
</div>
<div className="h-16 flex items-center justify-center shrink-0 px-3 text-lg select-text">
Navigate between columns.
</div>
<div className="flex-1 w-3/4 h-full pb-10">
<video
className="h-auto w-full aspect-square rounded-lg shadow-md transform"
controls
muted
>
<source
src="https://video.nostr.build/692f71e2be47ecfc29edcbdaa198cc5979bfb9c900f05d78682895dd546d8d4f.mp4"
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
</div>
</div>
<div className="flex flex-col gap-6">
<div className="flex items-start gap-2 text-[13px]">
<Mide />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold">Mide</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
👋 Yo! I'm Mide, and I'll be your friendly guide to Nostr and
beyond. Looking forward to our adventure together!
</div>
</div>
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10 backdrop-blur-lg">
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
02.
</div>
<div className="flex items-start flex-row-reverse gap-2 text-[13px]">
<CurrentUser />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold text-end">You</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
How can I get started?
</div>
<button
type="button"
onClick={() => setUserType("new")}
className={cn(
"mt-1 px-3 py-2 shadow-primary flex items-center justify-between gap-6 bg-white hover:bg-blue-500 hover:text-white dark:bg-white/10 rounded-lg",
userType === "new"
? "bg-blue-500 text-white hover:bg-blue-600"
: "",
)}
>
I'm completely new to Nostr.
<ArrowRightIcon className="size-4" />
</button>
<button
type="button"
onClick={() => setUserType("veteran")}
className={cn(
"mt-1 px-3 py-2 shadow-primary flex items-center justify-between gap-6 bg-white hover:bg-blue-500 hover:text-white dark:bg-white/10 rounded-lg",
userType === "veteran"
? "bg-blue-500 text-white hover:bg-blue-600"
: "",
)}
>
I've already been using another Nostr client.
<ArrowRightIcon className="size-4" />
</button>
</div>
<div className="h-16 flex items-center justify-center shrink-0 px-3 text-lg select-text">
Switch between accounts.
</div>
<div className="flex-1 w-3/4 h-full pb-10">
<video
className="h-auto w-full aspect-square rounded-lg shadow-md transform"
controls
muted
>
<source
src="https://video.nostr.build/d33962520506d86acfb4b55a7b265821e10ae637f5ec830a173b7e6092b16ec8.mp4"
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
</div>
</div>
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10 backdrop-blur-lg">
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
03.
</div>
<div className="h-16 flex items-center justify-center shrink-0 px-3 text-lg select-text">
Open Lume Store.
</div>
<div className="flex-1 w-3/4 h-full pb-10">
<video
className="h-auto w-full aspect-square rounded-lg shadow-md transform"
controls
muted
>
<source
src="https://video.nostr.build/927abbfde2097e470ac751181b1db456b7e4b9149550408efff1a966a7ffb9a8.mp4"
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
</div>
</div>
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10 backdrop-blur-lg">
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
04.
</div>
<div className="h-16 flex items-center justify-center shrink-0 px-3 text-lg select-text">
Use the Tray Menu.
</div>
<div className="flex-1 w-3/4 h-full pb-10">
<video
className="h-auto w-full rounded-lg shadow-md transform"
controls
muted
>
<source
src="https://video.nostr.build/513de4824b6abaf7e9698c1dad2f68096574356848c0c200bc8cb8074df29410.mp4"
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
</div>
{userType === "veteran" ? (
<div className="flex items-start gap-2 text-[13px]">
<Mide />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold">Mide</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
So, I'm excited to give you a quick intro to Lume and all the
awesome features it has to offer. Let's dive in!
</div>
</div>
</div>
) : null}
{userType === "veteran" ? (
<div className="flex items-start flex-row-reverse gap-2 text-[13px]">
<CurrentUser />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold text-end">You</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
Thanks! But I already know about Lume.
</div>
<button
type="button"
onClick={() =>
install({
label: "newsfeed",
name: "Newsfeed",
content: "/newsfeed",
})
}
className="mt-1 px-3 py-2 shadow-primary flex items-center justify-between gap-6 bg-white hover:bg-blue-500 hover:text-white dark:bg-white/10 rounded-lg"
>
Skip! Show my newsfeed
<ArrowRightIcon className="size-4" />
</button>
</div>
</div>
) : null}
{userType === "veteran" ? (
<div className="flex items-start gap-2 text-[13px]">
<Mide />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold">Mide</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
First off, Lume is a social media client for Nostr. It's a
place where you can follow friends, dive into chats, and post
what's on your mind.
</div>
</div>
</div>
) : null}
{userType === "veteran" ? (
<div className="flex items-start gap-2 text-[13px]">
<Mide />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold">Mide</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
That's not all! What makes Lume unique is the column system.
You can enhance your experience by adding new columns from the
Lume Store.
</div>
<div className="mt-1 p-2 bg-black/5 dark:bg-white/5 rounded-lg">
If you're confused about the term "Column," you can imagine it
as mini-apps, with each column providing its own experience.
</div>
<div className="mt-1 p-2 bg-black/5 dark:bg-white/5 rounded-lg">
Here is a quick guide for how to add a new column:
</div>
<div className="mt-1 rounded-lg">
<video
className="h-auto w-full rounded-lg object-cover aspect-video outline outline-1 -outline-offset-1 outline-black/15"
controls
muted
>
<source
src="https://samplelib.com/lib/preview/mp4/sample-5s.mp4"
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
</div>
</div>
</div>
) : null}
{userType === "veteran" ? (
<div className="flex items-start flex-row-reverse gap-2 text-[13px]">
<CurrentUser />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold text-end">You</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
Can you introduce me to the UI? I am still confused.
</div>
</div>
</div>
) : null}
{userType === "veteran" ? (
<div className="flex items-start gap-2 text-[13px]">
<Mide />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold">Mide</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
Of course, here is a quick introduction video for Lume.
</div>
<div className="mt-1 rounded-lg">
<video
className="h-auto w-full rounded-lg object-cover aspect-video outline outline-1 -outline-offset-1 outline-black/15"
controls
muted
>
<source
src="https://samplelib.com/lib/preview/mp4/sample-5s.mp4"
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
</div>
</div>
</div>
) : null}
{userType === "new" ? (
<div className="flex items-start gap-2 text-[13px]">
<Mide />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold">Mide</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
Diving into new social media platforms like Nostr can be a bit
overwhelming, but don't worry! Here are some handy tips to
help you navigate and discover what interests you.
</div>
<button
type="button"
onClick={() =>
install({
label: "foryou",
name: "For you",
content: "/foryou",
})
}
className="mt-1 px-3 py-2 shadow-primary flex items-center justify-between bg-white hover:bg-blue-500 hover:text-white dark:bg-white/10 rounded-lg"
>
Add some topics that you're interested in.
<ArrowRightIcon className="size-4" />
</button>
<button
type="button"
onClick={() =>
install({
label: "trending_users",
name: "Trending",
content: "/trending/users",
})
}
className="mt-1 px-3 py-2 shadow-primary flex items-center justify-between bg-white hover:bg-blue-500 hover:text-white dark:bg-white/10 rounded-lg"
>
Follow some users.
<ArrowRightIcon className="size-4" />
</button>
</div>
</div>
) : null}
{userType === "new" ? (
<div className="flex items-start flex-row-reverse gap-2 text-[13px]">
<CurrentUser />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold text-end">You</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
My girlfriend introduced Nostr to me, and I have her NPUB. Can
I get the same experiences as her?
</div>
</div>
</div>
) : null}
{userType === "new" ? (
<div className="flex items-start gap-2 text-[13px]">
<Mide />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold">Mide</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
Absolutely! Since your girlfriend shared her NPUB with you,
you can dive into Nostr and explore it just like she does.
It's a great way to share experiences and discover what Nostr
has to offer together!
</div>
<form
onSubmit={handleSubmit(friendToFriend)}
className="mt-1 flex flex-col items-end bg-white dark:bg-white/10 rounded-lg shadow-primary"
>
<input
{...register("npub", { required: true })}
name="npub"
placeholder="Enter npub here..."
className="w-full h-14 px-3 rounded-t-lg bg-transparent border-b border-x-0 border-t-0 border-neutral-100 dark:border-white/5 focus:border-neutral-200 dark:focus:border-white/20 focus:outline-none focus:ring-0 placeholder:text-neutral-600 dark:placeholder:text-neutral-400"
/>
<div className="h-10 flex items-center px-1">
<button
type="submit"
disabled={!isValid || isSubmitting}
className="px-2 h-8 w-20 inline-flex items-center justify-center bg-blue-500 text-white rounded-md text-sm font-medium hover:bg-blue-600"
>
{isSubmitting ? <Spinner className="size-4" /> : "Submit"}
</button>
</div>
</form>
</div>
</div>
) : null}
{userType ? (
<>
<div className="flex items-start flex-row-reverse gap-2 text-[13px]">
<CurrentUser />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold text-end">You</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
Thank you. I can use Lume and explore Nostr by myself from
now on.
</div>
</div>
</div>
<div className="flex items-start gap-2 text-[13px]">
<Mide />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold">Mide</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
I really hope you enjoy your time on Nostr! If you're keen
to dive deeper, here are some helpful resources to get you
started:
</div>
<a
href="https://nostr.org"
target="_blank"
className="mt-1 px-3 py-2 shadow-primary flex items-center justify-between bg-white hover:bg-blue-500 hover:text-white dark:bg-white/10 rounded-lg"
rel="noreferrer"
>
[Website] nostr.org
<ArrowRightIcon className="size-4" />
</a>
<a
href="https://www.youtube.com/watch?v=5W-jtbbh3eA"
target="_blank"
className="mt-1 px-3 py-2 shadow-primary flex items-center justify-between bg-white hover:bg-blue-500 hover:text-white dark:bg-white/10 rounded-lg"
rel="noreferrer"
>
[Video] What is Nostr?
<ArrowRightIcon className="size-4" />
</a>
<a
href="https://github.com/nostr-protocol/nostr"
target="_blank"
className="mt-1 px-3 py-2 shadow-primary flex items-center justify-between bg-white hover:bg-blue-500 hover:text-white dark:bg-white/10 rounded-lg"
rel="noreferrer"
>
[Develop] Github
<ArrowRightIcon className="size-4" />
</a>
<a
href="https://www.nostrapps.com/"
target="_blank"
className="mt-1 px-3 py-2 shadow-primary flex items-center justify-between bg-white hover:bg-blue-500 hover:text-white dark:bg-white/10 rounded-lg"
rel="noreferrer"
>
[Ecosystem] nostrapps.com
<ArrowRightIcon className="size-4" />
</a>
</div>
</div>
<div className="flex items-start gap-2 text-[13px]">
<Mide />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold">Mide</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
If you want to close this onboarding board, you can click
the button below.
</div>
<button
type="button"
onClick={() => close()}
className="mt-1 px-3 py-2 shadow-primary flex items-center justify-between bg-white hover:bg-blue-500 hover:text-white dark:bg-white/10 rounded-lg"
>
Close
<CancelIcon className="size-4" />
</button>
</div>
</div>
</>
) : null}
</div>
</div>
</div>
);
}
function Mide() {
return (
<img
src="/ai.jpg"
alt="Ai-chan"
className="shrink-0 size-10 rounded-full outline outline-1 -outline-offset-1 outline-black/15"
/>
);
}
function CurrentUser() {
const { account } = Route.useSearch();
return (
<User.Provider pubkey={account}>
<User.Root className="shrink-0">
<User.Avatar className="size-10 rounded-full outline outline-1 -outline-offset-1 outline-black/15" />
</User.Root>
</User.Provider>
);
}

View File

@@ -11,7 +11,7 @@ export const Route = createFileRoute("/settings/backup")({
component: Screen,
loader: async ({ context }) => {
const ark = context.ark;
const npubs = await ark.get_all_accounts();
const npubs = await ark.get_accounts();
const accounts: Account[] = [];

View File

@@ -19,7 +19,7 @@ export const Route = createFileRoute("/settings/user")({
function Screen() {
const { ark, profile } = Route.useRouteContext();
const { register, handleSubmit } = useForm();
const { register, handleSubmit } = useForm({ defaultValues: profile });
const [loading, setLoading] = useState(false);
const [picture, setPicture] = useState<string>("");
@@ -28,8 +28,8 @@ function Screen() {
try {
setLoading(true);
const profile = { ...data, picture };
await ark.create_profile(profile);
const newProfile: Metadata = { ...profile, ...data, picture };
await ark.create_profile(newProfile);
setLoading(false);
} catch (e) {
@@ -87,7 +87,7 @@ function Screen() {
</label>
<input
name="display_name"
{...register("display_name", { required: true, minLength: 1 })}
{...register("display_name")}
spellCheck={false}
className="h-9 w-full rounded-lg border-neutral-300 bg-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/>

View File

@@ -1,13 +1,15 @@
import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
import { type ColumnRouteSearch, type Event, Kind } from "@lume/types";
import { type ColumnRouteSearch, type Event, Kind, Topic } from "@lume/types";
import { Spinner } from "@lume/ui";
import { useInfiniteQuery } from "@tanstack/react-query";
import { Link, createFileRoute, redirect } from "@tanstack/react-router";
import { Virtualizer } from "virtua";
export const Route = createFileRoute("/foryou")({
export const Route = createFileRoute("/topic")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return {
account: search.account,
@@ -17,21 +19,28 @@ export const Route = createFileRoute("/foryou")({
},
beforeLoad: async ({ search, context }) => {
const ark = context.ark;
const interests = await ark.get_interest();
const key = `lume_topic_${search.label}`;
const topics = (await ark.get_nstore(key)) as unknown as Topic[];
const settings = await ark.get_settings();
if (!interests) {
if (!topics?.length) {
throw redirect({
to: "/interests",
to: "/create-topic",
search: {
...search,
redirect: "/foryou",
redirect: "/topic",
},
});
}
let hashtags: string[] = [];
for (const topic of topics) {
hashtags.push(...topic.content);
}
return {
interests,
hashtags,
settings,
};
},
@@ -39,8 +48,8 @@ export const Route = createFileRoute("/foryou")({
});
export function Screen() {
const { name, account } = Route.useSearch();
const { ark, interests } = Route.useRouteContext();
const { label, account } = Route.useSearch();
const { ark, hashtags } = Route.useRouteContext();
const {
data,
isLoading,
@@ -49,20 +58,13 @@ export function Screen() {
hasNextPage,
fetchNextPage,
} = useInfiniteQuery({
queryKey: [name, account],
queryKey: [label, account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events_from_interests(
interests.hashtags,
20,
pageParam,
);
const events = ark.get_hashtag_events(hashtags, 20, pageParam);
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage?.at(-1);
return lastEvent ? lastEvent.created_at - 1 : null;
},
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
select: (data) => data?.pages.flatMap((page) => page),
refetchOnWindowFocus: false,
});
@@ -72,15 +74,29 @@ export function Screen() {
switch (event.kind) {
case Kind.Repost:
return <RepostNote key={event.id} event={event} />;
default:
return <TextNote key={event.id} event={event} />;
default: {
const isConversation =
event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention")
.length > 0;
const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0;
if (isConversation) {
return <Conversation key={event.id} event={event} className="mb-3" />;
}
if (isQuote) {
return <Quote key={event.id} event={event} className="mb-3" />;
}
return <TextNote key={event.id} event={event} className="mb-3" />;
}
}
};
return (
<div className="p-2 w-full h-full overflow-y-auto scrollbar-none">
{isFetching && !isLoading && !isFetchingNextPage ? (
<div className="w-full h-11 flex items-center justify-center">
<div className="mb-3 w-full h-11 flex items-center justify-center bg-black/10 dark:bg-white/10 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<div className="flex items-center justify-center gap-2">
<Spinner className="size-5" />
<span className="text-sm font-medium">Fetching new notes...</span>

View File

@@ -1,5 +1,5 @@
import { TextNote } from "@/components/text";
import { type Event } from "@lume/types";
import type { Event } from "@lume/types";
import { Spinner } from "@lume/ui";
import { Await, createFileRoute } from "@tanstack/react-router";
import { defer } from "@tanstack/react-router";

View File

@@ -42,7 +42,7 @@ export function Screen() {
>
<Await promise={data}>
{(users) =>
users.profiles.map((item) => (
users.profiles.map((item: { pubkey: string }) => (
<div
key={item.pubkey}
className="h-max w-full overflow-hidden mb-3 p-2 bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl"

View File

@@ -6,7 +6,7 @@ import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { Event, Kind } from "@lume/types";
import { type Event, Kind } from "@lume/types";
import { Suspense } from "react";
import { Await } from "@tanstack/react-router";
@@ -19,7 +19,7 @@ export const Route = createFileRoute("/users/$pubkey")({
},
loader: async ({ params, context }) => {
const ark = context.ark;
return { data: defer(ark.get_events_from(params.pubkey, 50)) };
return { data: defer(ark.get_events_by(params.pubkey, 50)) };
},
component: Screen,
});

View File

@@ -3,7 +3,6 @@ import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon, InfoIcon } from "@lume/icons";
import { type Event, Kind } from "@lume/types";
import { Spinner } from "@lume/ui";
import { FETCH_LIMIT } from "@lume/utils";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useRouteContext } from "@tanstack/react-router";
@@ -14,7 +13,7 @@ export function EventList({ id }: { id: string }) {
queryKey: ["events", id],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events_from(id, FETCH_LIMIT, pageParam);
const events = await ark.get_events_by(id, 20, pageParam);
return events;
},
getNextPageParam: (lastPage) => {

View File

@@ -13,7 +13,7 @@
"@astrojs/check": "^0.5.10",
"@astrojs/tailwind": "^5.1.0",
"@fontsource/geist-mono": "^5.0.3",
"astro": "^4.7.0",
"astro": "^4.8.3",
"astro-seo-meta": "^4.1.1",
"astro-seo-schema": "^4.0.2",
"schema-dts": "^1.1.2",

View File

@@ -20,17 +20,17 @@
"node": ">=18"
},
"dependencies": {
"@tauri-apps/api": "2.0.0-beta.7",
"@tauri-apps/plugin-autostart": "2.0.0-beta.2",
"@tauri-apps/plugin-clipboard-manager": "2.1.0-beta.0",
"@tauri-apps/plugin-dialog": "2.0.0-beta.2",
"@tauri-apps/plugin-fs": "2.0.0-beta.2",
"@tauri-apps/plugin-http": "2.0.0-beta.2",
"@tauri-apps/plugin-notification": "2.0.0-beta.2",
"@tauri-apps/plugin-os": "2.0.0-beta.2",
"@tauri-apps/plugin-process": "2.0.0-beta.2",
"@tauri-apps/plugin-shell": "2.0.0-beta.2",
"@tauri-apps/plugin-updater": "2.0.0-beta.2",
"@tauri-apps/plugin-upload": "2.0.0-beta.3"
"@tauri-apps/api": "^2.0.0-beta.7",
"@tauri-apps/plugin-autostart": "2.0.0-beta.3",
"@tauri-apps/plugin-clipboard-manager": "2.1.0-beta.1",
"@tauri-apps/plugin-dialog": "2.0.0-beta.3",
"@tauri-apps/plugin-fs": "2.0.0-beta.3",
"@tauri-apps/plugin-http": "2.0.0-beta.3",
"@tauri-apps/plugin-notification": "2.0.0-beta.3",
"@tauri-apps/plugin-os": "2.0.0-beta.3",
"@tauri-apps/plugin-process": "2.0.0-beta.3",
"@tauri-apps/plugin-shell": "2.0.0-beta.4",
"@tauri-apps/plugin-updater": "2.0.0-beta.3",
"@tauri-apps/plugin-upload": "2.0.0-beta.4"
}
}

View File

@@ -5,13 +5,13 @@
"main": "./src/index.ts",
"dependencies": {
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.32.0",
"@tanstack/react-query": "^5.36.0",
"react": "^18.3.1"
},
"devDependencies": {
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.3.1",
"@types/react": "^18.3.2",
"typescript": "^5.4.5"
}
}

View File

@@ -1,10 +1,8 @@
import {
type Contact,
type Event,
type EventWithReplies,
type Interests,
type Keys,
Kind,
type LumeColumn,
type Metadata,
type Settings,
@@ -18,7 +16,6 @@ import { readFile } from "@tauri-apps/plugin-fs";
enum NSTORE_KEYS {
settings = "lume_user_settings",
interests = "lume_user_interests",
columns = "lume_user_columns",
}
@@ -32,22 +29,26 @@ export class Ark {
this.settings = undefined;
}
public async get_all_accounts() {
public async get_accounts() {
try {
const cmd: string[] = await invoke("get_accounts");
const accounts: string[] = cmd.map((item) => item.replace(".npub", ""));
const cmd: string = await invoke("get_accounts");
const parse = cmd.split(/\s+/).filter((v) => v.startsWith("npub1"));
const accounts = [...new Set(parse)];
if (!this.accounts) this.accounts = accounts;
if (!this.accounts) {
this.accounts = accounts;
}
return accounts;
} catch (e) {
throw new Error(String(e));
console.info(String(e));
return [];
}
}
public async load_selected_account(npub: string) {
public async load_account(npub: string) {
try {
const cmd: boolean = await invoke("load_selected_account", {
const cmd: boolean = await invoke("load_account", {
npub,
});
return cmd;
@@ -76,7 +77,7 @@ export class Ark {
public async create_keys() {
try {
const cmd: Keys = await invoke("create_keys");
const cmd: Keys = await invoke("create_account");
return cmd;
} catch (e) {
console.error(String(e));
@@ -85,7 +86,7 @@ export class Ark {
public async save_account(nsec: string, password = "") {
try {
const cmd: string = await invoke("save_key", {
const cmd: string = await invoke("save_account", {
nsec,
password,
});
@@ -166,24 +167,6 @@ export class Ark {
}
}
public async get_events_from(pubkey: string, limit: number, asOf?: number) {
try {
let until: string = undefined;
if (asOf && asOf > 0) until = asOf.toString();
const nostrEvents: Event[] = await invoke("get_events_from", {
publicKey: pubkey,
limit,
as_of: until,
});
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
} catch (e) {
console.error(String(e));
return [];
}
}
public async search(content: string, limit: number) {
try {
if (content.length < 1) return [];
@@ -200,100 +183,127 @@ export class Ark {
}
}
public async get_events(
private dedup_events(nostrEvents: Event[]) {
const seens = new Set<string>();
const events = nostrEvents.filter((event) => {
const eTags = event.tags.filter((el) => el[0] === "e");
const ids = eTags.map((item) => item[1]);
const isDup = ids.some((id) => seens.has(id));
// Add found ids to seen list
for (const id of ids) {
seens.add(id);
}
// Filter NSFW event
if (this.settings?.nsfw) {
const wTags = event.tags.filter((t) => t[0] === "content-warning");
const isLewd = wTags.length > 0;
return !isDup && !isLewd;
}
// Filter duplicate event
return !isDup;
});
return events;
}
public async get_local_events(
pubkeys: string[],
limit: number,
asOf?: number,
contacts?: string[],
global?: boolean,
) {
try {
let until: string = undefined;
const isGlobal = global ?? false;
if (asOf && asOf > 0) until = asOf.toString();
const seenIds = new Set<string>();
const nostrEvents: Event[] = await invoke("get_events", {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const nostrEvents: Event[] = await invoke("get_local_events", {
pubkeys,
limit,
until,
contacts,
global: isGlobal,
});
// remove duplicate event
for (const event of nostrEvents) {
if (event.kind === Kind.Repost) {
const repostId = event.tags.find((tag) => tag[0] === "e")?.[1];
seenIds.add(repostId);
}
const eventIds = event.tags
.filter((el) => el[0] === "e")
?.map((item) => item[1]);
if (eventIds?.length) {
for (const id of eventIds) {
seenIds.add(id);
}
}
}
const events = nostrEvents
.filter((event) => !seenIds.has(event.id))
.sort((a, b) => b.created_at - a.created_at);
if (this.settings?.nsfw) {
return events.filter(
(event) =>
event.tags.filter((event) => event[0] === "content-warning")
.length > 0,
);
}
const events = this.dedup_events(nostrEvents);
return events;
} catch (e) {
console.info(String(e));
console.error("[get_local_events] failed", String(e));
return [];
}
}
public async get_events_from_interests(
public async get_global_events(limit: number, asOf?: number) {
try {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const nostrEvents: Event[] = await invoke("get_global_events", {
limit,
until,
});
const events = this.dedup_events(nostrEvents);
return events;
} catch (e) {
console.error("[get_global_events] failed", String(e));
return [];
}
}
public async get_hashtag_events(
hashtags: string[],
limit: number,
asOf?: number,
) {
let until: string = undefined;
if (asOf && asOf > 0) until = asOf.toString();
try {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const nostrTags = hashtags.map((tag) => tag.replace("#", ""));
const nostrEvents: Event[] = await invoke("get_hashtag_events", {
hashtags: nostrTags,
limit,
until,
});
const events = this.dedup_events(nostrEvents);
const seenIds = new Set<string>();
const dedupQueue = new Set<string>();
const nostrTags = hashtags.map((tag) => tag.replace("#", "").toLowerCase());
const nostrEvents: Event[] = await invoke("get_events_from_interests", {
hashtags: nostrTags,
limit,
until,
});
for (const event of nostrEvents) {
const tags = event.tags
.filter((el) => el[0] === "e")
?.map((item) => item[1]);
if (tags.length) {
for (const tag of tags) {
if (seenIds.has(tag)) {
dedupQueue.add(event.id);
break;
}
seenIds.add(tag);
}
}
return events;
} catch (e) {
console.error("[get_hashtag_events] failed", String(e));
return [];
}
}
return nostrEvents
.filter((event) => !dedupQueue.has(event.id))
.sort((a, b) => b.created_at - a.created_at);
public async get_group_events(
contacts: string[],
limit: number,
asOf?: number,
) {
try {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const nostrEvents: Event[] = await invoke("get_group_events", {
list: contacts,
limit,
until,
});
const events = this.dedup_events(nostrEvents);
return events;
} catch (e) {
console.error("[get_group_events] failed", String(e));
return [];
}
}
public async get_events_by(pubkey: string, limit: number, asOf?: number) {
try {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const nostrEvents: Event[] = await invoke("get_events_by", {
publicKey: pubkey,
limit,
as_of: until,
});
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
} catch (e) {
console.error("[get_events_by] failed", String(e));
return [];
}
}
public async publish(
@@ -368,27 +378,9 @@ export class Ark {
}
}
public async upvote(id: string, author: string) {
try {
const cmd: string = await invoke("upvote", { id, pubkey: author });
return cmd;
} catch (e) {
throw new Error(String(e));
}
}
public async downvote(id: string, author: string) {
try {
const cmd: string = await invoke("downvote", { id, pubkey: author });
return cmd;
} catch (e) {
throw new Error(String(e));
}
}
public async get_event_thread(id: string) {
try {
const events: EventWithReplies[] = await invoke("get_event_thread", {
const events: EventWithReplies[] = await invoke("get_thread", {
id,
});
@@ -423,13 +415,23 @@ export class Ark {
}
}
public parse_event_thread(tags: string[][]) {
public get_thread(tags: string[][], gossip: boolean = false) {
let root: string = null;
let reply: string = null;
// Get all event references from tags, ignore mention
const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention");
if (gossip) {
const relays = tags.filter((el) => el[0] === "e" && el[2]?.length);
if (relays.length >= 1) {
for (const relay of relays) {
if (relay[2]?.length) this.add_relay(relay[2]);
}
}
}
if (events.length === 1) {
root = events[0][1];
}
@@ -490,6 +492,15 @@ export class Ark {
}
}
public async set_contact_list(pubkeys: string[]) {
try {
const cmd: boolean = await invoke("set_contact_list", { pubkeys });
return cmd;
} catch (e) {
throw new Error(String(e));
}
}
public async get_contact_list() {
try {
const cmd: string[] = await invoke("get_contact_list");
@@ -703,39 +714,6 @@ export class Ark {
}
}
public async get_interest() {
try {
const cmd: string = await invoke("get_nstore", {
key: NSTORE_KEYS.interests,
});
const interests: Interests = cmd ? JSON.parse(cmd) : null;
return interests;
} catch {
return null;
}
}
public async set_interest(
words: string[],
users: string[],
hashtags: string[],
) {
try {
const interests: Interests = {
words: words ?? [],
users: users ?? [],
hashtags: hashtags ?? [],
};
const cmd: string = await invoke("set_nstore", {
key: NSTORE_KEYS.interests,
content: JSON.stringify(interests),
});
return cmd;
} catch (e) {
throw new Error(String(e));
}
}
public async get_nstore(key: string) {
try {
const cmd: string = await invoke("get_nstore", {

View File

@@ -8,7 +8,7 @@
},
"devDependencies": {
"@lume/tsconfig": "workspace:*",
"@types/react": "^18.3.1",
"@types/react": "^18.3.2",
"typescript": "^5.4.5"
}
}

View File

@@ -65,6 +65,12 @@ export interface Account {
interests?: Interests;
}
export interface Topic {
icon: string;
title: string;
content: string[];
}
export interface Interests {
hashtags: string[];
users: string[];

View File

@@ -4,7 +4,7 @@
"private": true,
"main": "./src/index.ts",
"dependencies": {
"@getalby/sdk": "^3.5.0",
"@getalby/sdk": "^3.5.1",
"@lume/ark": "workspace:^",
"@lume/icons": "workspace:^",
"@lume/utils": "workspace:^",
@@ -17,11 +17,11 @@
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^5.32.0",
"@tanstack/react-router": "^1.31.3",
"framer-motion": "^11.1.7",
"@tanstack/react-query": "^5.36.0",
"@tanstack/react-router": "^1.32.5",
"framer-motion": "^11.2.0",
"get-urls": "^12.1.0",
"media-chrome": "^3.2.1",
"media-chrome": "^3.2.2",
"minidenticons": "^4.2.1",
"nanoid": "^5.0.7",
"qrcode.react": "^3.1.0",
@@ -29,24 +29,24 @@
"react": "^18.3.1",
"react-currency-input-field": "^3.8.0",
"react-dom": "^18.3.1",
"react-hook-form": "^7.51.3",
"react-hook-form": "^7.51.4",
"react-hotkeys-hook": "^4.5.0",
"react-i18next": "^14.1.1",
"react-snap-carousel": "^0.4.0",
"react-string-replace": "^1.1.1",
"slate": "^0.102.0",
"slate": "^0.103.0",
"slate-react": "^0.102.0",
"sonner": "^1.4.41",
"string-strip-html": "^13.4.8",
"uqr": "^0.1.2",
"use-debounce": "^10.0.0",
"virtua": "^0.30.2"
"virtua": "^0.31.0"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.3.1",
"@types/react": "^18.3.2",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5"
}

View File

@@ -1,16 +1,13 @@
import { ArrowLeftIcon, ArrowRightIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import type { ReactNode } from "react";
export function Container({
children,
withDrag = false,
withNavigate = true,
className,
}: {
children: ReactNode;
withDrag?: boolean;
withNavigate?: boolean;
className?: string;
}) {
return (
@@ -23,27 +20,8 @@ export function Container({
{withDrag ? (
<div
data-tauri-drag-region
className="bg-transparent flex h-11 w-full shrink-0 items-center justify-end pr-2"
>
{withNavigate ? (
<div className="flex items-center gap-1">
<button
type="button"
onClick={() => window.history.back()}
className="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"
>
<ArrowLeftIcon className="size-5" />
</button>
<button
type="button"
onClick={() => window.history.forward()}
className="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"
>
<ArrowRightIcon className="size-5" />
</button>
</div>
) : null}
</div>
className="bg-transparent flex h-11 w-full shrink-0"
/>
) : null}
{children}
</div>

View File

@@ -8,21 +8,21 @@
"access": "public"
},
"dependencies": {
"@tanstack/react-query": "^5.32.0",
"@tanstack/react-query": "^5.36.0",
"bitcoin-units": "^1.0.0",
"clsx": "^2.1.1",
"dayjs": "^1.11.11",
"light-bolt11-decoder": "^3.1.1",
"nostr-tools": "^2.5.1",
"nostr-tools": "^2.5.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"slate": "^0.102.0",
"slate": "^0.103.0",
"slate-react": "^0.102.0"
},
"devDependencies": {
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.3.1",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
"tailwind-merge": "^2.3.0",
"typescript": "^5.4.5"

View File

@@ -1,11 +1,3 @@
export const FETCH_LIMIT = 20;
export const LANGUAGES = [
{ label: "English", code: "en" },
{ label: "Japanese", code: "ja" },
{ label: "Français", code: "fr" },
];
export const NOSTR_MENTIONS = [
"@npub1",
"nostr:npub1",
@@ -32,8 +24,6 @@ export const NOSTR_EVENTS = [
"Nostr:nevent1",
];
export const BITCOINS = ["lnbc", "bc1p", "bc1q"];
export const IMAGES = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
export const VIDEOS = [
@@ -51,61 +41,48 @@ export const VIDEOS = [
export const AUDIOS = ["mp3", "ogg", "wav"];
export const COL_TYPES = {
default: 0,
user: 1,
thread: 2,
hashtag: 3,
group: 4,
antenas: 5,
global: 6,
trendingNotes: 9000,
waifu: 9001,
foryou: 9998,
newsfeed: 9999,
};
export const TOPICS = [
{
icon: "/anime.jpg",
title: "Anime & Manga",
icon: "📱",
title: "Technology",
content: [
"#animestr",
"#anime",
"#manga",
"#otaku",
"#frieren",
"#fate",
"#aot",
"#AttackOnTitan",
"#JujutsuKaisen",
"#OnePiece",
"#KimetsuNoYaiba",
"#Overlord",
"#Evangelion",
"#DemonSlayer",
"#JoJo",
"#SPYxFAMILY",
"#MatoSeiheinoSlave",
"#ghibli",
"#ChainsawMan",
"#Gintama",
"#animeart",
"#animegirl",
"#cosplay",
"#weeb",
"#animeworld",
"#fanart",
"#vocaloid",
"#vtuber",
"#hololive",
"#hololivemeet",
"#pixiv",
"#waifu",
"#Apple",
"#Tesla",
"#AMD",
"#Intel",
"#Xiaomi",
"#Huawei",
"#OpenAI",
"#BigTech",
"#ai",
"#IOS",
"#Android",
"#oppo",
"#nostr",
"#technology",
"#tech",
"#innovation",
"#engineering",
"#business",
"#iphone",
"#technews",
"#science",
"#gadgets",
"#software",
"#programming",
"#smartphone",
"#samsung",
"#coding",
"#computer",
"#security",
"#gadget",
"#mobile",
"#opensource",
"#tor",
],
},
{
icon: "/gaming.jpg",
icon: "🕹",
title: "Gaming",
content: [
"#gamestr",
@@ -163,8 +140,46 @@ export const TOPICS = [
],
},
{
icon: "/music.jpg",
title: "Music & Entertainment",
icon: "🍥",
title: "Anime",
content: [
"#animestr",
"#anime",
"#manga",
"#otaku",
"#frieren",
"#fate",
"#aot",
"#AttackOnTitan",
"#JujutsuKaisen",
"#OnePiece",
"#KimetsuNoYaiba",
"#Overlord",
"#Evangelion",
"#DemonSlayer",
"#JoJo",
"#SPYxFAMILY",
"#MatoSeiheinoSlave",
"#ghibli",
"#ChainsawMan",
"#Gintama",
"#animeart",
"#animegirl",
"#cosplay",
"#weeb",
"#animeworld",
"#fanart",
"#vocaloid",
"#vtuber",
"#hololive",
"#hololivemeet",
"#pixiv",
"#waifu",
],
},
{
icon: "📺",
title: "Entertainment",
content: [
"#audiostr",
"#musicstr",
@@ -208,12 +223,6 @@ export const TOPICS = [
"#dvd",
"#amass",
"#bluray",
],
},
{
icon: "/movie.jpg",
title: "Television",
content: [
"#filmstr",
"#moviestr",
"#movies",
@@ -244,46 +253,7 @@ export const TOPICS = [
],
},
{
icon: "/technology.jpg",
title: "Technology",
content: [
"#Apple",
"#Tesla",
"#AMD",
"#Intel",
"#Xiaomi",
"#Huawei",
"#OpenAI",
"#BigTech",
"#ai",
"#IOS",
"#Android",
"#oppo",
"#nostr",
"#technology",
"#tech",
"#innovation",
"#engineering",
"#business",
"#iphone",
"#technews",
"#science",
"#gadgets",
"#software",
"#programming",
"#smartphone",
"#samsung",
"#coding",
"#computer",
"#security",
"#gadget",
"#mobile",
"#opensource",
"#tor",
],
},
{
icon: "/photography.jpg",
icon: "📷",
title: "Photography",
content: [
"#photostr",
@@ -309,8 +279,8 @@ export const TOPICS = [
],
},
{
icon: "/art.jpg",
title: "Art & Design",
icon: "🧑‍🎨",
title: "Design",
content: [
"#nostrdesign",
"#artstr",
@@ -343,7 +313,7 @@ export const TOPICS = [
],
},
{
icon: "/nsfw.jpg",
icon: "🔞",
title: "NSFW",
content: [
"#pornstr",
@@ -365,20 +335,5 @@ export const TOPICS = [
},
];
export const QUOTES = [
"You can learn more about nostr here: https://usenostr.org",
"Nostr has a lot of awesome clients, you can spend a bit of time to try https://snort.social",
"Nostr has a lot of awesome clients, you can spend a bit of time to try https://iris.to",
"Nostr has a lot of awesome clients, you can spend a bit of time to try https://primal.net",
"Nostr has a lot of awesome clients, you can spend a bit of time to try https://nostrudel.ninja",
"If you're using iOS, you can use Nostr with https://damus.io",
"If you're using Android, you can use Nostr with Amethyst",
"If you want to curate and share your interests on Nostr, you can use https://pinstr.app",
"If you want to post anonymously on Nostr, you can use https://www.get-tao.app",
"If you want to read in-depth content and high quality insights, you can use https://habla.news",
"You can send secure messages on Nostr with https://0xchat.com/",
"Are you a fan of following topics, instead of people? Use https://zapddit.com",
];
// @ts-ignore, it works
export const VITE_FLATPAK_RESOURCE = import.meta.env.VITE_FLATPAK_RESOURCE;

1680
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

395
src-tauri/Cargo.lock generated
View File

@@ -107,55 +107,6 @@ dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
[[package]]
name = "anstyle-parse"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
]
[[package]]
name = "anyhow"
version = "1.0.83"
@@ -170,7 +121,7 @@ checksum = "9fb4009533e8ff8f1450a5bcbc30f4242a1d34442221f72314bea1f5dc9c7f89"
dependencies = [
"clipboard-win",
"core-graphics",
"image",
"image 0.25.1",
"log",
"objc2",
"objc2-app-kit",
@@ -489,17 +440,6 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "auto-launch"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471"
dependencies = [
"dirs",
"thiserror",
"winreg 0.10.1",
]
[[package]]
name = "autocfg"
version = "1.3.0"
@@ -556,6 +496,12 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "bit_field"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
[[package]]
name = "bitcoin"
version = "0.31.2"
@@ -927,33 +873,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "clap"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim 0.11.1",
]
[[package]]
name = "clap_lex"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "clipboard-win"
version = "5.3.1"
@@ -994,10 +913,10 @@ dependencies = [
]
[[package]]
name = "colorchoice"
version = "1.0.1"
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "combine"
@@ -1097,12 +1016,37 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-common"
version = "0.1.6"
@@ -1201,7 +1145,7 @@ dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim 0.10.0",
"strsim",
"syn 2.0.63",
]
@@ -1314,15 +1258,6 @@ dependencies = [
"subtle",
]
[[package]]
name = "dirs"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
@@ -1333,17 +1268,6 @@ dependencies = [
"dirs-sys-next",
]
[[package]]
name = "dirs-sys"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
@@ -1446,7 +1370,7 @@ dependencies = [
"rustc_version",
"toml 0.8.2",
"vswhom",
"winreg 0.52.0",
"winreg",
]
[[package]]
@@ -1572,6 +1496,22 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "exr"
version = "1.72.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4"
dependencies = [
"bit_field",
"flume",
"half",
"lebe",
"miniz_oxide",
"rayon-core",
"smallvec",
"zune-inflate",
]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
@@ -1650,6 +1590,15 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "flume"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
dependencies = [
"spin",
]
[[package]]
name = "fnv"
version = "1.0.7"
@@ -1999,6 +1948,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "gif"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
dependencies = [
"color_quant",
"weezl",
]
[[package]]
name = "gimli"
version = "0.28.1"
@@ -2184,6 +2143,16 @@ dependencies = [
"tracing",
]
[[package]]
name = "half"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
dependencies = [
"cfg-if",
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -2459,6 +2428,24 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "image"
version = "0.24.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"exr",
"gif",
"jpeg-decoder",
"num-traits",
"png",
"qoi",
"tiff",
]
[[package]]
name = "image"
version = "0.25.1"
@@ -2561,12 +2548,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
[[package]]
name = "itoa"
version = "0.4.8"
@@ -2638,6 +2619,9 @@ name = "jpeg-decoder"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
dependencies = [
"rayon",
]
[[package]]
name = "js-sys"
@@ -2684,6 +2668,21 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "keyring-search"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95edd18bc5d51d3544a788d86b6d96c20b3fea5518349559df1feaa4775fc632"
dependencies = [
"byteorder",
"lazy_static",
"linux-keyutils",
"regex",
"secret-service",
"security-framework",
"windows-sys 0.52.0",
]
[[package]]
name = "kuchikiki"
version = "0.8.2"
@@ -2703,6 +2702,12 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lebe"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libappindicator"
version = "0.9.0"
@@ -2872,6 +2877,7 @@ version = "4.0.0"
dependencies = [
"cocoa",
"keyring",
"keyring-search",
"nostr-sdk",
"objc",
"rand 0.8.5",
@@ -2879,9 +2885,8 @@ dependencies = [
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-autostart",
"tauri-plugin-cli",
"tauri-plugin-clipboard-manager",
"tauri-plugin-decorum",
"tauri-plugin-dialog",
"tauri-plugin-fs",
"tauri-plugin-http",
@@ -3146,9 +3151,9 @@ checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "nostr"
version = "0.30.0"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a27223888faca0c4ba9b97c2b7dc776e9a33d5f54e3558887471cf17798b5fbf"
checksum = "ef9b0b5429bb61b46c125eba70e0f7193c8c6d04df8a917c2abe5bea03f8bcb0"
dependencies = [
"aes 0.8.4",
"base64 0.21.7",
@@ -3176,9 +3181,9 @@ dependencies = [
[[package]]
name = "nostr-database"
version = "0.30.0"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f726b8c0904a838f64b51a931a1bf39e341f5584a5e04f06310fbfb847e2e924"
checksum = "a89506f743a5441695ab727794db41d8df1c1365ff96c25272985adf08f816b3"
dependencies = [
"async-trait",
"flatbuffers",
@@ -3191,9 +3196,9 @@ dependencies = [
[[package]]
name = "nostr-relay-pool"
version = "0.30.0"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52f0ccf9e81aa747abdfa130007651248b37c3699d37029bad701e68902257ce"
checksum = "f751acc8bbb1329718d673470c7c3a18cddd33963dd91b97bccc92037113d254"
dependencies = [
"async-utility",
"async-wsocket",
@@ -3207,9 +3212,9 @@ dependencies = [
[[package]]
name = "nostr-sdk"
version = "0.30.0"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1ffedac7ab488e0dfea52804d0c43fafc7e3eefc62d97726d3927a1390db05b"
checksum = "0e65cd9f4f26f3f8e10253c518aff9e61a9204f600dfe4c3c241b0230471c67f"
dependencies = [
"async-utility",
"lnurl-pay",
@@ -3227,9 +3232,9 @@ dependencies = [
[[package]]
name = "nostr-signer"
version = "0.30.0"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e568670664cf5cc14a794ae32dfc04bde385d63ff0f5b1c3745dd3ea69f73a"
checksum = "8be1878e91a0b4a95cfd8142349b6124b037b287375d76db9638ccc4b4cdf271"
dependencies = [
"async-utility",
"nostr",
@@ -3241,9 +3246,9 @@ dependencies = [
[[package]]
name = "nostr-sqlite"
version = "0.30.0"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a608d8db5ff0a8ebf7e2605fdd0b11fdcf0873724680fa97307a845ed262713c"
checksum = "1dca940d759c07d3928008842ad0ac63fa693efd83f4e8821c9a1badb0be226e"
dependencies = [
"async-trait",
"deadpool-sqlite",
@@ -3257,9 +3262,9 @@ dependencies = [
[[package]]
name = "nostr-zapper"
version = "0.30.0"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "420a7c6458d5c1dc502b3d36fb9f8598837743a737b84adb4ef8ea36b98c5e07"
checksum = "5558bb031cff46e5b580847f26617d516ded4c0f8fd27fb568ec875bcd8fb99c"
dependencies = [
"async-trait",
"nostr",
@@ -3388,9 +3393,9 @@ dependencies = [
[[package]]
name = "nwc"
version = "0.30.0"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e236611ea96d3545138f7b2152f2e4571e3c93436ddc91d1c458f366e5c6430f"
checksum = "bbd88cc13a04ae41037c182489893c2f421ba0c12a028564ec339882e7f96d61"
dependencies = [
"async-utility",
"nostr",
@@ -4076,6 +4081,15 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "qoi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
dependencies = [
"bytemuck",
]
[[package]]
name = "quick-xml"
version = "0.31.0"
@@ -4187,6 +4201,26 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cc3bcbdb1ddfc11e700e62968e6b4cc9c75bb466464ad28fb61c5b2c964418b"
[[package]]
name = "rayon"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "read-progress-stream"
version = "1.0.0"
@@ -4318,7 +4352,7 @@ dependencies = [
"wasm-streams",
"web-sys",
"webpki-roots",
"winreg 0.52.0",
"winreg",
]
[[package]]
@@ -4931,6 +4965,9 @@ name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "stable_deref_trait"
@@ -4985,12 +5022,6 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.5.0"
@@ -5276,43 +5307,14 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-autostart"
version = "2.0.0-beta.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c96bf3c316939431e021b48622f15e0e1126376e43f1c35a29e7d24c5760209"
dependencies = [
"auto-launch",
"log",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror",
]
[[package]]
name = "tauri-plugin-cli"
version = "2.0.0-beta.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "379e72dd4771d4d4f42c2ea8682f1ad4657fa5a042c3739a5e35300acc694c0c"
dependencies = [
"clap",
"log",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror",
]
[[package]]
name = "tauri-plugin-clipboard-manager"
version = "2.0.0-beta.2"
version = "2.1.0-beta.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8cf4b7fde295126b30b8279aa2addedda7689027a6a7fc4cdf9bea43a86ad84"
checksum = "56e8b139799d4d39d15e4c1a61dd5d86e4ac154e6324f9dd1032c7a354cde6c1"
dependencies = [
"arboard",
"image 0.24.9",
"log",
"serde",
"serde_json",
@@ -5321,6 +5323,23 @@ dependencies = [
"thiserror",
]
[[package]]
name = "tauri-plugin-decorum"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d44342c777f49441733f657ec22da85c0660056c97f4b5db507088a7cbf399e"
dependencies = [
"anyhow",
"cocoa",
"objc",
"rand 0.8.5",
"serde",
"tauri",
"tauri-plugin",
"thiserror",
"windows 0.56.0",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.0.0-beta.7"
@@ -6148,12 +6167,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.8.0"
@@ -6808,15 +6821,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"winapi",
]
[[package]]
name = "winreg"
version = "0.52.0"
@@ -7145,6 +7149,15 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "zune-inflate"
version = "0.2.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
dependencies = [
"simd-adler32",
]
[[package]]
name = "zvariant"
version = "3.15.1"

View File

@@ -11,7 +11,7 @@ rust-version = "1.68"
tauri-build = { version = "2.0.0-beta", features = [] }
[dependencies]
nostr-sdk = { version = "0.30", features = ["sqlite"] }
nostr-sdk = { version = "0.31", features = ["sqlite"] }
tokio = { version = "1", features = ["full"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
@@ -22,8 +22,7 @@ tauri = { version = "2.0.0-beta", features = [
"native-tls-vendored",
"protocol-asset",
] }
tauri-plugin-cli = "2.0.0-beta"
tauri-plugin-clipboard-manager = "2.0.0-beta"
tauri-plugin-clipboard-manager = "2.1.0-beta"
tauri-plugin-dialog = "2.0.0-beta"
tauri-plugin-fs = "2.0.0-beta"
tauri-plugin-http = "2.0.0-beta"
@@ -32,11 +31,12 @@ tauri-plugin-os = "2.0.0-beta"
tauri-plugin-process = "2.0.0-beta"
tauri-plugin-shell = "2.0.0-beta"
tauri-plugin-updater = "2.0.0-beta"
tauri-plugin-autostart = "2.0.0-beta"
tauri-plugin-upload = "2.0.0-beta"
tauri-plugin-window-state = "2.0.0-beta"
tauri-plugin-decorum = "0.1.0"
webpage = { version = "2.0", features = ["serde"] }
keyring = "2"
keyring-search = "0.2.0"
[target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.25.0"

View File

@@ -36,14 +36,21 @@
"window:allow-create",
"window:allow-close",
"window:allow-set-focus",
"clipboard-manager:allow-write",
"clipboard-manager:allow-read",
"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:default",
"dialog:allow-open",
"dialog:allow-ask",
"dialog:allow-message",
"process:allow-restart",

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","splash","settings","search","nwc","activity","zap-*","event-*","user-*","editor-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","updater:default","updater:allow-check","updater:allow-download-and-install","window:allow-start-dragging","window:allow-create","window:allow-close","window:allow-set-focus","clipboard-manager:allow-write","clipboard-manager:allow-read","webview:allow-create-webview-window","webview:allow-create-webview","webview:allow-set-webview-size","webview:allow-set-webview-position","webview:allow-webview-close","dialog:default","dialog:allow-ask","dialog:allow-message","fs:allow-read-file","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","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","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","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"]}}

View File

@@ -2545,75 +2545,6 @@
"app:deny-version"
]
},
{
"type": "string",
"enum": [
"autostart:default"
]
},
{
"description": "autostart:allow-disable -> Enables the disable command without any pre-configured scope.",
"type": "string",
"enum": [
"autostart:allow-disable"
]
},
{
"description": "autostart:allow-enable -> Enables the enable command without any pre-configured scope.",
"type": "string",
"enum": [
"autostart:allow-enable"
]
},
{
"description": "autostart:allow-is-enabled -> Enables the is_enabled command without any pre-configured scope.",
"type": "string",
"enum": [
"autostart:allow-is-enabled"
]
},
{
"description": "autostart:deny-disable -> Denies the disable command without any pre-configured scope.",
"type": "string",
"enum": [
"autostart:deny-disable"
]
},
{
"description": "autostart:deny-enable -> Denies the enable command without any pre-configured scope.",
"type": "string",
"enum": [
"autostart:deny-enable"
]
},
{
"description": "autostart:deny-is-enabled -> Denies the is_enabled command without any pre-configured scope.",
"type": "string",
"enum": [
"autostart:deny-is-enabled"
]
},
{
"description": "cli:default -> Allows reading the CLI matches",
"type": "string",
"enum": [
"cli:default"
]
},
{
"description": "cli:allow-cli-matches -> Enables the cli_matches command without any pre-configured scope.",
"type": "string",
"enum": [
"cli:allow-cli-matches"
]
},
{
"description": "cli:deny-cli-matches -> Denies the cli_matches command without any pre-configured scope.",
"type": "string",
"enum": [
"cli:deny-cli-matches"
]
},
{
"type": "string",
"enum": [
@@ -2621,31 +2552,107 @@
]
},
{
"description": "clipboard-manager:allow-read -> Enables the read command without any pre-configured scope.",
"description": "clipboard-manager:allow-clear -> Enables the clear command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:allow-read"
"clipboard-manager:allow-clear"
]
},
{
"description": "clipboard-manager:allow-write -> Enables the write command without any pre-configured scope.",
"description": "clipboard-manager:allow-read-image -> Enables the read_image command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:allow-write"
"clipboard-manager:allow-read-image"
]
},
{
"description": "clipboard-manager:deny-read -> Denies the read command without any pre-configured scope.",
"description": "clipboard-manager:allow-read-text -> Enables the read_text command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:deny-read"
"clipboard-manager:allow-read-text"
]
},
{
"description": "clipboard-manager:deny-write -> Denies the write command without any pre-configured scope.",
"description": "clipboard-manager:allow-write-html -> Enables the write_html command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:deny-write"
"clipboard-manager:allow-write-html"
]
},
{
"description": "clipboard-manager:allow-write-image -> Enables the write_image command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:allow-write-image"
]
},
{
"description": "clipboard-manager:allow-write-text -> Enables the write_text command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:allow-write-text"
]
},
{
"description": "clipboard-manager:deny-clear -> Denies the clear command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:deny-clear"
]
},
{
"description": "clipboard-manager:deny-read-image -> Denies the read_image command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:deny-read-image"
]
},
{
"description": "clipboard-manager:deny-read-text -> Denies the read_text command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:deny-read-text"
]
},
{
"description": "clipboard-manager:deny-write-html -> Denies the write_html command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:deny-write-html"
]
},
{
"description": "clipboard-manager:deny-write-image -> Denies the write_image command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:deny-write-image"
]
},
{
"description": "clipboard-manager:deny-write-text -> Denies the write_text command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:deny-write-text"
]
},
{
"type": "string",
"enum": [
"decorum:default"
]
},
{
"description": "decorum:allow-show-snap-overlay -> Enables the show_snap_overlay command without any pre-configured scope.",
"type": "string",
"enum": [
"decorum:allow-show-snap-overlay"
]
},
{
"description": "decorum:deny-show-snap-overlay -> Denies the show_snap_overlay command without any pre-configured scope.",
"type": "string",
"enum": [
"decorum:deny-show-snap-overlay"
]
},
{

View File

@@ -2545,75 +2545,6 @@
"app:deny-version"
]
},
{
"type": "string",
"enum": [
"autostart:default"
]
},
{
"description": "autostart:allow-disable -> Enables the disable command without any pre-configured scope.",
"type": "string",
"enum": [
"autostart:allow-disable"
]
},
{
"description": "autostart:allow-enable -> Enables the enable command without any pre-configured scope.",
"type": "string",
"enum": [
"autostart:allow-enable"
]
},
{
"description": "autostart:allow-is-enabled -> Enables the is_enabled command without any pre-configured scope.",
"type": "string",
"enum": [
"autostart:allow-is-enabled"
]
},
{
"description": "autostart:deny-disable -> Denies the disable command without any pre-configured scope.",
"type": "string",
"enum": [
"autostart:deny-disable"
]
},
{
"description": "autostart:deny-enable -> Denies the enable command without any pre-configured scope.",
"type": "string",
"enum": [
"autostart:deny-enable"
]
},
{
"description": "autostart:deny-is-enabled -> Denies the is_enabled command without any pre-configured scope.",
"type": "string",
"enum": [
"autostart:deny-is-enabled"
]
},
{
"description": "cli:default -> Allows reading the CLI matches",
"type": "string",
"enum": [
"cli:default"
]
},
{
"description": "cli:allow-cli-matches -> Enables the cli_matches command without any pre-configured scope.",
"type": "string",
"enum": [
"cli:allow-cli-matches"
]
},
{
"description": "cli:deny-cli-matches -> Denies the cli_matches command without any pre-configured scope.",
"type": "string",
"enum": [
"cli:deny-cli-matches"
]
},
{
"type": "string",
"enum": [
@@ -2621,31 +2552,107 @@
]
},
{
"description": "clipboard-manager:allow-read -> Enables the read command without any pre-configured scope.",
"description": "clipboard-manager:allow-clear -> Enables the clear command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:allow-read"
"clipboard-manager:allow-clear"
]
},
{
"description": "clipboard-manager:allow-write -> Enables the write command without any pre-configured scope.",
"description": "clipboard-manager:allow-read-image -> Enables the read_image command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:allow-write"
"clipboard-manager:allow-read-image"
]
},
{
"description": "clipboard-manager:deny-read -> Denies the read command without any pre-configured scope.",
"description": "clipboard-manager:allow-read-text -> Enables the read_text command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:deny-read"
"clipboard-manager:allow-read-text"
]
},
{
"description": "clipboard-manager:deny-write -> Denies the write command without any pre-configured scope.",
"description": "clipboard-manager:allow-write-html -> Enables the write_html command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:deny-write"
"clipboard-manager:allow-write-html"
]
},
{
"description": "clipboard-manager:allow-write-image -> Enables the write_image command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:allow-write-image"
]
},
{
"description": "clipboard-manager:allow-write-text -> Enables the write_text command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:allow-write-text"
]
},
{
"description": "clipboard-manager:deny-clear -> Denies the clear command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:deny-clear"
]
},
{
"description": "clipboard-manager:deny-read-image -> Denies the read_image command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:deny-read-image"
]
},
{
"description": "clipboard-manager:deny-read-text -> Denies the read_text command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:deny-read-text"
]
},
{
"description": "clipboard-manager:deny-write-html -> Denies the write_html command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:deny-write-html"
]
},
{
"description": "clipboard-manager:deny-write-image -> Denies the write_image command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:deny-write-image"
]
},
{
"description": "clipboard-manager:deny-write-text -> Denies the write_text command without any pre-configured scope.",
"type": "string",
"enum": [
"clipboard-manager:deny-write-text"
]
},
{
"type": "string",
"enum": [
"decorum:default"
]
},
{
"description": "decorum:allow-show-snap-overlay -> Enables the show_snap_overlay command without any pre-configured scope.",
"type": "string",
"enum": [
"decorum:allow-show-snap-overlay"
]
},
{
"description": "decorum:deny-show-snap-overlay -> Denies the show_snap_overlay command without any pre-configured scope.",
"type": "string",
"enum": [
"decorum:deny-show-snap-overlay"
]
},
{

File diff suppressed because it is too large Load Diff

View File

@@ -7,12 +7,12 @@
"cover": "/newsfeed.png",
"coverRetina": "/newsfeed@2x.png",
"author": "Lume",
"description": "Keep up to date with people you're following."
"description": "Keep up to date with the people you're following."
},
{
"label": "rRtguZwIpd5G8Wt54OTb7",
"name": "For you",
"content": "/foryou",
"name": "Topic",
"content": "/topic",
"logo": "",
"cover": "/foryou.png",
"coverRetina": "/foryou@2x.png",
@@ -21,13 +21,13 @@
},
{
"label": "fve9fk2fVyFWORPBkjd79",
"name": "Group Feeds",
"name": "Group",
"content": "/group",
"logo": "",
"cover": "/group.png",
"coverRetina": "/group@2x.png",
"author": "Lume",
"description": "Collective of people you're interested in."
"description": "Focus feeds for people you like."
},
{
"label": "gxtcIbgD8YNPbeI5o92I8",

View File

@@ -1,4 +1,7 @@
[
{ "label": "onboarding", "name": "Onboarding", "content": "/onboarding" },
{ "label": "lume_newsfeed", "name": "Newsfeed", "content": "/newsfeed" },
{ "label": "lume_topic", "name": "Topic", "content": "/topic" },
{ "label": "lume_group", "name": "Group", "content": "/group" },
{ "label": "open", "name": "Open", "content": "/open" }
]

View File

@@ -1,5 +1,4 @@
use std::process::Command;
use tauri::Manager;
#[tauri::command]
pub async fn show_in_folder(path: String) {
@@ -47,26 +46,3 @@ pub async fn show_in_folder(path: String) {
Command::new("open").args(["-R", &path]).spawn().unwrap();
}
}
#[tauri::command]
pub fn get_accounts(app_handle: tauri::AppHandle) -> Result<Vec<String>, ()> {
let dir = app_handle.path().home_dir().unwrap();
if let Ok(paths) = std::fs::read_dir(dir.join("Lume/")) {
let files = paths
.filter_map(|res| res.ok())
.map(|dir_entry| dir_entry.path())
.filter_map(|path| {
if path.extension().map_or(false, |ext| ext == "npub") {
Some(path.file_name().unwrap().to_str().unwrap().to_string())
} else {
None
}
})
.collect::<Vec<_>>();
Ok(files)
} else {
Err(())
}
}

View File

@@ -3,10 +3,11 @@ use cocoa::{appkit::NSApp, base::nil, foundation::NSString};
use std::path::PathBuf;
use tauri::utils::config::WindowEffectsConfig;
use tauri::window::Effect;
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
use tauri::Url;
use tauri::WebviewWindowBuilder;
use tauri::{LogicalPosition, LogicalSize, Manager, WebviewUrl};
use tauri_plugin_decorum::WebviewWindowExt;
#[tauri::command]
pub fn create_column(
@@ -57,22 +58,6 @@ pub fn close_column(label: &str, app_handle: tauri::AppHandle) -> Result<bool, (
}
}
#[tauri::command]
pub fn navigate(label: &str, url: &str, app_handle: tauri::AppHandle) -> Result<(), String> {
match app_handle.get_webview(label) {
Some(mut webview) => {
if let Ok(new_url) = Url::parse(url) {
println!("navigate to: {}", new_url);
webview.navigate(new_url);
Ok(())
} else {
Err("URL is not valid".into())
}
}
None => Err("Webview not found".into()),
}
}
#[tauri::command]
pub fn reposition_column(
label: &str,
@@ -129,7 +114,7 @@ pub fn open_window(
};
} else {
#[cfg(target_os = "macos")]
let _ = WebviewWindowBuilder::new(&app_handle, label, WebviewUrl::App(PathBuf::from(url)))
let window = WebviewWindowBuilder::new(&app_handle, label, WebviewUrl::App(PathBuf::from(url)))
.title(title)
.min_inner_size(width, height)
.inner_size(width, height)
@@ -145,17 +130,36 @@ pub fn open_window(
.build()
.unwrap();
// [macOS] Custom traffic light possition
// #[cfg(target_os = "macos")]
// setup_traffic_light_positioner(app_handle.get_window(label).unwrap());
#[cfg(target_os = "windows")]
let window = WebviewWindowBuilder::new(&app_handle, label, WebviewUrl::App(PathBuf::from(url)))
.title(title)
.min_inner_size(width, height)
.inner_size(width, height)
.transparent(true)
.effects(WindowEffectsConfig {
state: None,
effects: vec![Effect::Mica],
radius: None,
color: None,
})
.build()
.unwrap();
#[cfg(not(target_os = "macos"))]
let _ = WebviewWindowBuilder::new(&app_handle, label, WebviewUrl::App(PathBuf::from(url)))
#[cfg(target_os = "linux")]
let window = WebviewWindowBuilder::new(&app_handle, label, WebviewUrl::App(PathBuf::from(url)))
.title(title)
.min_inner_size(width, height)
.inner_size(width, height)
.build()
.unwrap();
#[cfg(target_os = "windows")]
// Create a custom titlebar for Windows
window.create_overlay_titlebar().unwrap();
// Set a custom inset to the traffic lights
#[cfg(target_os = "macos")]
window.set_traffic_lights_inset(8.0, 16.0).unwrap();
}
Ok(())

View File

@@ -5,7 +5,6 @@
pub mod commands;
pub mod nostr;
pub mod traffic_light;
pub mod tray;
#[cfg(target_os = "macos")]
@@ -18,9 +17,7 @@ extern crate objc;
use nostr_sdk::prelude::*;
use std::fs;
use tauri::Manager;
use tauri_plugin_autostart::MacosLauncher;
#[cfg(target_os = "macos")]
use traffic_light::setup_traffic_light_positioner;
use tauri_plugin_decorum::WebviewWindowExt;
pub struct Nostr {
client: Client,
@@ -29,8 +26,15 @@ pub struct Nostr {
fn main() {
tauri::Builder::default()
.setup(|app| {
let main_window = app.get_webview_window("main").unwrap();
// Create a custom titlebar for Windows
#[cfg(target_os = "windows")]
main_window.create_overlay_titlebar().unwrap();
// Set a custom inset to the traffic lights
#[cfg(target_os = "macos")]
setup_traffic_light_positioner(app.get_window("main").unwrap());
main_window.set_traffic_lights_inset(8.0, 16.0).unwrap();
// Setup app tray
let handle = app.handle().clone();
@@ -83,6 +87,7 @@ fn main() {
}
_ => {}
})
.plugin(tauri_plugin_decorum::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
@@ -93,22 +98,16 @@ fn main() {
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_upload::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_autostart::init(
MacosLauncher::LaunchAgent,
Some(vec![]),
))
.invoke_handler(tauri::generate_handler![
nostr::relay::get_relays,
nostr::relay::list_connected_relays,
nostr::relay::connect_relay,
nostr::relay::remove_relay,
nostr::keys::create_keys,
nostr::keys::save_key,
nostr::keys::get_accounts,
nostr::keys::create_account,
nostr::keys::save_account,
nostr::keys::get_encrypted_key,
nostr::keys::get_stored_nsec,
nostr::keys::nostr_connect,
nostr::keys::verify_signer,
nostr::keys::load_selected_account,
nostr::keys::load_account,
nostr::keys::event_to_bech32,
nostr::keys::user_to_bech32,
nostr::keys::to_npub,
@@ -118,6 +117,7 @@ fn main() {
nostr::metadata::get_current_user_profile,
nostr::metadata::get_profile,
nostr::metadata::get_contact_list,
nostr::metadata::set_contact_list,
nostr::metadata::create_profile,
nostr::metadata::follow,
nostr::metadata::unfollow,
@@ -130,23 +130,22 @@ fn main() {
nostr::metadata::zap_event,
nostr::metadata::friend_to_friend,
nostr::event::get_event,
nostr::event::get_events_from,
nostr::event::get_events,
nostr::event::get_events_from_interests,
nostr::event::get_event_thread,
nostr::event::get_thread,
nostr::event::get_events_by,
nostr::event::get_local_events,
nostr::event::get_global_events,
nostr::event::get_hashtag_events,
nostr::event::get_group_events,
nostr::event::publish,
nostr::event::repost,
nostr::event::search,
commands::folder::show_in_folder,
commands::folder::get_accounts,
commands::opg::fetch_opg,
commands::window::create_column,
commands::window::close_column,
commands::window::reposition_column,
commands::window::resize_column,
commands::window::open_window,
commands::window::navigate,
commands::window::set_badge
commands::window::set_badge,
commands::opg::fetch_opg,
])
.run(tauri::generate_context!())
.expect("error while running tauri application")

View File

@@ -9,7 +9,15 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<String, Stri
let event_id: Option<EventId> = match Nip19::from_bech32(id) {
Ok(val) => match val {
Nip19::EventId(id) => Some(id),
Nip19::Event(event) => Some(event.event_id),
Nip19::Event(event) => {
let relays = event.relays;
for relay in relays.into_iter() {
let url = Url::from_str(&relay).unwrap();
let _ = client.add_relay(url.clone()).await.unwrap_or_default();
client.connect_relay(url).await.unwrap_or_default();
}
Some(event.event_id)
}
_ => None,
},
Err(_) => match EventId::from_hex(id) {
@@ -18,28 +26,47 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<String, Stri
},
};
if let Some(id) = event_id {
let filter = Filter::new().id(id);
match event_id {
Some(id) => {
let filter = Filter::new().id(id);
if let Ok(events) = &client
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
.await
{
if let Some(event) = events.first() {
Ok(event.as_json())
} else {
Err("Event not found with current relay list".into())
match client
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
.await
{
Ok(events) => {
if let Some(event) = events.first() {
Ok(event.as_json())
} else {
Err("Cannot found this event with current relay list".into())
}
}
Err(err) => Err(err.to_string()),
}
} else {
Err("Event not found with current relay list".into())
}
} else {
Err("EventId is not valid".into())
None => Err("Event ID is not valid.".into()),
}
}
#[tauri::command]
pub async fn get_events_from(
pub async fn get_thread(id: &str, state: State<'_, Nostr>) -> Result<Vec<Event>, String> {
let client = &state.client;
match EventId::from_hex(id) {
Ok(event_id) => {
let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id);
match client.get_events_of(vec![filter], None).await {
Ok(events) => Ok(events),
Err(err) => Err(err.to_string()),
}
}
Err(_) => Err("Event ID is not valid".into()),
}
}
#[tauri::command]
pub async fn get_events_by(
public_key: &str,
limit: usize,
as_of: Option<&str>,
@@ -47,32 +74,62 @@ pub async fn get_events_from(
) -> Result<Vec<Event>, String> {
let client = &state.client;
if let Ok(author) = PublicKey::from_str(public_key) {
let until = match as_of {
Some(until) => Timestamp::from_str(until).unwrap(),
None => Timestamp::now(),
};
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.author(author)
.limit(limit)
.until(until);
match PublicKey::from_str(public_key) {
Ok(author) => {
let until = match as_of {
Some(until) => Timestamp::from_str(until).unwrap(),
None => Timestamp::now(),
};
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.author(author)
.limit(limit)
.until(until);
match client.get_events_of(vec![filter], None).await {
Ok(events) => Ok(events),
Err(err) => Err(err.to_string()),
match client.get_events_of(vec![filter], None).await {
Ok(events) => Ok(events),
Err(err) => Err(err.to_string()),
}
}
} else {
Err("Public Key is not valid, please check again.".into())
Err(err) => Err(err.to_string()),
}
}
#[tauri::command]
pub async fn get_events(
pub async fn get_local_events(
pubkeys: Vec<String>,
limit: usize,
until: Option<&str>,
state: State<'_, Nostr>,
) -> Result<Vec<Event>, String> {
let client = &state.client;
let as_of = match until {
Some(until) => Timestamp::from_str(until).unwrap(),
None => Timestamp::now(),
};
let authors: Vec<PublicKey> = pubkeys
.into_iter()
.map(|p| PublicKey::from_hex(p).unwrap())
.collect();
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(limit)
.authors(authors)
.until(as_of);
match client
.get_events_of(vec![filter], Some(Duration::from_secs(8)))
.await
{
Ok(events) => Ok(events),
Err(err) => Err(err.to_string()),
}
}
#[tauri::command]
pub async fn get_global_events(
limit: usize,
until: Option<&str>,
contacts: Option<Vec<&str>>,
global: bool,
state: State<'_, Nostr>,
) -> Result<Vec<Event>, String> {
let client = &state.client;
@@ -81,66 +138,22 @@ pub async fn get_events(
None => Timestamp::now(),
};
match global {
true => {
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(limit)
.until(as_of);
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(limit)
.until(as_of);
match client
.get_events_of(vec![filter], Some(Duration::from_secs(15)))
.await
{
Ok(events) => Ok(events),
Err(err) => Err(err.to_string()),
}
}
false => {
let authors = match contacts {
Some(val) => {
let c: Vec<PublicKey> = val
.into_iter()
.map(|key| PublicKey::from_str(key).unwrap())
.collect();
Some(c)
}
None => {
match client
.get_contact_list_public_keys(Some(Duration::from_secs(10)))
.await
{
Ok(val) => Some(val),
Err(_) => None,
}
}
};
match authors {
Some(val) => {
if val.is_empty() {
Err("Get local events but contact list is empty".into())
} else {
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(limit)
.authors(val.clone())
.until(as_of);
match client.get_events_of(vec![filter], None).await {
Ok(events) => Ok(events),
Err(err) => Err(err.to_string()),
}
}
}
None => Err("Get local events but contact list is empty".into()),
}
}
match client
.get_events_of(vec![filter], Some(Duration::from_secs(8)))
.await
{
Ok(events) => Ok(events),
Err(err) => Err(err.to_string()),
}
}
#[tauri::command]
pub async fn get_events_from_interests(
pub async fn get_hashtag_events(
hashtags: Vec<&str>,
limit: usize,
until: Option<&str>,
@@ -164,19 +177,30 @@ pub async fn get_events_from_interests(
}
#[tauri::command]
pub async fn get_event_thread(id: &str, state: State<'_, Nostr>) -> Result<Vec<Event>, String> {
pub async fn get_group_events(
list: Vec<&str>,
limit: usize,
until: Option<&str>,
state: State<'_, Nostr>,
) -> Result<Vec<Event>, String> {
let client = &state.client;
let as_of = match until {
Some(until) => Timestamp::from_str(until).unwrap(),
None => Timestamp::now(),
};
let authors: Vec<PublicKey> = list
.into_iter()
.map(|hex| PublicKey::from_hex(hex).unwrap())
.collect();
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(limit)
.until(as_of)
.authors(authors);
match EventId::from_hex(id) {
Ok(event_id) => {
let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id);
match client.get_events_of(vec![filter], None).await {
Ok(events) => Ok(events),
Err(err) => Err(err.to_string()),
}
}
Err(_) => Err("Event ID is not valid".into()),
match client.get_events_of(vec![filter], None).await {
Ok(events) => Ok(events),
Err(err) => Err(err.to_string()),
}
}
@@ -200,32 +224,8 @@ pub async fn repost(raw: &str, state: State<'_, Nostr>) -> Result<EventId, Strin
let client = &state.client;
let event = Event::from_json(raw).unwrap();
if let Ok(event_id) = client.repost(&event, None).await {
Ok(event_id)
} else {
Err("Repost failed".into())
}
}
#[tauri::command]
pub async fn search(
content: &str,
limit: usize,
state: State<'_, Nostr>,
) -> Result<Vec<Event>, String> {
println!("search: {}", content);
let client = &state.client;
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Metadata])
.search(content)
.limit(limit);
match client
.get_events_of(vec![filter], Some(Duration::from_secs(15)))
.await
{
Ok(events) => Ok(events),
match client.repost(&event, None).await {
Ok(event_id) => Ok(event_id),
Err(err) => Err(err.to_string()),
}
}

View File

@@ -1,35 +1,46 @@
use crate::Nostr;
use keyring::Entry;
use keyring_search::{Limit, List, Search};
use nostr_sdk::prelude::*;
use std::str::FromStr;
use std::time::Duration;
use std::{fs::File, str::FromStr};
use tauri::{Manager, State};
use tauri::State;
#[derive(serde::Serialize)]
pub struct CreateKeysResponse {
pub struct Account {
npub: String,
nsec: String,
}
#[tauri::command]
pub fn create_keys() -> Result<CreateKeysResponse, ()> {
pub fn get_accounts() -> Result<String, String> {
let search = Search::new().unwrap();
let results = search.by("Account", "nostr_secret");
match List::list_credentials(results, Limit::All) {
Ok(list) => Ok(list),
Err(_) => Err("Empty.".into()),
}
}
#[tauri::command]
pub fn create_account() -> Result<Account, ()> {
let keys = Keys::generate();
let public_key = keys.public_key();
let secret_key = keys.secret_key().expect("secret key failed");
let secret_key = keys.secret_key().unwrap();
let result = CreateKeysResponse {
npub: public_key.to_bech32().expect("npub failed"),
nsec: secret_key.to_bech32().expect("nsec failed"),
let result = Account {
npub: public_key.to_bech32().unwrap(),
nsec: secret_key.to_bech32().unwrap(),
};
Ok(result)
}
#[tauri::command]
pub async fn save_key(
pub async fn save_account(
nsec: &str,
password: &str,
app_handle: tauri::AppHandle,
state: State<'_, Nostr>,
) -> Result<String, String> {
let secret_key: Result<SecretKey, String>;
@@ -38,12 +49,12 @@ pub async fn save_key(
let encrypted_key = EncryptedSecretKey::from_bech32(nsec).unwrap();
secret_key = match encrypted_key.to_secret_key(password) {
Ok(val) => Ok(val),
Err(_) => Err("Wrong passphase".into()),
Err(err) => Err(err.to_string()),
};
} else {
secret_key = match SecretKey::from_bech32(nsec) {
Ok(val) => Ok(val),
Err(_) => Err("nsec is not valid".into()),
Err(err) => Err(err.to_string()),
}
}
@@ -53,13 +64,7 @@ pub async fn save_key(
let npub = nostr_keys.public_key().to_bech32().unwrap();
let nsec = nostr_keys.secret_key().unwrap().to_bech32().unwrap();
let home_dir = app_handle.path().home_dir().unwrap();
let app_dir = home_dir.join("Lume/");
let file_path = npub.clone() + ".npub";
let _ = File::create(app_dir.join(file_path)).unwrap();
let keyring = Entry::new("Lume Secret Storage", &npub).unwrap();
let keyring = Entry::new(&npub, "nostr_secret").unwrap();
let _ = keyring.set_password(&nsec);
let signer = NostrSigner::Keys(nostr_keys);
@@ -75,94 +80,29 @@ pub async fn save_key(
}
#[tauri::command]
pub async fn nostr_connect(
npub: &str,
uri: &str,
app_handle: tauri::AppHandle,
state: State<'_, Nostr>,
) -> Result<String, String> {
pub async fn load_account(npub: &str, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client;
let app_keys = Keys::generate();
match NostrConnectURI::parse(uri) {
Ok(bunker_uri) => {
println!("connecting... {}", uri);
match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(120), None).await {
Ok(signer) => {
let home_dir = app_handle.path().home_dir().unwrap();
let app_dir = home_dir.join("Lume/");
let file_path = npub.to_owned() + ".npub";
let keyring = Entry::new("Lume Secret Storage", npub).unwrap();
let _ = File::create(app_dir.join(file_path)).unwrap();
let _ = keyring.set_password(uri);
let _ = client.set_signer(Some(signer.into())).await;
Ok(npub.into())
}
Err(err) => Err(err.to_string()),
}
}
Err(err) => Err(err.to_string()),
}
}
#[tauri::command]
pub async fn verify_signer(state: State<'_, Nostr>) -> Result<bool, ()> {
let client = &state.client;
if (client.signer().await).is_ok() {
Ok(true)
} else {
Ok(false)
}
}
#[tauri::command(async)]
pub fn get_encrypted_key(npub: &str, password: &str) -> Result<String, String> {
let keyring = Entry::new("Lume Secret Storage", npub).unwrap();
if let Ok(nsec) = keyring.get_password() {
let secret_key = SecretKey::from_bech32(nsec).expect("Get secret key failed");
let new_key = EncryptedSecretKey::new(&secret_key, password, 16, KeySecurity::Unknown);
if let Ok(key) = new_key {
Ok(key.to_bech32().unwrap())
} else {
Err("Encrypt key failed".into())
}
} else {
Err("Key not found".into())
}
}
#[tauri::command]
pub fn get_stored_nsec(npub: &str) -> Result<String, String> {
let keyring = Entry::new("Lume Secret Storage", npub).unwrap();
if let Ok(nsec) = keyring.get_password() {
Ok(nsec)
} else {
Err("Key not found".into())
}
}
#[tauri::command]
pub async fn load_selected_account(npub: &str, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client;
let keyring = Entry::new("Lume Secret Storage", npub).unwrap();
let keyring = Entry::new(&npub, "nostr_secret").unwrap();
match keyring.get_password() {
Ok(password) => {
if password.starts_with("bunker://") {
let app_keys = Keys::generate();
let bunker_uri = NostrConnectURI::parse(password).unwrap();
let signer = Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(60), None)
.await
.unwrap();
let local_keyring = Entry::new(&npub, "bunker_local_account").unwrap();
// Update signer
client.set_signer(Some(signer.into())).await;
match local_keyring.get_password() {
Ok(local_password) => {
let secret_key = SecretKey::from_bech32(local_password).unwrap();
let app_keys = Keys::new(secret_key);
let bunker_uri = NostrConnectURI::parse(password).unwrap();
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!(),
}
} else {
let secret_key = SecretKey::from_bech32(password).expect("Get secret key failed");
let keys = Keys::new(secret_key);
@@ -195,7 +135,7 @@ pub async fn load_selected_account(npub: &str, state: State<'_, Nostr>) -> Resul
let relay_url = item.0.to_string();
let opts = match item.1 {
Some(val) => {
if val == RelayMetadata::Read {
if val == &RelayMetadata::Read {
RelayOptions::new().read(true).write(false)
} else {
RelayOptions::new().write(true).read(false)
@@ -206,15 +146,12 @@ pub async fn load_selected_account(npub: &str, state: State<'_, Nostr>) -> Resul
// Add relay to relay pool
let _ = client
.add_relay_with_opts(relay_url, opts)
.add_relay_with_opts(relay_url.clone(), opts)
.await
.unwrap_or_default();
// Connect relay
client
.connect_relay(item.0.to_string())
.await
.unwrap_or_default();
client.connect_relay(relay_url).await.unwrap_or_default();
}
}
}
@@ -227,6 +164,62 @@ pub async fn load_selected_account(npub: &str, state: State<'_, Nostr>) -> Resul
}
}
#[tauri::command]
pub async fn nostr_connect(
npub: &str,
uri: &str,
state: State<'_, Nostr>,
) -> Result<String, String> {
let client = &state.client;
let local_key = Keys::generate();
match NostrConnectURI::parse(uri) {
Ok(bunker_uri) => {
match Nip46Signer::new(
bunker_uri,
local_key.clone(),
Duration::from_secs(120),
None,
)
.await
{
Ok(signer) => {
let local_secret = local_key.secret_key().unwrap().to_bech32().unwrap();
let secret_keyring = Entry::new(&npub, "nostr_secret").unwrap();
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
let _ = client.set_signer(Some(signer.into())).await;
Ok(npub.into())
}
Err(err) => Err(err.to_string()),
}
}
Err(err) => Err(err.to_string()),
}
}
#[tauri::command(async)]
pub fn get_encrypted_key(npub: &str, password: &str) -> Result<String, String> {
let keyring = Entry::new(npub, "nostr_secret").unwrap();
if let Ok(nsec) = keyring.get_password() {
let secret_key = SecretKey::from_bech32(nsec).unwrap();
let new_key = EncryptedSecretKey::new(&secret_key, password, 16, KeySecurity::Medium);
if let Ok(key) = new_key {
Ok(key.to_bech32().unwrap())
} else {
Err("Encrypt key failed".into())
}
} else {
Err("Key not found".into())
}
}
#[tauri::command]
pub fn event_to_bech32(id: &str, relays: Vec<String>) -> Result<String, ()> {
let event_id = EventId::from_hex(id).unwrap();
@@ -251,14 +244,13 @@ pub fn to_npub(hex: &str) -> Result<String, ()> {
Ok(npub.to_bech32().unwrap())
}
#[tauri::command(async)]
pub async fn verify_nip05(key: &str, nip05: &str) -> Result<bool, ()> {
let public_key = PublicKey::from_str(key).unwrap();
let status = nip05::verify(public_key, nip05, None).await;
if status.is_ok() {
Ok(true)
} else {
Ok(false)
#[tauri::command]
pub async fn verify_nip05(key: &str, nip05: &str) -> Result<bool, String> {
match PublicKey::from_str(key) {
Ok(public_key) => {
let status = nip05::verify(&public_key, nip05, None).await;
Ok(status.is_ok())
}
Err(err) => Err(err.to_string()),
}
}

View File

@@ -91,12 +91,12 @@ pub async fn friend_to_friend(npub: &str, state: State<'_, Nostr>) -> Result<boo
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 Tag::PublicKey {
if let Some(TagStandard::PublicKey {
public_key,
relay_url,
alias,
uppercase: false,
} = tag
}) = tag.to_standardized()
{
contact_list.push(Contact::new(public_key, relay_url, alias))
}
@@ -104,8 +104,6 @@ pub async fn friend_to_friend(npub: &str, state: State<'_, Nostr>) -> Result<boo
}
}
println!("contact list: {}", contact_list.len());
match client.set_contact_list(contact_list).await {
Ok(_) => Ok(true),
Err(err) => Err(err.to_string()),
@@ -184,19 +182,38 @@ pub async fn get_profile(id: &str, state: State<'_, Nostr>) -> Result<Metadata,
}
}
#[tauri::command]
pub async fn set_contact_list(pubkeys: Vec<&str>, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client;
let contact_list: Vec<Contact> = pubkeys
.into_iter()
.map(|p| Contact::new(PublicKey::from_hex(p).unwrap(), None, Some("")))
.collect();
match client.set_contact_list(contact_list).await {
Ok(_) => Ok(true),
Err(err) => Err(err.to_string()),
}
}
#[tauri::command]
pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
let client = &state.client;
if let Ok(contact_list) = client.get_contact_list(Some(Duration::from_secs(10))).await {
let list = contact_list
.into_iter()
.map(|f| f.public_key.to_hex())
.collect();
match client.get_contact_list(Some(Duration::from_secs(10))).await {
Ok(contact_list) => {
if !contact_list.is_empty() {
let list = contact_list
.into_iter()
.map(|f| f.public_key.to_hex())
.collect();
Ok(list)
} else {
Err("Contact list not found".into())
Ok(list)
} else {
Err("Empty.".into())
}
}
Err(err) => Err(err.to_string()),
}
}
@@ -302,14 +319,11 @@ pub async fn set_nstore(
let public_key = signer.public_key().await.unwrap();
let encrypted = signer.nip44_encrypt(public_key, content).await.unwrap();
let tag = Tag::Identifier(key.into());
let tag = Tag::identifier(key);
let builder = EventBuilder::new(Kind::ApplicationSpecificData, encrypted, vec![tag]);
match client.send_event_builder(builder).await {
Ok(event_id) => {
println!("set nstore: {}", event_id);
Ok(event_id)
}
Ok(event_id) => Ok(event_id),
Err(err) => Err(err.to_string()),
}
}
@@ -322,38 +336,29 @@ pub async fn get_nstore(key: &str, state: State<'_, Nostr>) -> Result<String, St
let client = &state.client;
if let Ok(signer) = client.signer().await {
let public_key = signer.public_key().await;
let public_key = signer.public_key().await.unwrap();
let filter = Filter::new()
.author(public_key)
.kind(Kind::ApplicationSpecificData)
.identifier(key)
.limit(1);
if let Ok(author) = public_key {
let filter = Filter::new()
.author(author)
.kind(Kind::ApplicationSpecificData)
.identifier(key)
.limit(1);
let query = client
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
.await;
if let Ok(events) = query {
match client
.get_events_of(vec![filter], Some(Duration::from_secs(5)))
.await
{
Ok(events) => {
if let Some(event) = events.first() {
println!("get nstore key: {} - received: {}", key, event.id);
let content = event.content();
match signer.nip44_decrypt(author, content).await {
match signer.nip44_decrypt(public_key, content).await {
Ok(decrypted) => Ok(decrypted),
Err(_) => Err(event.content.to_string()),
}
} else {
println!("get nstore key: {}", key);
Err("Value not found".into())
}
} else {
Err("Query nstore event failed".into())
}
} else {
Err("Something is wrong".into())
Err(err) => Err(err.to_string()),
}
} else {
Err("Signer is required".into())

View File

@@ -72,21 +72,17 @@ pub async fn get_relays(state: State<'_, Nostr>) -> Result<Relays, ()> {
}
}
#[tauri::command]
pub async fn list_connected_relays(state: State<'_, Nostr>) -> Result<Vec<Url>, ()> {
let client = &state.client;
let connected_relays = client.relays().await;
let list = connected_relays.into_keys().collect();
Ok(list)
}
#[tauri::command]
pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool, ()> {
let client = &state.client;
if let Ok(_) = client.add_relay(relay).await {
let _ = client.connect_relay(relay);
Ok(true)
if let Ok(status) = client.add_relay(relay).await {
if status == true {
println!("connecting to relay: {}", relay);
let _ = client.connect_relay(relay);
Ok(true)
} else {
Ok(false)
}
} else {
Ok(false)
}

View File

@@ -1,343 +0,0 @@
#[cfg(target_os = "macos")]
use objc::{msg_send, sel, sel_impl};
#[cfg(target_os = "macos")]
use rand::{distributions::Alphanumeric, Rng};
use tauri::{
plugin::{Builder, TauriPlugin},
Manager, Runtime, Window,
}; // 0.8
const WINDOW_CONTROL_PAD_X: f64 = 8.0;
const WINDOW_CONTROL_PAD_Y: f64 = 16.0;
struct UnsafeWindowHandle(*mut std::ffi::c_void);
unsafe impl Send for UnsafeWindowHandle {}
unsafe impl Sync for UnsafeWindowHandle {}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("traffic_light_positioner")
.on_window_ready(|window| {
#[cfg(target_os = "macos")]
setup_traffic_light_positioner(window);
})
.build()
}
#[cfg(target_os = "macos")]
fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64) {
use cocoa::appkit::{NSView, NSWindow, NSWindowButton};
use cocoa::foundation::NSRect;
let ns_window = ns_window_handle.0 as cocoa::base::id;
unsafe {
let close = ns_window.standardWindowButton_(NSWindowButton::NSWindowCloseButton);
let miniaturize = ns_window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
let zoom = ns_window.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
let title_bar_container_view = close.superview().superview();
let close_rect: NSRect = msg_send![close, frame];
let button_height = close_rect.size.height;
let title_bar_frame_height = button_height + y;
let mut title_bar_rect = NSView::frame(title_bar_container_view);
title_bar_rect.size.height = title_bar_frame_height;
title_bar_rect.origin.y = NSView::frame(ns_window).size.height - title_bar_frame_height;
let _: () = msg_send![title_bar_container_view, setFrame: title_bar_rect];
let window_buttons = vec![close, miniaturize, zoom];
let space_between = NSView::frame(miniaturize).origin.x - NSView::frame(close).origin.x;
for (i, button) in window_buttons.into_iter().enumerate() {
let mut rect: NSRect = NSView::frame(button);
rect.origin.x = x + (i as f64 * space_between);
button.setFrameOrigin(rect.origin);
}
}
}
#[cfg(target_os = "macos")]
#[derive(Debug)]
struct WindowState<R: Runtime> {
window: Window<R>,
}
#[cfg(target_os = "macos")]
pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
use cocoa::appkit::NSWindow;
use cocoa::base::{id, BOOL};
use cocoa::foundation::NSUInteger;
use objc::runtime::{Object, Sel};
use std::ffi::c_void;
// Do the initial positioning
position_traffic_lights(
UnsafeWindowHandle(window.ns_window().expect("Failed to create window handle")),
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
);
// Ensure they stay in place while resizing the window.
fn with_window_state<R: Runtime, F: FnOnce(&mut WindowState<R>) -> T, T>(this: &Object, func: F) {
let ptr = unsafe {
let x: *mut c_void = *this.get_ivar("app_box");
&mut *(x as *mut WindowState<R>)
};
func(ptr);
}
unsafe {
let ns_win = window
.ns_window()
.expect("NS Window should exist to mount traffic light delegate.") as id;
let current_delegate: id = ns_win.delegate();
extern "C" fn on_window_should_close(this: &Object, _cmd: Sel, sender: id) -> BOOL {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, windowShouldClose: sender]
}
}
extern "C" fn on_window_will_close(this: &Object, _cmd: Sel, notification: id) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowWillClose: notification];
}
}
extern "C" fn on_window_did_resize<R: Runtime>(this: &Object, _cmd: Sel, notification: id) {
unsafe {
with_window_state(this, |state: &mut WindowState<R>| {
let id = state
.window
.ns_window()
.expect("NS window should exist on state to handle resize") as id;
#[cfg(target_os = "macos")]
position_traffic_lights(
UnsafeWindowHandle(id as *mut std::ffi::c_void),
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
);
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidResize: notification];
}
}
extern "C" fn on_window_did_move(this: &Object, _cmd: Sel, notification: id) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidMove: notification];
}
}
extern "C" fn on_window_did_change_backing_properties(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidChangeBackingProperties: notification];
}
}
extern "C" fn on_window_did_become_key(this: &Object, _cmd: Sel, notification: id) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidBecomeKey: notification];
}
}
extern "C" fn on_window_did_resign_key(this: &Object, _cmd: Sel, notification: id) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidResignKey: notification];
}
}
extern "C" fn on_dragging_entered(this: &Object, _cmd: Sel, notification: id) -> BOOL {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, draggingEntered: notification]
}
}
extern "C" fn on_prepare_for_drag_operation(
this: &Object,
_cmd: Sel,
notification: id,
) -> BOOL {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, prepareForDragOperation: notification]
}
}
extern "C" fn on_perform_drag_operation(this: &Object, _cmd: Sel, sender: id) -> BOOL {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, performDragOperation: sender]
}
}
extern "C" fn on_conclude_drag_operation(this: &Object, _cmd: Sel, notification: id) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, concludeDragOperation: notification];
}
}
extern "C" fn on_dragging_exited(this: &Object, _cmd: Sel, notification: id) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, draggingExited: notification];
}
}
extern "C" fn on_window_will_use_full_screen_presentation_options(
this: &Object,
_cmd: Sel,
window: id,
proposed_options: NSUInteger,
) -> NSUInteger {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, window: window willUseFullScreenPresentationOptions: proposed_options]
}
}
extern "C" fn on_window_did_enter_full_screen<R: Runtime>(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
with_window_state(this, |state: &mut WindowState<R>| {
state
.window
.emit("did-enter-fullscreen", ())
.expect("Failed to emit event");
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidEnterFullScreen: notification];
}
}
extern "C" fn on_window_will_enter_full_screen<R: Runtime>(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
with_window_state(this, |state: &mut WindowState<R>| {
state
.window
.emit("will-enter-fullscreen", ())
.expect("Failed to emit event");
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowWillEnterFullScreen: notification];
}
}
extern "C" fn on_window_did_exit_full_screen<R: Runtime>(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
with_window_state(this, |state: &mut WindowState<R>| {
state
.window
.emit("did-exit-fullscreen", ())
.expect("Failed to emit event");
let id = state.window.ns_window().expect("Failed to emit event") as id;
position_traffic_lights(
UnsafeWindowHandle(id as *mut std::ffi::c_void),
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
);
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidExitFullScreen: notification];
}
}
extern "C" fn on_window_will_exit_full_screen<R: Runtime>(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
with_window_state(this, |state: &mut WindowState<R>| {
state
.window
.emit("will-exit-fullscreen", ())
.expect("Failed to emit event");
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowWillExitFullScreen: notification];
}
}
extern "C" fn on_window_did_fail_to_enter_full_screen(this: &Object, _cmd: Sel, window: id) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidFailToEnterFullScreen: window];
}
}
extern "C" fn on_effective_appearance_did_change(this: &Object, _cmd: Sel, notification: id) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, effectiveAppearanceDidChange: notification];
}
}
extern "C" fn on_effective_appearance_did_changed_on_main_thread(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![
super_del,
effectiveAppearanceDidChangedOnMainThread: notification
];
}
}
// Are we deallocing this properly ? (I miss safe Rust :( )
let window_label = window.label().to_string();
let app_state = WindowState { window };
let app_box = Box::into_raw(Box::new(app_state)) as *mut c_void;
let random_str: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(20)
.map(char::from)
.collect();
// We need to ensure we have a unique delegate name, otherwise we will panic while trying to create a duplicate
// delegate with the same name.
let delegate_name = format!("windowDelegate_{}_{}", window_label, random_str);
ns_win.setDelegate_(cocoa::delegate!(&delegate_name, {
window: id = ns_win,
app_box: *mut c_void = app_box,
toolbar: id = cocoa::base::nil,
super_delegate: id = current_delegate,
(windowShouldClose:) => on_window_should_close as extern fn(&Object, Sel, id) -> BOOL,
(windowWillClose:) => on_window_will_close as extern fn(&Object, Sel, id),
(windowDidResize:) => on_window_did_resize::<R> as extern fn(&Object, Sel, id),
(windowDidMove:) => on_window_did_move as extern fn(&Object, Sel, id),
(windowDidChangeBackingProperties:) => on_window_did_change_backing_properties as extern fn(&Object, Sel, id),
(windowDidBecomeKey:) => on_window_did_become_key as extern fn(&Object, Sel, id),
(windowDidResignKey:) => on_window_did_resign_key as extern fn(&Object, Sel, id),
(draggingEntered:) => on_dragging_entered as extern fn(&Object, Sel, id) -> BOOL,
(prepareForDragOperation:) => on_prepare_for_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
(performDragOperation:) => on_perform_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
(concludeDragOperation:) => on_conclude_drag_operation as extern fn(&Object, Sel, id),
(draggingExited:) => on_dragging_exited as extern fn(&Object, Sel, id),
(window:willUseFullScreenPresentationOptions:) => on_window_will_use_full_screen_presentation_options as extern fn(&Object, Sel, id, NSUInteger) -> NSUInteger,
(windowDidEnterFullScreen:) => on_window_did_enter_full_screen::<R> as extern fn(&Object, Sel, id),
(windowWillEnterFullScreen:) => on_window_will_enter_full_screen::<R> as extern fn(&Object, Sel, id),
(windowDidExitFullScreen:) => on_window_did_exit_full_screen::<R> as extern fn(&Object, Sel, id),
(windowWillExitFullScreen:) => on_window_will_exit_full_screen::<R> as extern fn(&Object, Sel, id),
(windowDidFailToEnterFullScreen:) => on_window_did_fail_to_enter_full_screen as extern fn(&Object, Sel, id),
(effectiveAppearanceDidChange:) => on_effective_appearance_did_change as extern fn(&Object, Sel, id),
(effectiveAppearanceDidChangedOnMainThread:) => on_effective_appearance_did_changed_on_main_thread as extern fn(&Object, Sel, id)
}))
}
}

View File

@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"productName": "Lume",
"version": "4.0.1",
"version": "4.0.4",
"identifier": "nu.lume.Lume",
"build": {
"beforeBuildCommand": "pnpm desktop:build",

View File

@@ -1,15 +1,19 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"app": {
"windows": [
{
"title": "Lume",
"label": "main",
"width": 500,
"height": 800,
"minWidth": 500,
"minHeight": 800
}
]
}
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"app": {
"windows": [
{
"title": "Lume",
"label": "main",
"width": 500,
"height": 800,
"minWidth": 500,
"minHeight": 800,
"transparent": true,
"windowEffects": {
"effects": ["mica"]
}
}
]
}
}