Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7449000f5f | ||
|
|
dc7762ca11 | ||
|
|
3a3f960dde | ||
|
|
12e066ff2e | ||
|
|
fe4f965ed5 | ||
|
|
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;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const { ark } = useRouteContext({ strict: false });
|
const { ark } = useRouteContext({ strict: false });
|
||||||
const thread = ark.parse_event_thread(event.tags);
|
const thread = ark.get_thread(event.tags);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Note.Provider event={event}>
|
<Note.Provider event={event}>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEvent } from "@lume/ark";
|
import { useEvent } from "@lume/ark";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { Note } from ".";
|
import { Note } from ".";
|
||||||
|
import { InfoIcon } from "@lume/icons";
|
||||||
|
|
||||||
export function NoteChild({
|
export function NoteChild({
|
||||||
eventId,
|
eventId,
|
||||||
@@ -12,11 +13,25 @@ export function NoteChild({
|
|||||||
const { isLoading, isError, data } = useEvent(eventId);
|
const { isLoading, isError, data } = useEvent(eventId);
|
||||||
|
|
||||||
if (isLoading) {
|
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) {
|
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 (
|
return (
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useRouteContext } from "@tanstack/react-router";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { User } from "@/components/user";
|
import { User } from "@/components/user";
|
||||||
|
import { Spinner } from "@lume/ui";
|
||||||
|
|
||||||
export function MentionNote({
|
export function MentionNote({
|
||||||
eventId,
|
eventId,
|
||||||
@@ -18,15 +19,15 @@ export function MentionNote({
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full cursor-default items-center justify-between rounded-xl border border-black/10 p-3 dark:border-white/10">
|
<div className="mt-2 w-full flex h-20 items-center justify-center rounded-xl border border-black/10 dark:border-white/10">
|
||||||
<p>Loading...</p>
|
<Spinner className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError || !data) {
|
if (isError || !data) {
|
||||||
return (
|
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")}
|
{t("note.error")}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
PlusIcon,
|
PlusIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
} from "@lume/icons";
|
} from "@lume/icons";
|
||||||
import { Event, Kind } from "@lume/types";
|
import { type Event, Kind } from "@lume/types";
|
||||||
import { User } from "@/components/user";
|
import { User } from "@/components/user";
|
||||||
import {
|
import {
|
||||||
cn,
|
cn,
|
||||||
@@ -23,7 +23,7 @@ import * as Popover from "@radix-ui/react-popover";
|
|||||||
export const Route = createFileRoute("/$account")({
|
export const Route = createFileRoute("/$account")({
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: async ({ context }) => {
|
||||||
const ark = context.ark;
|
const ark = context.ark;
|
||||||
const accounts = await ark.get_all_accounts();
|
const accounts = await ark.get_accounts();
|
||||||
|
|
||||||
return { accounts };
|
return { accounts };
|
||||||
},
|
},
|
||||||
@@ -106,7 +106,7 @@ function Accounts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// change current account and update signer
|
// change current account and update signer
|
||||||
const select = await ark.load_selected_account(npub);
|
const select = await ark.load_account(npub);
|
||||||
|
|
||||||
if (select) {
|
if (select) {
|
||||||
return navigate({ to: "/$account/home", params: { account: npub } });
|
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"
|
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>
|
</div>
|
||||||
<button
|
<div className="flex flex-col gap-1 items-center">
|
||||||
type="button"
|
<button
|
||||||
onClick={() => submit()}
|
type="button"
|
||||||
disabled={loading}
|
onClick={() => submit()}
|
||||||
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"
|
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 ? <Spinner /> : "Login"}
|
||||||
|
</button>
|
||||||
|
{loading ? (
|
||||||
|
<p className="text-neutral-600 dark:text-neutral-400 text-sm text-center">
|
||||||
|
Waiting confirmation...
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</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 type { ColumnRouteSearch } from "@lume/types";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { User } from "@/components/user";
|
import { User } from "@/components/user";
|
||||||
import { createFileRoute, useRouter } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -23,104 +23,174 @@ export const Route = createFileRoute("/create-group")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const contacts = Route.useLoaderData();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { ark } = Route.useRouteContext();
|
const { ark } = Route.useRouteContext();
|
||||||
const { label, redirect } = Route.useSearch();
|
|
||||||
|
|
||||||
const [title, setTitle] = useState<string>("Just a new group");
|
const [title, setTitle] = useState("");
|
||||||
const [users, setUsers] = useState<Array<string>>([]);
|
const [npub, setNpub] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [users, setUsers] = useState<string[]>([
|
||||||
const [isDone, setIsDone] = useState(false);
|
"npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445", // reya
|
||||||
|
]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const contacts = Route.useLoaderData();
|
||||||
|
const search = Route.useSearch();
|
||||||
|
const navigate = Route.useNavigate();
|
||||||
|
|
||||||
const toggleUser = (pubkey: string) => {
|
const toggleUser = (pubkey: string) => {
|
||||||
const arr = users.includes(pubkey)
|
setUsers((prev) =>
|
||||||
? users.filter((i) => i !== pubkey)
|
prev.includes(pubkey)
|
||||||
: [...users, pubkey];
|
? prev.filter((i) => i !== pubkey)
|
||||||
setUsers(arr);
|
: [...prev, pubkey],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addUser = () => {
|
||||||
|
if (!npub.startsWith("npub1")) return;
|
||||||
|
if (users.includes(npub)) return;
|
||||||
|
|
||||||
|
setUsers((prev) => [...prev, npub]);
|
||||||
|
setNpub("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
try {
|
try {
|
||||||
if (isDone) return router.history.push(redirect);
|
setIsLoading(true);
|
||||||
|
|
||||||
// start loading
|
const key = `lume_group_${search.label}`;
|
||||||
setLoading(true);
|
const createGroup = await ark.set_nstore(key, JSON.stringify(users));
|
||||||
|
|
||||||
const groups = await ark.set_nstore(
|
if (createGroup) {
|
||||||
`lume_group_${label}`,
|
return navigate({ to: search.redirect, search: { ...search } });
|
||||||
JSON.stringify(users),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (groups) {
|
|
||||||
toast.success("Group has been created successfully.");
|
|
||||||
// start loading
|
|
||||||
setIsDone(true);
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setLoading(false);
|
setIsLoading(false);
|
||||||
toast.error(e);
|
toast.error(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full overflow-y-auto scrollbar-none">
|
<div className="w-full h-full flex flex-col items-center justify-center gap-4">
|
||||||
<div className="flex flex-col gap-5 p-3">
|
<div className="text-center flex flex-col items-center justify-center">
|
||||||
<div className="flex flex-col gap-1">
|
<h1 className="text-2xl font-serif font-medium">
|
||||||
<label htmlFor="name" className="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
|
Name
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
name="name"
|
name="name"
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
placeholder="Nostrichs..."
|
placeholder="Enter a name for this group"
|
||||||
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"
|
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>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="w-full flex flex-col items-center gap-3">
|
||||||
<div className="inline-flex items-center justify-between">
|
<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">
|
||||||
<span className="font-medium">Pick user</span>
|
<div className="flex gap-2">
|
||||||
<span className="text-neutral-600 dark:text-neutral-400">{`${users.length} / ∞`}</span>
|
<input
|
||||||
</div>
|
name="npub"
|
||||||
<div className="flex flex-col gap-2">
|
value={npub}
|
||||||
{contacts.map((item: string) => (
|
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
|
<button
|
||||||
key={item}
|
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleUser(item)}
|
onClick={() => addUser()}
|
||||||
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"
|
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}>
|
<PlusIcon className="size-6" />
|
||||||
<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}
|
|
||||||
</button>
|
</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>
|
|
||||||
<div className="fixed z-10 flex items-center justify-center w-full bottom-6">
|
|
||||||
{users.length >= 1 ? (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => submit()}
|
onClick={() => submit()}
|
||||||
disabled={users.length < 1}
|
disabled={isLoading || 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"
|
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>
|
</button>
|
||||||
) : null}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 { ark } = Route.useRouteContext();
|
||||||
|
|
||||||
|
const [topics, setTopics] = useState<Topic[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const search = Route.useSearch();
|
||||||
|
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_${search.label}`;
|
||||||
|
const createTopic = await ark.set_nstore(key, JSON.stringify(topics));
|
||||||
|
|
||||||
|
if (createTopic) {
|
||||||
|
return navigate({ to: search.redirect, search: { ...search } });
|
||||||
|
}
|
||||||
|
} 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() {
|
export function Screen() {
|
||||||
const { account } = Route.useSearch();
|
const { label, account } = Route.useSearch();
|
||||||
const { ark } = Route.useRouteContext();
|
const { ark } = Route.useRouteContext();
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -37,16 +37,13 @@ export function Screen() {
|
|||||||
hasNextPage,
|
hasNextPage,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
} = useInfiniteQuery({
|
} = useInfiniteQuery({
|
||||||
queryKey: ["global", account],
|
queryKey: [label, account],
|
||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
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;
|
return events;
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage) => {
|
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
|
||||||
const lastEvent = lastPage?.at(-1);
|
|
||||||
return lastEvent ? lastEvent.created_at - 1 : null;
|
|
||||||
},
|
|
||||||
select: (data) => data?.pages.flatMap((page) => page),
|
select: (data) => data?.pages.flatMap((page) => page),
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Conversation } from "@/components/conversation";
|
||||||
|
import { Quote } from "@/components/quote";
|
||||||
import { RepostNote } from "@/components/repost";
|
import { RepostNote } from "@/components/repost";
|
||||||
import { TextNote } from "@/components/text";
|
import { TextNote } from "@/components/text";
|
||||||
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
|
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
|
||||||
@@ -17,12 +19,11 @@ export const Route = createFileRoute("/group")({
|
|||||||
},
|
},
|
||||||
beforeLoad: async ({ search, context }) => {
|
beforeLoad: async ({ search, context }) => {
|
||||||
const ark = context.ark;
|
const ark = context.ark;
|
||||||
const groups = (await ark.get_nstore(
|
const key = `lume_group_${search.label}`;
|
||||||
`lume_group_${search.label}`,
|
const groups = (await ark.get_nstore(key)) as string[];
|
||||||
)) as string[];
|
|
||||||
const settings = await ark.get_settings();
|
const settings = await ark.get_settings();
|
||||||
|
|
||||||
if (!groups) {
|
if (!groups?.length) {
|
||||||
throw redirect({
|
throw redirect({
|
||||||
to: "/create-group",
|
to: "/create-group",
|
||||||
search: {
|
search: {
|
||||||
@@ -41,7 +42,7 @@ export const Route = createFileRoute("/group")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function Screen() {
|
export function Screen() {
|
||||||
const { name, account } = Route.useSearch();
|
const { label, account } = Route.useSearch();
|
||||||
const { ark, groups } = Route.useRouteContext();
|
const { ark, groups } = Route.useRouteContext();
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -51,16 +52,13 @@ export function Screen() {
|
|||||||
hasNextPage,
|
hasNextPage,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
} = useInfiniteQuery({
|
} = useInfiniteQuery({
|
||||||
queryKey: [name, account],
|
queryKey: [label, account],
|
||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
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;
|
return events;
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage) => {
|
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
|
||||||
const lastEvent = lastPage?.at(-1);
|
|
||||||
return lastEvent ? lastEvent.created_at - 1 : null;
|
|
||||||
},
|
|
||||||
select: (data) =>
|
select: (data) =>
|
||||||
data?.pages.flatMap((page) => page.filter((ev) => ev.kind === Kind.Text)),
|
data?.pages.flatMap((page) => page.filter((ev) => ev.kind === Kind.Text)),
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
@@ -71,15 +69,29 @@ export function Screen() {
|
|||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
case Kind.Repost:
|
case Kind.Repost:
|
||||||
return <RepostNote key={event.id} event={event} />;
|
return <RepostNote key={event.id} event={event} />;
|
||||||
default:
|
default: {
|
||||||
return <TextNote key={event.id} event={event} />;
|
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 (
|
return (
|
||||||
<div className="p-2 w-full h-full overflow-y-auto scrollbar-none">
|
<div className="p-2 w-full h-full overflow-y-auto scrollbar-none">
|
||||||
{isFetching && !isLoading && !isFetchingNextPage ? (
|
{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">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-5" />
|
||||||
<span className="text-sm font-medium">Fetching new notes...</span>
|
<span className="text-sm font-medium">Fetching new notes...</span>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const Route = createFileRoute("/")({
|
|||||||
await checkForAppUpdates(true);
|
await checkForAppUpdates(true);
|
||||||
|
|
||||||
const ark = context.ark;
|
const ark = context.ark;
|
||||||
const accounts = await ark.get_all_accounts();
|
const accounts = await ark.get_accounts();
|
||||||
|
|
||||||
if (!accounts.length) {
|
if (!accounts.length) {
|
||||||
throw redirect({
|
throw redirect({
|
||||||
@@ -41,7 +41,7 @@ function Screen() {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const loadAccount = await ark.load_selected_account(npub);
|
const loadAccount = await ark.load_account(npub);
|
||||||
if (loadAccount) {
|
if (loadAccount) {
|
||||||
return navigate({
|
return navigate({
|
||||||
to: "/$account/home",
|
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 { type ColumnRouteSearch, type Event, Kind } from "@lume/types";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
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 { createFileRoute } from "@tanstack/react-router";
|
||||||
import { Virtualizer } from "virtua";
|
import { Virtualizer } from "virtua";
|
||||||
|
|
||||||
@@ -18,18 +18,29 @@ export const Route = createFileRoute("/newsfeed")({
|
|||||||
name: search.name,
|
name: search.name,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: async ({ search, context }) => {
|
||||||
const ark = context.ark;
|
const ark = context.ark;
|
||||||
const settings = await ark.get_settings();
|
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,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function Screen() {
|
export function Screen() {
|
||||||
const { label, account } = Route.useSearch();
|
const { label, account } = Route.useSearch();
|
||||||
const { ark } = Route.useRouteContext();
|
const { ark, contacts } = Route.useRouteContext();
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -41,13 +52,10 @@ export function Screen() {
|
|||||||
queryKey: [label, account],
|
queryKey: [label, account],
|
||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
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;
|
return events;
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage) => {
|
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
|
||||||
const lastEvent = lastPage?.at(-1);
|
|
||||||
return lastEvent ? lastEvent.created_at - 1 : null;
|
|
||||||
},
|
|
||||||
select: (data) => data?.pages.flatMap((page) => page),
|
select: (data) => data?.pages.flatMap((page) => page),
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
@@ -79,7 +87,7 @@ export function Screen() {
|
|||||||
return (
|
return (
|
||||||
<div className="p-2 w-full h-full overflow-y-auto scrollbar-none">
|
<div className="p-2 w-full h-full overflow-y-auto scrollbar-none">
|
||||||
{isFetching && !isLoading && !isFetchingNextPage ? (
|
{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">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-5" />
|
||||||
<span className="text-sm font-medium">Fetching new notes...</span>
|
<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>
|
<span className="text-sm font-medium">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
) : !data.length ? (
|
) : !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}>
|
<Virtualizer overscan={3}>
|
||||||
{data.map((item) => renderItem(item))}
|
{data.map((item) => renderItem(item))}
|
||||||
@@ -120,35 +130,3 @@ export function Screen() {
|
|||||||
</div>
|
</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 } from "@lume/types";
|
||||||
import type { ColumnRouteSearch, LumeColumn } from "@lume/types";
|
|
||||||
import { Spinner } from "@lume/ui";
|
|
||||||
import { User } from "@/components/user";
|
|
||||||
import { cn } from "@lume/utils";
|
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
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")({
|
export const Route = createFileRoute("/onboarding")({
|
||||||
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
|
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
|
||||||
@@ -22,54 +13,6 @@ export const Route = createFileRoute("/onboarding")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
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 (
|
return (
|
||||||
<div className="h-full flex flex-col py-6 gap-6 overflow-y-auto scrollbar-none">
|
<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">
|
<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.
|
Here are a few suggestions to help you get started.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-3">
|
<div className="px-3 flex flex-col gap-3">
|
||||||
<div className="mb-6 w-full h-44">
|
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10 backdrop-blur-lg">
|
||||||
<img
|
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
|
||||||
src="/lock-screen.jpg"
|
01.
|
||||||
srcSet="/lock-screen@2x.jpg 2x"
|
</div>
|
||||||
alt="background"
|
<div className="h-16 flex items-center justify-center shrink-0 px-3 text-lg select-text">
|
||||||
className="h-full w-full object-cover rounded-xl outline outline-1 -outline-offset-1 outline-black/15"
|
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>
|
||||||
<div className="flex flex-col gap-6">
|
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10 backdrop-blur-lg">
|
||||||
<div className="flex items-start gap-2 text-[13px]">
|
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
|
||||||
<Mide />
|
02.
|
||||||
<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>
|
</div>
|
||||||
<div className="flex items-start flex-row-reverse gap-2 text-[13px]">
|
<div className="h-16 flex items-center justify-center shrink-0 px-3 text-lg select-text">
|
||||||
<CurrentUser />
|
Switch between accounts.
|
||||||
<div className="flex flex-col gap-0.5">
|
</div>
|
||||||
<h3 className="font-semibold text-end">You</h3>
|
<div className="flex-1 w-3/4 h-full pb-10">
|
||||||
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
|
<video
|
||||||
How can I get started?
|
className="h-auto w-full aspect-square rounded-lg shadow-md transform"
|
||||||
</div>
|
controls
|
||||||
<button
|
muted
|
||||||
type="button"
|
>
|
||||||
onClick={() => setUserType("new")}
|
<source
|
||||||
className={cn(
|
src="https://video.nostr.build/d33962520506d86acfb4b55a7b265821e10ae637f5ec830a173b7e6092b16ec8.mp4"
|
||||||
"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",
|
type="video/mp4"
|
||||||
userType === "new"
|
/>
|
||||||
? "bg-blue-500 text-white hover:bg-blue-600"
|
Your browser does not support the video tag.
|
||||||
: "",
|
</video>
|
||||||
)}
|
</div>
|
||||||
>
|
</div>
|
||||||
I'm completely new to Nostr.
|
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10 backdrop-blur-lg">
|
||||||
<ArrowRightIcon className="size-4" />
|
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
|
||||||
</button>
|
03.
|
||||||
<button
|
</div>
|
||||||
type="button"
|
<div className="h-16 flex items-center justify-center shrink-0 px-3 text-lg select-text">
|
||||||
onClick={() => setUserType("veteran")}
|
Open Lume Store.
|
||||||
className={cn(
|
</div>
|
||||||
"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",
|
<div className="flex-1 w-3/4 h-full pb-10">
|
||||||
userType === "veteran"
|
<video
|
||||||
? "bg-blue-500 text-white hover:bg-blue-600"
|
className="h-auto w-full aspect-square rounded-lg shadow-md transform"
|
||||||
: "",
|
controls
|
||||||
)}
|
muted
|
||||||
>
|
>
|
||||||
I've already been using another Nostr client.
|
<source
|
||||||
<ArrowRightIcon className="size-4" />
|
src="https://video.nostr.build/927abbfde2097e470ac751181b1db456b7e4b9149550408efff1a966a7ffb9a8.mp4"
|
||||||
</button>
|
type="video/mp4"
|
||||||
</div>
|
/>
|
||||||
|
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>
|
</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>
|
</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,
|
component: Screen,
|
||||||
loader: async ({ context }) => {
|
loader: async ({ context }) => {
|
||||||
const ark = context.ark;
|
const ark = context.ark;
|
||||||
const npubs = await ark.get_all_accounts();
|
const npubs = await ark.get_accounts();
|
||||||
|
|
||||||
const accounts: Account[] = [];
|
const accounts: Account[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
import { Conversation } from "@/components/conversation";
|
||||||
|
import { Quote } from "@/components/quote";
|
||||||
import { RepostNote } from "@/components/repost";
|
import { RepostNote } from "@/components/repost";
|
||||||
import { TextNote } from "@/components/text";
|
import { TextNote } from "@/components/text";
|
||||||
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
|
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 { Spinner } from "@lume/ui";
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
import { Link, createFileRoute, redirect } from "@tanstack/react-router";
|
import { Link, createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
import { Virtualizer } from "virtua";
|
import { Virtualizer } from "virtua";
|
||||||
|
|
||||||
export const Route = createFileRoute("/foryou")({
|
export const Route = createFileRoute("/topic")({
|
||||||
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
|
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
|
||||||
return {
|
return {
|
||||||
account: search.account,
|
account: search.account,
|
||||||
@@ -17,21 +19,28 @@ export const Route = createFileRoute("/foryou")({
|
|||||||
},
|
},
|
||||||
beforeLoad: async ({ search, context }) => {
|
beforeLoad: async ({ search, context }) => {
|
||||||
const ark = context.ark;
|
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();
|
const settings = await ark.get_settings();
|
||||||
|
|
||||||
if (!interests) {
|
if (!topics?.length) {
|
||||||
throw redirect({
|
throw redirect({
|
||||||
to: "/interests",
|
to: "/create-topic",
|
||||||
search: {
|
search: {
|
||||||
...search,
|
...search,
|
||||||
redirect: "/foryou",
|
redirect: "/topic",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let hashtags: string[] = [];
|
||||||
|
|
||||||
|
for (const topic of topics) {
|
||||||
|
hashtags.push(...topic.content);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
interests,
|
hashtags,
|
||||||
settings,
|
settings,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -39,8 +48,8 @@ export const Route = createFileRoute("/foryou")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function Screen() {
|
export function Screen() {
|
||||||
const { name, account } = Route.useSearch();
|
const { label, account } = Route.useSearch();
|
||||||
const { ark, interests } = Route.useRouteContext();
|
const { ark, hashtags } = Route.useRouteContext();
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -49,20 +58,13 @@ export function Screen() {
|
|||||||
hasNextPage,
|
hasNextPage,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
} = useInfiniteQuery({
|
} = useInfiniteQuery({
|
||||||
queryKey: [name, account],
|
queryKey: [label, account],
|
||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||||
const events = await ark.get_events_from_interests(
|
const events = ark.get_hashtag_events(hashtags, 20, pageParam);
|
||||||
interests.hashtags,
|
|
||||||
20,
|
|
||||||
pageParam,
|
|
||||||
);
|
|
||||||
return events;
|
return events;
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage) => {
|
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
|
||||||
const lastEvent = lastPage?.at(-1);
|
|
||||||
return lastEvent ? lastEvent.created_at - 1 : null;
|
|
||||||
},
|
|
||||||
select: (data) => data?.pages.flatMap((page) => page),
|
select: (data) => data?.pages.flatMap((page) => page),
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
@@ -72,15 +74,29 @@ export function Screen() {
|
|||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
case Kind.Repost:
|
case Kind.Repost:
|
||||||
return <RepostNote key={event.id} event={event} />;
|
return <RepostNote key={event.id} event={event} />;
|
||||||
default:
|
default: {
|
||||||
return <TextNote key={event.id} event={event} />;
|
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 (
|
return (
|
||||||
<div className="p-2 w-full h-full overflow-y-auto scrollbar-none">
|
<div className="p-2 w-full h-full overflow-y-auto scrollbar-none">
|
||||||
{isFetching && !isLoading && !isFetchingNextPage ? (
|
{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">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-5" />
|
||||||
<span className="text-sm font-medium">Fetching new notes...</span>
|
<span className="text-sm font-medium">Fetching new notes...</span>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TextNote } from "@/components/text";
|
import { TextNote } from "@/components/text";
|
||||||
import { type Event } from "@lume/types";
|
import type { Event } from "@lume/types";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { Await, createFileRoute } from "@tanstack/react-router";
|
import { Await, createFileRoute } from "@tanstack/react-router";
|
||||||
import { defer } from "@tanstack/react-router";
|
import { defer } from "@tanstack/react-router";
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function Screen() {
|
|||||||
>
|
>
|
||||||
<Await promise={data}>
|
<Await promise={data}>
|
||||||
{(users) =>
|
{(users) =>
|
||||||
users.profiles.map((item) => (
|
users.profiles.map((item: { pubkey: string }) => (
|
||||||
<div
|
<div
|
||||||
key={item.pubkey}
|
key={item.pubkey}
|
||||||
className="h-max w-full overflow-hidden mb-3 p-2 bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl"
|
className="h-max w-full overflow-hidden mb-3 p-2 bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Conversation } from "@/components/conversation";
|
|||||||
import { Quote } from "@/components/quote";
|
import { Quote } from "@/components/quote";
|
||||||
import { RepostNote } from "@/components/repost";
|
import { RepostNote } from "@/components/repost";
|
||||||
import { TextNote } from "@/components/text";
|
import { TextNote } from "@/components/text";
|
||||||
import { Event, Kind } from "@lume/types";
|
import { type Event, Kind } from "@lume/types";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { Await } from "@tanstack/react-router";
|
import { Await } from "@tanstack/react-router";
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ export const Route = createFileRoute("/users/$pubkey")({
|
|||||||
},
|
},
|
||||||
loader: async ({ params, context }) => {
|
loader: async ({ params, context }) => {
|
||||||
const ark = context.ark;
|
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,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { TextNote } from "@/components/text";
|
|||||||
import { ArrowRightCircleIcon, InfoIcon } from "@lume/icons";
|
import { ArrowRightCircleIcon, InfoIcon } from "@lume/icons";
|
||||||
import { type Event, Kind } from "@lume/types";
|
import { type Event, Kind } from "@lume/types";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { FETCH_LIMIT } from "@lume/utils";
|
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
import { useRouteContext } from "@tanstack/react-router";
|
import { useRouteContext } from "@tanstack/react-router";
|
||||||
|
|
||||||
@@ -14,7 +13,7 @@ export function EventList({ id }: { id: string }) {
|
|||||||
queryKey: ["events", id],
|
queryKey: ["events", id],
|
||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
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;
|
return events;
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage) => {
|
getNextPageParam: (lastPage) => {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.5.10",
|
"@astrojs/check": "^0.5.10",
|
||||||
"@astrojs/tailwind": "^5.1.0",
|
"@astrojs/tailwind": "^5.1.0",
|
||||||
"@fontsource/geist-mono": "^5.0.3",
|
"@fontsource/alice": "^5.0.13",
|
||||||
"astro": "^4.8.3",
|
"astro": "^4.8.3",
|
||||||
"astro-seo-meta": "^4.1.1",
|
"astro-seo-meta": "^4.1.1",
|
||||||
"astro-seo-schema": "^4.0.2",
|
"astro-seo-schema": "^4.0.2",
|
||||||
|
|||||||
|
After Width: | Height: | Size: 889 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
@@ -1,37 +0,0 @@
|
|||||||
<svg width="824" height="824" viewBox="0 0 824 824" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_564_71)">
|
|
||||||
<rect width="824" height="824" rx="184" fill="white" style="fill:white;fill-opacity:1;"/>
|
|
||||||
<circle cx="267" cy="594" r="42" fill="black" style="fill:black;fill-opacity:1;"/>
|
|
||||||
<circle cx="267" cy="594" r="42" fill="url(#paint0_radial_564_71)" fill-opacity="0.5" style=""/>
|
|
||||||
<circle cx="267" cy="594" r="42" fill="url(#paint1_radial_564_71)" fill-opacity="0.3" style=""/>
|
|
||||||
<circle cx="557" cy="594" r="42" fill="black" style="fill:black;fill-opacity:1;"/>
|
|
||||||
<circle cx="557" cy="594" r="42" fill="url(#paint2_radial_564_71)" fill-opacity="0.5" style=""/>
|
|
||||||
<circle cx="557" cy="594" r="42" fill="url(#paint3_radial_564_71)" fill-opacity="0.3" style=""/>
|
|
||||||
<path d="M412 691C382.859 691 353.717 686.063 337.654 682.804C333.024 681.865 329.866 686.676 333.074 690.144C345.098 703.138 370.814 724 412 724C453.186 724 478.902 703.138 490.926 690.144C494.134 686.676 490.976 681.865 486.346 682.804C470.283 686.063 441.141 691 412 691Z" fill="url(#paint4_linear_564_71)" style=""/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<radialGradient id="paint0_radial_564_71" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(241.807 578.038) rotate(112.103) scale(88.2816 69.6512)">
|
|
||||||
<stop stop-color="white" style="stop-color:white;stop-opacity:1;"/>
|
|
||||||
<stop offset="1" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
|
||||||
</radialGradient>
|
|
||||||
<radialGradient id="paint1_radial_564_71" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(288.309 621.165) rotate(126.504) scale(25.5816 15.4047)">
|
|
||||||
<stop stop-color="white" style="stop-color:white;stop-opacity:1;"/>
|
|
||||||
<stop offset="1" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
|
||||||
</radialGradient>
|
|
||||||
<radialGradient id="paint2_radial_564_71" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(531.807 578.038) rotate(112.103) scale(88.2816 69.6512)">
|
|
||||||
<stop stop-color="white" style="stop-color:white;stop-opacity:1;"/>
|
|
||||||
<stop offset="1" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
|
||||||
</radialGradient>
|
|
||||||
<radialGradient id="paint3_radial_564_71" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(578.309 621.165) rotate(126.504) scale(25.5816 15.4047)">
|
|
||||||
<stop stop-color="white" style="stop-color:white;stop-opacity:1;"/>
|
|
||||||
<stop offset="1" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
|
||||||
</radialGradient>
|
|
||||||
<linearGradient id="paint4_linear_564_71" x1="293.565" y1="686.595" x2="316.497" y2="774.784" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#FF9F5A" style="stop-color:#FF9F5A;stop-color:color(display-p3 1.0000 0.6235 0.3529);stop-opacity:1;"/>
|
|
||||||
<stop offset="1" stop-color="#FF9F5A" style="stop-color:#FF9F5A;stop-color:color(display-p3 1.0000 0.6235 0.3529);stop-opacity:1;"/>
|
|
||||||
</linearGradient>
|
|
||||||
<clipPath id="clip0_564_71">
|
|
||||||
<rect width="824" height="824" fill="white" style="fill:white;fill-opacity:1;"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 714 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 318 KiB |
@@ -3,153 +3,96 @@ import { Seo } from "astro-seo-meta";
|
|||||||
---
|
---
|
||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
<title>Lume</title>
|
<title>Lume: The nostr client for desktop</title>
|
||||||
<Seo
|
<Seo
|
||||||
title="Lume"
|
title="Lume"
|
||||||
description="A multiple columns Nostr client for desktop."
|
description="A friendly and scalable Nostr desktop client."
|
||||||
keywords={[
|
keywords={[
|
||||||
"nostr",
|
"nostr",
|
||||||
"nostr client",
|
"nostr client",
|
||||||
"social network",
|
"social network",
|
||||||
"desktop app",
|
"desktop app",
|
||||||
"timeline",
|
"timeline",
|
||||||
"application",
|
"application",
|
||||||
"columns",
|
"columns",
|
||||||
]}
|
"tweetdeck",
|
||||||
themeColor="#fafafa"
|
]}
|
||||||
colorScheme="light"
|
themeColor="#fafafa"
|
||||||
facebook={{
|
colorScheme="light"
|
||||||
image: "/og-image.jpg",
|
facebook={{
|
||||||
url: "https://lume.nu",
|
image: "/og-image.jpg",
|
||||||
type: "website",
|
url: "https://lume.nu",
|
||||||
}}
|
type: "website",
|
||||||
twitter={{
|
}}
|
||||||
image: "/og-image.jpg",
|
twitter={{
|
||||||
card: "summary",
|
image: "/og-image.jpg",
|
||||||
}}
|
card: "summary",
|
||||||
/>
|
}}
|
||||||
</head>
|
/>
|
||||||
<body
|
</head>
|
||||||
class="w-full h-full antialiased font-mono bg-neutral-50 dark:bg-neutral-950 text-neutral-950 dark:text-neutral-50"
|
<body
|
||||||
>
|
class="w-full h-full antialiased bg-neutral-50 dark:bg-neutral-950 text-neutral-950 dark:text-neutral-50"
|
||||||
<div class="max-w-2xl mx-auto w-full py-16 md:px-0 px-2">
|
>
|
||||||
<div class="flex flex-col gap-16">
|
<div class="py-10 flex flex-col gap-10">
|
||||||
<div class="prose dark:prose-invert prose-neutral max-w-none">
|
<div class="mx-auto max-w-xl w-full flex flex-col gap-2">
|
||||||
<h3>About Lume</h3>
|
<div class="mb-5">
|
||||||
<p>
|
<img
|
||||||
Lume is a <b>Nostr client</b> for desktop include Linux, Windows and
|
src="/icon.png"
|
||||||
macOS. It is free and open source, you can look at source code <a
|
alt="App Icon"
|
||||||
href="https://github.com/lumehq/lume"
|
class="size-14 shadow-md shadow-neutral-500/50 rounded-xl object-cover transform-gpu -rotate-6 hover:animate-spin"
|
||||||
target="_blank">on Github</a
|
/>
|
||||||
>. Lume is actively improving the app and adding new features, you
|
</div>
|
||||||
can expect new update every month.
|
<h1 class="text-xl font-serif font-semibold">
|
||||||
</p>
|
A friendly and scalable Nostr desktop client.
|
||||||
<a href="#download">Download</a>
|
</h1>
|
||||||
<h3>What is nostr & how does it work?</h3>
|
<p class="text-sm font-medium text-neutral-700">
|
||||||
<p>
|
Lume is a <b>Nostr client</b> for desktop, including Linux, Windows, and
|
||||||
Nostr stands for Notes and Other Stuff Transmitted by Relays. It is
|
macOS. It is free and open-source; you can look at the source code on <a
|
||||||
an open, permission-less protocol that aims to provide
|
href="https://github.com/lumehq/lume">GitHub</a
|
||||||
censorship-resistance and interoperability. It can be used to create
|
>. Lume is actively improving the app and adding new features; you can
|
||||||
social networks or just about any other type of app (other stuff
|
expect a new update every month.
|
||||||
part of the acronym). It is not a single website or app, but the
|
</p>
|
||||||
glue that holds together many apps (clients) and <b>Lume</b> is one of
|
<p class="text-sm font-medium text-neutral-700">
|
||||||
it.
|
<b>Latest version</b>: 4.0.4
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<div
|
||||||
At its core, nostr consists of relays and events. A person does
|
class="w-full h-[120px] sm:h-[80px] flex flex-col sm:flex-row sm:items-center sm:justify-start justify-center gap-2"
|
||||||
something (event) and this event is sent to a relay. The relay
|
>
|
||||||
stores the event, then waits for another person to request it. The
|
<a
|
||||||
most common types of events are notes and reactions - the stuff
|
href="https://github.com/lumehq/lume/releases/latest"
|
||||||
social media is made of, but there are many other types of events.
|
class="inline-flex items-center justify-center w-44 h-11 rounded-full bg-black hover:ring-2 ring-blue-500 ring-offset-2 text-white font-medium text-sm"
|
||||||
It works very similar to how any other app would work with a
|
>Download for macOS</a
|
||||||
database, except in nostr there is no single database, rather a
|
>
|
||||||
large number of relays that store the events.
|
<span class="italic text-xs text-neutral-700"
|
||||||
</p>
|
>(Windows & Linux are coming later)</span
|
||||||
<h3>Lume is multiple columns experience</h3>
|
>
|
||||||
<p>
|
</div>
|
||||||
Lume is display your timeline as multiple column, each column is
|
<div class="text-sm italic text-neutral-600">
|
||||||
each different content and you can define your experience
|
* If you still need to use Lume on Windows and Linux, you can try v3 <a
|
||||||
</p>
|
href="https://github.com/lumehq/lume/releases/tag/v3.0.2"
|
||||||
<p>
|
class="text-blue-500">here</a
|
||||||
You can create a column to display newsfeed from specific people,
|
>
|
||||||
you can create a column to display all contents related to some
|
</div>
|
||||||
hashtags. It all up to you.
|
</div>
|
||||||
</p>
|
<div class="sm:max-w-3xl w-full mx-auto px-3 sm:px-0">
|
||||||
<img
|
<video
|
||||||
src="https://image.nostr.build/fd3e3cdeb4fb9f0f3de5c5e668a11dcae55f50cc9a78fc2b57b063240191a0f9.png"
|
class="aspect-video w-full h-auto rounded-xl"
|
||||||
alt="columns"
|
autoplay
|
||||||
loading="lazy"
|
muted
|
||||||
decoding="async"
|
controls
|
||||||
class="w-full h-auto rounded-lg"
|
>
|
||||||
/>
|
<source
|
||||||
<h3>"For You"</h3>
|
src="https://video.nostr.build/4cc4df88caeb861b62e3f73bddbb5e0b5cf63617472a97d22f427e273ee0e127.mp4"
|
||||||
<p>
|
type="video/mp4"
|
||||||
Unlike some social networks, they feed you by algorithm. In Lume,
|
/>
|
||||||
you totally control what to will see
|
Your browser does not support the video tag.
|
||||||
</p>
|
</video>
|
||||||
<img
|
</div>
|
||||||
src="https://image.nostr.build/5afd79de15929a4ac6f6e933791c942555baa4206fecee54fed61dde9fe167e1.png"
|
</div>
|
||||||
alt="for you"
|
</body>
|
||||||
loading="lazy"
|
|
||||||
decoding="async"
|
|
||||||
class="w-full h-auto rounded-lg"
|
|
||||||
/>
|
|
||||||
<h3 id="download">Download and Explore</h3>
|
|
||||||
<p>
|
|
||||||
(Universal) macOS: <a
|
|
||||||
href="https://github.com/lumehq/lume/releases/download/v3.0.0/Lume_3.0.0_universal.dmg"
|
|
||||||
>Lume_3.0.0_universal.dmg
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
(x86-64) Windows 11: <a
|
|
||||||
href="https://github.com/lumehq/lume/releases/download/v3.0.0/Lume_3.0.0_x64-setup.exe"
|
|
||||||
>Lume_3.0.0_x64-setup.exe
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
(x86-64) Ubuntu: <a
|
|
||||||
href="https://github.com/lumehq/lume/releases/download/v3.0.0/lume_3.0.0_amd64.deb"
|
|
||||||
>lume_3.0.0_amd64.deb
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
(x86-64) Fedora: <a
|
|
||||||
href="https://github.com/lumehq/lume/releases/download/v3.0.0/lume-3.0.0-1.x86_64.rpm"
|
|
||||||
>lume-3.0.0-1.x86_64.rpm
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
(x86-64) Linux Flatpak: <a
|
|
||||||
href="https://github.com/lumehq/lume/releases/download/v3.0.0/lume_3.0.0_amd64.flatpak"
|
|
||||||
>lume_3.0.0_amd64.flatpak
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
(x86-64) Linux AppImage: <a
|
|
||||||
href="https://github.com/lumehq/lume/releases/download/v3.0.0/lume_3.0.0_amd64.AppImage"
|
|
||||||
>lume_3.0.0_amd64.AppImage
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Support for ARM, RISC-V and Loongarch architecture are coming soon.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<p class="text-sm text-neutral-500 dark:text-neutral-600">
|
|
||||||
Supported by <a
|
|
||||||
href="https://opensats.org"
|
|
||||||
target="_blank"
|
|
||||||
class="text-orange-500">Open Sats</a
|
|
||||||
> and Community
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
mono: ["Geist Mono", ...defaultTheme.fontFamily.mono],
|
serif: ["Alice", ...defaultTheme.fontFamily.serif],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
type Contact,
|
|
||||||
type Event,
|
type Event,
|
||||||
type EventWithReplies,
|
type EventWithReplies,
|
||||||
type Interests,
|
type Interests,
|
||||||
type Keys,
|
type Keys,
|
||||||
Kind,
|
|
||||||
type LumeColumn,
|
type LumeColumn,
|
||||||
type Metadata,
|
type Metadata,
|
||||||
type Settings,
|
type Settings,
|
||||||
@@ -18,7 +16,6 @@ import { readFile } from "@tauri-apps/plugin-fs";
|
|||||||
|
|
||||||
enum NSTORE_KEYS {
|
enum NSTORE_KEYS {
|
||||||
settings = "lume_user_settings",
|
settings = "lume_user_settings",
|
||||||
interests = "lume_user_interests",
|
|
||||||
columns = "lume_user_columns",
|
columns = "lume_user_columns",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,22 +29,26 @@ export class Ark {
|
|||||||
this.settings = undefined;
|
this.settings = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get_all_accounts() {
|
public async get_accounts() {
|
||||||
try {
|
try {
|
||||||
const cmd: string[] = await invoke("get_accounts");
|
const cmd: string = await invoke("get_accounts");
|
||||||
const accounts: string[] = cmd.map((item) => item.replace(".npub", ""));
|
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;
|
return accounts;
|
||||||
} catch (e) {
|
} 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 {
|
try {
|
||||||
const cmd: boolean = await invoke("load_selected_account", {
|
const cmd: boolean = await invoke("load_account", {
|
||||||
npub,
|
npub,
|
||||||
});
|
});
|
||||||
return cmd;
|
return cmd;
|
||||||
@@ -76,7 +77,7 @@ export class Ark {
|
|||||||
|
|
||||||
public async create_keys() {
|
public async create_keys() {
|
||||||
try {
|
try {
|
||||||
const cmd: Keys = await invoke("create_keys");
|
const cmd: Keys = await invoke("create_account");
|
||||||
return cmd;
|
return cmd;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(String(e));
|
console.error(String(e));
|
||||||
@@ -85,7 +86,7 @@ export class Ark {
|
|||||||
|
|
||||||
public async save_account(nsec: string, password = "") {
|
public async save_account(nsec: string, password = "") {
|
||||||
try {
|
try {
|
||||||
const cmd: string = await invoke("save_key", {
|
const cmd: string = await invoke("save_account", {
|
||||||
nsec,
|
nsec,
|
||||||
password,
|
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) {
|
public async search(content: string, limit: number) {
|
||||||
try {
|
try {
|
||||||
if (content.length < 1) return [];
|
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,
|
limit: number,
|
||||||
asOf?: number,
|
asOf?: number,
|
||||||
contacts?: string[],
|
|
||||||
global?: boolean,
|
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
||||||
const isGlobal = global ?? false;
|
const nostrEvents: Event[] = await invoke("get_local_events", {
|
||||||
const seens = new Set<string>();
|
pubkeys,
|
||||||
|
|
||||||
const nostrEvents: Event[] = await invoke("get_events", {
|
|
||||||
limit,
|
limit,
|
||||||
until,
|
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;
|
return events;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[get_events] failed", String(e));
|
console.error("[get_local_events] failed", String(e));
|
||||||
return [];
|
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[],
|
hashtags: string[],
|
||||||
limit: number,
|
limit: number,
|
||||||
asOf?: number,
|
asOf?: number,
|
||||||
) {
|
) {
|
||||||
let until: string = undefined;
|
try {
|
||||||
if (asOf && asOf > 0) until = asOf.toString();
|
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>();
|
return events;
|
||||||
const dedupQueue = new Set<string>();
|
} catch (e) {
|
||||||
const nostrTags = hashtags.map((tag) => tag.replace("#", "").toLowerCase());
|
console.error("[get_hashtag_events] failed", String(e));
|
||||||
|
return [];
|
||||||
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 nostrEvents
|
public async get_group_events(
|
||||||
.filter((event) => !dedupQueue.has(event.id))
|
contacts: string[],
|
||||||
.sort((a, b) => b.created_at - a.created_at);
|
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(
|
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) {
|
public async get_event_thread(id: string) {
|
||||||
try {
|
try {
|
||||||
const events: EventWithReplies[] = await invoke("get_event_thread", {
|
const events: EventWithReplies[] = await invoke("get_thread", {
|
||||||
id,
|
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 root: string = null;
|
||||||
let reply: string = null;
|
let reply: string = null;
|
||||||
|
|
||||||
// Get all event references from tags, ignore mention
|
// Get all event references from tags, ignore mention
|
||||||
const events = tags.filter((el) => el[0] === "e" && el[3] !== "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) {
|
if (events.length === 1) {
|
||||||
root = events[0][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() {
|
public async get_contact_list() {
|
||||||
try {
|
try {
|
||||||
const cmd: string[] = await invoke("get_contact_list");
|
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) {
|
public async get_nstore(key: string) {
|
||||||
try {
|
try {
|
||||||
const cmd: string = await invoke("get_nstore", {
|
const cmd: string = await invoke("get_nstore", {
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ export interface Account {
|
|||||||
interests?: Interests;
|
interests?: Interests;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Topic {
|
||||||
|
icon: string;
|
||||||
|
title: string;
|
||||||
|
content: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface Interests {
|
export interface Interests {
|
||||||
hashtags: string[];
|
hashtags: string[];
|
||||||
users: string[];
|
users: string[];
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
import { ArrowLeftIcon, ArrowRightIcon } from "@lume/icons";
|
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
export function Container({
|
export function Container({
|
||||||
children,
|
children,
|
||||||
withDrag = false,
|
withDrag = false,
|
||||||
withNavigate = true,
|
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
withDrag?: boolean;
|
withDrag?: boolean;
|
||||||
withNavigate?: boolean;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@@ -23,27 +20,8 @@ export function Container({
|
|||||||
{withDrag ? (
|
{withDrag ? (
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="bg-transparent flex h-11 w-full shrink-0 items-center justify-end pr-2"
|
className="bg-transparent flex h-11 w-full shrink-0"
|
||||||
>
|
/>
|
||||||
{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>
|
|
||||||
) : null}
|
) : null}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</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 = [
|
export const NOSTR_MENTIONS = [
|
||||||
"@npub1",
|
"@npub1",
|
||||||
"nostr:npub1",
|
"nostr:npub1",
|
||||||
@@ -32,8 +24,6 @@ export const NOSTR_EVENTS = [
|
|||||||
"Nostr:nevent1",
|
"Nostr:nevent1",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const BITCOINS = ["lnbc", "bc1p", "bc1q"];
|
|
||||||
|
|
||||||
export const IMAGES = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
|
export const IMAGES = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
|
||||||
|
|
||||||
export const VIDEOS = [
|
export const VIDEOS = [
|
||||||
@@ -51,61 +41,48 @@ export const VIDEOS = [
|
|||||||
|
|
||||||
export const AUDIOS = ["mp3", "ogg", "wav"];
|
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 = [
|
export const TOPICS = [
|
||||||
{
|
{
|
||||||
icon: "/anime.jpg",
|
icon: "📱",
|
||||||
title: "Anime & Manga",
|
title: "Technology",
|
||||||
content: [
|
content: [
|
||||||
"#animestr",
|
"#Apple",
|
||||||
"#anime",
|
"#Tesla",
|
||||||
"#manga",
|
"#AMD",
|
||||||
"#otaku",
|
"#Intel",
|
||||||
"#frieren",
|
"#Xiaomi",
|
||||||
"#fate",
|
"#Huawei",
|
||||||
"#aot",
|
"#OpenAI",
|
||||||
"#AttackOnTitan",
|
"#BigTech",
|
||||||
"#JujutsuKaisen",
|
"#ai",
|
||||||
"#OnePiece",
|
"#IOS",
|
||||||
"#KimetsuNoYaiba",
|
"#Android",
|
||||||
"#Overlord",
|
"#oppo",
|
||||||
"#Evangelion",
|
"#nostr",
|
||||||
"#DemonSlayer",
|
"#technology",
|
||||||
"#JoJo",
|
"#tech",
|
||||||
"#SPYxFAMILY",
|
"#innovation",
|
||||||
"#MatoSeiheinoSlave",
|
"#engineering",
|
||||||
"#ghibli",
|
"#business",
|
||||||
"#ChainsawMan",
|
"#iphone",
|
||||||
"#Gintama",
|
"#technews",
|
||||||
"#animeart",
|
"#science",
|
||||||
"#animegirl",
|
"#gadgets",
|
||||||
"#cosplay",
|
"#software",
|
||||||
"#weeb",
|
"#programming",
|
||||||
"#animeworld",
|
"#smartphone",
|
||||||
"#fanart",
|
"#samsung",
|
||||||
"#vocaloid",
|
"#coding",
|
||||||
"#vtuber",
|
"#computer",
|
||||||
"#hololive",
|
"#security",
|
||||||
"#hololivemeet",
|
"#gadget",
|
||||||
"#pixiv",
|
"#mobile",
|
||||||
"#waifu",
|
"#opensource",
|
||||||
|
"#tor",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "/gaming.jpg",
|
icon: "🕹",
|
||||||
title: "Gaming",
|
title: "Gaming",
|
||||||
content: [
|
content: [
|
||||||
"#gamestr",
|
"#gamestr",
|
||||||
@@ -163,8 +140,46 @@ export const TOPICS = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "/music.jpg",
|
icon: "🍥",
|
||||||
title: "Music & Entertainment",
|
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: [
|
content: [
|
||||||
"#audiostr",
|
"#audiostr",
|
||||||
"#musicstr",
|
"#musicstr",
|
||||||
@@ -208,12 +223,6 @@ export const TOPICS = [
|
|||||||
"#dvd",
|
"#dvd",
|
||||||
"#amass",
|
"#amass",
|
||||||
"#bluray",
|
"#bluray",
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: "/movie.jpg",
|
|
||||||
title: "Television",
|
|
||||||
content: [
|
|
||||||
"#filmstr",
|
"#filmstr",
|
||||||
"#moviestr",
|
"#moviestr",
|
||||||
"#movies",
|
"#movies",
|
||||||
@@ -244,46 +253,7 @@ export const TOPICS = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "/technology.jpg",
|
icon: "📷",
|
||||||
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",
|
|
||||||
title: "Photography",
|
title: "Photography",
|
||||||
content: [
|
content: [
|
||||||
"#photostr",
|
"#photostr",
|
||||||
@@ -309,8 +279,8 @@ export const TOPICS = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "/art.jpg",
|
icon: "🧑🎨",
|
||||||
title: "Art & Design",
|
title: "Design",
|
||||||
content: [
|
content: [
|
||||||
"#nostrdesign",
|
"#nostrdesign",
|
||||||
"#artstr",
|
"#artstr",
|
||||||
@@ -343,7 +313,7 @@ export const TOPICS = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "/nsfw.jpg",
|
icon: "🔞",
|
||||||
title: "NSFW",
|
title: "NSFW",
|
||||||
content: [
|
content: [
|
||||||
"#pornstr",
|
"#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
|
// @ts-ignore, it works
|
||||||
export const VITE_FLATPAK_RESOURCE = import.meta.env.VITE_FLATPAK_RESOURCE;
|
export const VITE_FLATPAK_RESOURCE = import.meta.env.VITE_FLATPAK_RESOURCE;
|
||||||
|
|||||||
@@ -214,9 +214,9 @@ importers:
|
|||||||
'@astrojs/tailwind':
|
'@astrojs/tailwind':
|
||||||
specifier: ^5.1.0
|
specifier: ^5.1.0
|
||||||
version: 5.1.0(astro@4.8.3)(tailwindcss@3.4.3)
|
version: 5.1.0(astro@4.8.3)(tailwindcss@3.4.3)
|
||||||
'@fontsource/geist-mono':
|
'@fontsource/alice':
|
||||||
specifier: ^5.0.3
|
specifier: ^5.0.13
|
||||||
version: 5.0.3
|
version: 5.0.13
|
||||||
astro:
|
astro:
|
||||||
specifier: ^4.8.3
|
specifier: ^4.8.3
|
||||||
version: 4.8.3(typescript@5.4.5)
|
version: 4.8.3(typescript@5.4.5)
|
||||||
@@ -1465,8 +1465,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==}
|
resolution: {integrity: sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@fontsource/geist-mono@5.0.3:
|
/@fontsource/alice@5.0.13:
|
||||||
resolution: {integrity: sha512-ekY5FNiK7aMxan/c6lgVQa/rIwQ/AMEJZJWY7768jBrLVdkrCxfEfCe1ePpe7C3JnySy7c9R6HC4Xh4ksfjTaw==}
|
resolution: {integrity: sha512-7ncjjSpRSRKvjJEoru092iFiEoC89lz4oG4+SGg9hh7DI/5SXf+kE+dg+6Fv/bwiK/WJCo4Q2gvPZGRlCE5mcA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@getalby/sdk@3.5.1(typescript@5.4.5):
|
/@getalby/sdk@3.5.1(typescript@5.4.5):
|
||||||
@@ -6251,6 +6251,7 @@ packages:
|
|||||||
|
|
||||||
/tslib@2.6.2:
|
/tslib@2.6.2:
|
||||||
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
||||||
|
requiresBuild: true
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/turbo-darwin-64@1.13.3:
|
/turbo-darwin-64@1.13.3:
|
||||||
|
|||||||
@@ -107,55 +107,6 @@ dependencies = [
|
|||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.83"
|
version = "1.0.83"
|
||||||
@@ -221,11 +172,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-channel"
|
name = "async-channel"
|
||||||
version = "2.3.1"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
|
checksum = "9f2776ead772134d55b62dd45e59a79e21612d85d0af729b8b7d3967d601a62a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
|
"event-listener 5.3.0",
|
||||||
"event-listener-strategy 0.5.2",
|
"event-listener-strategy 0.5.2",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@@ -488,17 +440,6 @@ version = "1.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
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]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
@@ -709,9 +650,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.16.0"
|
version = "1.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5"
|
checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
@@ -776,9 +717,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "camino"
|
name = "camino"
|
||||||
version = "1.1.7"
|
version = "1.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239"
|
checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -932,33 +873,6 @@ dependencies = [
|
|||||||
"zeroize",
|
"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]]
|
[[package]]
|
||||||
name = "clipboard-win"
|
name = "clipboard-win"
|
||||||
version = "5.3.1"
|
version = "5.3.1"
|
||||||
@@ -1004,12 +918,6 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "colorchoice"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "4.6.7"
|
version = "4.6.7"
|
||||||
@@ -1219,9 +1127,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.20.9"
|
version = "0.20.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1"
|
checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core",
|
||||||
"darling_macro",
|
"darling_macro",
|
||||||
@@ -1229,9 +1137,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling_core"
|
name = "darling_core"
|
||||||
version = "0.20.9"
|
version = "0.20.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120"
|
checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fnv",
|
"fnv",
|
||||||
"ident_case",
|
"ident_case",
|
||||||
@@ -1243,9 +1151,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling_macro"
|
name = "darling_macro"
|
||||||
version = "0.20.9"
|
version = "0.20.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178"
|
checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1350,15 +1258,6 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dirs"
|
|
||||||
version = "4.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
|
|
||||||
dependencies = [
|
|
||||||
"dirs-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs-next"
|
name = "dirs-next"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -1369,17 +1268,6 @@ dependencies = [
|
|||||||
"dirs-sys-next",
|
"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]]
|
[[package]]
|
||||||
name = "dirs-sys-next"
|
name = "dirs-sys-next"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -1482,7 +1370,7 @@ dependencies = [
|
|||||||
"rustc_version",
|
"rustc_version",
|
||||||
"toml 0.8.2",
|
"toml 0.8.2",
|
||||||
"vswhom",
|
"vswhom",
|
||||||
"winreg 0.52.0",
|
"winreg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2316,9 +2204,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hex-conservative"
|
name = "hex-conservative"
|
||||||
version = "0.1.2"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20"
|
checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hex_lit"
|
name = "hex_lit"
|
||||||
@@ -2660,12 +2548,6 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "is_terminal_polyfill"
|
|
||||||
version = "1.70.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "0.4.8"
|
version = "0.4.8"
|
||||||
@@ -2786,6 +2668,21 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"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]]
|
[[package]]
|
||||||
name = "kuchikiki"
|
name = "kuchikiki"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
@@ -2980,6 +2877,7 @@ version = "4.0.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cocoa",
|
"cocoa",
|
||||||
"keyring",
|
"keyring",
|
||||||
|
"keyring-search",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"objc",
|
"objc",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
@@ -2987,9 +2885,8 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-autostart",
|
|
||||||
"tauri-plugin-cli",
|
|
||||||
"tauri-plugin-clipboard-manager",
|
"tauri-plugin-clipboard-manager",
|
||||||
|
"tauri-plugin-decorum",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-fs",
|
"tauri-plugin-fs",
|
||||||
"tauri-plugin-http",
|
"tauri-plugin-http",
|
||||||
@@ -3146,9 +3043,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "muda"
|
name = "muda"
|
||||||
version = "0.13.4"
|
version = "0.13.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1717c136c99673f55640c14125a0349a5cd7fee6efcfb0adbfe4c289e3b3f7f2"
|
checksum = "c6fde56ead0971b4caae4aa0f19502e49d1fac2af9d0c60068e2d235e26ce709"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cocoa",
|
"cocoa",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
@@ -3254,9 +3151,9 @@ checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr"
|
name = "nostr"
|
||||||
version = "0.30.0"
|
version = "0.31.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a27223888faca0c4ba9b97c2b7dc776e9a33d5f54e3558887471cf17798b5fbf"
|
checksum = "ef9b0b5429bb61b46c125eba70e0f7193c8c6d04df8a917c2abe5bea03f8bcb0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes 0.8.4",
|
"aes 0.8.4",
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
@@ -3284,9 +3181,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-database"
|
name = "nostr-database"
|
||||||
version = "0.30.0"
|
version = "0.31.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f726b8c0904a838f64b51a931a1bf39e341f5584a5e04f06310fbfb847e2e924"
|
checksum = "a89506f743a5441695ab727794db41d8df1c1365ff96c25272985adf08f816b3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"flatbuffers",
|
"flatbuffers",
|
||||||
@@ -3299,9 +3196,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-relay-pool"
|
name = "nostr-relay-pool"
|
||||||
version = "0.30.0"
|
version = "0.31.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52f0ccf9e81aa747abdfa130007651248b37c3699d37029bad701e68902257ce"
|
checksum = "f751acc8bbb1329718d673470c7c3a18cddd33963dd91b97bccc92037113d254"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"async-wsocket",
|
"async-wsocket",
|
||||||
@@ -3315,9 +3212,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-sdk"
|
name = "nostr-sdk"
|
||||||
version = "0.30.0"
|
version = "0.31.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d1ffedac7ab488e0dfea52804d0c43fafc7e3eefc62d97726d3927a1390db05b"
|
checksum = "0e65cd9f4f26f3f8e10253c518aff9e61a9204f600dfe4c3c241b0230471c67f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"lnurl-pay",
|
"lnurl-pay",
|
||||||
@@ -3335,9 +3232,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-signer"
|
name = "nostr-signer"
|
||||||
version = "0.30.0"
|
version = "0.31.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "22e568670664cf5cc14a794ae32dfc04bde385d63ff0f5b1c3745dd3ea69f73a"
|
checksum = "8be1878e91a0b4a95cfd8142349b6124b037b287375d76db9638ccc4b4cdf271"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -3349,9 +3246,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-sqlite"
|
name = "nostr-sqlite"
|
||||||
version = "0.30.0"
|
version = "0.31.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a608d8db5ff0a8ebf7e2605fdd0b11fdcf0873724680fa97307a845ed262713c"
|
checksum = "1dca940d759c07d3928008842ad0ac63fa693efd83f4e8821c9a1badb0be226e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"deadpool-sqlite",
|
"deadpool-sqlite",
|
||||||
@@ -3365,9 +3262,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-zapper"
|
name = "nostr-zapper"
|
||||||
version = "0.30.0"
|
version = "0.31.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "420a7c6458d5c1dc502b3d36fb9f8598837743a737b84adb4ef8ea36b98c5e07"
|
checksum = "5558bb031cff46e5b580847f26617d516ded4c0f8fd27fb568ec875bcd8fb99c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -3496,9 +3393,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nwc"
|
name = "nwc"
|
||||||
version = "0.30.0"
|
version = "0.31.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e236611ea96d3545138f7b2152f2e4571e3c93436ddc91d1c458f366e5c6430f"
|
checksum = "bbd88cc13a04ae41037c182489893c2f421ba0c12a028564ec339882e7f96d61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -4455,7 +4352,7 @@ dependencies = [
|
|||||||
"wasm-streams",
|
"wasm-streams",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"webpki-roots",
|
"webpki-roots",
|
||||||
"winreg 0.52.0",
|
"winreg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4596,9 +4493,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.17"
|
version = "1.0.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
|
checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
@@ -4783,18 +4680,18 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.202"
|
version = "1.0.201"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
|
checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.202"
|
version = "1.0.201"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
|
checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -4803,9 +4700,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive_internals"
|
name = "serde_derive_internals"
|
||||||
version = "0.29.1"
|
version = "0.29.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -4837,9 +4734,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "0.6.6"
|
version = "0.6.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
|
checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -5121,9 +5018,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
@@ -5410,36 +5307,6 @@ dependencies = [
|
|||||||
"walkdir",
|
"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]]
|
[[package]]
|
||||||
name = "tauri-plugin-clipboard-manager"
|
name = "tauri-plugin-clipboard-manager"
|
||||||
version = "2.1.0-beta.2"
|
version = "2.1.0-beta.2"
|
||||||
@@ -5456,6 +5323,23 @@ dependencies = [
|
|||||||
"thiserror",
|
"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]]
|
[[package]]
|
||||||
name = "tauri-plugin-dialog"
|
name = "tauri-plugin-dialog"
|
||||||
version = "2.0.0-beta.7"
|
version = "2.0.0-beta.7"
|
||||||
@@ -6283,12 +6167,6 @@ version = "0.7.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "utf8parse"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
@@ -6943,15 +6821,6 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winreg"
|
|
||||||
version = "0.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
|
||||||
dependencies = [
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winreg"
|
name = "winreg"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ rust-version = "1.68"
|
|||||||
tauri-build = { version = "2.0.0-beta", features = [] }
|
tauri-build = { version = "2.0.0-beta", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nostr-sdk = { version = "0.30", features = ["sqlite"] }
|
nostr-sdk = { version = "0.31", features = ["sqlite"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
@@ -22,7 +22,6 @@ tauri = { version = "2.0.0-beta", features = [
|
|||||||
"native-tls-vendored",
|
"native-tls-vendored",
|
||||||
"protocol-asset",
|
"protocol-asset",
|
||||||
] }
|
] }
|
||||||
tauri-plugin-cli = "2.0.0-beta"
|
|
||||||
tauri-plugin-clipboard-manager = "2.1.0-beta"
|
tauri-plugin-clipboard-manager = "2.1.0-beta"
|
||||||
tauri-plugin-dialog = "2.0.0-beta"
|
tauri-plugin-dialog = "2.0.0-beta"
|
||||||
tauri-plugin-fs = "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-process = "2.0.0-beta"
|
||||||
tauri-plugin-shell = "2.0.0-beta"
|
tauri-plugin-shell = "2.0.0-beta"
|
||||||
tauri-plugin-updater = "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-upload = "2.0.0-beta"
|
||||||
tauri-plugin-window-state = "2.0.0-beta"
|
tauri-plugin-window-state = "2.0.0-beta"
|
||||||
|
tauri-plugin-decorum = "0.1.0"
|
||||||
webpage = { version = "2.0", features = ["serde"] }
|
webpage = { version = "2.0", features = ["serde"] }
|
||||||
keyring = "2"
|
keyring = "2"
|
||||||
|
keyring-search = "0.2.0"
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
cocoa = "0.25.0"
|
cocoa = "0.25.0"
|
||||||
|
|||||||
@@ -1,75 +1,82 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "desktop-capability",
|
"identifier": "desktop-capability",
|
||||||
"description": "Capability for the desktop",
|
"description": "Capability for the desktop",
|
||||||
"platforms": ["linux", "macOS", "windows"],
|
"platforms": ["linux", "macOS", "windows"],
|
||||||
"windows": [
|
"windows": [
|
||||||
"main",
|
"main",
|
||||||
"splash",
|
"splash",
|
||||||
"settings",
|
"settings",
|
||||||
"search",
|
"search",
|
||||||
"nwc",
|
"nwc",
|
||||||
"activity",
|
"activity",
|
||||||
"zap-*",
|
"zap-*",
|
||||||
"event-*",
|
"event-*",
|
||||||
"user-*",
|
"user-*",
|
||||||
"editor-*",
|
"editor-*",
|
||||||
"column-*"
|
"column-*"
|
||||||
],
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"path:default",
|
"path:default",
|
||||||
"event:default",
|
"event:default",
|
||||||
"window:default",
|
"window:default",
|
||||||
"app:default",
|
"app:default",
|
||||||
"resources:default",
|
"resources:default",
|
||||||
"menu:default",
|
"menu:default",
|
||||||
"tray:default",
|
"tray:default",
|
||||||
"notification:allow-is-permission-granted",
|
"notification:allow-is-permission-granted",
|
||||||
"notification:allow-request-permission",
|
"notification:allow-request-permission",
|
||||||
"notification:default",
|
"notification:default",
|
||||||
"os:allow-locale",
|
"os:allow-locale",
|
||||||
"os:allow-platform",
|
"os:allow-platform",
|
||||||
"updater:default",
|
"updater:default",
|
||||||
"updater:allow-check",
|
"updater:allow-check",
|
||||||
"updater:allow-download-and-install",
|
"updater:allow-download-and-install",
|
||||||
"window:allow-start-dragging",
|
"window:allow-start-dragging",
|
||||||
"window:allow-create",
|
"window:allow-create",
|
||||||
"window:allow-close",
|
"window:allow-close",
|
||||||
"window:allow-set-focus",
|
"window:allow-set-focus",
|
||||||
"clipboard-manager:allow-write-text",
|
"window:allow-center",
|
||||||
"clipboard-manager:allow-read-text",
|
"window:allow-minimize",
|
||||||
"webview:allow-create-webview-window",
|
"window:allow-maximize",
|
||||||
"webview:allow-create-webview",
|
"window:allow-set-size",
|
||||||
"webview:allow-set-webview-size",
|
"window:allow-set-focus",
|
||||||
"webview:allow-set-webview-position",
|
"window:allow-start-dragging",
|
||||||
"webview:allow-webview-close",
|
"decorum:allow-show-snap-overlay",
|
||||||
"dialog:allow-open",
|
"clipboard-manager:allow-write-text",
|
||||||
"dialog:allow-ask",
|
"clipboard-manager:allow-read-text",
|
||||||
"dialog:allow-message",
|
"webview:allow-create-webview-window",
|
||||||
"process:allow-restart",
|
"webview:allow-create-webview",
|
||||||
"fs:allow-read-file",
|
"webview:allow-set-webview-size",
|
||||||
"shell:allow-open",
|
"webview:allow-set-webview-position",
|
||||||
{
|
"webview:allow-webview-close",
|
||||||
"identifier": "http:default",
|
"dialog:allow-open",
|
||||||
"allow": [
|
"dialog:allow-ask",
|
||||||
{
|
"dialog:allow-message",
|
||||||
"url": "http://**/"
|
"process:allow-restart",
|
||||||
},
|
"fs:allow-read-file",
|
||||||
{
|
"shell:allow-open",
|
||||||
"url": "https://**/"
|
{
|
||||||
}
|
"identifier": "http:default",
|
||||||
]
|
"allow": [
|
||||||
},
|
{
|
||||||
{
|
"url": "http://**/"
|
||||||
"identifier": "fs:allow-read-text-file",
|
},
|
||||||
"allow": [
|
{
|
||||||
{
|
"url": "https://**/"
|
||||||
"path": "$RESOURCE/locales/*"
|
}
|
||||||
},
|
]
|
||||||
{
|
},
|
||||||
"path": "$RESOURCE/resources/*"
|
{
|
||||||
}
|
"identifier": "fs:allow-read-text-file",
|
||||||
]
|
"allow": [
|
||||||
}
|
{
|
||||||
]
|
"path": "$RESOURCE/locales/*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "$RESOURCE/resources/*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","splash","settings","search","nwc","activity","zap-*","event-*","user-*","editor-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","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"
|
"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",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -2704,6 +2635,26 @@
|
|||||||
"clipboard-manager:deny-write-text"
|
"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",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|||||||
@@ -2545,75 +2545,6 @@
|
|||||||
"app:deny-version"
|
"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",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -2704,6 +2635,26 @@
|
|||||||
"clipboard-manager:deny-write-text"
|
"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",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|||||||
@@ -1,298 +1,298 @@
|
|||||||
{
|
{
|
||||||
"global": {
|
"global": {
|
||||||
"relay": "Relay",
|
"relay": "Relay",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"loading": "Loading",
|
"loading": "Loading",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"moveLeft": "Move Left",
|
"moveLeft": "Move Left",
|
||||||
"moveRight": "Move Right",
|
"moveRight": "Move Right",
|
||||||
"newColumn": "New Column",
|
"newColumn": "New Column",
|
||||||
"inspect": "Inspect",
|
"inspect": "Inspect",
|
||||||
"loadMore": "Load more",
|
"loadMore": "Load more",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"post": "Post",
|
"post": "Post",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
"noResult": "No results found.",
|
"noResult": "No results found.",
|
||||||
"emptyFeedTitle": "This feed is empty",
|
"emptyFeedTitle": "This feed is empty",
|
||||||
"emptyFeedSubtitle": "You can follow more users to build up your timeline",
|
"emptyFeedSubtitle": "You can follow more users to build up your timeline",
|
||||||
"apiKey": "API Key",
|
"apiKey": "API Key",
|
||||||
"skip": "Skip",
|
"skip": "Skip",
|
||||||
"close": "Close"
|
"close": "Close"
|
||||||
},
|
},
|
||||||
"nip89": {
|
"nip89": {
|
||||||
"unsupported": "Lume isn't support this event",
|
"unsupported": "Lume isn't support this event",
|
||||||
"openWith": "Open with"
|
"openWith": "Open with"
|
||||||
},
|
},
|
||||||
"note": {
|
"note": {
|
||||||
"showThread": "Show thread",
|
"showThread": "Show thread",
|
||||||
"showMore": "Show more",
|
"showMore": "Show more",
|
||||||
"error": "Failed to fetch event.",
|
"error": "Failed to fetch event.",
|
||||||
"posted": "posted",
|
"posted": "posted",
|
||||||
"replied": "replied",
|
"replied": "replied",
|
||||||
"reposted": "reposted",
|
"reposted": "reposted",
|
||||||
"menu": {
|
"menu": {
|
||||||
"viewThread": "View thread",
|
"viewThread": "View thread",
|
||||||
"copyLink": "Copy shareable link",
|
"copyLink": "Copy shareable link",
|
||||||
"copyNoteId": "Copy note ID",
|
"copyNoteId": "Copy note ID",
|
||||||
"copyAuthorId": "Copy author ID",
|
"copyAuthorId": "Copy author ID",
|
||||||
"viewAuthor": "View author",
|
"viewAuthor": "View author",
|
||||||
"pinAuthor": "Pin author",
|
"pinAuthor": "Pin author",
|
||||||
"copyRaw": "Copy raw event",
|
"copyRaw": "Copy raw event",
|
||||||
"mute": "Mute"
|
"mute": "Mute"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"pin": "Pin",
|
"pin": "Pin",
|
||||||
"pinTooltip": "Pin Note",
|
"pinTooltip": "Pin Note",
|
||||||
"repost": "Repost",
|
"repost": "Repost",
|
||||||
"quote": "Quote",
|
"quote": "Quote",
|
||||||
"viewProfile": "View profile",
|
"viewProfile": "View profile",
|
||||||
"reply": "Reply this note",
|
"reply": "Reply this note",
|
||||||
"open": "Open in new window"
|
"open": "Open in new window"
|
||||||
},
|
},
|
||||||
"zap": {
|
"zap": {
|
||||||
"zap": "Zap",
|
"zap": "Zap",
|
||||||
"tooltip": "Send zap",
|
"tooltip": "Send zap",
|
||||||
"modalTitle": "Send zap to",
|
"modalTitle": "Send zap to",
|
||||||
"messagePlaceholder": "Enter message (optional)",
|
"messagePlaceholder": "Enter message (optional)",
|
||||||
"buttonFinish": "Zapped",
|
"buttonFinish": "Zapped",
|
||||||
"buttonLoading": "Processing...",
|
"buttonLoading": "Processing...",
|
||||||
"invoiceButton": "Scan to zap",
|
"invoiceButton": "Scan to zap",
|
||||||
"invoiceFooter": "You must use Bitcoin wallet which support Lightning\nsuch as: Blue Wallet, Bitkit, Phoenix,..."
|
"invoiceFooter": "You must use Bitcoin wallet which support Lightning\nsuch as: Blue Wallet, Bitkit, Phoenix,..."
|
||||||
},
|
},
|
||||||
"reply": {
|
"reply": {
|
||||||
"single": "reply",
|
"single": "reply",
|
||||||
"plural": "replies",
|
"plural": "replies",
|
||||||
"empty": "Be the first to Reply!"
|
"empty": "Be the first to Reply!"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"avatar": "Avatar",
|
"avatar": "Avatar",
|
||||||
"displayName": "Display Name",
|
"displayName": "Display Name",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"bio": "Bio",
|
"bio": "Bio",
|
||||||
"lna": "Lightning address",
|
"lna": "Lightning address",
|
||||||
"website": "Website",
|
"website": "Website",
|
||||||
"verified": "Verified",
|
"verified": "Verified",
|
||||||
"unverified": "Unverified",
|
"unverified": "Unverified",
|
||||||
"follow": "Follow",
|
"follow": "Follow",
|
||||||
"unfollow": "Unfollow",
|
"unfollow": "Unfollow",
|
||||||
"latestPosts": "Latest posts",
|
"latestPosts": "Latest posts",
|
||||||
"avatarButton": "Change avatar",
|
"avatarButton": "Change avatar",
|
||||||
"coverButton": "Change cover",
|
"coverButton": "Change cover",
|
||||||
"editProfile": "Edit profile",
|
"editProfile": "Edit profile",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"logout": "Log out",
|
"logout": "Log out",
|
||||||
"logoutConfirmTitle": "Are you sure!",
|
"logoutConfirmTitle": "Are you sure!",
|
||||||
"logoutConfirmSubtitle": "You can always log back in at any time. If you just want to switch accounts, you can do that by adding an existing account."
|
"logoutConfirmSubtitle": "You can always log back in at any time. If you just want to switch accounts, you can do that by adding an existing account."
|
||||||
},
|
},
|
||||||
"editor": {
|
"editor": {
|
||||||
"title": "New Post",
|
"title": "New Post",
|
||||||
"placeholder": "What are you up to?",
|
"placeholder": "What are you up to?",
|
||||||
"successMessage": "Your note has been published successfully.",
|
"successMessage": "Your note has been published successfully.",
|
||||||
"replyPlaceholder": "Post your reply"
|
"replyPlaceholder": "Post your reply"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Type something to search...",
|
"placeholder": "Type something to search...",
|
||||||
"empty": "Try searching for people, notes, or keywords"
|
"empty": "Try searching for people, notes, or keywords"
|
||||||
},
|
},
|
||||||
"welcome": {
|
"welcome": {
|
||||||
"title": "Lume is a magnificent client for Nostr to meet, explore\nand freely share your thoughts with everyone.",
|
"title": "Lume is a magnificent client for Nostr to meet, explore\nand freely share your thoughts with everyone.",
|
||||||
"signup": "Join Nostr",
|
"signup": "Join Nostr",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"footer": "Before joining Nostr, you can take time to learn more about Nostr"
|
"footer": "Before joining Nostr, you can take time to learn more about Nostr"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Welcome back, anon!",
|
"title": "Welcome back, anon!",
|
||||||
"subtitle": "We're so excited to see you again!",
|
"subtitle": "We're so excited to see you again!",
|
||||||
"footer": "Lume will put your Private Key in Secure Storage depended on your OS Platform. It will be secured by Password or Biometric ID",
|
"footer": "Lume will put your Private Key in Secure Storage depended on your OS Platform. It will be secured by Password or Biometric ID",
|
||||||
"loginWithAddress": "Login with Nostr Address",
|
"loginWithAddress": "Login with Nostr Address",
|
||||||
"loginWithBunker": "Login with nsecBunker",
|
"loginWithBunker": "Login with nsecBunker",
|
||||||
"or": "Or continue with",
|
"or": "Or continue with",
|
||||||
"loginWithPrivkey": "Login with Private Key"
|
"loginWithPrivkey": "Login with Private Key"
|
||||||
},
|
},
|
||||||
"loginWithAddress": {
|
"loginWithAddress": {
|
||||||
"title": "Enter your Nostr Address"
|
"title": "Enter your Nostr Address"
|
||||||
},
|
},
|
||||||
"loginWithBunker": {
|
"loginWithBunker": {
|
||||||
"title": "Enter your nsecbunker token"
|
"title": "Enter your nsecbunker token"
|
||||||
},
|
},
|
||||||
"loginWithPrivkey": {
|
"loginWithPrivkey": {
|
||||||
"title": "Enter your Private Key",
|
"title": "Enter your Private Key",
|
||||||
"subtitle": "Lume will put your private key to <1>{{service}}</1>.\nIt will be secured by your OS."
|
"subtitle": "Lume will put your private key to <1>{{service}}</1>.\nIt will be secured by your OS."
|
||||||
},
|
},
|
||||||
"signup": {
|
"signup": {
|
||||||
"title": "Let's Get Started",
|
"title": "Let's Get Started",
|
||||||
"subtitle": "Choose one of methods below to create your account",
|
"subtitle": "Choose one of methods below to create your account",
|
||||||
"selfManageMethod": "Self-Managed",
|
"selfManageMethod": "Self-Managed",
|
||||||
"selfManageMethodDescription": "You create your keys and keep them safe.",
|
"selfManageMethodDescription": "You create your keys and keep them safe.",
|
||||||
"providerMethod": "Managed by Provider",
|
"providerMethod": "Managed by Provider",
|
||||||
"providerMethodDescription": "A 3rd party provider will handle your sign in keys for you."
|
"providerMethodDescription": "A 3rd party provider will handle your sign in keys for you."
|
||||||
},
|
},
|
||||||
"backup": {
|
"backup": {
|
||||||
"title": "This is your new sign in key",
|
"title": "This is your new sign in key",
|
||||||
"subtitle": "Keep your key in safe place. If you lose this key, you will lose access to your account.",
|
"subtitle": "Keep your key in safe place. If you lose this key, you will lose access to your account.",
|
||||||
"confirm1": "I understand the risk of lost private key.",
|
"confirm1": "I understand the risk of lost private key.",
|
||||||
"confirm2": "I will make sure keep it safe and not sharing with anyone.",
|
"confirm2": "I will make sure keep it safe and not sharing with anyone.",
|
||||||
"confirm3": "I understand I cannot recover private key.",
|
"confirm3": "I understand I cannot recover private key.",
|
||||||
"button": "Save & Continue"
|
"button": "Save & Continue"
|
||||||
},
|
},
|
||||||
"signupWithProvider": {
|
"signupWithProvider": {
|
||||||
"title": "Let's set up your account on Nostr",
|
"title": "Let's set up your account on Nostr",
|
||||||
"username": "Username *",
|
"username": "Username *",
|
||||||
"chooseProvider": "Choose a Provider",
|
"chooseProvider": "Choose a Provider",
|
||||||
"usernameFooter": "Use to login to Lume and other Nostr apps. You can choose provider you trust to manage your account",
|
"usernameFooter": "Use to login to Lume and other Nostr apps. You can choose provider you trust to manage your account",
|
||||||
"email": "Backup Email (optional)",
|
"email": "Backup Email (optional)",
|
||||||
"emailFooter": "Use for recover your account if you lose your password"
|
"emailFooter": "Use for recover your account if you lose your password"
|
||||||
},
|
},
|
||||||
"onboardingSettings": {
|
"onboardingSettings": {
|
||||||
"title": "You're almost ready to use Lume.",
|
"title": "You're almost ready to use Lume.",
|
||||||
"subtitle": "Let's start personalizing your experience.",
|
"subtitle": "Let's start personalizing your experience.",
|
||||||
"notification": {
|
"notification": {
|
||||||
"title": "Push notification",
|
"title": "Push notification",
|
||||||
"subtitle": "Enabling push notifications will allow you to receive notifications from Lume."
|
"subtitle": "Enabling push notifications will allow you to receive notifications from Lume."
|
||||||
},
|
},
|
||||||
"lowPower": {
|
"lowPower": {
|
||||||
"title": "Low Power Mode",
|
"title": "Low Power Mode",
|
||||||
"subtitle": "Limited relay connection and hide all media, sustainable for low network environment."
|
"subtitle": "Limited relay connection and hide all media, sustainable for low network environment."
|
||||||
},
|
},
|
||||||
"translation": {
|
"translation": {
|
||||||
"title": "Translation (nostr.wine)",
|
"title": "Translation (nostr.wine)",
|
||||||
"subtitle": "Translate text to your preferred language, powered by Nostr Wine."
|
"subtitle": "Translate text to your preferred language, powered by Nostr Wine."
|
||||||
},
|
},
|
||||||
"footer": "There are many more settings you can configure from the 'Settings' Screen. Be sure to visit it later."
|
"footer": "There are many more settings you can configure from the 'Settings' Screen. Be sure to visit it later."
|
||||||
},
|
},
|
||||||
"relays": {
|
"relays": {
|
||||||
"global": "Global",
|
"global": "Global",
|
||||||
"follows": "Follows",
|
"follows": "Follows",
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"title": "Connected relays",
|
"title": "Connected relays",
|
||||||
"empty": "Empty."
|
"empty": "Empty."
|
||||||
},
|
},
|
||||||
"relayView": {
|
"relayView": {
|
||||||
"empty": "Could not load relay information 😬",
|
"empty": "Could not load relay information 😬",
|
||||||
"owner": "Owner",
|
"owner": "Owner",
|
||||||
"contact": "Contact",
|
"contact": "Contact",
|
||||||
"software": "Software",
|
"software": "Software",
|
||||||
"nips": "Supported NIPs",
|
"nips": "Supported NIPs",
|
||||||
"limit": "Limitation",
|
"limit": "Limitation",
|
||||||
"payment": "Open payment website",
|
"payment": "Open payment website",
|
||||||
"paymentNote": "You need to make a payment to connect this relay"
|
"paymentNote": "You need to make a payment to connect this relay"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"suggestion": {
|
"suggestion": {
|
||||||
"title": "Suggested Follows",
|
"title": "Suggested Follows",
|
||||||
"error": "Error. Cannot get trending users",
|
"error": "Error. Cannot get trending users",
|
||||||
"button": "Save & Go back"
|
"button": "Save & Go back"
|
||||||
},
|
},
|
||||||
"interests": {
|
"interests": {
|
||||||
"title": "Interests",
|
"title": "Interests",
|
||||||
"subtitle": "Pick things you'd like to see in your home feed.",
|
"subtitle": "Pick things you'd like to see in your home feed.",
|
||||||
"edit": "Edit Interest",
|
"edit": "Edit Interest",
|
||||||
"followAll": "Follow All",
|
"followAll": "Follow All",
|
||||||
"unfollowAll": "Unfollow All"
|
"unfollowAll": "Unfollow All"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"general": {
|
"general": {
|
||||||
"title": "General",
|
"title": "General",
|
||||||
"update": {
|
"update": {
|
||||||
"title": "Update",
|
"title": "Update",
|
||||||
"subtitle": "Automatically download new update"
|
"subtitle": "Automatically download new update"
|
||||||
},
|
},
|
||||||
"lowPower": {
|
"lowPower": {
|
||||||
"title": "Low Power",
|
"title": "Low Power",
|
||||||
"subtitle": "Sustainable for low network environment"
|
"subtitle": "Sustainable for low network environment"
|
||||||
},
|
},
|
||||||
"startup": {
|
"startup": {
|
||||||
"title": "Startup",
|
"title": "Startup",
|
||||||
"subtitle": "Launch Lume at Login"
|
"subtitle": "Launch Lume at Login"
|
||||||
},
|
},
|
||||||
"media": {
|
"media": {
|
||||||
"title": "Media",
|
"title": "Media",
|
||||||
"subtitle": "Automatically load media"
|
"subtitle": "Automatically load media"
|
||||||
},
|
},
|
||||||
"hashtag": {
|
"hashtag": {
|
||||||
"title": "Hashtag",
|
"title": "Hashtag",
|
||||||
"subtitle": "Show all hashtags in content"
|
"subtitle": "Show all hashtags in content"
|
||||||
},
|
},
|
||||||
"notification": {
|
"notification": {
|
||||||
"title": "Notification",
|
"title": "Notification",
|
||||||
"subtitle": "Automatically send notification"
|
"subtitle": "Automatically send notification"
|
||||||
},
|
},
|
||||||
"translation": {
|
"translation": {
|
||||||
"title": "Translation",
|
"title": "Translation",
|
||||||
"subtitle": "Translate text to your language"
|
"subtitle": "Translate text to your language"
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Appearance",
|
"title": "Appearance",
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
"dark": "Dark",
|
"dark": "Dark",
|
||||||
"system": "System"
|
"system": "System"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"title": "Account"
|
"title": "Account"
|
||||||
},
|
},
|
||||||
"zap": {
|
"zap": {
|
||||||
"title": "Zap",
|
"title": "Zap",
|
||||||
"nwc": "Connection String"
|
"nwc": "Connection String"
|
||||||
},
|
},
|
||||||
"backup": {
|
"backup": {
|
||||||
"title": "Backup",
|
"title": "Backup",
|
||||||
"privkey": {
|
"privkey": {
|
||||||
"title": "Private key",
|
"title": "Private key",
|
||||||
"button": "Remove private key"
|
"button": "Remove private key"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"advanced": {
|
"advanced": {
|
||||||
"title": "Advanced",
|
"title": "Advanced",
|
||||||
"cache": {
|
"cache": {
|
||||||
"title": "Cache",
|
"title": "Cache",
|
||||||
"subtitle": "Use for boost up nostr connection",
|
"subtitle": "Use for boost up nostr connection",
|
||||||
"button": "Clear"
|
"button": "Clear"
|
||||||
},
|
},
|
||||||
"instant": {
|
"instant": {
|
||||||
"title": "Instant Zap",
|
"title": "Instant Zap",
|
||||||
"subtitle": "Zap with default amount, no confirmation"
|
"subtitle": "Zap with default amount, no confirmation"
|
||||||
},
|
},
|
||||||
"defaultAmount": "Default amount"
|
"defaultAmount": "Default amount"
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"title": "About",
|
"title": "About",
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
"checkUpdate": "Check for update",
|
"checkUpdate": "Check for update",
|
||||||
"installUpdate": "Install"
|
"installUpdate": "Install"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"home": {
|
"home": {
|
||||||
"title": "Your account was successfully created!",
|
"title": "Your account was successfully created!",
|
||||||
"subtitle": "For starters, let's set up your profile.",
|
"subtitle": "For starters, let's set up your profile.",
|
||||||
"profileSettings": "Profile Settings"
|
"profileSettings": "Profile Settings"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"title": "About you",
|
"title": "About you",
|
||||||
"subtitle": "Tell Lume about yourself to start building your home feed."
|
"subtitle": "Tell Lume about yourself to start building your home feed."
|
||||||
},
|
},
|
||||||
"finish": {
|
"finish": {
|
||||||
"title": "Profile setup complete!",
|
"title": "Profile setup complete!",
|
||||||
"subtitle": "You can exit the setup here and start using Lume.",
|
"subtitle": "You can exit the setup here and start using Lume.",
|
||||||
"report": "Report a issue"
|
"report": "Report a issue"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"activity": {
|
"activity": {
|
||||||
"title": "Activity",
|
"title": "Activity",
|
||||||
"empty": "Yo! Nothing new yet.",
|
"empty": "Yo! Nothing new yet.",
|
||||||
"mention": "mention you",
|
"mention": "mention you",
|
||||||
"repost": "reposted",
|
"repost": "reposted",
|
||||||
"zap": "zapped",
|
"zap": "zapped",
|
||||||
"newReply": "New reply",
|
"newReply": "New reply",
|
||||||
"boost": "Boost",
|
"boost": "Boost",
|
||||||
"boostSubtitle": "@ Someone has reposted to your note",
|
"boostSubtitle": "@ Someone has reposted to your note",
|
||||||
"conversation": "Conversation",
|
"conversation": "Conversation",
|
||||||
"conversationSubtitle": "@ Someone has replied to your note"
|
"conversationSubtitle": "@ Someone has replied to your note"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,52 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"label": "lZfXLFgPPR4NNrgjlWDxn",
|
"label": "lZfXLFgPPR4NNrgjlWDxn",
|
||||||
"name": "Newsfeed",
|
"name": "Newsfeed",
|
||||||
"content": "/newsfeed",
|
"content": "/newsfeed",
|
||||||
"logo": "",
|
"logo": "",
|
||||||
"cover": "/newsfeed.png",
|
"cover": "/newsfeed.png",
|
||||||
"coverRetina": "/newsfeed@2x.png",
|
"coverRetina": "/newsfeed@2x.png",
|
||||||
"author": "Lume",
|
"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",
|
"label": "rRtguZwIpd5G8Wt54OTb7",
|
||||||
"name": "For you",
|
"name": "Topic",
|
||||||
"content": "/foryou",
|
"content": "/topic",
|
||||||
"logo": "",
|
"logo": "",
|
||||||
"cover": "/foryou.png",
|
"cover": "/foryou.png",
|
||||||
"coverRetina": "/foryou@2x.png",
|
"coverRetina": "/foryou@2x.png",
|
||||||
"author": "Lume",
|
"author": "Lume",
|
||||||
"description": "Keep up to date with content based on your interests."
|
"description": "Keep up to date with content based on your interests."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "fve9fk2fVyFWORPBkjd79",
|
"label": "fve9fk2fVyFWORPBkjd79",
|
||||||
"name": "Group Feeds",
|
"name": "Group",
|
||||||
"content": "/group",
|
"content": "/group",
|
||||||
"logo": "",
|
"logo": "",
|
||||||
"cover": "/group.png",
|
"cover": "/group.png",
|
||||||
"coverRetina": "/group@2x.png",
|
"coverRetina": "/group@2x.png",
|
||||||
"author": "Lume",
|
"author": "Lume",
|
||||||
"description": "Collective of people you're interested in."
|
"description": "Focus feeds for people you like."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "gxtcIbgD8YNPbeI5o92I8",
|
"label": "gxtcIbgD8YNPbeI5o92I8",
|
||||||
"name": "Trending",
|
"name": "Trending",
|
||||||
"content": "/trending/notes",
|
"content": "/trending/notes",
|
||||||
"logo": "",
|
"logo": "",
|
||||||
"cover": "/trending.png",
|
"cover": "/trending.png",
|
||||||
"coverRetina": "/trending@2x.png",
|
"coverRetina": "/trending@2x.png",
|
||||||
"author": "Lume",
|
"author": "Lume",
|
||||||
"description": "What is trending on Nostr?."
|
"description": "What is trending on Nostr?."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "GLFm44za8rhJDP04LMr3M",
|
"label": "GLFm44za8rhJDP04LMr3M",
|
||||||
"name": "Global",
|
"name": "Global",
|
||||||
"content": "/global",
|
"content": "/global",
|
||||||
"logo": "",
|
"logo": "",
|
||||||
"cover": "/global.png",
|
"cover": "/global.png",
|
||||||
"coverRetina": "/global@2x.png",
|
"coverRetina": "/global@2x.png",
|
||||||
"author": "Lume",
|
"author": "Lume",
|
||||||
"description": "All events from connected relays."
|
"description": "All events from connected relays."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
[
|
[
|
||||||
{ "label": "onboarding", "name": "Onboarding", "content": "/onboarding" },
|
{ "label": "onboarding", "name": "Onboarding", "content": "/onboarding" },
|
||||||
{ "label": "open", "name": "Open", "content": "/open" }
|
{ "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 std::process::Command;
|
||||||
use tauri::Manager;
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn show_in_folder(path: String) {
|
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();
|
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 std::path::PathBuf;
|
||||||
use tauri::utils::config::WindowEffectsConfig;
|
use tauri::utils::config::WindowEffectsConfig;
|
||||||
use tauri::window::Effect;
|
use tauri::window::Effect;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
use tauri::TitleBarStyle;
|
use tauri::TitleBarStyle;
|
||||||
use tauri::Url;
|
|
||||||
use tauri::WebviewWindowBuilder;
|
use tauri::WebviewWindowBuilder;
|
||||||
use tauri::{LogicalPosition, LogicalSize, Manager, WebviewUrl};
|
use tauri::{LogicalPosition, LogicalSize, Manager, WebviewUrl};
|
||||||
|
use tauri_plugin_decorum::WebviewWindowExt;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn create_column(
|
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]
|
#[tauri::command]
|
||||||
pub fn reposition_column(
|
pub fn reposition_column(
|
||||||
label: &str,
|
label: &str,
|
||||||
@@ -129,7 +114,7 @@ pub fn open_window(
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
#[cfg(target_os = "macos")]
|
#[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)
|
.title(title)
|
||||||
.min_inner_size(width, height)
|
.min_inner_size(width, height)
|
||||||
.inner_size(width, height)
|
.inner_size(width, height)
|
||||||
@@ -145,17 +130,36 @@ pub fn open_window(
|
|||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// [macOS] Custom traffic light possition
|
#[cfg(target_os = "windows")]
|
||||||
// #[cfg(target_os = "macos")]
|
let window = WebviewWindowBuilder::new(&app_handle, label, WebviewUrl::App(PathBuf::from(url)))
|
||||||
// setup_traffic_light_positioner(app_handle.get_window(label).unwrap());
|
.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"))]
|
#[cfg(target_os = "linux")]
|
||||||
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)
|
.title(title)
|
||||||
.min_inner_size(width, height)
|
.min_inner_size(width, height)
|
||||||
.inner_size(width, height)
|
.inner_size(width, height)
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.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(())
|
Ok(())
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod nostr;
|
pub mod nostr;
|
||||||
pub mod traffic_light;
|
|
||||||
pub mod tray;
|
pub mod tray;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
@@ -18,9 +17,7 @@ extern crate objc;
|
|||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use tauri_plugin_autostart::MacosLauncher;
|
use tauri_plugin_decorum::WebviewWindowExt;
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
use traffic_light::setup_traffic_light_positioner;
|
|
||||||
|
|
||||||
pub struct Nostr {
|
pub struct Nostr {
|
||||||
client: Client,
|
client: Client,
|
||||||
@@ -29,8 +26,15 @@ pub struct Nostr {
|
|||||||
fn main() {
|
fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.setup(|app| {
|
.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")]
|
#[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
|
// Setup app tray
|
||||||
let handle = app.handle().clone();
|
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_clipboard_manager::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_fs::init())
|
.plugin(tauri_plugin_fs::init())
|
||||||
@@ -93,22 +98,16 @@ fn main() {
|
|||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_upload::init())
|
.plugin(tauri_plugin_upload::init())
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.plugin(tauri_plugin_autostart::init(
|
|
||||||
MacosLauncher::LaunchAgent,
|
|
||||||
Some(vec![]),
|
|
||||||
))
|
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
nostr::relay::get_relays,
|
nostr::relay::get_relays,
|
||||||
nostr::relay::list_connected_relays,
|
|
||||||
nostr::relay::connect_relay,
|
nostr::relay::connect_relay,
|
||||||
nostr::relay::remove_relay,
|
nostr::relay::remove_relay,
|
||||||
nostr::keys::create_keys,
|
nostr::keys::get_accounts,
|
||||||
nostr::keys::save_key,
|
nostr::keys::create_account,
|
||||||
|
nostr::keys::save_account,
|
||||||
nostr::keys::get_encrypted_key,
|
nostr::keys::get_encrypted_key,
|
||||||
nostr::keys::get_stored_nsec,
|
|
||||||
nostr::keys::nostr_connect,
|
nostr::keys::nostr_connect,
|
||||||
nostr::keys::verify_signer,
|
nostr::keys::load_account,
|
||||||
nostr::keys::load_selected_account,
|
|
||||||
nostr::keys::event_to_bech32,
|
nostr::keys::event_to_bech32,
|
||||||
nostr::keys::user_to_bech32,
|
nostr::keys::user_to_bech32,
|
||||||
nostr::keys::to_npub,
|
nostr::keys::to_npub,
|
||||||
@@ -118,6 +117,7 @@ fn main() {
|
|||||||
nostr::metadata::get_current_user_profile,
|
nostr::metadata::get_current_user_profile,
|
||||||
nostr::metadata::get_profile,
|
nostr::metadata::get_profile,
|
||||||
nostr::metadata::get_contact_list,
|
nostr::metadata::get_contact_list,
|
||||||
|
nostr::metadata::set_contact_list,
|
||||||
nostr::metadata::create_profile,
|
nostr::metadata::create_profile,
|
||||||
nostr::metadata::follow,
|
nostr::metadata::follow,
|
||||||
nostr::metadata::unfollow,
|
nostr::metadata::unfollow,
|
||||||
@@ -130,23 +130,22 @@ fn main() {
|
|||||||
nostr::metadata::zap_event,
|
nostr::metadata::zap_event,
|
||||||
nostr::metadata::friend_to_friend,
|
nostr::metadata::friend_to_friend,
|
||||||
nostr::event::get_event,
|
nostr::event::get_event,
|
||||||
nostr::event::get_events_from,
|
nostr::event::get_thread,
|
||||||
nostr::event::get_events,
|
nostr::event::get_events_by,
|
||||||
nostr::event::get_events_from_interests,
|
nostr::event::get_local_events,
|
||||||
nostr::event::get_event_thread,
|
nostr::event::get_global_events,
|
||||||
|
nostr::event::get_hashtag_events,
|
||||||
|
nostr::event::get_group_events,
|
||||||
nostr::event::publish,
|
nostr::event::publish,
|
||||||
nostr::event::repost,
|
nostr::event::repost,
|
||||||
nostr::event::search,
|
|
||||||
commands::folder::show_in_folder,
|
commands::folder::show_in_folder,
|
||||||
commands::folder::get_accounts,
|
|
||||||
commands::opg::fetch_opg,
|
|
||||||
commands::window::create_column,
|
commands::window::create_column,
|
||||||
commands::window::close_column,
|
commands::window::close_column,
|
||||||
commands::window::reposition_column,
|
commands::window::reposition_column,
|
||||||
commands::window::resize_column,
|
commands::window::resize_column,
|
||||||
commands::window::open_window,
|
commands::window::open_window,
|
||||||
commands::window::navigate,
|
commands::window::set_badge,
|
||||||
commands::window::set_badge
|
commands::opg::fetch_opg,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application")
|
.expect("error while running tauri application")
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<String, Stri
|
|||||||
let relays = event.relays;
|
let relays = event.relays;
|
||||||
for relay in relays.into_iter() {
|
for relay in relays.into_iter() {
|
||||||
let url = Url::from_str(&relay).unwrap();
|
let url = Url::from_str(&relay).unwrap();
|
||||||
let _ = client.add_relay(url.clone()).await.unwrap_or_default();
|
let _ = client.add_relay(&url).await.unwrap_or_default();
|
||||||
client.connect_relay(url).await.unwrap_or_default();
|
client.connect_relay(&url).await.unwrap_or_default();
|
||||||
}
|
}
|
||||||
Some(event.event_id)
|
Some(event.event_id)
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<String, Stri
|
|||||||
Some(id) => {
|
Some(id) => {
|
||||||
let filter = Filter::new().id(id);
|
let filter = Filter::new().id(id);
|
||||||
|
|
||||||
match &client
|
match client
|
||||||
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
|
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -49,7 +49,24 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<String, Stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[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,
|
public_key: &str,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
as_of: Option<&str>,
|
as_of: Option<&str>,
|
||||||
@@ -57,32 +74,68 @@ pub async fn get_events_from(
|
|||||||
) -> Result<Vec<Event>, String> {
|
) -> Result<Vec<Event>, String> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
|
|
||||||
if let Ok(author) = PublicKey::from_str(public_key) {
|
match PublicKey::from_str(public_key) {
|
||||||
let until = match as_of {
|
Ok(author) => {
|
||||||
Some(until) => Timestamp::from_str(until).unwrap(),
|
let until = match as_of {
|
||||||
None => Timestamp::now(),
|
Some(until) => Timestamp::from_str(until).unwrap(),
|
||||||
};
|
None => Timestamp::now(),
|
||||||
let filter = Filter::new()
|
};
|
||||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
let filter = Filter::new()
|
||||||
.author(author)
|
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||||
.limit(limit)
|
.author(author)
|
||||||
.until(until);
|
.limit(limit)
|
||||||
|
.until(until);
|
||||||
|
|
||||||
match client.get_events_of(vec![filter], None).await {
|
match client.get_events_of(vec![filter], None).await {
|
||||||
Ok(events) => Ok(events),
|
Ok(events) => Ok(events),
|
||||||
Err(err) => Err(err.to_string()),
|
Err(err) => Err(err.to_string()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
Err(err) => Err(err.to_string()),
|
||||||
Err("Public Key is not valid, please check again.".into())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[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| {
|
||||||
|
if p.starts_with("npub1") {
|
||||||
|
PublicKey::from_bech32(p).unwrap()
|
||||||
|
} else {
|
||||||
|
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,
|
limit: usize,
|
||||||
until: Option<&str>,
|
until: Option<&str>,
|
||||||
contacts: Option<Vec<&str>>,
|
|
||||||
global: bool,
|
|
||||||
state: State<'_, Nostr>,
|
state: State<'_, Nostr>,
|
||||||
) -> Result<Vec<Event>, String> {
|
) -> Result<Vec<Event>, String> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
@@ -91,66 +144,22 @@ pub async fn get_events(
|
|||||||
None => Timestamp::now(),
|
None => Timestamp::now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
match global {
|
let filter = Filter::new()
|
||||||
true => {
|
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||||
let filter = Filter::new()
|
.limit(limit)
|
||||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
.until(as_of);
|
||||||
.limit(limit)
|
|
||||||
.until(as_of);
|
|
||||||
|
|
||||||
match client
|
match client
|
||||||
.get_events_of(vec![filter], Some(Duration::from_secs(15)))
|
.get_events_of(vec![filter], Some(Duration::from_secs(8)))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(events) => Ok(events),
|
Ok(events) => Ok(events),
|
||||||
Err(err) => Err(err.to_string()),
|
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()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_events_from_interests(
|
pub async fn get_hashtag_events(
|
||||||
hashtags: Vec<&str>,
|
hashtags: Vec<&str>,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
until: Option<&str>,
|
until: Option<&str>,
|
||||||
@@ -174,19 +183,30 @@ pub async fn get_events_from_interests(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[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 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) {
|
match client.get_events_of(vec![filter], None).await {
|
||||||
Ok(event_id) => {
|
Ok(events) => Ok(events),
|
||||||
let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id);
|
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()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => Err("Event ID is not valid".into()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,30 +230,8 @@ pub async fn repost(raw: &str, state: State<'_, Nostr>) -> Result<EventId, Strin
|
|||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let event = Event::from_json(raw).unwrap();
|
let event = Event::from_json(raw).unwrap();
|
||||||
|
|
||||||
if let Ok(event_id) = client.repost(&event, None).await {
|
match client.repost(&event, None).await {
|
||||||
Ok(event_id)
|
Ok(event_id) => 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),
|
|
||||||
Err(err) => Err(err.to_string()),
|
Err(err) => Err(err.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,46 @@
|
|||||||
use crate::Nostr;
|
use crate::Nostr;
|
||||||
use keyring::Entry;
|
use keyring::Entry;
|
||||||
|
use keyring_search::{Limit, List, Search};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::{fs::File, str::FromStr};
|
use tauri::State;
|
||||||
use tauri::{Manager, State};
|
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
pub struct CreateKeysResponse {
|
pub struct Account {
|
||||||
npub: String,
|
npub: String,
|
||||||
nsec: String,
|
nsec: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[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 keys = Keys::generate();
|
||||||
let public_key = keys.public_key();
|
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 {
|
let result = Account {
|
||||||
npub: public_key.to_bech32().expect("npub failed"),
|
npub: public_key.to_bech32().unwrap(),
|
||||||
nsec: secret_key.to_bech32().expect("nsec failed"),
|
nsec: secret_key.to_bech32().unwrap(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn save_key(
|
pub async fn save_account(
|
||||||
nsec: &str,
|
nsec: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
app_handle: tauri::AppHandle,
|
|
||||||
state: State<'_, Nostr>,
|
state: State<'_, Nostr>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let secret_key: Result<SecretKey, String>;
|
let secret_key: Result<SecretKey, String>;
|
||||||
@@ -38,12 +49,12 @@ pub async fn save_key(
|
|||||||
let encrypted_key = EncryptedSecretKey::from_bech32(nsec).unwrap();
|
let encrypted_key = EncryptedSecretKey::from_bech32(nsec).unwrap();
|
||||||
secret_key = match encrypted_key.to_secret_key(password) {
|
secret_key = match encrypted_key.to_secret_key(password) {
|
||||||
Ok(val) => Ok(val),
|
Ok(val) => Ok(val),
|
||||||
Err(_) => Err("Wrong passphase".into()),
|
Err(err) => Err(err.to_string()),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
secret_key = match SecretKey::from_bech32(nsec) {
|
secret_key = match SecretKey::from_bech32(nsec) {
|
||||||
Ok(val) => Ok(val),
|
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 npub = nostr_keys.public_key().to_bech32().unwrap();
|
||||||
let nsec = nostr_keys.secret_key().unwrap().to_bech32().unwrap();
|
let nsec = nostr_keys.secret_key().unwrap().to_bech32().unwrap();
|
||||||
|
|
||||||
let home_dir = app_handle.path().home_dir().unwrap();
|
let keyring = Entry::new(&npub, "nostr_secret").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.set_password(&nsec);
|
let _ = keyring.set_password(&nsec);
|
||||||
|
|
||||||
let signer = NostrSigner::Keys(nostr_keys);
|
let signer = NostrSigner::Keys(nostr_keys);
|
||||||
@@ -75,94 +80,29 @@ pub async fn save_key(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn nostr_connect(
|
pub async fn load_account(npub: &str, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||||
npub: &str,
|
|
||||||
uri: &str,
|
|
||||||
app_handle: tauri::AppHandle,
|
|
||||||
state: State<'_, Nostr>,
|
|
||||||
) -> Result<String, String> {
|
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let app_keys = Keys::generate();
|
let keyring = Entry::new(&npub, "nostr_secret").unwrap();
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
match keyring.get_password() {
|
match keyring.get_password() {
|
||||||
Ok(password) => {
|
Ok(password) => {
|
||||||
if password.starts_with("bunker://") {
|
if password.starts_with("bunker://") {
|
||||||
let app_keys = Keys::generate();
|
let local_keyring = Entry::new(&npub, "bunker_local_account").unwrap();
|
||||||
let bunker_uri = NostrConnectURI::parse(password).unwrap();
|
|
||||||
let signer = Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(60), None)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Update signer
|
match local_keyring.get_password() {
|
||||||
client.set_signer(Some(signer.into())).await;
|
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 {
|
} else {
|
||||||
let secret_key = SecretKey::from_bech32(password).expect("Get secret key failed");
|
let secret_key = SecretKey::from_bech32(password).expect("Get secret key failed");
|
||||||
let keys = Keys::new(secret_key);
|
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 relay_url = item.0.to_string();
|
||||||
let opts = match item.1 {
|
let opts = match item.1 {
|
||||||
Some(val) => {
|
Some(val) => {
|
||||||
if val == RelayMetadata::Read {
|
if val == &RelayMetadata::Read {
|
||||||
RelayOptions::new().read(true).write(false)
|
RelayOptions::new().read(true).write(false)
|
||||||
} else {
|
} else {
|
||||||
RelayOptions::new().write(true).read(false)
|
RelayOptions::new().write(true).read(false)
|
||||||
@@ -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]
|
#[tauri::command]
|
||||||
pub fn event_to_bech32(id: &str, relays: Vec<String>) -> Result<String, ()> {
|
pub fn event_to_bech32(id: &str, relays: Vec<String>) -> Result<String, ()> {
|
||||||
let event_id = EventId::from_hex(id).unwrap();
|
let event_id = EventId::from_hex(id).unwrap();
|
||||||
@@ -249,9 +245,12 @@ pub fn to_npub(hex: &str) -> Result<String, ()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn verify_nip05(key: &str, nip05: &str) -> Result<bool, ()> {
|
pub async fn verify_nip05(key: &str, nip05: &str) -> Result<bool, String> {
|
||||||
let public_key = PublicKey::from_str(key).unwrap();
|
match PublicKey::from_str(key) {
|
||||||
let status = nip05::verify(public_key, nip05, None).await;
|
Ok(public_key) => {
|
||||||
|
let status = nip05::verify(&public_key, nip05, None).await;
|
||||||
Ok(status.is_ok())
|
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 {
|
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 event in contact_list_events.into_iter() {
|
||||||
for tag in event.into_iter_tags() {
|
for tag in event.into_iter_tags() {
|
||||||
if let Tag::PublicKey {
|
if let Some(TagStandard::PublicKey {
|
||||||
public_key,
|
public_key,
|
||||||
relay_url,
|
relay_url,
|
||||||
alias,
|
alias,
|
||||||
uppercase: false,
|
uppercase: false,
|
||||||
} = tag
|
}) = tag.to_standardized()
|
||||||
{
|
{
|
||||||
contact_list.push(Contact::new(public_key, relay_url, alias))
|
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]
|
#[tauri::command]
|
||||||
pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
|
|
||||||
if let Ok(contact_list) = client.get_contact_list(Some(Duration::from_secs(10))).await {
|
match client.get_contact_list(Some(Duration::from_secs(10))).await {
|
||||||
let list = contact_list
|
Ok(contact_list) => {
|
||||||
.into_iter()
|
if !contact_list.is_empty() {
|
||||||
.map(|f| f.public_key.to_hex())
|
let list = contact_list
|
||||||
.collect();
|
.into_iter()
|
||||||
|
.map(|f| f.public_key.to_hex())
|
||||||
|
.collect();
|
||||||
|
|
||||||
Ok(list)
|
Ok(list)
|
||||||
} else {
|
} else {
|
||||||
Err("Contact list not found".into())
|
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 public_key = signer.public_key().await.unwrap();
|
||||||
let encrypted = signer.nip44_encrypt(public_key, content).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]);
|
let builder = EventBuilder::new(Kind::ApplicationSpecificData, encrypted, vec![tag]);
|
||||||
|
|
||||||
match client.send_event_builder(builder).await {
|
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]
|
#[tauri::command]
|
||||||
pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool, ()> {
|
pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool, ()> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
if let Ok(_) = client.add_relay(relay).await {
|
if let Ok(status) = client.add_relay(relay).await {
|
||||||
let _ = client.connect_relay(relay);
|
if status == true {
|
||||||
Ok(true)
|
println!("connecting to relay: {}", relay);
|
||||||
|
let _ = client.connect_relay(relay);
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(false)
|
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",
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"productName": "Lume",
|
"productName": "Lume",
|
||||||
"version": "4.0.3",
|
"version": "4.0.5",
|
||||||
"identifier": "nu.lume.Lume",
|
"identifier": "nu.lume.Lume",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeBuildCommand": "pnpm desktop:build",
|
"beforeBuildCommand": "pnpm desktop:build",
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "Lume",
|
"title": "Lume",
|
||||||
"label": "main",
|
"label": "main",
|
||||||
"titleBarStyle": "Overlay",
|
"titleBarStyle": "Overlay",
|
||||||
"width": 500,
|
"width": 500,
|
||||||
"height": 800,
|
"height": 800,
|
||||||
"minWidth": 500,
|
"minWidth": 500,
|
||||||
"minHeight": 800
|
"minHeight": 800
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "Lume",
|
"title": "Lume",
|
||||||
"label": "main",
|
"label": "main",
|
||||||
"titleBarStyle": "Overlay",
|
"titleBarStyle": "Overlay",
|
||||||
"width": 500,
|
"width": 500,
|
||||||
"height": 800,
|
"height": 800,
|
||||||
"minWidth": 500,
|
"minWidth": 500,
|
||||||
"minHeight": 800,
|
"minHeight": 800,
|
||||||
"hiddenTitle": true,
|
"hiddenTitle": true,
|
||||||
"decorations": true,
|
"decorations": true,
|
||||||
"transparent": true,
|
"transparent": true,
|
||||||
"windowEffects": {
|
"windowEffects": {
|
||||||
"effects": ["windowBackground"]
|
"effects": ["windowBackground"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "Lume",
|
"title": "Lume",
|
||||||
"label": "main",
|
"label": "main",
|
||||||
"width": 500,
|
"width": 500,
|
||||||
"height": 800,
|
"height": 800,
|
||||||
"minWidth": 500,
|
"minWidth": 500,
|
||||||
"minHeight": 800
|
"minHeight": 800,
|
||||||
}
|
"transparent": true,
|
||||||
]
|
"windowEffects": {
|
||||||
}
|
"effects": ["mica"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||