diff --git a/apps/desktop2/src/routes/foryou.tsx b/apps/desktop2/src/routes/foryou.tsx index 36798713..593c5104 100644 --- a/apps/desktop2/src/routes/foryou.tsx +++ b/apps/desktop2/src/routes/foryou.tsx @@ -1,3 +1,5 @@ +import { Conversation } from "@/components/conversation"; +import { Quote } from "@/components/quote"; import { RepostNote } from "@/components/repost"; import { TextNote } from "@/components/text"; import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons"; @@ -8,148 +10,159 @@ import { Link, createFileRoute, redirect } from "@tanstack/react-router"; import { Virtualizer } from "virtua"; export const Route = createFileRoute("/foryou")({ - validateSearch: (search: Record): ColumnRouteSearch => { - return { - account: search.account, - label: search.label, - name: search.name, - }; - }, - beforeLoad: async ({ search, context }) => { - const ark = context.ark; - const interests = await ark.get_interest(); - const settings = await ark.get_settings(); + validateSearch: (search: Record): ColumnRouteSearch => { + return { + account: search.account, + label: search.label, + name: search.name, + }; + }, + beforeLoad: async ({ search, context }) => { + const ark = context.ark; + const interests = await ark.get_interest(); + const settings = await ark.get_settings(); - if (!interests) { - throw redirect({ - to: "/interests", - search: { - ...search, - redirect: "/foryou", - }, - }); - } + if (!interests) { + throw redirect({ + to: "/interests", + search: { + ...search, + redirect: "/foryou", + }, + }); + } - return { - interests, - settings, - }; - }, - component: Screen, + return { + interests, + settings, + }; + }, + component: Screen, }); export function Screen() { - const { name, account } = Route.useSearch(); - const { ark, interests } = Route.useRouteContext(); - const { - data, - isLoading, - isFetching, - isFetchingNextPage, - hasNextPage, - fetchNextPage, - } = useInfiniteQuery({ - queryKey: [name, account], - initialPageParam: 0, - queryFn: async ({ pageParam }: { pageParam: number }) => { - const events = await ark.get_events_from_interests( - interests.hashtags, - 20, - pageParam, - ); - return events; - }, - getNextPageParam: (lastPage) => { - const lastEvent = lastPage?.at(-1); - return lastEvent ? lastEvent.created_at - 1 : null; - }, - select: (data) => data?.pages.flatMap((page) => page), - refetchOnWindowFocus: false, - }); + const { label, account } = Route.useSearch(); + const { ark, interests } = Route.useRouteContext(); + const { + data, + isLoading, + isFetching, + isFetchingNextPage, + hasNextPage, + fetchNextPage, + } = useInfiniteQuery({ + queryKey: [label, account], + initialPageParam: 0, + queryFn: async ({ pageParam }: { pageParam: number }) => { + const events = await ark.get_hashtag_events( + interests.hashtags, + 20, + pageParam, + ); + return events; + }, + getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1, + select: (data) => data?.pages.flatMap((page) => page), + refetchOnWindowFocus: false, + }); - const renderItem = (event: Event) => { - if (!event) return; - switch (event.kind) { - case Kind.Repost: - return ; - default: - return ; - } - }; + const renderItem = (event: Event) => { + if (!event) return; + switch (event.kind) { + case Kind.Repost: + return ; + default: { + const isConversation = + event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention") + .length > 0; + const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0; - return ( -
- {isFetching && !isLoading && !isFetchingNextPage ? ( -
-
- - Fetching new notes... -
-
- ) : null} - {isLoading ? ( -
- - Loading... -
- ) : !data.length ? ( - - ) : ( - - {data.map((item) => renderItem(item))} - - )} - {data?.length && hasNextPage ? ( -
- -
- ) : null} -
- ); + if (isConversation) { + return ; + } + + if (isQuote) { + return ; + } + + return ; + } + } + }; + + return ( +
+ {isFetching && !isLoading && !isFetchingNextPage ? ( +
+
+ + Fetching new notes... +
+
+ ) : null} + {isLoading ? ( +
+ + Loading... +
+ ) : !data.length ? ( + + ) : ( + + {data.map((item) => renderItem(item))} + + )} + {data?.length && hasNextPage ? ( +
+ +
+ ) : null} +
+ ); } function Empty() { - return ( -
-
-
-
-
-

Your newsfeed is empty

-

- Here are few suggestions to get started. -

-
-
- - - Show trending notes - - - - Discover trending users - -
-
- ); + return ( +
+
+
+
+
+

Your newsfeed is empty

+

+ Here are few suggestions to get started. +

+
+
+ + + Show trending notes + + + + Discover trending users + +
+
+ ); } diff --git a/apps/desktop2/src/routes/global.tsx b/apps/desktop2/src/routes/global.tsx index e67525f7..6dbaa22a 100644 --- a/apps/desktop2/src/routes/global.tsx +++ b/apps/desktop2/src/routes/global.tsx @@ -10,144 +10,141 @@ import { Link, createFileRoute } from "@tanstack/react-router"; import { Virtualizer } from "virtua"; export const Route = createFileRoute("/global")({ - validateSearch: (search: Record): ColumnRouteSearch => { - return { - account: search.account, - label: search.label, - name: search.name, - }; - }, - beforeLoad: async ({ context }) => { - const ark = context.ark; - const settings = await ark.get_settings(); + validateSearch: (search: Record): ColumnRouteSearch => { + return { + account: search.account, + label: search.label, + name: search.name, + }; + }, + beforeLoad: async ({ context }) => { + const ark = context.ark; + const settings = await ark.get_settings(); - return { settings }; - }, - component: Screen, + return { settings }; + }, + component: Screen, }); export function Screen() { - const { account } = Route.useSearch(); - const { ark } = Route.useRouteContext(); - const { - data, - isLoading, - isFetching, - isFetchingNextPage, - hasNextPage, - fetchNextPage, - } = useInfiniteQuery({ - queryKey: ["global", account], - initialPageParam: 0, - queryFn: async ({ pageParam }: { pageParam: number }) => { - const events = await ark.get_events(20, pageParam, undefined, true); - return events; - }, - getNextPageParam: (lastPage) => { - const lastEvent = lastPage?.at(-1); - return lastEvent ? lastEvent.created_at - 1 : null; - }, - select: (data) => data?.pages.flatMap((page) => page), - refetchOnWindowFocus: false, - }); + const { label, account } = Route.useSearch(); + const { ark } = Route.useRouteContext(); + const { + data, + isLoading, + isFetching, + isFetchingNextPage, + hasNextPage, + fetchNextPage, + } = useInfiniteQuery({ + queryKey: [label, account], + initialPageParam: 0, + queryFn: async ({ pageParam }: { pageParam: number }) => { + const events = await ark.get_global_events(20, pageParam); + return events; + }, + getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1, + select: (data) => data?.pages.flatMap((page) => page), + refetchOnWindowFocus: false, + }); - const renderItem = (event: Event) => { - if (!event) return; - switch (event.kind) { - case Kind.Repost: - return ; - default: { - const isConversation = - event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention") - .length > 0; - const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0; + const renderItem = (event: Event) => { + if (!event) return; + switch (event.kind) { + case Kind.Repost: + return ; + default: { + const isConversation = + event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention") + .length > 0; + const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0; - if (isConversation) { - return ; - } + if (isConversation) { + return ; + } - if (isQuote) { - return ; - } + if (isQuote) { + return ; + } - return ; - } - } - }; + return ; + } + } + }; - return ( -
- {isFetching && !isLoading && !isFetchingNextPage ? ( -
-
- - Fetching new notes... -
-
- ) : null} - {isLoading ? ( -
- - Loading... -
- ) : !data.length ? ( - - ) : ( - - {data.map((item) => renderItem(item))} - - )} - {data?.length && hasNextPage ? ( -
- -
- ) : null} -
- ); + return ( +
+ {isFetching && !isLoading && !isFetchingNextPage ? ( +
+
+ + Fetching new notes... +
+
+ ) : null} + {isLoading ? ( +
+ + Loading... +
+ ) : !data.length ? ( + + ) : ( + + {data.map((item) => renderItem(item))} + + )} + {data?.length && hasNextPage ? ( +
+ +
+ ) : null} +
+ ); } function Empty() { - return ( -
-
-
-
-
-

Your newsfeed is empty

-

- Here are few suggestions to get started. -

-
-
- - - Show trending notes - - - - Discover trending users - -
-
- ); + return ( +
+
+
+
+
+

Your newsfeed is empty

+

+ Here are few suggestions to get started. +

+
+
+ + + Show trending notes + + + + Discover trending users + +
+
+ ); } diff --git a/apps/desktop2/src/routes/group.tsx b/apps/desktop2/src/routes/group.tsx index fbfcd189..ad71798e 100644 --- a/apps/desktop2/src/routes/group.tsx +++ b/apps/desktop2/src/routes/group.tsx @@ -1,3 +1,5 @@ +import { Conversation } from "@/components/conversation"; +import { Quote } from "@/components/quote"; import { RepostNote } from "@/components/repost"; import { TextNote } from "@/components/text"; import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons"; @@ -8,147 +10,158 @@ import { Link, createFileRoute, redirect } from "@tanstack/react-router"; import { Virtualizer } from "virtua"; export const Route = createFileRoute("/group")({ - validateSearch: (search: Record): ColumnRouteSearch => { - return { - account: search.account, - label: search.label, - name: search.name, - }; - }, - beforeLoad: async ({ search, context }) => { - const ark = context.ark; - const groups = (await ark.get_nstore( - `lume_group_${search.label}`, - )) as string[]; - const settings = await ark.get_settings(); + validateSearch: (search: Record): ColumnRouteSearch => { + return { + account: search.account, + label: search.label, + name: search.name, + }; + }, + beforeLoad: async ({ search, context }) => { + const ark = context.ark; + const groups = (await ark.get_nstore( + `lume_group_${search.label}`, + )) as string[]; + const settings = await ark.get_settings(); - if (!groups) { - throw redirect({ - to: "/create-group", - search: { - ...search, - redirect: "/group", - }, - }); - } + if (!groups) { + throw redirect({ + to: "/create-group", + search: { + ...search, + redirect: "/group", + }, + }); + } - return { - groups, - settings, - }; - }, - component: Screen, + return { + groups, + settings, + }; + }, + component: Screen, }); export function Screen() { - const { name, account } = Route.useSearch(); - const { ark, groups } = Route.useRouteContext(); - const { - data, - isLoading, - isFetching, - isFetchingNextPage, - hasNextPage, - fetchNextPage, - } = useInfiniteQuery({ - queryKey: [name, account], - initialPageParam: 0, - queryFn: async ({ pageParam }: { pageParam: number }) => { - const events = await ark.get_events(20, pageParam, groups); - return events; - }, - getNextPageParam: (lastPage) => { - const lastEvent = lastPage?.at(-1); - return lastEvent ? lastEvent.created_at - 1 : null; - }, - select: (data) => - data?.pages.flatMap((page) => page.filter((ev) => ev.kind === Kind.Text)), - refetchOnWindowFocus: false, - }); + const { label, account } = Route.useSearch(); + const { ark, groups } = Route.useRouteContext(); + const { + data, + isLoading, + isFetching, + isFetchingNextPage, + hasNextPage, + fetchNextPage, + } = useInfiniteQuery({ + queryKey: [label, account], + initialPageParam: 0, + queryFn: async ({ pageParam }: { pageParam: number }) => { + const events = await ark.get_group_events(groups, 20, pageParam); + return events; + }, + getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1, + select: (data) => + data?.pages.flatMap((page) => page.filter((ev) => ev.kind === Kind.Text)), + refetchOnWindowFocus: false, + }); - const renderItem = (event: Event) => { - if (!event) return; - switch (event.kind) { - case Kind.Repost: - return ; - default: - return ; - } - }; + const renderItem = (event: Event) => { + if (!event) return; + switch (event.kind) { + case Kind.Repost: + return ; + default: { + const isConversation = + event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention") + .length > 0; + const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0; - return ( -
- {isFetching && !isLoading && !isFetchingNextPage ? ( -
-
- - Fetching new notes... -
-
- ) : null} - {isLoading ? ( -
- - Loading... -
- ) : !data.length ? ( - - ) : ( - - {data.map((item) => renderItem(item))} - - )} - {data?.length && hasNextPage ? ( -
- -
- ) : null} -
- ); + if (isConversation) { + return ; + } + + if (isQuote) { + return ; + } + + return ; + } + } + }; + + return ( +
+ {isFetching && !isLoading && !isFetchingNextPage ? ( +
+
+ + Fetching new notes... +
+
+ ) : null} + {isLoading ? ( +
+ + Loading... +
+ ) : !data.length ? ( + + ) : ( + + {data.map((item) => renderItem(item))} + + )} + {data?.length && hasNextPage ? ( +
+ +
+ ) : null} +
+ ); } function Empty() { - return ( -
-
-
-
-
-

Your newsfeed is empty

-

- Here are few suggestions to get started. -

-
-
- - - Show trending notes - - - - Discover trending users - -
-
- ); + return ( +
+
+
+
+
+

Your newsfeed is empty

+

+ Here are few suggestions to get started. +

+
+
+ + + Show trending notes + + + + Discover trending users + +
+
+ ); } diff --git a/apps/desktop2/src/routes/newsfeed.tsx b/apps/desktop2/src/routes/newsfeed.tsx index e58b8dfc..c056ec9d 100644 --- a/apps/desktop2/src/routes/newsfeed.tsx +++ b/apps/desktop2/src/routes/newsfeed.tsx @@ -11,144 +11,141 @@ import { createFileRoute } from "@tanstack/react-router"; import { Virtualizer } from "virtua"; export const Route = createFileRoute("/newsfeed")({ - validateSearch: (search: Record): ColumnRouteSearch => { - return { - account: search.account, - label: search.label, - name: search.name, - }; - }, - beforeLoad: async ({ context }) => { - const ark = context.ark; - const settings = await ark.get_settings(); + validateSearch: (search: Record): ColumnRouteSearch => { + return { + account: search.account, + label: search.label, + name: search.name, + }; + }, + beforeLoad: async ({ context }) => { + const ark = context.ark; + const settings = await ark.get_settings(); - return { settings }; - }, - component: Screen, + return { settings }; + }, + component: Screen, }); export function Screen() { - const { label, account } = Route.useSearch(); - const { ark } = Route.useRouteContext(); - const { - data, - isLoading, - isFetching, - isFetchingNextPage, - hasNextPage, - fetchNextPage, - } = useInfiniteQuery({ - queryKey: [label, account], - initialPageParam: 0, - queryFn: async ({ pageParam }: { pageParam: number }) => { - const events = await ark.get_events(20, pageParam); - return events; - }, - getNextPageParam: (lastPage) => { - const lastEvent = lastPage?.at(-1); - return lastEvent ? lastEvent.created_at - 1 : null; - }, - select: (data) => data?.pages.flatMap((page) => page), - refetchOnWindowFocus: false, - }); + const { label, account } = Route.useSearch(); + const { ark } = Route.useRouteContext(); + const { + data, + isLoading, + isFetching, + isFetchingNextPage, + hasNextPage, + fetchNextPage, + } = useInfiniteQuery({ + queryKey: [label, account], + initialPageParam: 0, + queryFn: async ({ pageParam }: { pageParam: number }) => { + const events = await ark.get_local_events(20, pageParam); + return events; + }, + getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1, + select: (data) => data?.pages.flatMap((page) => page), + refetchOnWindowFocus: false, + }); - const renderItem = (event: Event) => { - if (!event) return; - switch (event.kind) { - case Kind.Repost: - return ; - default: { - const isConversation = - event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention") - .length > 0; - const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0; + const renderItem = (event: Event) => { + if (!event) return; + switch (event.kind) { + case Kind.Repost: + return ; + default: { + const isConversation = + event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention") + .length > 0; + const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0; - if (isConversation) { - return ; - } + if (isConversation) { + return ; + } - if (isQuote) { - return ; - } + if (isQuote) { + return ; + } - return ; - } - } - }; + return ; + } + } + }; - return ( -
- {isFetching && !isLoading && !isFetchingNextPage ? ( -
-
- - Fetching new notes... -
-
- ) : null} - {isLoading ? ( -
- - Loading... -
- ) : !data.length ? ( - - ) : ( - - {data.map((item) => renderItem(item))} - - )} - {data?.length && hasNextPage ? ( -
- -
- ) : null} -
- ); + return ( +
+ {isFetching && !isLoading && !isFetchingNextPage ? ( +
+
+ + Fetching new notes... +
+
+ ) : null} + {isLoading ? ( +
+ + Loading... +
+ ) : !data.length ? ( + + ) : ( + + {data.map((item) => renderItem(item))} + + )} + {data?.length && hasNextPage ? ( +
+ +
+ ) : null} +
+ ); } function Empty() { - return ( -
-
-
-
-
-

Your newsfeed is empty

-

- Here are few suggestions to get started. -

-
-
- - - Show trending notes - - - - Discover trending users - -
-
- ); + return ( +
+
+
+
+
+

Your newsfeed is empty

+

+ Here are few suggestions to get started. +

+
+
+ + + Show trending notes + + + + Discover trending users + +
+
+ ); } diff --git a/apps/desktop2/src/routes/users/$pubkey.tsx b/apps/desktop2/src/routes/users/$pubkey.tsx index ab666107..0d9ec5fc 100644 --- a/apps/desktop2/src/routes/users/$pubkey.tsx +++ b/apps/desktop2/src/routes/users/$pubkey.tsx @@ -11,86 +11,86 @@ import { Suspense } from "react"; import { Await } from "@tanstack/react-router"; export const Route = createFileRoute("/users/$pubkey")({ - beforeLoad: async ({ context }) => { - const ark = context.ark; - const settings = await ark.get_settings(); + beforeLoad: async ({ context }) => { + const ark = context.ark; + const settings = await ark.get_settings(); - return { settings }; - }, - loader: async ({ params, context }) => { - const ark = context.ark; - return { data: defer(ark.get_events_from(params.pubkey, 50)) }; - }, - component: Screen, + return { settings }; + }, + loader: async ({ params, context }) => { + const ark = context.ark; + return { data: defer(ark.get_events_by(params.pubkey, 50)) }; + }, + component: Screen, }); function Screen() { - const { pubkey } = Route.useParams(); - const { data } = Route.useLoaderData(); + const { pubkey } = Route.useParams(); + const { data } = Route.useLoaderData(); - const renderItem = (event: Event) => { - if (!event) return; - switch (event.kind) { - case Kind.Repost: - return ; - default: { - const isConversation = - event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention") - .length > 0; - const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0; + const renderItem = (event: Event) => { + if (!event) return; + switch (event.kind) { + case Kind.Repost: + return ; + default: { + const isConversation = + event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention") + .length > 0; + const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0; - if (isConversation) { - return ; - } + if (isConversation) { + return ; + } - if (isQuote) { - return ; - } + if (isQuote) { + return ; + } - return ; - } - } - }; + return ; + } + } + }; - return ( - - - - - - -
- -
-
- - -
- -
- -
-
-
-
-
-

Latest notes

-
- - - Loading... -
- } - > - - {(events) => events.map((event) => renderItem(event))} - - -
- - - - ); + return ( + + + + + + +
+ +
+
+ + +
+ +
+ +
+
+
+
+
+

Latest notes

+
+ + + Loading... +
+ } + > + + {(events) => events.map((event) => renderItem(event))} + + +
+ + + + ); } diff --git a/apps/desktop2/src/routes/users/-components/eventList.tsx b/apps/desktop2/src/routes/users/-components/eventList.tsx index fd9fc088..6510ce52 100644 --- a/apps/desktop2/src/routes/users/-components/eventList.tsx +++ b/apps/desktop2/src/routes/users/-components/eventList.tsx @@ -8,65 +8,65 @@ import { useInfiniteQuery } from "@tanstack/react-query"; import { useRouteContext } from "@tanstack/react-router"; export function EventList({ id }: { id: string }) { - const { ark } = useRouteContext({ strict: false }); - const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } = - useInfiniteQuery({ - queryKey: ["events", id], - initialPageParam: 0, - queryFn: async ({ pageParam }: { pageParam: number }) => { - const events = await ark.get_events_from(id, FETCH_LIMIT, pageParam); - return events; - }, - getNextPageParam: (lastPage) => { - const lastEvent = lastPage?.at(-1); - return lastEvent ? lastEvent.created_at - 1 : null; - }, - refetchOnWindowFocus: false, - }); + const { ark } = useRouteContext({ strict: false }); + const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } = + useInfiniteQuery({ + queryKey: ["events", id], + initialPageParam: 0, + queryFn: async ({ pageParam }: { pageParam: number }) => { + const events = await ark.get_events_by(id, FETCH_LIMIT, pageParam); + return events; + }, + getNextPageParam: (lastPage) => { + const lastEvent = lastPage?.at(-1); + return lastEvent ? lastEvent.created_at - 1 : null; + }, + refetchOnWindowFocus: false, + }); - const renderItem = (event: Event) => { - if (!event) return; - switch (event.kind) { - case Kind.Repost: - return ; - default: - return ; - } - }; + const renderItem = (event: Event) => { + if (!event) return; + switch (event.kind) { + case Kind.Repost: + return ; + default: + return ; + } + }; - return ( -
- {isLoading ? ( -
- -
- ) : !data.length ? ( -
- -

Empty newsfeed.

-
- ) : ( - data.map((item) => renderItem(item)) - )} -
- {hasNextPage ? ( - - ) : null} -
-
- ); + return ( +
+ {isLoading ? ( +
+ +
+ ) : !data.length ? ( +
+ +

Empty newsfeed.

+
+ ) : ( + data.map((item) => renderItem(item)) + )} +
+ {hasNextPage ? ( + + ) : null} +
+
+ ); } diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts index c158c21e..a4dceca8 100644 --- a/packages/ark/src/ark.ts +++ b/packages/ark/src/ark.ts @@ -1,12 +1,12 @@ import { - type Event, - type EventWithReplies, - type Interests, - type Keys, - type LumeColumn, - type Metadata, - type Settings, - Relays, + type Event, + type EventWithReplies, + type Interests, + type Keys, + type LumeColumn, + type Metadata, + type Settings, + Relays, } from "@lume/types"; import { generateContentTags } from "@lume/utils"; import { invoke } from "@tauri-apps/api/core"; @@ -15,908 +15,904 @@ import { open } from "@tauri-apps/plugin-dialog"; import { readFile } from "@tauri-apps/plugin-fs"; enum NSTORE_KEYS { - settings = "lume_user_settings", - interests = "lume_user_interests", - columns = "lume_user_columns", + settings = "lume_user_settings", + interests = "lume_user_interests", + columns = "lume_user_columns", } export class Ark { - public windows: WebviewWindow[]; - public settings: Settings; - public accounts: string[]; - - constructor() { - this.windows = []; - this.settings = undefined; - } - - public async get_all_accounts() { - try { - const cmd: string[] = await invoke("get_accounts"); - const accounts: string[] = cmd.map((item) => item.replace(".npub", "")); - - if (!this.accounts) this.accounts = accounts; - - return accounts; - } catch (e) { - throw new Error(String(e)); - } - } - - public async load_selected_account(npub: string) { - try { - const cmd: boolean = await invoke("load_selected_account", { - npub, - }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async nostr_connect(uri: string) { - try { - const remoteKey = uri.replace("bunker://", "").split("?")[0]; - const npub: string = await invoke("to_npub", { hex: remoteKey }); - - if (npub) { - const connect: string = await invoke("nostr_connect", { - npub, - uri, - }); - - return connect; - } - } catch (e) { - throw new Error(String(e)); - } - } - - public async create_keys() { - try { - const cmd: Keys = await invoke("create_keys"); - return cmd; - } catch (e) { - console.error(String(e)); - } - } - - public async save_account(nsec: string, password = "") { - try { - const cmd: string = await invoke("save_key", { - nsec, - password, - }); - - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async event_to_bech32(id: string, relays: string[]) { - try { - const cmd: string = await invoke("event_to_bech32", { - id, - relays, - }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async get_relays() { - try { - const cmd: Relays = await invoke("get_relays"); - return cmd; - } catch (e) { - console.error(String(e)); - return null; - } - } - - public async add_relay(url: string) { - try { - const relayUrl = new URL(url); - - if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") { - const cmd: boolean = await invoke("connect_relay", { relay: relayUrl }); - return cmd; - } - } catch (e) { - throw new Error(String(e)); - } - } - - public async remove_relay(url: string) { - try { - const relayUrl = new URL(url); - - if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") { - const cmd: boolean = await invoke("remove_relay", { relay: relayUrl }); - return cmd; - } - } catch (e) { - throw new Error(String(e)); - } - } - - public async get_activities(account: string, kind: "1" | "6" | "9735" = "1") { - try { - const events: Event[] = await invoke("get_activities", { account, kind }); - return events; - } catch (e) { - console.error(String(e)); - return null; - } - } - - public async get_event(id: string) { - try { - const eventId: string = id.replace("nostr:", "").replace(/[^\w\s]/gi, ""); - const cmd: string = await invoke("get_event", { id: eventId }); - const event: Event = JSON.parse(cmd); - return event; - } catch (e) { - console.error(id, String(e)); - throw new Error(String(e)); - } - } - - public async get_events_from(pubkey: string, limit: number, asOf?: number) { - try { - let until: string = undefined; - if (asOf && asOf > 0) until = asOf.toString(); - - const nostrEvents: Event[] = await invoke("get_events_from", { - publicKey: pubkey, - limit, - as_of: until, - }); - - return nostrEvents.sort((a, b) => b.created_at - a.created_at); - } catch (e) { - console.error(String(e)); - return []; - } - } - - public async search(content: string, limit: number) { - try { - if (content.length < 1) return []; - - const events: Event[] = await invoke("search", { - content: content.trim(), - limit, - }); - - return events; - } catch (e) { - console.info(String(e)); - return []; - } - } - - public async get_events( - limit: number, - asOf?: number, - contacts?: string[], - global?: boolean, - ) { - try { - const until: string = asOf && asOf > 0 ? asOf.toString() : undefined; - const isGlobal = global ?? false; - const seens = new Set(); - - const nostrEvents: Event[] = await invoke("get_events", { - limit, - until, - contacts, - global: isGlobal, - }); - - const events = nostrEvents.filter((event) => { - const eTags = event.tags.filter((el) => el[0] === "e"); - const ids = eTags.map((item) => item[1]); - const isDup = ids.some((id) => seens.has(id)); - - // Add found ids to seen list - for (const id of ids) { - seens.add(id); - } - - // Filter NSFW event - if (this.settings?.nsfw) { - const wTags = event.tags.filter((t) => t[0] === "content-warning"); - const isLewd = wTags.length > 0; - - return !isDup && !isLewd; - } - - // Filter duplicate event - return !isDup; - }); - - return events; - } catch (e) { - console.error("[get_events] failed", String(e)); - return []; - } - } - - public async get_events_from_interests( - hashtags: string[], - limit: number, - asOf?: number, - ) { - let until: string = undefined; - if (asOf && asOf > 0) until = asOf.toString(); - - const seenIds = new Set(); - const dedupQueue = new Set(); - const nostrTags = hashtags.map((tag) => tag.replace("#", "").toLowerCase()); - - const nostrEvents: Event[] = await invoke("get_events_from_interests", { - hashtags: nostrTags, - limit, - until, - }); - - for (const event of nostrEvents) { - const tags = event.tags - .filter((el) => el[0] === "e") - ?.map((item) => item[1]); - - if (tags.length) { - for (const tag of tags) { - if (seenIds.has(tag)) { - dedupQueue.add(event.id); - break; - } - seenIds.add(tag); - } - } - } - - return nostrEvents - .filter((event) => !dedupQueue.has(event.id)) - .sort((a, b) => b.created_at - a.created_at); - } - - public async publish( - content: string, - reply_to?: string, - quote?: boolean, - nsfw?: boolean, - ) { - try { - const g = await generateContentTags(content); - - const eventContent = g.content; - const eventTags = g.tags; - - if (reply_to) { - const replyEvent = await this.get_event(reply_to); - const relayHint = - replyEvent.tags.find((ev) => ev[0] === "e")?.[0][2] ?? ""; - - if (quote) { - eventTags.push(["e", replyEvent.id, relayHint, "mention"]); - eventTags.push(["q", replyEvent.id]); - } else { - const rootEvent = replyEvent.tags.find((ev) => ev[3] === "root"); - - if (rootEvent) { - eventTags.push([ - "e", - rootEvent[1], - rootEvent[2] || relayHint, - "root", - ]); - } - - eventTags.push(["e", replyEvent.id, relayHint, "reply"]); - eventTags.push(["p", replyEvent.pubkey]); - } - } - - if (nsfw) { - eventTags.push(["L", "content-warning"]); - eventTags.push(["l", "reason", "content-warning"]); - eventTags.push(["content-warning", "nsfw"]); - } - - const cmd: string = await invoke("publish", { - content: eventContent, - tags: eventTags, - }); - - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async reply_to(content: string, tags: string[]) { - try { - const cmd: string = await invoke("reply_to", { content, tags }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async repost(id: string, author: string) { - try { - const cmd: string = await invoke("repost", { id, pubkey: author }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async upvote(id: string, author: string) { - try { - const cmd: string = await invoke("upvote", { id, pubkey: author }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async downvote(id: string, author: string) { - try { - const cmd: string = await invoke("downvote", { id, pubkey: author }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async get_event_thread(id: string) { - try { - const events: EventWithReplies[] = await invoke("get_event_thread", { - id, - }); - - if (events.length > 0) { - const replies = new Set(); - for (const event of events) { - const tags = event.tags.filter( - (el) => el[0] === "e" && el[1] !== id && el[3] !== "mention", - ); - if (tags.length > 0) { - for (const tag of tags) { - const rootIndex = events.findIndex((el) => el.id === tag[1]); - if (rootIndex !== -1) { - const rootEvent = events[rootIndex]; - if (rootEvent?.replies) { - rootEvent.replies.push(event); - } else { - rootEvent.replies = [event]; - } - replies.add(event.id); - } - } - } - } - const cleanEvents = events.filter((ev) => !replies.has(ev.id)); - return cleanEvents; - } - - return events; - } catch (e) { - return []; - } - } - - public get_thread(tags: string[][], gossip: boolean = false) { - let root: string = null; - let reply: string = null; - - // Get all event references from tags, ignore mention - const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention"); - - if (gossip) { - const relays = tags.filter((el) => el[0] === "e" && el[2]?.length); - - if (relays.length >= 1) { - for (const relay of relays) { - if (relay[2]?.length) this.add_relay(relay[2]); - } - } - } - - if (events.length === 1) { - root = events[0][1]; - } - - if (events.length > 1) { - root = events.find((el) => el[3] === "root")?.[1] ?? events[0][1]; - reply = events.find((el) => el[3] === "reply")?.[1] ?? events[1][1]; - } - - // Fix some rare case when root === reply - if (root && reply && root === reply) { - reply = null; - } - - return { - root, - reply, - }; - } - - public async get_profile(pubkey: string) { - try { - const id = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, ""); - const cmd: Metadata = await invoke("get_profile", { id }); - - return cmd; - } catch (e) { - console.error(pubkey, String(e)); - return null; - } - } - - public async get_current_user_profile() { - try { - const cmd: Metadata = await invoke("get_current_user_profile"); - return cmd; - } catch { - return null; - } - } - - public async create_profile(profile: Metadata) { - try { - const event: string = await invoke("create_profile", { - name: profile.name || "", - display_name: profile.display_name || "", - displayName: profile.display_name || "", - about: profile.about || "", - picture: profile.picture || "", - banner: profile.banner || "", - nip05: profile.nip05 || "", - lud16: profile.lud16 || "", - website: profile.website || "", - }); - return event; - } catch (e) { - throw new Error(String(e)); - } - } - - public async get_contact_list() { - try { - const cmd: string[] = await invoke("get_contact_list"); - return cmd; - } catch (e) { - console.error(e); - return []; - } - } - - public async follow(id: string, alias?: string) { - try { - const cmd: string = await invoke("follow", { id, alias }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async unfollow(id: string) { - try { - const cmd: string = await invoke("unfollow", { id }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async user_to_bech32(key: string, relays: string[]) { - try { - const cmd: string = await invoke("user_to_bech32", { - key, - relays, - }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async verify_nip05(pubkey: string, nip05: string) { - try { - const cmd: boolean = await invoke("verify_nip05", { - key: pubkey, - nip05, - }); - return cmd; - } catch { - return false; - } - } - - public async set_nwc(uri: string) { - try { - const cmd: boolean = await invoke("set_nwc", { uri }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async load_nwc() { - try { - const cmd: boolean = await invoke("load_nwc"); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async get_balance() { - try { - const cmd: number = await invoke("get_balance"); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async zap_profile(id: string, amount: number, message: string) { - try { - const cmd: boolean = await invoke("zap_profile", { id, amount, message }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async zap_event(id: string, amount: number, message: string) { - try { - const cmd: boolean = await invoke("zap_event", { id, amount, message }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async 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)); - } - } - - public async get_columns() { - try { - const cmd: string = await invoke("get_nstore", { - key: NSTORE_KEYS.columns, - }); - const columns: LumeColumn[] = cmd ? JSON.parse(cmd) : []; - return columns; - } catch { - return []; - } - } - - public async set_columns(columns: LumeColumn[]) { - try { - const cmd: string = await invoke("set_nstore", { - key: NSTORE_KEYS.columns, - content: JSON.stringify(columns), - }); - return cmd; - } catch (e) { - throw new Error(e); - } - } - - public async get_settings() { - try { - if (this.settings) return this.settings; - - const cmd: string = await invoke("get_nstore", { - key: NSTORE_KEYS.settings, - }); - const settings: Settings = cmd ? JSON.parse(cmd) : null; - - this.settings = settings; - - return settings; - } catch { - const defaultSettings: Settings = { - autoUpdate: false, - enhancedPrivacy: false, - notification: false, - zap: false, - nsfw: false, - }; - this.settings = defaultSettings; - return defaultSettings; - } - } - - public async set_settings(settings: Settings) { - try { - const cmd: string = await invoke("set_nstore", { - key: NSTORE_KEYS.settings, - content: JSON.stringify(settings), - }); - return cmd; - } catch (e) { - throw new Error(e); - } - } - - public async get_interest() { - try { - const cmd: string = await invoke("get_nstore", { - key: NSTORE_KEYS.interests, - }); - const interests: Interests = cmd ? JSON.parse(cmd) : null; - return interests; - } catch { - return null; - } - } - - public async set_interest( - words: string[], - users: string[], - hashtags: string[], - ) { - try { - const interests: Interests = { - words: words ?? [], - users: users ?? [], - hashtags: hashtags ?? [], - }; - const cmd: string = await invoke("set_nstore", { - key: NSTORE_KEYS.interests, - content: JSON.stringify(interests), - }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async get_nstore(key: string) { - try { - const cmd: string = await invoke("get_nstore", { - key, - }); - const parse: string | string[] = cmd ? JSON.parse(cmd) : null; - if (!parse.length) return null; - return parse; - } catch { - return null; - } - } - - public async set_nstore(key: string, content: string) { - try { - const cmd: string = await invoke("set_nstore", { - key, - content, - }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async open_event_id(id: string) { - try { - const label = `event-${id}`; - const url = `/events/${id}`; - - await invoke("open_window", { - label, - title: "Thread", - url, - width: 500, - height: 800, - }); - } catch (e) { - throw new Error(String(e)); - } - } - - public async open_event(event: Event) { - try { - let root: string = undefined; - let reply: string = undefined; - - const eTags = event.tags.filter( - (tag) => tag[0] === "e" || tag[0] === "q", - ); - - root = eTags.find((el) => el[3] === "root")?.[1]; - reply = eTags.find((el) => el[3] === "reply")?.[1]; - - if (!root) root = eTags[0]?.[1]; - if (!reply) reply = eTags[1]?.[1]; - - const label = `event-${event.id}`; - const url = `/events/${root ?? reply ?? event.id}`; - - await invoke("open_window", { - label, - title: "Thread", - url, - width: 500, - height: 800, - }); - } catch (e) { - throw new Error(String(e)); - } - } - - public async open_profile(pubkey: string) { - try { - const label = `user-${pubkey}`; - await invoke("open_window", { - label, - title: "Profile", - url: `/users/${pubkey}`, - width: 500, - height: 800, - }); - } catch (e) { - throw new Error(String(e)); - } - } - - public async open_editor(reply_to?: string, quote = false) { - try { - let url: string; - - if (reply_to) { - url = `/editor?reply_to=${reply_to}"e=${quote}`; - } else { - url = "/editor"; - } - - const label = `editor-${reply_to ? reply_to : 0}`; - - await invoke("open_window", { - label, - title: "Editor", - url, - width: 560, - height: 340, - }); - } catch (e) { - throw new Error(String(e)); - } - } - - public async open_nwc() { - try { - const label = "nwc"; - await invoke("open_window", { - label, - title: "Nostr Wallet Connect", - url: "/nwc", - width: 400, - height: 600, - }); - } catch (e) { - throw new Error(String(e)); - } - } - - public async open_zap(id: string, pubkey: string, account: string) { - try { - const label = `zap-${id}`; - await invoke("open_window", { - label, - title: "Zap", - url: `/zap/${id}?pubkey=${pubkey}&account=${account}`, - width: 400, - height: 500, - }); - } catch (e) { - throw new Error(String(e)); - } - } - - public async open_settings() { - try { - const label = "settings"; - await invoke("open_window", { - label, - title: "Settings", - url: "/settings", - width: 800, - height: 500, - }); - } catch (e) { - throw new Error(String(e)); - } - } - - public async open_search() { - try { - const label = "search"; - await invoke("open_window", { - label, - title: "Search", - url: "/search", - width: 400, - height: 600, - }); - } catch (e) { - throw new Error(String(e)); - } - } - - public async open_activity(account: string) { - try { - const label = "activity"; - await invoke("open_window", { - label, - title: "Activity", - url: `/activity/${account}/texts`, - width: 400, - height: 600, - }); - } catch (e) { - throw new Error(String(e)); - } - } + public windows: WebviewWindow[]; + public settings: Settings; + public accounts: string[]; + + constructor() { + this.windows = []; + this.settings = undefined; + } + + public async get_all_accounts() { + try { + const cmd: string[] = await invoke("get_accounts"); + const accounts: string[] = cmd.map((item) => item.replace(".npub", "")); + + if (!this.accounts) this.accounts = accounts; + + return accounts; + } catch (e) { + throw new Error(String(e)); + } + } + + public async load_selected_account(npub: string) { + try { + const cmd: boolean = await invoke("load_selected_account", { + npub, + }); + return cmd; + } catch (e) { + throw new Error(String(e)); + } + } + + public async nostr_connect(uri: string) { + try { + const remoteKey = uri.replace("bunker://", "").split("?")[0]; + const npub: string = await invoke("to_npub", { hex: remoteKey }); + + if (npub) { + const connect: string = await invoke("nostr_connect", { + npub, + uri, + }); + + return connect; + } + } catch (e) { + throw new Error(String(e)); + } + } + + public async create_keys() { + try { + const cmd: Keys = await invoke("create_keys"); + return cmd; + } catch (e) { + console.error(String(e)); + } + } + + public async save_account(nsec: string, password = "") { + try { + const cmd: string = await invoke("save_key", { + nsec, + password, + }); + + return cmd; + } catch (e) { + throw new Error(String(e)); + } + } + + public async event_to_bech32(id: string, relays: string[]) { + try { + const cmd: string = await invoke("event_to_bech32", { + id, + relays, + }); + return cmd; + } catch (e) { + throw new Error(String(e)); + } + } + + public async get_relays() { + try { + const cmd: Relays = await invoke("get_relays"); + return cmd; + } catch (e) { + console.error(String(e)); + return null; + } + } + + public async add_relay(url: string) { + try { + const relayUrl = new URL(url); + + if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") { + const cmd: boolean = await invoke("connect_relay", { relay: relayUrl }); + return cmd; + } + } catch (e) { + throw new Error(String(e)); + } + } + + public async remove_relay(url: string) { + try { + const relayUrl = new URL(url); + + if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") { + const cmd: boolean = await invoke("remove_relay", { relay: relayUrl }); + return cmd; + } + } catch (e) { + throw new Error(String(e)); + } + } + + public async get_activities(account: string, kind: "1" | "6" | "9735" = "1") { + try { + const events: Event[] = await invoke("get_activities", { account, kind }); + return events; + } catch (e) { + console.error(String(e)); + return null; + } + } + + public async get_event(id: string) { + try { + const eventId: string = id.replace("nostr:", "").replace(/[^\w\s]/gi, ""); + const cmd: string = await invoke("get_event", { id: eventId }); + const event: Event = JSON.parse(cmd); + return event; + } catch (e) { + console.error(id, String(e)); + throw new Error(String(e)); + } + } + + public async search(content: string, limit: number) { + try { + if (content.length < 1) return []; + + const events: Event[] = await invoke("search", { + content: content.trim(), + limit, + }); + + return events; + } catch (e) { + console.info(String(e)); + return []; + } + } + + private dedup_events(nostrEvents: Event[]) { + const seens = new Set(); + 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(limit: number, asOf?: number) { + try { + const until: string = asOf && asOf > 0 ? asOf.toString() : undefined; + const nostrEvents: Event[] = await invoke("get_local_events", { + limit, + until, + }); + const events = this.dedup_events(nostrEvents); + + return events; + } catch (e) { + console.error("[get_local_events] failed", String(e)); + return []; + } + } + + public async get_global_events(limit: number, asOf?: number) { + try { + const until: string = asOf && asOf > 0 ? asOf.toString() : undefined; + const nostrEvents: Event[] = await invoke("get_global_events", { + limit, + until, + }); + const events = this.dedup_events(nostrEvents); + + return events; + } catch (e) { + console.error("[get_global_events] failed", String(e)); + return []; + } + } + + public async get_hashtag_events( + hashtags: string[], + limit: number, + asOf?: number, + ) { + try { + const until: string = asOf && asOf > 0 ? asOf.toString() : undefined; + const nostrTags = hashtags.map((tag) => tag.replace("#", "")); + const nostrEvents: Event[] = await invoke("get_hashtag_events", { + hashtags: nostrTags, + limit, + until, + }); + const events = this.dedup_events(nostrEvents); + + return events; + } catch (e) { + console.error("[get_hashtag_events] failed", String(e)); + return []; + } + } + + public async get_group_events( + contacts: string[], + limit: number, + asOf?: number, + ) { + try { + const until: string = asOf && asOf > 0 ? asOf.toString() : undefined; + const nostrEvents: Event[] = await invoke("get_group_events", { + list: contacts, + limit, + until, + }); + const events = this.dedup_events(nostrEvents); + + return events; + } catch (e) { + console.error("[get_group_events] failed", String(e)); + return []; + } + } + + public async get_events_by(pubkey: string, limit: number, asOf?: number) { + try { + const until: string = asOf && asOf > 0 ? asOf.toString() : undefined; + const nostrEvents: Event[] = await invoke("get_events_by", { + publicKey: pubkey, + limit, + as_of: until, + }); + + return nostrEvents.sort((a, b) => b.created_at - a.created_at); + } catch (e) { + console.error("[get_events_by] failed", String(e)); + return []; + } + } + + public async publish( + content: string, + reply_to?: string, + quote?: boolean, + nsfw?: boolean, + ) { + try { + const g = await generateContentTags(content); + + const eventContent = g.content; + const eventTags = g.tags; + + if (reply_to) { + const replyEvent = await this.get_event(reply_to); + const relayHint = + replyEvent.tags.find((ev) => ev[0] === "e")?.[0][2] ?? ""; + + if (quote) { + eventTags.push(["e", replyEvent.id, relayHint, "mention"]); + eventTags.push(["q", replyEvent.id]); + } else { + const rootEvent = replyEvent.tags.find((ev) => ev[3] === "root"); + + if (rootEvent) { + eventTags.push([ + "e", + rootEvent[1], + rootEvent[2] || relayHint, + "root", + ]); + } + + eventTags.push(["e", replyEvent.id, relayHint, "reply"]); + eventTags.push(["p", replyEvent.pubkey]); + } + } + + if (nsfw) { + eventTags.push(["L", "content-warning"]); + eventTags.push(["l", "reason", "content-warning"]); + eventTags.push(["content-warning", "nsfw"]); + } + + const cmd: string = await invoke("publish", { + content: eventContent, + tags: eventTags, + }); + + return cmd; + } catch (e) { + throw new Error(String(e)); + } + } + + public async reply_to(content: string, tags: string[]) { + try { + const cmd: string = await invoke("reply_to", { content, tags }); + return cmd; + } catch (e) { + throw new Error(String(e)); + } + } + + public async repost(id: string, author: string) { + try { + const cmd: string = await invoke("repost", { id, pubkey: author }); + return cmd; + } catch (e) { + throw new Error(String(e)); + } + } + + public async get_event_thread(id: string) { + try { + const events: EventWithReplies[] = await invoke("get_thread", { + id, + }); + + if (events.length > 0) { + const replies = new Set(); + for (const event of events) { + const tags = event.tags.filter( + (el) => el[0] === "e" && el[1] !== id && el[3] !== "mention", + ); + if (tags.length > 0) { + for (const tag of tags) { + const rootIndex = events.findIndex((el) => el.id === tag[1]); + if (rootIndex !== -1) { + const rootEvent = events[rootIndex]; + if (rootEvent?.replies) { + rootEvent.replies.push(event); + } else { + rootEvent.replies = [event]; + } + replies.add(event.id); + } + } + } + } + const cleanEvents = events.filter((ev) => !replies.has(ev.id)); + return cleanEvents; + } + + return events; + } catch (e) { + return []; + } + } + + public get_thread(tags: string[][], gossip: boolean = false) { + let root: string = null; + let reply: string = null; + + // Get all event references from tags, ignore mention + const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention"); + + if (gossip) { + const relays = tags.filter((el) => el[0] === "e" && el[2]?.length); + + if (relays.length >= 1) { + for (const relay of relays) { + if (relay[2]?.length) this.add_relay(relay[2]); + } + } + } + + if (events.length === 1) { + root = events[0][1]; + } + + if (events.length > 1) { + root = events.find((el) => el[3] === "root")?.[1] ?? events[0][1]; + reply = events.find((el) => el[3] === "reply")?.[1] ?? events[1][1]; + } + + // Fix some rare case when root === reply + if (root && reply && root === reply) { + reply = null; + } + + return { + root, + reply, + }; + } + + public async get_profile(pubkey: string) { + try { + const id = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, ""); + const cmd: Metadata = await invoke("get_profile", { id }); + + return cmd; + } catch (e) { + console.error(pubkey, String(e)); + return null; + } + } + + public async get_current_user_profile() { + try { + const cmd: Metadata = await invoke("get_current_user_profile"); + return cmd; + } catch { + return null; + } + } + + public async create_profile(profile: Metadata) { + try { + const event: string = await invoke("create_profile", { + name: profile.name || "", + display_name: profile.display_name || "", + displayName: profile.display_name || "", + about: profile.about || "", + picture: profile.picture || "", + banner: profile.banner || "", + nip05: profile.nip05 || "", + lud16: profile.lud16 || "", + website: profile.website || "", + }); + return event; + } catch (e) { + throw new Error(String(e)); + } + } + + public async get_contact_list() { + try { + const cmd: string[] = await invoke("get_contact_list"); + return cmd; + } catch (e) { + console.error(e); + return []; + } + } + + public async follow(id: string, alias?: string) { + try { + const cmd: string = await invoke("follow", { id, alias }); + return cmd; + } catch (e) { + throw new Error(String(e)); + } + } + + public async unfollow(id: string) { + try { + const cmd: string = await invoke("unfollow", { id }); + return cmd; + } catch (e) { + throw new Error(String(e)); + } + } + + public async user_to_bech32(key: string, relays: string[]) { + try { + const cmd: string = await invoke("user_to_bech32", { + key, + relays, + }); + return cmd; + } catch (e) { + throw new Error(String(e)); + } + } + + public async verify_nip05(pubkey: string, nip05: string) { + try { + const cmd: boolean = await invoke("verify_nip05", { + key: pubkey, + nip05, + }); + return cmd; + } catch { + return false; + } + } + + public async set_nwc(uri: string) { + try { + const cmd: boolean = await invoke("set_nwc", { uri }); + return cmd; + } catch (e) { + throw new Error(String(e)); + } + } + + public async load_nwc() { + try { + const cmd: boolean = await invoke("load_nwc"); + return cmd; + } catch (e) { + throw new Error(String(e)); + } + } + + public async get_balance() { + try { + const cmd: number = await invoke("get_balance"); + return cmd; + } catch (e) { + throw new Error(String(e)); + } + } + + public async zap_profile(id: string, amount: number, message: string) { + try { + const cmd: boolean = await invoke("zap_profile", { id, amount, message }); + return cmd; + } catch (e) { + throw new Error(String(e)); + } + } + + public async zap_event(id: string, amount: number, message: string) { + try { + const cmd: boolean = await invoke("zap_event", { id, amount, message }); + return cmd; + } catch (e) { + throw new Error(String(e)); + } + } + + public async 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)); + } + } + + public async get_columns() { + try { + const cmd: string = await invoke("get_nstore", { + key: NSTORE_KEYS.columns, + }); + const columns: LumeColumn[] = cmd ? JSON.parse(cmd) : []; + return columns; + } catch { + return []; + } + } + + public async set_columns(columns: LumeColumn[]) { + try { + const cmd: string = await invoke("set_nstore", { + key: NSTORE_KEYS.columns, + content: JSON.stringify(columns), + }); + return cmd; + } catch (e) { + throw new Error(e); + } + } + + public async get_settings() { + try { + if (this.settings) return this.settings; + + const cmd: string = await invoke("get_nstore", { + key: NSTORE_KEYS.settings, + }); + const settings: Settings = cmd ? JSON.parse(cmd) : null; + + this.settings = settings; + + return settings; + } catch { + const defaultSettings: Settings = { + autoUpdate: false, + enhancedPrivacy: false, + notification: false, + zap: false, + nsfw: false, + }; + this.settings = defaultSettings; + return defaultSettings; + } + } + + public async set_settings(settings: Settings) { + try { + const cmd: string = await invoke("set_nstore", { + key: NSTORE_KEYS.settings, + content: JSON.stringify(settings), + }); + return cmd; + } catch (e) { + throw new Error(e); + } + } + + public async get_interest() { + try { + const cmd: string = await invoke("get_nstore", { + key: NSTORE_KEYS.interests, + }); + const interests: Interests = cmd ? JSON.parse(cmd) : null; + return interests; + } catch { + return null; + } + } + + public async set_interest( + words: string[], + users: string[], + hashtags: string[], + ) { + try { + const interests: Interests = { + words: words ?? [], + users: users ?? [], + hashtags: hashtags ?? [], + }; + const cmd: string = await invoke("set_nstore", { + key: NSTORE_KEYS.interests, + content: JSON.stringify(interests), + }); + return cmd; + } catch (e) { + throw new Error(String(e)); + } + } + + public async get_nstore(key: string) { + try { + const cmd: string = await invoke("get_nstore", { + key, + }); + const parse: string | string[] = cmd ? JSON.parse(cmd) : null; + if (!parse.length) return null; + return parse; + } catch { + return null; + } + } + + public async set_nstore(key: string, content: string) { + try { + const cmd: string = await invoke("set_nstore", { + key, + content, + }); + return cmd; + } catch (e) { + throw new Error(String(e)); + } + } + + public async open_event_id(id: string) { + try { + const label = `event-${id}`; + const url = `/events/${id}`; + + await invoke("open_window", { + label, + title: "Thread", + url, + width: 500, + height: 800, + }); + } catch (e) { + throw new Error(String(e)); + } + } + + public async open_event(event: Event) { + try { + let root: string = undefined; + let reply: string = undefined; + + const eTags = event.tags.filter( + (tag) => tag[0] === "e" || tag[0] === "q", + ); + + root = eTags.find((el) => el[3] === "root")?.[1]; + reply = eTags.find((el) => el[3] === "reply")?.[1]; + + if (!root) root = eTags[0]?.[1]; + if (!reply) reply = eTags[1]?.[1]; + + const label = `event-${event.id}`; + const url = `/events/${root ?? reply ?? event.id}`; + + await invoke("open_window", { + label, + title: "Thread", + url, + width: 500, + height: 800, + }); + } catch (e) { + throw new Error(String(e)); + } + } + + public async open_profile(pubkey: string) { + try { + const label = `user-${pubkey}`; + await invoke("open_window", { + label, + title: "Profile", + url: `/users/${pubkey}`, + width: 500, + height: 800, + }); + } catch (e) { + throw new Error(String(e)); + } + } + + public async open_editor(reply_to?: string, quote = false) { + try { + let url: string; + + if (reply_to) { + url = `/editor?reply_to=${reply_to}"e=${quote}`; + } else { + url = "/editor"; + } + + const label = `editor-${reply_to ? reply_to : 0}`; + + await invoke("open_window", { + label, + title: "Editor", + url, + width: 560, + height: 340, + }); + } catch (e) { + throw new Error(String(e)); + } + } + + public async open_nwc() { + try { + const label = "nwc"; + await invoke("open_window", { + label, + title: "Nostr Wallet Connect", + url: "/nwc", + width: 400, + height: 600, + }); + } catch (e) { + throw new Error(String(e)); + } + } + + public async open_zap(id: string, pubkey: string, account: string) { + try { + const label = `zap-${id}`; + await invoke("open_window", { + label, + title: "Zap", + url: `/zap/${id}?pubkey=${pubkey}&account=${account}`, + width: 400, + height: 500, + }); + } catch (e) { + throw new Error(String(e)); + } + } + + public async open_settings() { + try { + const label = "settings"; + await invoke("open_window", { + label, + title: "Settings", + url: "/settings", + width: 800, + height: 500, + }); + } catch (e) { + throw new Error(String(e)); + } + } + + public async open_search() { + try { + const label = "search"; + await invoke("open_window", { + label, + title: "Search", + url: "/search", + width: 400, + height: 600, + }); + } catch (e) { + throw new Error(String(e)); + } + } + + public async open_activity(account: string) { + try { + const label = "activity"; + await invoke("open_window", { + label, + title: "Activity", + url: `/activity/${account}/texts`, + width: 400, + height: 600, + }); + } catch (e) { + throw new Error(String(e)); + } + } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 10b8888a..2f50a9d7 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -127,10 +127,12 @@ fn main() { nostr::metadata::zap_event, nostr::metadata::friend_to_friend, nostr::event::get_event, - nostr::event::get_events_from, - nostr::event::get_events, - nostr::event::get_events_from_interests, - nostr::event::get_event_thread, + nostr::event::get_thread, + nostr::event::get_events_by, + nostr::event::get_local_events, + nostr::event::get_global_events, + nostr::event::get_hashtag_events, + nostr::event::get_group_events, nostr::event::publish, nostr::event::repost, commands::folder::show_in_folder, diff --git a/src-tauri/src/nostr/event.rs b/src-tauri/src/nostr/event.rs index 10312867..6bda3546 100644 --- a/src-tauri/src/nostr/event.rs +++ b/src-tauri/src/nostr/event.rs @@ -30,7 +30,7 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result { let filter = Filter::new().id(id); - match &client + match client .get_events_of(vec![filter], Some(Duration::from_secs(10))) .await { @@ -49,7 +49,24 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result) -> Result, String> { + let client = &state.client; + + match EventId::from_hex(id) { + Ok(event_id) => { + let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id); + + match client.get_events_of(vec![filter], None).await { + Ok(events) => Ok(events), + Err(err) => Err(err.to_string()), + } + } + Err(_) => Err("Event ID is not valid".into()), + } +} + +#[tauri::command] +pub async fn get_events_by( public_key: &str, limit: usize, as_of: Option<&str>, @@ -57,32 +74,31 @@ pub async fn get_events_from( ) -> Result, String> { let client = &state.client; - if let Ok(author) = PublicKey::from_str(public_key) { - let until = match as_of { - Some(until) => Timestamp::from_str(until).unwrap(), - None => Timestamp::now(), - }; - let filter = Filter::new() - .kinds(vec![Kind::TextNote, Kind::Repost]) - .author(author) - .limit(limit) - .until(until); + match PublicKey::from_str(public_key) { + Ok(author) => { + let until = match as_of { + Some(until) => Timestamp::from_str(until).unwrap(), + None => Timestamp::now(), + }; + let filter = Filter::new() + .kinds(vec![Kind::TextNote, Kind::Repost]) + .author(author) + .limit(limit) + .until(until); - match client.get_events_of(vec![filter], None).await { - Ok(events) => Ok(events), - Err(err) => Err(err.to_string()), + match client.get_events_of(vec![filter], None).await { + Ok(events) => Ok(events), + Err(err) => Err(err.to_string()), + } } - } else { - Err("Public Key is not valid, please check again.".into()) + Err(err) => Err(err.to_string()), } } #[tauri::command] -pub async fn get_events( +pub async fn get_local_events( limit: usize, until: Option<&str>, - contacts: Option>, - global: bool, state: State<'_, Nostr>, ) -> Result, String> { let client = &state.client; @@ -91,66 +107,57 @@ pub async fn get_events( None => Timestamp::now(), }; - match global { - true => { + match client + .get_contact_list_public_keys(Some(Duration::from_secs(10))) + .await + { + Ok(contacts) => { let filter = Filter::new() .kinds(vec![Kind::TextNote, Kind::Repost]) .limit(limit) + .authors(contacts) .until(as_of); match client - .get_events_of(vec![filter], Some(Duration::from_secs(15))) + .get_events_of(vec![filter], Some(Duration::from_secs(8))) .await { Ok(events) => Ok(events), Err(err) => Err(err.to_string()), } } - false => { - let authors = match contacts { - Some(val) => { - let c: Vec = 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()), - } - } + Err(err) => Err(err.to_string()), } } #[tauri::command] -pub async fn get_events_from_interests( +pub async fn get_global_events( + limit: usize, + until: Option<&str>, + state: State<'_, Nostr>, +) -> Result, String> { + let client = &state.client; + let as_of = match until { + Some(until) => Timestamp::from_str(until).unwrap(), + None => Timestamp::now(), + }; + + let filter = Filter::new() + .kinds(vec![Kind::TextNote, Kind::Repost]) + .limit(limit) + .until(as_of); + + match client + .get_events_of(vec![filter], Some(Duration::from_secs(8))) + .await + { + Ok(events) => Ok(events), + Err(err) => Err(err.to_string()), + } +} + +#[tauri::command] +pub async fn get_hashtag_events( hashtags: Vec<&str>, limit: usize, until: Option<&str>, @@ -174,19 +181,30 @@ pub async fn get_events_from_interests( } #[tauri::command] -pub async fn get_event_thread(id: &str, state: State<'_, Nostr>) -> Result, String> { +pub async fn get_group_events( + list: Vec<&str>, + limit: usize, + until: Option<&str>, + state: State<'_, Nostr>, +) -> Result, String> { let client = &state.client; + let as_of = match until { + Some(until) => Timestamp::from_str(until).unwrap(), + None => Timestamp::now(), + }; + let authors: Vec = list + .into_iter() + .map(|hex| PublicKey::from_hex(hex).unwrap()) + .collect(); + let filter = Filter::new() + .kinds(vec![Kind::TextNote, Kind::Repost]) + .limit(limit) + .until(as_of) + .authors(authors); - match EventId::from_hex(id) { - Ok(event_id) => { - let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id); - - match client.get_events_of(vec![filter], None).await { - Ok(events) => Ok(events), - Err(err) => Err(err.to_string()), - } - } - Err(_) => Err("Event ID is not valid".into()), + match client.get_events_of(vec![filter], None).await { + Ok(events) => Ok(events), + Err(err) => Err(err.to_string()), } } @@ -210,9 +228,8 @@ pub async fn repost(raw: &str, state: State<'_, Nostr>) -> Result Ok(event_id), + Err(err) => Err(err.to_string()), } }