Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d3f2264e9 | ||
|
|
407fe40b67 | ||
|
|
1f38eba2cc | ||
|
|
9b5867f80c | ||
|
|
cac774a0c1 | ||
|
|
82689bf3c3 | ||
|
|
f60e438a64 | ||
|
|
ca06f2b6ed | ||
|
|
99d9c70826 | ||
|
|
60afbf090b |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 26 KiB |
@@ -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}>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
PlusIcon,
|
||||
SearchIcon,
|
||||
} from "@lume/icons";
|
||||
import { Event, Kind } from "@lume/types";
|
||||
import { type Event, Kind } from "@lume/types";
|
||||
import { User } from "@/components/user";
|
||||
import {
|
||||
cn,
|
||||
@@ -23,7 +23,7 @@ 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 };
|
||||
},
|
||||
@@ -106,7 +106,7 @@ function Accounts() {
|
||||
}
|
||||
|
||||
// change current account and update signer
|
||||
const select = await ark.load_selected_account(npub);
|
||||
const select = await ark.load_account(npub);
|
||||
|
||||
if (select) {
|
||||
return navigate({ to: "/$account/home", params: { account: npub } });
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
86
apps/desktop2/src/routes/create-newsfeed.f2f.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
53
apps/desktop2/src/routes/create-newsfeed.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
128
apps/desktop2/src/routes/create-newsfeed.users.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
104
apps/desktop2/src/routes/create-topic.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -14,7 +14,7 @@ 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({
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,90 +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 {
|
||||
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
||||
const isGlobal = global ?? false;
|
||||
const seens = new Set<string>();
|
||||
|
||||
const nostrEvents: Event[] = await invoke("get_events", {
|
||||
const nostrEvents: Event[] = await invoke("get_local_events", {
|
||||
pubkeys,
|
||||
limit,
|
||||
until,
|
||||
contacts,
|
||||
global: isGlobal,
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
const events = this.dedup_events(nostrEvents);
|
||||
|
||||
return events;
|
||||
} catch (e) {
|
||||
console.error("[get_events] failed", 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(
|
||||
@@ -358,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,
|
||||
});
|
||||
|
||||
@@ -413,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];
|
||||
}
|
||||
@@ -480,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");
|
||||
@@ -693,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", {
|
||||
|
||||
6
packages/types/index.d.ts
vendored
@@ -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[];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
293
src-tauri/Cargo.lock
generated
@@ -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"
|
||||
@@ -221,11 +172,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.3.1"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
|
||||
checksum = "9f2776ead772134d55b62dd45e59a79e21612d85d0af729b8b7d3967d601a62a"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener 5.3.0",
|
||||
"event-listener-strategy 0.5.2",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
@@ -488,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"
|
||||
@@ -709,9 +650,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.16.0"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5"
|
||||
checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
@@ -776,9 +717,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
version = "1.1.7"
|
||||
version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239"
|
||||
checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -932,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",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -1004,12 +918,6 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@@ -1219,9 +1127,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.9"
|
||||
version = "0.20.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1"
|
||||
checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
@@ -1229,9 +1137,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.9"
|
||||
version = "0.20.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120"
|
||||
checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
@@ -1243,9 +1151,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.9"
|
||||
version = "0.20.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178"
|
||||
checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
@@ -1350,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"
|
||||
@@ -1369,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"
|
||||
@@ -1482,7 +1370,7 @@ dependencies = [
|
||||
"rustc_version",
|
||||
"toml 0.8.2",
|
||||
"vswhom",
|
||||
"winreg 0.52.0",
|
||||
"winreg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2316,9 +2204,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hex-conservative"
|
||||
version = "0.1.2"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20"
|
||||
checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2"
|
||||
|
||||
[[package]]
|
||||
name = "hex_lit"
|
||||
@@ -2660,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"
|
||||
@@ -2786,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"
|
||||
@@ -2980,6 +2877,7 @@ version = "4.0.0"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"keyring",
|
||||
"keyring-search",
|
||||
"nostr-sdk",
|
||||
"objc",
|
||||
"rand 0.8.5",
|
||||
@@ -2987,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 +3043,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "muda"
|
||||
version = "0.13.4"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1717c136c99673f55640c14125a0349a5cd7fee6efcfb0adbfe4c289e3b3f7f2"
|
||||
checksum = "c6fde56ead0971b4caae4aa0f19502e49d1fac2af9d0c60068e2d235e26ce709"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"crossbeam-channel",
|
||||
@@ -3254,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",
|
||||
@@ -3284,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",
|
||||
@@ -3299,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",
|
||||
@@ -3315,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",
|
||||
@@ -3335,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",
|
||||
@@ -3349,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",
|
||||
@@ -3365,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",
|
||||
@@ -3496,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",
|
||||
@@ -4455,7 +4352,7 @@ dependencies = [
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"winreg 0.52.0",
|
||||
"winreg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4596,9 +4493,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.17"
|
||||
version = "1.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
|
||||
checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
@@ -4783,18 +4680,18 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.202"
|
||||
version = "1.0.201"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
|
||||
checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.202"
|
||||
version = "1.0.201"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
|
||||
checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4803,9 +4700,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive_internals"
|
||||
version = "0.29.1"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
||||
checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4837,9 +4734,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.6"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
|
||||
checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -5121,9 +5018,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
@@ -5410,36 +5307,6 @@ 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.1.0-beta.2"
|
||||
@@ -5456,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"
|
||||
@@ -6283,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"
|
||||
@@ -6943,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"
|
||||
|
||||
@@ -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,7 +22,6 @@ tauri = { version = "2.0.0-beta", features = [
|
||||
"native-tls-vendored",
|
||||
"protocol-asset",
|
||||
] }
|
||||
tauri-plugin-cli = "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"
|
||||
@@ -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"
|
||||
|
||||
@@ -36,6 +36,13 @@
|
||||
"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",
|
||||
|
||||
@@ -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-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"]}}
|
||||
{"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"]}}
|
||||
@@ -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": [
|
||||
@@ -2704,6 +2635,26 @@
|
||||
"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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
@@ -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": [
|
||||
@@ -2704,6 +2635,26 @@
|
||||
"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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" }
|
||||
]
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -30,7 +30,7 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<String, Stri
|
||||
Some(id) => {
|
||||
let filter = Filter::new().id(id);
|
||||
|
||||
match &client
|
||||
match client
|
||||
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
|
||||
.await
|
||||
{
|
||||
@@ -49,7 +49,24 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<String, Stri
|
||||
}
|
||||
|
||||
#[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>,
|
||||
@@ -57,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;
|
||||
@@ -91,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>,
|
||||
@@ -174,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()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,30 +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> {
|
||||
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()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -224,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();
|
||||
@@ -249,9 +245,12 @@ pub fn to_npub(hex: &str) -> Result<String, ()> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
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;
|
||||
|
||||
Ok(status.is_ok())
|
||||
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()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -182,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()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,7 +319,7 @@ 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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||
"productName": "Lume",
|
||||
"version": "4.0.3",
|
||||
"version": "4.0.4",
|
||||
"identifier": "nu.lume.Lume",
|
||||
"build": {
|
||||
"beforeBuildCommand": "pnpm desktop:build",
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||