chore: clean up
This commit is contained in:
@@ -6,7 +6,7 @@ use std::{str::FromStr, time::Duration};
|
||||
use tauri::State;
|
||||
use tauri_specta::Event;
|
||||
|
||||
use crate::{NewSettings, Nostr, Settings};
|
||||
use crate::{common::get_latest_event, NewSettings, Nostr, Settings};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Type)]
|
||||
pub struct Profile {
|
||||
@@ -229,14 +229,14 @@ pub async fn get_lume_store(key: String, state: State<'_, Nostr>) -> Result<Stri
|
||||
.author(public_key)
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.identifier(key)
|
||||
.limit(1);
|
||||
.limit(10);
|
||||
|
||||
match client
|
||||
.get_events_of(vec![filter], EventSource::Database)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.first() {
|
||||
if let Some(event) = get_latest_event(&events) {
|
||||
match signer.nip44_decrypt(public_key, event.content()).await {
|
||||
Ok(decrypted) => Ok(decrypted),
|
||||
Err(_) => Err(event.content.to_string()),
|
||||
|
||||
@@ -4,7 +4,8 @@ import type {
|
||||
PersistedQuery,
|
||||
} from "@tanstack/query-persist-client-core";
|
||||
import { Store } from "@tanstack/store";
|
||||
import { ask, message } from "@tauri-apps/plugin-dialog";
|
||||
import { ask, message, open } from "@tauri-apps/plugin-dialog";
|
||||
import { readFile } from "@tauri-apps/plugin-fs";
|
||||
import { relaunch } from "@tauri-apps/plugin-process";
|
||||
import type { Store as TauriStore } from "@tauri-apps/plugin-store";
|
||||
import { check } from "@tauri-apps/plugin-updater";
|
||||
@@ -241,6 +242,61 @@ export async function checkForAppUpdates(silent: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function upload(filePath?: string) {
|
||||
const allowExts = [
|
||||
"png",
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"gif",
|
||||
"mp4",
|
||||
"mp3",
|
||||
"webm",
|
||||
"mkv",
|
||||
"avi",
|
||||
"mov",
|
||||
];
|
||||
|
||||
const selected =
|
||||
filePath ||
|
||||
(
|
||||
await open({
|
||||
multiple: false,
|
||||
filters: [
|
||||
{
|
||||
name: "Media",
|
||||
extensions: allowExts,
|
||||
},
|
||||
],
|
||||
})
|
||||
).path;
|
||||
|
||||
// User cancelled action
|
||||
if (!selected) return null;
|
||||
|
||||
try {
|
||||
const file = await readFile(selected);
|
||||
const blob = new Blob([file]);
|
||||
|
||||
const data = new FormData();
|
||||
data.append("fileToUpload", blob);
|
||||
data.append("submit", "Upload Image");
|
||||
|
||||
const res = await fetch("https://nostr.build/api/v2/upload/files", {
|
||||
method: "POST",
|
||||
body: data,
|
||||
});
|
||||
|
||||
if (!res.ok) return null;
|
||||
|
||||
const json = await res.json();
|
||||
const content = json.data[0];
|
||||
|
||||
return content.url as string;
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
export function toLumeEvents(richEvents: RichEvent[]) {
|
||||
const events = richEvents.map((item) => {
|
||||
const nostrEvent: NostrEvent = JSON.parse(item.raw);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { appSettings, cn } from "@/commons";
|
||||
import { LumeWindow } from "@/system";
|
||||
import { Lightning } from "@phosphor-icons/react";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
import { useStore } from "@tanstack/react-store";
|
||||
import { useNoteContext } from "../provider";
|
||||
|
||||
export function NoteZap({ large = false }: { large?: boolean }) {
|
||||
const search = useSearch({ strict: false });
|
||||
const visible = useStore(appSettings, (state) => state.display_zap_button);
|
||||
const event = useNoteContext();
|
||||
|
||||
@@ -13,7 +15,7 @@ export function NoteZap({ large = false }: { large?: boolean }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => LumeWindow.openZap(event.id)}
|
||||
onClick={() => LumeWindow.openZap(event.id, search.account)}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
|
||||
large
|
||||
|
||||
@@ -2,8 +2,7 @@ import { cn } from "@/commons";
|
||||
import { Spinner } from "@/components";
|
||||
import { Note } from "@/components/note";
|
||||
import { User } from "@/components/user";
|
||||
import { type LumeEvent, NostrQuery } from "@/system";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { type LumeEvent, useEvent } from "@/system";
|
||||
import { memo } from "react";
|
||||
|
||||
export const RepostNote = memo(function RepostNote({
|
||||
@@ -13,22 +12,7 @@ export const RepostNote = memo(function RepostNote({
|
||||
event: LumeEvent;
|
||||
className?: string;
|
||||
}) {
|
||||
const { isLoading, isError, data } = useQuery({
|
||||
queryKey: ["event", event.repostId],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const data = await NostrQuery.getRepostEvent(event);
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
staleTime: Number.POSITIVE_INFINITY,
|
||||
retry: 2,
|
||||
});
|
||||
const { isLoading, isError, data } = useEvent(event.repostId);
|
||||
|
||||
return (
|
||||
<Note.Root
|
||||
|
||||
@@ -1,21 +1,34 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { cn } from "@/commons";
|
||||
import { Spinner } from "@/components";
|
||||
import { NostrAccount } from "@/system";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useRouteContext } from "@tanstack/react-router";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
import { useTransition } from "react";
|
||||
import { useUserContext } from "./provider";
|
||||
|
||||
export function UserFollowButton({
|
||||
simple = false,
|
||||
className,
|
||||
}: {
|
||||
simple?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
export function UserFollowButton({ className }: { className?: string }) {
|
||||
const user = useUserContext();
|
||||
|
||||
const [followed, setFollowed] = useState(false);
|
||||
const { queryClient } = useRouteContext({ strict: false });
|
||||
const {
|
||||
isLoading,
|
||||
isError,
|
||||
data: isFollow,
|
||||
} = useQuery({
|
||||
queryKey: ["status", user.pubkey],
|
||||
queryFn: async () => {
|
||||
const res = await commands.checkContact(user.pubkey);
|
||||
|
||||
if (res.status === "ok") {
|
||||
return res.data;
|
||||
} else {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const toggleFollow = () => {
|
||||
@@ -23,7 +36,17 @@ export function UserFollowButton({
|
||||
const res = await commands.toggleContact(user.pubkey, null);
|
||||
|
||||
if (res.status === "ok") {
|
||||
setFollowed((prev) => !prev);
|
||||
queryClient.setQueryData(
|
||||
["status", user.pubkey],
|
||||
(prev: boolean) => !prev,
|
||||
);
|
||||
|
||||
// invalidate cache
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["status", user.pubkey],
|
||||
});
|
||||
|
||||
return;
|
||||
} else {
|
||||
await message(res.error, { kind: "error" });
|
||||
return;
|
||||
@@ -31,18 +54,6 @@ export function UserFollowButton({
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
NostrAccount.checkContact(user.pubkey).then((status) => {
|
||||
if (mounted) setFollowed(status);
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@@ -50,15 +61,9 @@ export function UserFollowButton({
|
||||
onClick={() => toggleFollow()}
|
||||
className={cn("w-max", className)}
|
||||
>
|
||||
{isPending ? (
|
||||
<Spinner className="size-4" />
|
||||
) : followed ? (
|
||||
!simple ? (
|
||||
"Unfollow"
|
||||
) : null
|
||||
) : (
|
||||
"Follow"
|
||||
)}
|
||||
{isError ? "Error" : null}
|
||||
{isPending || isLoading ? <Spinner className="size-4" /> : null}
|
||||
{isFollow ? "Unfollow" : "Follow"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { displayLongHandle, displayNpub } from "@/commons";
|
||||
import { NostrQuery } from "@/system";
|
||||
import { SealCheck } from "@phosphor-icons/react";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
@@ -10,14 +10,13 @@ export function UserNip05() {
|
||||
const { isLoading, data: verified } = useQuery({
|
||||
queryKey: ["nip05", user?.pubkey],
|
||||
queryFn: async () => {
|
||||
if (!user.profile?.nip05?.length) return false;
|
||||
const res = await commands.verifyNip05(user.pubkey, user.profile?.nip05);
|
||||
|
||||
const verify = await NostrQuery.verifyNip05(
|
||||
user.pubkey,
|
||||
user.profile?.nip05,
|
||||
);
|
||||
|
||||
return verify;
|
||||
if (res.status === "ok") {
|
||||
return res.data;
|
||||
} else {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
},
|
||||
enabled: !!user.profile?.nip05?.length,
|
||||
refetchOnMount: false,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { Spinner } from "@/components";
|
||||
import { Column } from "@/components/column";
|
||||
import { LumeWindow, NostrQuery } from "@/system";
|
||||
import { LumeWindow } from "@/system";
|
||||
import type { ColumnEvent, LumeColumn } from "@/types";
|
||||
import { ArrowLeft, ArrowRight, Plus, StackPlus } from "@phosphor-icons/react";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
@@ -24,7 +25,7 @@ export const Route = createLazyFileRoute("/$account/_app/home")({
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const initialColumnList = Route.useLoaderData();
|
||||
const { initialColumns } = Route.useRouteContext();
|
||||
|
||||
const [columns, setColumns] = useState<LumeColumn[]>([]);
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel({
|
||||
@@ -105,6 +106,18 @@ function Screen() {
|
||||
event.preventDefault();
|
||||
}, 150);
|
||||
|
||||
const saveAllColumns = useDebouncedCallback(async () => {
|
||||
const key = "lume_v4:columns";
|
||||
const content = JSON.stringify(columns);
|
||||
const res = await commands.setLumeStore(key, content);
|
||||
|
||||
if (res.status === "ok") {
|
||||
return res.data;
|
||||
} else {
|
||||
console.log(res.error);
|
||||
}
|
||||
}, 200);
|
||||
|
||||
useEffect(() => {
|
||||
if (emblaApi) {
|
||||
emblaApi.on("scroll", emitScrollEvent);
|
||||
@@ -120,14 +133,12 @@ function Screen() {
|
||||
}, [emblaApi, emitScrollEvent, emitResizeEvent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (columns) {
|
||||
NostrQuery.setColumns(columns).then(() => console.log("saved"));
|
||||
}
|
||||
if (columns) saveAllColumns();
|
||||
}, [columns]);
|
||||
|
||||
useEffect(() => {
|
||||
setColumns(initialColumnList);
|
||||
}, [initialColumnList]);
|
||||
setColumns(initialColumns);
|
||||
}, [initialColumns]);
|
||||
|
||||
// Listen for keyboard event
|
||||
useEffect(() => {
|
||||
|
||||
@@ -3,16 +3,18 @@ import type { LumeColumn } from "@/types";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/$account/_app/home")({
|
||||
loader: async ({ context }) => {
|
||||
beforeLoad: async ({ context }) => {
|
||||
const key = "lume_v4:columns";
|
||||
const defaultColumns = context.systemColumns.filter((col) => col.default);
|
||||
const query = await commands.getLumeStore(key);
|
||||
|
||||
let initialColumns: LumeColumn[] = defaultColumns;
|
||||
|
||||
if (query.status === "ok") {
|
||||
const columns: LumeColumn[] = JSON.parse(query.data);
|
||||
return columns;
|
||||
} else {
|
||||
return defaultColumns;
|
||||
initialColumns = JSON.parse(query.data);
|
||||
return { initialColumns };
|
||||
}
|
||||
|
||||
return { initialColumns };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { NostrAccount } from "@/system";
|
||||
import { Button } from "@getalby/bitcoin-connect-react";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
@@ -11,8 +12,13 @@ export const Route = createLazyFileRoute("/$account/_settings/bitcoin-connect")(
|
||||
|
||||
function Screen() {
|
||||
const setNwcUri = async (uri: string) => {
|
||||
const cmd = await NostrAccount.setWallet(uri);
|
||||
if (cmd) getCurrentWebviewWindow().close();
|
||||
const res = await commands.setWallet(uri);
|
||||
|
||||
if (res.status === "ok") {
|
||||
await getCurrentWebviewWindow().close();
|
||||
} else {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { cn } from "@/commons";
|
||||
import { type Profile, commands } from "@/commands.gen";
|
||||
import { cn, upload } from "@/commons";
|
||||
import { Spinner } from "@/components";
|
||||
import { NostrAccount, NostrQuery } from "@/system";
|
||||
import type { Metadata } from "@/types";
|
||||
import { Plus } from "@phosphor-icons/react";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
@@ -27,15 +25,16 @@ function Screen() {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [picture, setPicture] = useState<string>("");
|
||||
|
||||
const onSubmit = (data: Metadata) => {
|
||||
const onSubmit = (data: Profile) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const newProfile: Metadata = { ...profile, ...data, picture };
|
||||
await NostrAccount.createProfile(newProfile);
|
||||
} catch (e) {
|
||||
await message(String(e), { title: "Profile", kind: "error" });
|
||||
return;
|
||||
const newProfile: Profile = { ...profile, ...data, picture };
|
||||
const res = await commands.setProfile(newProfile);
|
||||
|
||||
if (res.status === "error") {
|
||||
await message(res.error, { title: "Profile", kind: "error" });
|
||||
}
|
||||
|
||||
return;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -220,17 +219,18 @@ function AvatarUploader({
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const uploadAvatar = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const image = await NostrQuery.upload();
|
||||
setPicture(image);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
await message(String(e), { title: "Lume", kind: "error" });
|
||||
}
|
||||
const uploadAvatar = () => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const image = await upload();
|
||||
setPicture(image);
|
||||
} catch (e) {
|
||||
await message(String(e));
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -239,7 +239,7 @@ function AvatarUploader({
|
||||
onClick={() => uploadAvatar()}
|
||||
className={cn("size-4", className)}
|
||||
>
|
||||
{loading ? <Spinner className="size-4" /> : children}
|
||||
{isPending ? <Spinner className="size-4" /> : children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import type { Metadata } from "@/types";
|
||||
import { type Profile, commands } from "@/commands.gen";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/$account/_settings/profile")({
|
||||
@@ -7,7 +6,7 @@ export const Route = createFileRoute("/$account/_settings/profile")({
|
||||
const res = await commands.getProfile(params.account);
|
||||
|
||||
if (res.status === "ok") {
|
||||
const profile: Metadata = JSON.parse(res.data);
|
||||
const profile: Profile = JSON.parse(res.data);
|
||||
return { profile };
|
||||
} else {
|
||||
throw new Error(res.error);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { NostrQuery } from "@/system";
|
||||
import { Plus, X } from "@phosphor-icons/react";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
@@ -16,6 +15,16 @@ function Screen() {
|
||||
const [newRelay, setNewRelay] = useState("");
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const removeRelay = async (relay: string) => {
|
||||
const res = await commands.removeRelay(relay);
|
||||
|
||||
if (res.status === "ok") {
|
||||
return res.data;
|
||||
} else {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
const addNewRelay = () => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
@@ -69,7 +78,7 @@ function Screen() {
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => NostrQuery.removeRelay(relay)}
|
||||
onClick={() => removeRelay(relay)}
|
||||
className="inline-flex items-center justify-center rounded-md size-7 hover:bg-black/10 dark:hover:bg-white/10"
|
||||
>
|
||||
<X className="size-4" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NostrAccount } from "@/system";
|
||||
import { commands } from "@/commands.gen";
|
||||
import { createLazyFileRoute, redirect } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createLazyFileRoute("/$account/_settings/wallet")({
|
||||
@@ -10,10 +10,14 @@ function Screen() {
|
||||
const { balance } = Route.useRouteContext();
|
||||
|
||||
const disconnect = async () => {
|
||||
window.localStorage.removeItem("bc:config");
|
||||
await NostrAccount.removeWallet();
|
||||
const res = await commands.removeWallet();
|
||||
|
||||
return redirect({ to: "/$account/bitcoin-connect", params: { account } });
|
||||
if (res.status === "ok") {
|
||||
window.localStorage.removeItem("bc:config");
|
||||
return redirect({ to: "/$account/bitcoin-connect", params: { account } });
|
||||
} else {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { upload } from "@/commons";
|
||||
import { Frame, GoBack, Spinner } from "@/components";
|
||||
import { NostrQuery } from "@/system";
|
||||
import { Plus } from "@phosphor-icons/react";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
@@ -20,7 +20,7 @@ function Screen() {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const uploadAvatar = async () => {
|
||||
const file = await NostrQuery.upload();
|
||||
const file = await upload();
|
||||
|
||||
if (file) {
|
||||
setPicture(file);
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { NostrAccount } from "@/system";
|
||||
import { commands } from "@/commands.gen";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/columns/_layout/create-group")({
|
||||
loader: async () => {
|
||||
const contacts = await NostrAccount.getContactList();
|
||||
return contacts;
|
||||
const res = await commands.getContactList();
|
||||
|
||||
if (res.status === "ok") {
|
||||
return res.data;
|
||||
} else {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -56,9 +56,9 @@ function Screen() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => install(column)}
|
||||
className="text-xs uppercase font-semibold w-max h-7 pl-2.5 pr-2 hidden group-hover:inline-flex items-center justify-center rounded-full bg-neutral-200 hover:bg-blue-500 hover:text-white dark:bg-black/10"
|
||||
className="text-xs uppercase font-semibold w-16 h-7 hidden group-hover:inline-flex items-center justify-center rounded-full bg-neutral-200 hover:bg-blue-500 hover:text-white dark:bg-black/10"
|
||||
>
|
||||
Install
|
||||
Open
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Spinner } from "@/components";
|
||||
import { Conversation } from "@/components/conversation";
|
||||
import { Quote } from "@/components/quote";
|
||||
import { RepostNote } from "@/components/repost";
|
||||
import { TextNote } from "@/components/text";
|
||||
import { type LumeEvent, NostrQuery } from "@/system";
|
||||
import { type ColumnRouteSearch, Kind } from "@/types";
|
||||
import { commands } from "@/commands.gen";
|
||||
import { toLumeEvents } from "@/commons";
|
||||
import { Quote, RepostNote, Spinner, TextNote } from "@/components";
|
||||
import type { LumeEvent } from "@/system";
|
||||
import { Kind } from "@/types";
|
||||
import { ArrowCircleRight } from "@phosphor-icons/react";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
@@ -13,17 +11,6 @@ import { useCallback, useRef } from "react";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
export const Route = createFileRoute("/columns/_layout/global")({
|
||||
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
|
||||
return {
|
||||
account: search.account,
|
||||
label: search.label,
|
||||
name: search.name,
|
||||
};
|
||||
},
|
||||
beforeLoad: async () => {
|
||||
const settings = await NostrQuery.getUserSettings();
|
||||
return { settings };
|
||||
},
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
@@ -40,8 +27,14 @@ export function Screen() {
|
||||
queryKey: [label, account],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||
const events = await NostrQuery.getGlobalEvents(pageParam);
|
||||
return events;
|
||||
const until = pageParam > 0 ? pageParam.toString() : undefined;
|
||||
const res = await commands.getGlobalEvents(until);
|
||||
|
||||
if (res.status === "error") {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
return toLumeEvents(res.data);
|
||||
},
|
||||
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
|
||||
select: (data) => data?.pages.flat(),
|
||||
@@ -57,15 +50,11 @@ export function Screen() {
|
||||
case Kind.Repost:
|
||||
return <RepostNote key={event.id} event={event} className="mb-3" />;
|
||||
default: {
|
||||
if (event.isConversation) {
|
||||
return (
|
||||
<Conversation key={event.id} className="mb-3" event={event} />
|
||||
);
|
||||
}
|
||||
if (event.isQuote) {
|
||||
return <Quote key={event.id} event={event} className="mb-3" />;
|
||||
} else {
|
||||
return <TextNote key={event.id} event={event} className="mb-3" />;
|
||||
}
|
||||
return <TextNote key={event.id} event={event} className="mb-3" />;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -81,12 +70,10 @@ export function Screen() {
|
||||
<ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
|
||||
<Virtualizer scrollRef={ref}>
|
||||
{isFetching && !isLoading && !isFetchingNextPage ? (
|
||||
<div className="flex items-center justify-center w-full mb-3 h-12 bg-black/5 dark:bg-white/5 rounded-xl">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Spinner className="size-5" />
|
||||
<span className="text-sm font-medium">
|
||||
Getting new notes...
|
||||
</span>
|
||||
<div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3">
|
||||
<div className="w-max h-8 pl-2 pr-3 inline-flex items-center justify-center gap-1.5 rounded-full shadow-lg text-sm font-medium text-white bg-black dark:text-black dark:bg-white">
|
||||
<Spinner className="size-4" />
|
||||
Getting new notes...
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -96,13 +83,13 @@ export function Screen() {
|
||||
<span className="text-sm font-medium">Loading...</span>
|
||||
</div>
|
||||
) : !data.length ? (
|
||||
<div className="flex items-center justify-center">
|
||||
Yo. You're catching up on all the things happening around you.
|
||||
<div className="mb-3 flex items-center justify-center h-20 text-sm rounded-xl bg-black/5 dark:bg-white/5">
|
||||
🎉 Yo. You're catching up on all latest notes.
|
||||
</div>
|
||||
) : (
|
||||
data.map((item) => renderItem(item))
|
||||
)}
|
||||
{data?.length && hasNextPage ? (
|
||||
{hasNextPage ? (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { decodeZapInvoice, formatCreatedAt } from "@/commons";
|
||||
import { Spinner } from "@/components";
|
||||
import { Note } from "@/components/note";
|
||||
import { User } from "@/components/user";
|
||||
import { type LumeEvent, NostrQuery, useEvent } from "@/system";
|
||||
import { Kind } from "@/types";
|
||||
import { Note, Spinner, User } from "@/components";
|
||||
import { LumeEvent, useEvent } from "@/system";
|
||||
import { Kind, type NostrEvent } from "@/types";
|
||||
import { Info, Repeat } from "@phosphor-icons/react";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import * as Tabs from "@radix-ui/react-tabs";
|
||||
@@ -24,7 +23,15 @@ function Screen() {
|
||||
const { isLoading, data } = useQuery({
|
||||
queryKey: ["notification", account],
|
||||
queryFn: async () => {
|
||||
const events = await NostrQuery.getNotifications();
|
||||
const res = await commands.getNotifications();
|
||||
|
||||
if (res.status === "error") {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const data: NostrEvent[] = res.data.map((item) => JSON.parse(item));
|
||||
const events = data.map((ev) => new LumeEvent(ev));
|
||||
|
||||
return events;
|
||||
},
|
||||
select: (events) => {
|
||||
|
||||
@@ -1,72 +1,62 @@
|
||||
import { insertImage, isImagePath } from "@/commons";
|
||||
import { insertImage, isImagePath, upload } from "@/commons";
|
||||
import { Spinner } from "@/components";
|
||||
import { NostrQuery } from "@/system";
|
||||
import { Images } from "@phosphor-icons/react";
|
||||
import type { UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useTransition } from "react";
|
||||
import { useSlateStatic } from "slate-react";
|
||||
|
||||
export function MediaButton() {
|
||||
const editor = useSlateStatic();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const upload = async () => {
|
||||
try {
|
||||
// start loading
|
||||
setLoading(true);
|
||||
|
||||
const image = await NostrQuery.upload();
|
||||
insertImage(editor, image);
|
||||
|
||||
// reset loading
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
await message(String(e), { title: "Upload", kind: "error" });
|
||||
}
|
||||
const uploadMedia = () => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const image = await upload();
|
||||
return insertImage(editor, image);
|
||||
} catch (e) {
|
||||
await message(String(e), { title: "Upload", kind: "error" });
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let unlisten: UnlistenFn = undefined;
|
||||
const unlisten = getCurrentWindow().listen("tauri://file-drop", (event) => {
|
||||
startTransition(async () => {
|
||||
// @ts-ignore, lfg !!!
|
||||
const items: string[] = event.payload.paths;
|
||||
|
||||
async function listenFileDrop() {
|
||||
const window = getCurrentWindow();
|
||||
if (!unlisten) {
|
||||
unlisten = await window.listen("tauri://file-drop", async (event) => {
|
||||
// @ts-ignore, lfg !!!
|
||||
const items: string[] = event.payload.paths;
|
||||
// start loading
|
||||
setLoading(true);
|
||||
// upload all images
|
||||
for (const item of items) {
|
||||
if (isImagePath(item)) {
|
||||
const image = await NostrQuery.upload(item);
|
||||
insertImage(editor, image);
|
||||
}
|
||||
// upload all images
|
||||
for (const item of items) {
|
||||
if (isImagePath(item)) {
|
||||
const image = await upload(item);
|
||||
insertImage(editor, image);
|
||||
}
|
||||
// stop loading
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
listenFileDrop();
|
||||
return;
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (unlisten) unlisten();
|
||||
unlisten.then((f) => f());
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => upload()}
|
||||
disabled={loading}
|
||||
onClick={() => uploadMedia()}
|
||||
disabled={isPending}
|
||||
className="inline-flex items-center h-8 gap-2 px-2.5 text-sm rounded-lg text-black/70 dark:text-white/70 w-max hover:bg-black/10 dark:hover:bg-white/10"
|
||||
>
|
||||
{loading ? <Spinner className="size-4" /> : <Images className="size-4" />}
|
||||
{isPending ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<Images className="size-4" />
|
||||
)}
|
||||
Add media
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { checkForAppUpdates } from "@/commons";
|
||||
import { NostrAccount } from "@/system";
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
@@ -8,7 +8,7 @@ export const Route = createFileRoute("/")({
|
||||
await checkForAppUpdates(true);
|
||||
|
||||
// Get all accounts
|
||||
const accounts = await NostrAccount.getAccounts();
|
||||
const accounts = await commands.getAccounts();
|
||||
|
||||
if (accounts.length < 1) {
|
||||
throw redirect({
|
||||
|
||||
@@ -2,7 +2,7 @@ import { User } from "@/components/user";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useState } from "react";
|
||||
import { useState, useTransition } from "react";
|
||||
import CurrencyInput from "react-currency-input-field";
|
||||
|
||||
const DEFAULT_VALUES = [21, 50, 100, 200];
|
||||
@@ -17,28 +17,26 @@ function Screen() {
|
||||
const [amount, setAmount] = useState(21);
|
||||
const [content, setContent] = useState("");
|
||||
const [isCompleted, setIsCompleted] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
// start loading
|
||||
setIsLoading(true);
|
||||
const submit = () => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const val = await event.zap(amount, content);
|
||||
|
||||
// Zap
|
||||
const val = await event.zap(amount, content);
|
||||
|
||||
if (val) {
|
||||
setIsCompleted(true);
|
||||
// close current window
|
||||
await getCurrentWebviewWindow().close();
|
||||
if (val) {
|
||||
setIsCompleted(true);
|
||||
// close current window
|
||||
await getCurrentWebviewWindow().close();
|
||||
}
|
||||
} catch (e) {
|
||||
await message(String(e), {
|
||||
title: "Zap",
|
||||
kind: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
await message(String(e), {
|
||||
title: "Zap",
|
||||
kind: "error",
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -104,7 +102,7 @@ function Screen() {
|
||||
onClick={() => submit()}
|
||||
className="inline-flex items-center justify-center w-full h-10 font-medium rounded-xl bg-neutral-950 text-neutral-50 hover:bg-neutral-900 dark:bg-white/20 dark:hover:bg-white/30"
|
||||
>
|
||||
{isCompleted ? "Zapped" : isLoading ? "Processing..." : "Zap"}
|
||||
{isCompleted ? "Zapped" : isPending ? "Processing..." : "Zap"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
import { NostrQuery } from "@/system";
|
||||
import { commands } from "@/commands.gen";
|
||||
import { LumeEvent } from "@/system";
|
||||
import type { NostrEvent } from "@/types";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/zap/$id")({
|
||||
beforeLoad: async ({ params }) => {
|
||||
const event = await NostrQuery.getEvent(params.id);
|
||||
return { event };
|
||||
const res = await commands.getEvent(params.id);
|
||||
|
||||
if (res.status === "ok") {
|
||||
const data = res.data;
|
||||
const raw: NostrEvent = JSON.parse(data.raw);
|
||||
|
||||
if (data.parsed) {
|
||||
raw.meta = data.parsed;
|
||||
}
|
||||
|
||||
return { event: new LumeEvent(raw) };
|
||||
} else {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
import { type Result, commands } from "@/commands.gen";
|
||||
import type { Metadata } from "@/types";
|
||||
|
||||
export const NostrAccount = {
|
||||
getAccounts: async () => {
|
||||
const query = await commands.getAccounts();
|
||||
return query;
|
||||
},
|
||||
loadAccount: async (npub: string) => {
|
||||
const bunker: string = localStorage.getItem(`${npub}_bunker`);
|
||||
let query: Result<boolean, string>;
|
||||
|
||||
if (bunker?.length && bunker?.startsWith("bunker://")) {
|
||||
query = await commands.loadAccount(npub, bunker);
|
||||
} else {
|
||||
query = await commands.loadAccount(npub, null);
|
||||
}
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
},
|
||||
createAccount: async () => {
|
||||
const query = await commands.createAccount();
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
},
|
||||
createProfile: async (profile: Metadata) => {
|
||||
const query = await commands.createProfile(
|
||||
profile.name || "",
|
||||
profile.display_name || "",
|
||||
profile.about || "",
|
||||
profile.picture || "",
|
||||
profile.banner || "",
|
||||
profile.nip05 || "",
|
||||
profile.lud16 || "",
|
||||
profile.website || "",
|
||||
);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
},
|
||||
saveAccount: async (nsec: string, password = "") => {
|
||||
const query = await commands.saveAccount(nsec, password);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
},
|
||||
connectRemoteAccount: async (uri: string) => {
|
||||
const connect = await commands.connectRemoteAccount(uri);
|
||||
|
||||
if (connect.status === "ok") {
|
||||
const npub = connect.data;
|
||||
const parsed = new URL(uri);
|
||||
parsed.searchParams.delete("secret");
|
||||
|
||||
// save connection string
|
||||
localStorage.setItem(`${npub}_bunker`, parsed.toString());
|
||||
|
||||
return npub;
|
||||
} else {
|
||||
throw new Error(connect.error);
|
||||
}
|
||||
},
|
||||
setContactList: async (pubkeys: string[]) => {
|
||||
const query = await commands.setContactList(pubkeys);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
},
|
||||
loadWallet: async () => {
|
||||
const query = await commands.loadWallet();
|
||||
|
||||
if (query.status === "ok") {
|
||||
return Number.parseInt(query.data);
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
},
|
||||
setWallet: async (uri: string) => {
|
||||
const query = await commands.setWallet(uri);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
},
|
||||
removeWallet: async () => {
|
||||
const query = await commands.removeWallet();
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
},
|
||||
getProfile: async () => {
|
||||
const query = await commands.getCurrentProfile();
|
||||
|
||||
if (query.status === "ok") {
|
||||
return JSON.parse(query.data) as Metadata;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
getContactList: async () => {
|
||||
const query = await commands.getContactList();
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
isContactListEmpty: async () => {
|
||||
const query = await commands.isContactListEmpty();
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
checkContact: async (pubkey: string) => {
|
||||
const query = await commands.checkContact(pubkey);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
},
|
||||
toggleContact: async (pubkey: string, alias?: string) => {
|
||||
const query = await commands.toggleContact(pubkey, alias);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
},
|
||||
f2f: async (npub: string) => {
|
||||
const query = await commands.copyFriend(npub);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -16,7 +16,7 @@ export function useEvent(id: string) {
|
||||
|
||||
// Define query
|
||||
let query: Result<RichEvent, string>;
|
||||
let relayHint: string;
|
||||
let relayHint: string = null;
|
||||
|
||||
if (normalizeId.startsWith("nevent1")) {
|
||||
const decoded = nip19.decode(normalizeId);
|
||||
@@ -27,14 +27,11 @@ export function useEvent(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(relayHint);
|
||||
|
||||
// Build query
|
||||
if (relayHint) {
|
||||
try {
|
||||
const url = new URL(relayHint);
|
||||
query = await commands.getEventFrom(normalizeId, url.toString());
|
||||
} catch {
|
||||
query = await commands.getEvent(normalizeId);
|
||||
}
|
||||
if (relayHint?.length) {
|
||||
query = await commands.getEventFrom(normalizeId, relayHint);
|
||||
} else {
|
||||
query = await commands.getEvent(normalizeId);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
export * from "./event";
|
||||
export * from "./account";
|
||||
export * from "./query";
|
||||
export * from "./window";
|
||||
export * from "./hooks/useEvent";
|
||||
export * from "./hooks/useProfile";
|
||||
|
||||
@@ -1,336 +0,0 @@
|
||||
import { type Result, type RichEvent, commands } from "@/commands.gen";
|
||||
import type { LumeColumn, Metadata, NostrEvent, Relay } from "@/types";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { readFile } from "@tauri-apps/plugin-fs";
|
||||
import { relaunch } from "@tauri-apps/plugin-process";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { LumeEvent } from "./event";
|
||||
|
||||
function toLumeEvents(richEvents: RichEvent[]) {
|
||||
const events = richEvents.map((item) => {
|
||||
const nostrEvent: NostrEvent = JSON.parse(item.raw);
|
||||
|
||||
if (item.parsed) {
|
||||
nostrEvent.meta = item.parsed;
|
||||
} else {
|
||||
nostrEvent.meta = null;
|
||||
}
|
||||
|
||||
const lumeEvent = new LumeEvent(nostrEvent);
|
||||
|
||||
return lumeEvent;
|
||||
});
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
export const NostrQuery = {
|
||||
upload: async (filePath?: string) => {
|
||||
const allowExts = [
|
||||
"png",
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"gif",
|
||||
"mp4",
|
||||
"mp3",
|
||||
"webm",
|
||||
"mkv",
|
||||
"avi",
|
||||
"mov",
|
||||
];
|
||||
|
||||
const selected =
|
||||
filePath ||
|
||||
(
|
||||
await open({
|
||||
multiple: false,
|
||||
filters: [
|
||||
{
|
||||
name: "Media",
|
||||
extensions: allowExts,
|
||||
},
|
||||
],
|
||||
})
|
||||
).path;
|
||||
|
||||
// User cancelled action
|
||||
if (!selected) return null;
|
||||
|
||||
try {
|
||||
const file = await readFile(selected);
|
||||
const blob = new Blob([file]);
|
||||
|
||||
const data = new FormData();
|
||||
data.append("fileToUpload", blob);
|
||||
data.append("submit", "Upload Image");
|
||||
|
||||
const res = await fetch("https://nostr.build/api/v2/upload/files", {
|
||||
method: "POST",
|
||||
body: data,
|
||||
});
|
||||
|
||||
if (!res.ok) return null;
|
||||
|
||||
const json = await res.json();
|
||||
const content = json.data[0];
|
||||
|
||||
return content.url as string;
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
},
|
||||
getNotifications: async () => {
|
||||
const query = await commands.getNotifications();
|
||||
|
||||
if (query.status === "ok") {
|
||||
const data = query.data.map((item) => JSON.parse(item) as NostrEvent);
|
||||
const events = data.map((ev) => new LumeEvent(ev));
|
||||
|
||||
return events;
|
||||
} else {
|
||||
console.error(query.error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
getProfile: async (pubkey: string) => {
|
||||
const normalize = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, "");
|
||||
const query = await commands.getProfile(normalize);
|
||||
|
||||
if (query.status === "ok") {
|
||||
const profile: Metadata = JSON.parse(query.data);
|
||||
return profile;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
getEvent: async (id: string, hint?: string) => {
|
||||
// Validate ID
|
||||
const normalizeId: string = id
|
||||
.replace("nostr:", "")
|
||||
.replace(/[^\w\s]/gi, "");
|
||||
|
||||
// Define query
|
||||
let query: Result<RichEvent, string>;
|
||||
let relayHint: string = hint;
|
||||
|
||||
if (normalizeId.startsWith("nevent1")) {
|
||||
const decoded = nip19.decode(normalizeId);
|
||||
if (decoded.type === "nevent") relayHint = decoded.data.relays[0];
|
||||
}
|
||||
|
||||
// Build query
|
||||
if (relayHint) {
|
||||
try {
|
||||
const url = new URL(relayHint);
|
||||
query = await commands.getEventFrom(normalizeId, url.toString());
|
||||
} catch {
|
||||
query = await commands.getEvent(normalizeId);
|
||||
}
|
||||
} else {
|
||||
query = await commands.getEvent(normalizeId);
|
||||
}
|
||||
|
||||
if (query.status === "ok") {
|
||||
const data = query.data;
|
||||
const raw = JSON.parse(data.raw) as NostrEvent;
|
||||
|
||||
if (data?.parsed) {
|
||||
raw.meta = data.parsed;
|
||||
}
|
||||
|
||||
const event = new LumeEvent(raw);
|
||||
|
||||
return event;
|
||||
} else {
|
||||
console.log("[getEvent]: ", query.error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
getRepostEvent: async (event: LumeEvent) => {
|
||||
try {
|
||||
const embed: NostrEvent = JSON.parse(event.content);
|
||||
const query = await commands.getEventMeta(embed.content);
|
||||
|
||||
if (query.status === "ok") {
|
||||
embed.meta = query.data;
|
||||
const lumeEvent = new LumeEvent(embed);
|
||||
return lumeEvent;
|
||||
}
|
||||
} catch {
|
||||
const query = await commands.getEvent(event.repostId);
|
||||
|
||||
if (query.status === "ok") {
|
||||
const data = query.data;
|
||||
const raw = JSON.parse(data.raw) as NostrEvent;
|
||||
|
||||
if (data?.parsed) {
|
||||
raw.meta = data.parsed;
|
||||
}
|
||||
|
||||
const event = new LumeEvent(raw);
|
||||
|
||||
return event;
|
||||
} else {
|
||||
console.log("[getRepostEvent]: ", query.error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
getGroupEvents: async (pubkeys: string[], asOf?: number) => {
|
||||
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
||||
const query = await commands.getGroupEvents(pubkeys, until);
|
||||
|
||||
if (query.status === "ok") {
|
||||
const data = toLumeEvents(query.data);
|
||||
return data;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
getGlobalEvents: async (asOf?: number) => {
|
||||
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
||||
const query = await commands.getGlobalEvents(until);
|
||||
|
||||
if (query.status === "ok") {
|
||||
const data = toLumeEvents(query.data);
|
||||
return data;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
getHashtagEvents: async (hashtags: string[], asOf?: number) => {
|
||||
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
||||
const nostrTags = hashtags.map((tag) => tag.replace("#", ""));
|
||||
const query = await commands.getHashtagEvents(nostrTags, until);
|
||||
|
||||
if (query.status === "ok") {
|
||||
const data = toLumeEvents(query.data);
|
||||
return data;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
verifyNip05: async (pubkey: string, nip05?: string) => {
|
||||
const query = await commands.verifyNip05(pubkey, nip05);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
getNstore: async (key: string) => {
|
||||
const query = await commands.getLumeStore(key);
|
||||
|
||||
if (query.status === "ok") {
|
||||
const data = query.data ? JSON.parse(query.data) : null;
|
||||
return data;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
setNstore: async (key: string, value: string) => {
|
||||
const query = await commands.setLumeStore(key, value);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
},
|
||||
getUserSettings: async () => {
|
||||
const query = await commands.getSettings();
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
return query.error;
|
||||
}
|
||||
},
|
||||
setUserSettings: async (newSettings: string) => {
|
||||
const query = await commands.setNewSettings(newSettings);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
return query.error;
|
||||
}
|
||||
},
|
||||
setColumns: async (columns: LumeColumn[]) => {
|
||||
const key = "lume_v4:columns";
|
||||
const content = JSON.stringify(columns);
|
||||
const query = await commands.setLumeStore(key, content);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
},
|
||||
getRelays: async () => {
|
||||
const query = await commands.getRelays();
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
},
|
||||
connectRelay: async (url: string) => {
|
||||
const relayUrl = new URL(url);
|
||||
|
||||
if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") {
|
||||
const query = await commands.connectRelay(relayUrl.toString());
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
},
|
||||
removeRelay: async (url: string) => {
|
||||
const relayUrl = new URL(url);
|
||||
|
||||
if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") {
|
||||
const query = await commands.removeRelay(relayUrl.toString());
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
},
|
||||
getBootstrapRelays: async () => {
|
||||
const query = await commands.getBootstrapRelays();
|
||||
|
||||
if (query.status === "ok") {
|
||||
const relays: Relay[] = [];
|
||||
|
||||
for (const item of query.data) {
|
||||
const line = item.split(",");
|
||||
const url = line[0];
|
||||
const purpose = line[1] ?? "";
|
||||
|
||||
relays.push({ url, purpose });
|
||||
}
|
||||
|
||||
return relays;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
saveBootstrapRelays: async (relays: string[]) => {
|
||||
const text = relays
|
||||
.map((relay) => Object.values(relay).join(","))
|
||||
.join("\n");
|
||||
const query = await commands.saveBootstrapRelays(text);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return await relaunch();
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -114,7 +114,7 @@ export const LumeWindow = {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
},
|
||||
openZap: async (id: string) => {
|
||||
openZap: async (id: string, account?: string) => {
|
||||
const wallet = await commands.loadWallet();
|
||||
|
||||
if (wallet.status === "ok") {
|
||||
@@ -129,7 +129,7 @@ export const LumeWindow = {
|
||||
hidden_title: true,
|
||||
});
|
||||
} else {
|
||||
await LumeWindow.openSettings("bitcoin-connect");
|
||||
await LumeWindow.openSettings(account, "bitcoin-connect");
|
||||
}
|
||||
},
|
||||
openSettings: async (account: string, path?: string) => {
|
||||
|
||||
Reference in New Issue
Block a user