feat: improve tauri commands

This commit is contained in:
reya 2024-05-20 07:05:30 +07:00
parent cac774a0c1
commit 9b5867f80c
9 changed files with 1682 additions and 1647 deletions

View File

@ -1,3 +1,5 @@
import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost"; import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text"; import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons"; import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
@ -8,148 +10,159 @@ import { Link, createFileRoute, redirect } from "@tanstack/react-router";
import { Virtualizer } from "virtua"; import { Virtualizer } from "virtua";
export const Route = createFileRoute("/foryou")({ export const Route = createFileRoute("/foryou")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => { validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return { return {
account: search.account, account: search.account,
label: search.label, label: search.label,
name: search.name, name: search.name,
}; };
}, },
beforeLoad: async ({ search, context }) => { beforeLoad: async ({ search, context }) => {
const ark = context.ark; const ark = context.ark;
const interests = await ark.get_interest(); const interests = await ark.get_interest();
const settings = await ark.get_settings(); const settings = await ark.get_settings();
if (!interests) { if (!interests) {
throw redirect({ throw redirect({
to: "/interests", to: "/interests",
search: { search: {
...search, ...search,
redirect: "/foryou", redirect: "/foryou",
}, },
}); });
} }
return { return {
interests, interests,
settings, settings,
}; };
}, },
component: Screen, component: Screen,
}); });
export function Screen() { export function Screen() {
const { name, account } = Route.useSearch(); const { label, account } = Route.useSearch();
const { ark, interests } = Route.useRouteContext(); const { ark, interests } = Route.useRouteContext();
const { const {
data, data,
isLoading, isLoading,
isFetching, isFetching,
isFetchingNextPage, isFetchingNextPage,
hasNextPage, hasNextPage,
fetchNextPage, fetchNextPage,
} = useInfiniteQuery({ } = useInfiniteQuery({
queryKey: [name, account], queryKey: [label, account],
initialPageParam: 0, initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => { queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events_from_interests( const events = await ark.get_hashtag_events(
interests.hashtags, interests.hashtags,
20, 20,
pageParam, pageParam,
); );
return events; return events;
}, },
getNextPageParam: (lastPage) => { getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
const lastEvent = lastPage?.at(-1); select: (data) => data?.pages.flatMap((page) => page),
return lastEvent ? lastEvent.created_at - 1 : null; refetchOnWindowFocus: false,
}, });
select: (data) => data?.pages.flatMap((page) => page),
refetchOnWindowFocus: false,
});
const renderItem = (event: Event) => { const renderItem = (event: Event) => {
if (!event) return; if (!event) return;
switch (event.kind) { switch (event.kind) {
case Kind.Repost: case Kind.Repost:
return <RepostNote key={event.id} event={event} />; return <RepostNote key={event.id} event={event} />;
default: default: {
return <TextNote key={event.id} event={event} />; const isConversation =
} event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention")
}; .length > 0;
const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0;
return ( if (isConversation) {
<div className="p-2 w-full h-full overflow-y-auto scrollbar-none"> return <Conversation key={event.id} event={event} className="mb-3" />;
{isFetching && !isLoading && !isFetchingNextPage ? ( }
<div className="w-full h-11 flex items-center justify-center">
<div className="flex items-center justify-center gap-2"> if (isQuote) {
<Spinner className="size-5" /> return <Quote key={event.id} event={event} className="mb-3" />;
<span className="text-sm font-medium">Fetching new notes...</span> }
</div>
</div> return <TextNote key={event.id} event={event} className="mb-3" />;
) : null} }
{isLoading ? ( }
<div className="flex h-16 w-full items-center justify-center gap-2"> };
<Spinner className="size-5" />
<span className="text-sm font-medium">Loading...</span> return (
</div> <div className="p-2 w-full h-full overflow-y-auto scrollbar-none">
) : !data.length ? ( {isFetching && !isLoading && !isFetchingNextPage ? (
<Empty /> <div className="w-full h-11 flex items-center justify-center">
) : ( <div className="flex items-center justify-center gap-2">
<Virtualizer overscan={3}> <Spinner className="size-5" />
{data.map((item) => renderItem(item))} <span className="text-sm font-medium">Fetching new notes...</span>
</Virtualizer> </div>
)} </div>
{data?.length && hasNextPage ? ( ) : null}
<div> {isLoading ? (
<button <div className="flex h-16 w-full items-center justify-center gap-2">
type="button" <Spinner className="size-5" />
onClick={() => fetchNextPage()} <span className="text-sm font-medium">Loading...</span>
disabled={isFetchingNextPage || isLoading} </div>
className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-xl bg-neutral-100 px-3 font-medium hover:bg-neutral-50 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20" ) : !data.length ? (
> <Empty />
{isFetchingNextPage ? ( ) : (
<Spinner className="size-5" /> <Virtualizer overscan={3}>
) : ( {data.map((item) => renderItem(item))}
<> </Virtualizer>
<ArrowRightCircleIcon className="size-5" /> )}
Load more {data?.length && hasNextPage ? (
</> <div>
)} <button
</button> type="button"
</div> onClick={() => fetchNextPage()}
) : null} disabled={isFetchingNextPage || isLoading}
</div> className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-xl bg-neutral-100 px-3 font-medium hover:bg-neutral-50 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
); >
{isFetchingNextPage ? (
<Spinner className="size-5" />
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
</>
)}
</button>
</div>
) : null}
</div>
);
} }
function Empty() { function Empty() {
return ( return (
<div className="flex flex-col py-10 gap-10"> <div className="flex flex-col py-10 gap-10">
<div className="text-center flex flex-col items-center justify-center"> <div className="text-center flex flex-col items-center justify-center">
<div className="size-24 bg-blue-100 flex flex-col items-center justify-end overflow-hidden dark:bg-blue-900 rounded-full mb-8"> <div className="size-24 bg-blue-100 flex flex-col items-center justify-end overflow-hidden dark:bg-blue-900 rounded-full mb-8">
<div className="w-12 h-16 bg-gradient-to-b from-blue-500 dark:from-blue-200 to-blue-50 dark:to-blue-900 rounded-t-lg" /> <div className="w-12 h-16 bg-gradient-to-b from-blue-500 dark:from-blue-200 to-blue-50 dark:to-blue-900 rounded-t-lg" />
</div> </div>
<p className="text-lg font-medium">Your newsfeed is empty</p> <p className="text-lg font-medium">Your newsfeed is empty</p>
<p className="leading-tight text-neutral-700 dark:text-neutral-300"> <p className="leading-tight text-neutral-700 dark:text-neutral-300">
Here are few suggestions to get started. Here are few suggestions to get started.
</p> </p>
</div> </div>
<div className="flex flex-col px-3 gap-2"> <div className="flex flex-col px-3 gap-2">
<Link <Link
to="/trending/notes" to="/trending/notes"
className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3" className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3"
> >
<ArrowRightIcon className="size-5" /> <ArrowRightIcon className="size-5" />
Show trending notes Show trending notes
</Link> </Link>
<Link <Link
to="/trending/users" to="/trending/users"
className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3" className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3"
> >
<ArrowRightIcon className="size-5" /> <ArrowRightIcon className="size-5" />
Discover trending users Discover trending users
</Link> </Link>
</div> </div>
</div> </div>
); );
} }

View File

@ -10,144 +10,141 @@ import { Link, createFileRoute } from "@tanstack/react-router";
import { Virtualizer } from "virtua"; import { Virtualizer } from "virtua";
export const Route = createFileRoute("/global")({ export const Route = createFileRoute("/global")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => { validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return { return {
account: search.account, account: search.account,
label: search.label, label: search.label,
name: search.name, name: search.name,
}; };
}, },
beforeLoad: async ({ context }) => { beforeLoad: async ({ context }) => {
const ark = context.ark; const ark = context.ark;
const settings = await ark.get_settings(); const settings = await ark.get_settings();
return { settings }; return { settings };
}, },
component: Screen, component: Screen,
}); });
export function Screen() { export function Screen() {
const { account } = Route.useSearch(); const { label, account } = Route.useSearch();
const { ark } = Route.useRouteContext(); const { ark } = Route.useRouteContext();
const { const {
data, data,
isLoading, isLoading,
isFetching, isFetching,
isFetchingNextPage, isFetchingNextPage,
hasNextPage, hasNextPage,
fetchNextPage, fetchNextPage,
} = useInfiniteQuery({ } = useInfiniteQuery({
queryKey: ["global", account], queryKey: [label, account],
initialPageParam: 0, initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => { queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events(20, pageParam, undefined, true); const events = await ark.get_global_events(20, pageParam);
return events; return events;
}, },
getNextPageParam: (lastPage) => { getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
const lastEvent = lastPage?.at(-1); select: (data) => data?.pages.flatMap((page) => page),
return lastEvent ? lastEvent.created_at - 1 : null; refetchOnWindowFocus: false,
}, });
select: (data) => data?.pages.flatMap((page) => page),
refetchOnWindowFocus: false,
});
const renderItem = (event: Event) => { const renderItem = (event: Event) => {
if (!event) return; if (!event) return;
switch (event.kind) { switch (event.kind) {
case Kind.Repost: case Kind.Repost:
return <RepostNote key={event.id} event={event} />; return <RepostNote key={event.id} event={event} />;
default: { default: {
const isConversation = const isConversation =
event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention") event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention")
.length > 0; .length > 0;
const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0; const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0;
if (isConversation) { if (isConversation) {
return <Conversation key={event.id} event={event} className="mb-3" />; return <Conversation key={event.id} event={event} className="mb-3" />;
} }
if (isQuote) { if (isQuote) {
return <Quote key={event.id} event={event} className="mb-3" />; return <Quote key={event.id} event={event} className="mb-3" />;
} }
return <TextNote key={event.id} event={event} className="mb-3" />; return <TextNote key={event.id} event={event} className="mb-3" />;
} }
} }
}; };
return ( return (
<div className="p-2 w-full h-full overflow-y-auto scrollbar-none"> <div className="p-2 w-full h-full overflow-y-auto scrollbar-none">
{isFetching && !isLoading && !isFetchingNextPage ? ( {isFetching && !isLoading && !isFetchingNextPage ? (
<div className="w-full h-11 flex items-center justify-center"> <div className="w-full h-11 flex items-center justify-center">
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<Spinner className="size-5" /> <Spinner className="size-5" />
<span className="text-sm font-medium">Fetching new notes...</span> <span className="text-sm font-medium">Fetching new notes...</span>
</div> </div>
</div> </div>
) : null} ) : null}
{isLoading ? ( {isLoading ? (
<div className="flex h-16 w-full items-center justify-center gap-2"> <div className="flex h-16 w-full items-center justify-center gap-2">
<Spinner className="size-5" /> <Spinner className="size-5" />
<span className="text-sm font-medium">Loading...</span> <span className="text-sm font-medium">Loading...</span>
</div> </div>
) : !data.length ? ( ) : !data.length ? (
<Empty /> <Empty />
) : ( ) : (
<Virtualizer overscan={3}> <Virtualizer overscan={3}>
{data.map((item) => renderItem(item))} {data.map((item) => renderItem(item))}
</Virtualizer> </Virtualizer>
)} )}
{data?.length && hasNextPage ? ( {data?.length && hasNextPage ? (
<div> <div>
<button <button
type="button" type="button"
onClick={() => fetchNextPage()} onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading} disabled={isFetchingNextPage || isLoading}
className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-xl bg-black/5 px-3 font-medium hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20" className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-xl bg-black/5 px-3 font-medium hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
> >
{isFetchingNextPage ? ( {isFetchingNextPage ? (
<Spinner className="size-5" /> <Spinner className="size-5" />
) : ( ) : (
<> <>
<ArrowRightCircleIcon className="size-5" /> <ArrowRightCircleIcon className="size-5" />
Load more Load more
</> </>
)} )}
</button> </button>
</div> </div>
) : null} ) : null}
</div> </div>
); );
} }
function Empty() { function Empty() {
return ( return (
<div className="flex flex-col py-10 gap-10"> <div className="flex flex-col py-10 gap-10">
<div className="text-center flex flex-col items-center justify-center"> <div className="text-center flex flex-col items-center justify-center">
<div className="size-24 bg-blue-100 flex flex-col items-center justify-end overflow-hidden dark:bg-blue-900 rounded-full mb-8"> <div className="size-24 bg-blue-100 flex flex-col items-center justify-end overflow-hidden dark:bg-blue-900 rounded-full mb-8">
<div className="w-12 h-16 bg-gradient-to-b from-blue-500 dark:from-blue-200 to-blue-50 dark:to-blue-900 rounded-t-lg" /> <div className="w-12 h-16 bg-gradient-to-b from-blue-500 dark:from-blue-200 to-blue-50 dark:to-blue-900 rounded-t-lg" />
</div> </div>
<p className="text-lg font-medium">Your newsfeed is empty</p> <p className="text-lg font-medium">Your newsfeed is empty</p>
<p className="leading-tight text-neutral-700 dark:text-neutral-300"> <p className="leading-tight text-neutral-700 dark:text-neutral-300">
Here are few suggestions to get started. Here are few suggestions to get started.
</p> </p>
</div> </div>
<div className="flex flex-col px-3 gap-2"> <div className="flex flex-col px-3 gap-2">
<Link <Link
to="/trending/notes" to="/trending/notes"
className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3" className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3"
> >
<ArrowRightIcon className="size-5" /> <ArrowRightIcon className="size-5" />
Show trending notes Show trending notes
</Link> </Link>
<Link <Link
to="/trending/users" to="/trending/users"
className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3" className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3"
> >
<ArrowRightIcon className="size-5" /> <ArrowRightIcon className="size-5" />
Discover trending users Discover trending users
</Link> </Link>
</div> </div>
</div> </div>
); );
} }

View File

@ -1,3 +1,5 @@
import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost"; import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text"; import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons"; import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
@ -8,147 +10,158 @@ import { Link, createFileRoute, redirect } from "@tanstack/react-router";
import { Virtualizer } from "virtua"; import { Virtualizer } from "virtua";
export const Route = createFileRoute("/group")({ export const Route = createFileRoute("/group")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => { validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return { return {
account: search.account, account: search.account,
label: search.label, label: search.label,
name: search.name, name: search.name,
}; };
}, },
beforeLoad: async ({ search, context }) => { beforeLoad: async ({ search, context }) => {
const ark = context.ark; const ark = context.ark;
const groups = (await ark.get_nstore( const groups = (await ark.get_nstore(
`lume_group_${search.label}`, `lume_group_${search.label}`,
)) as string[]; )) as string[];
const settings = await ark.get_settings(); const settings = await ark.get_settings();
if (!groups) { if (!groups) {
throw redirect({ throw redirect({
to: "/create-group", to: "/create-group",
search: { search: {
...search, ...search,
redirect: "/group", redirect: "/group",
}, },
}); });
} }
return { return {
groups, groups,
settings, settings,
}; };
}, },
component: Screen, component: Screen,
}); });
export function Screen() { export function Screen() {
const { name, account } = Route.useSearch(); const { label, account } = Route.useSearch();
const { ark, groups } = Route.useRouteContext(); const { ark, groups } = Route.useRouteContext();
const { const {
data, data,
isLoading, isLoading,
isFetching, isFetching,
isFetchingNextPage, isFetchingNextPage,
hasNextPage, hasNextPage,
fetchNextPage, fetchNextPage,
} = useInfiniteQuery({ } = useInfiniteQuery({
queryKey: [name, account], queryKey: [label, account],
initialPageParam: 0, initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => { queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events(20, pageParam, groups); const events = await ark.get_group_events(groups, 20, pageParam);
return events; return events;
}, },
getNextPageParam: (lastPage) => { getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
const lastEvent = lastPage?.at(-1); select: (data) =>
return lastEvent ? lastEvent.created_at - 1 : null; data?.pages.flatMap((page) => page.filter((ev) => ev.kind === Kind.Text)),
}, refetchOnWindowFocus: false,
select: (data) => });
data?.pages.flatMap((page) => page.filter((ev) => ev.kind === Kind.Text)),
refetchOnWindowFocus: false,
});
const renderItem = (event: Event) => { const renderItem = (event: Event) => {
if (!event) return; if (!event) return;
switch (event.kind) { switch (event.kind) {
case Kind.Repost: case Kind.Repost:
return <RepostNote key={event.id} event={event} />; return <RepostNote key={event.id} event={event} />;
default: default: {
return <TextNote key={event.id} event={event} />; const isConversation =
} event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention")
}; .length > 0;
const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0;
return ( if (isConversation) {
<div className="p-2 w-full h-full overflow-y-auto scrollbar-none"> return <Conversation key={event.id} event={event} className="mb-3" />;
{isFetching && !isLoading && !isFetchingNextPage ? ( }
<div className="w-full h-11 flex items-center justify-center">
<div className="flex items-center justify-center gap-2"> if (isQuote) {
<Spinner className="size-5" /> return <Quote key={event.id} event={event} className="mb-3" />;
<span className="text-sm font-medium">Fetching new notes...</span> }
</div>
</div> return <TextNote key={event.id} event={event} className="mb-3" />;
) : null} }
{isLoading ? ( }
<div className="flex h-16 w-full items-center justify-center gap-2"> };
<Spinner className="size-5" />
<span className="text-sm font-medium">Loading...</span> return (
</div> <div className="p-2 w-full h-full overflow-y-auto scrollbar-none">
) : !data.length ? ( {isFetching && !isLoading && !isFetchingNextPage ? (
<Empty /> <div className="w-full h-11 flex items-center justify-center">
) : ( <div className="flex items-center justify-center gap-2">
<Virtualizer overscan={3}> <Spinner className="size-5" />
{data.map((item) => renderItem(item))} <span className="text-sm font-medium">Fetching new notes...</span>
</Virtualizer> </div>
)} </div>
{data?.length && hasNextPage ? ( ) : null}
<div> {isLoading ? (
<button <div className="flex h-16 w-full items-center justify-center gap-2">
type="button" <Spinner className="size-5" />
onClick={() => fetchNextPage()} <span className="text-sm font-medium">Loading...</span>
disabled={isFetchingNextPage || isLoading} </div>
className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-xl bg-neutral-100 px-3 font-medium hover:bg-neutral-50 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20" ) : !data.length ? (
> <Empty />
{isFetchingNextPage ? ( ) : (
<Spinner className="size-5" /> <Virtualizer overscan={3}>
) : ( {data.map((item) => renderItem(item))}
<> </Virtualizer>
<ArrowRightCircleIcon className="size-5" /> )}
Load more {data?.length && hasNextPage ? (
</> <div>
)} <button
</button> type="button"
</div> onClick={() => fetchNextPage()}
) : null} disabled={isFetchingNextPage || isLoading}
</div> className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-xl bg-neutral-100 px-3 font-medium hover:bg-neutral-50 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
); >
{isFetchingNextPage ? (
<Spinner className="size-5" />
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
</>
)}
</button>
</div>
) : null}
</div>
);
} }
function Empty() { function Empty() {
return ( return (
<div className="flex flex-col py-10 gap-10"> <div className="flex flex-col py-10 gap-10">
<div className="text-center flex flex-col items-center justify-center"> <div className="text-center flex flex-col items-center justify-center">
<div className="size-24 bg-blue-100 flex flex-col items-center justify-end overflow-hidden dark:bg-blue-900 rounded-full mb-8"> <div className="size-24 bg-blue-100 flex flex-col items-center justify-end overflow-hidden dark:bg-blue-900 rounded-full mb-8">
<div className="w-12 h-16 bg-gradient-to-b from-blue-500 dark:from-blue-200 to-blue-50 dark:to-blue-900 rounded-t-lg" /> <div className="w-12 h-16 bg-gradient-to-b from-blue-500 dark:from-blue-200 to-blue-50 dark:to-blue-900 rounded-t-lg" />
</div> </div>
<p className="text-lg font-medium">Your newsfeed is empty</p> <p className="text-lg font-medium">Your newsfeed is empty</p>
<p className="leading-tight text-neutral-700 dark:text-neutral-300"> <p className="leading-tight text-neutral-700 dark:text-neutral-300">
Here are few suggestions to get started. Here are few suggestions to get started.
</p> </p>
</div> </div>
<div className="flex flex-col px-3 gap-2"> <div className="flex flex-col px-3 gap-2">
<Link <Link
to="/trending/notes" to="/trending/notes"
className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3" className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3"
> >
<ArrowRightIcon className="size-5" /> <ArrowRightIcon className="size-5" />
Show trending notes Show trending notes
</Link> </Link>
<Link <Link
to="/trending/users" to="/trending/users"
className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3" className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3"
> >
<ArrowRightIcon className="size-5" /> <ArrowRightIcon className="size-5" />
Discover trending users Discover trending users
</Link> </Link>
</div> </div>
</div> </div>
); );
} }

View File

@ -11,144 +11,141 @@ import { createFileRoute } from "@tanstack/react-router";
import { Virtualizer } from "virtua"; import { Virtualizer } from "virtua";
export const Route = createFileRoute("/newsfeed")({ export const Route = createFileRoute("/newsfeed")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => { validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return { return {
account: search.account, account: search.account,
label: search.label, label: search.label,
name: search.name, name: search.name,
}; };
}, },
beforeLoad: async ({ context }) => { beforeLoad: async ({ context }) => {
const ark = context.ark; const ark = context.ark;
const settings = await ark.get_settings(); const settings = await ark.get_settings();
return { settings }; return { settings };
}, },
component: Screen, component: Screen,
}); });
export function Screen() { export function Screen() {
const { label, account } = Route.useSearch(); const { label, account } = Route.useSearch();
const { ark } = Route.useRouteContext(); const { ark } = Route.useRouteContext();
const { const {
data, data,
isLoading, isLoading,
isFetching, isFetching,
isFetchingNextPage, isFetchingNextPage,
hasNextPage, hasNextPage,
fetchNextPage, fetchNextPage,
} = useInfiniteQuery({ } = useInfiniteQuery({
queryKey: [label, account], queryKey: [label, account],
initialPageParam: 0, initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => { queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events(20, pageParam); const events = await ark.get_local_events(20, pageParam);
return events; return events;
}, },
getNextPageParam: (lastPage) => { getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
const lastEvent = lastPage?.at(-1); select: (data) => data?.pages.flatMap((page) => page),
return lastEvent ? lastEvent.created_at - 1 : null; refetchOnWindowFocus: false,
}, });
select: (data) => data?.pages.flatMap((page) => page),
refetchOnWindowFocus: false,
});
const renderItem = (event: Event) => { const renderItem = (event: Event) => {
if (!event) return; if (!event) return;
switch (event.kind) { switch (event.kind) {
case Kind.Repost: case Kind.Repost:
return <RepostNote key={event.id} event={event} />; return <RepostNote key={event.id} event={event} />;
default: { default: {
const isConversation = const isConversation =
event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention") event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention")
.length > 0; .length > 0;
const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0; const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0;
if (isConversation) { if (isConversation) {
return <Conversation key={event.id} event={event} className="mb-3" />; return <Conversation key={event.id} event={event} className="mb-3" />;
} }
if (isQuote) { if (isQuote) {
return <Quote key={event.id} event={event} className="mb-3" />; return <Quote key={event.id} event={event} className="mb-3" />;
} }
return <TextNote key={event.id} event={event} className="mb-3" />; return <TextNote key={event.id} event={event} className="mb-3" />;
} }
} }
}; };
return ( return (
<div className="p-2 w-full h-full overflow-y-auto scrollbar-none"> <div className="p-2 w-full h-full overflow-y-auto scrollbar-none">
{isFetching && !isLoading && !isFetchingNextPage ? ( {isFetching && !isLoading && !isFetchingNextPage ? (
<div className="w-full h-11 flex items-center justify-center"> <div className="w-full h-11 flex items-center justify-center bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<Spinner className="size-5" /> <Spinner className="size-5" />
<span className="text-sm font-medium">Fetching new notes...</span> <span className="text-sm font-medium">Fetching new notes...</span>
</div> </div>
</div> </div>
) : null} ) : null}
{isLoading ? ( {isLoading ? (
<div className="flex h-16 w-full items-center justify-center gap-2"> <div className="flex h-16 w-full items-center justify-center gap-2">
<Spinner className="size-5" /> <Spinner className="size-5" />
<span className="text-sm font-medium">Loading...</span> <span className="text-sm font-medium">Loading...</span>
</div> </div>
) : !data.length ? ( ) : !data.length ? (
<Empty /> <Empty />
) : ( ) : (
<Virtualizer overscan={3}> <Virtualizer overscan={3}>
{data.map((item) => renderItem(item))} {data.map((item) => renderItem(item))}
</Virtualizer> </Virtualizer>
)} )}
{data?.length && hasNextPage ? ( {data?.length && hasNextPage ? (
<div> <div>
<button <button
type="button" type="button"
onClick={() => fetchNextPage()} onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading} disabled={isFetchingNextPage || isLoading}
className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-xl bg-black/5 px-3 font-medium hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20" className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-xl bg-black/5 px-3 font-medium hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
> >
{isFetchingNextPage ? ( {isFetchingNextPage ? (
<Spinner className="size-5" /> <Spinner className="size-5" />
) : ( ) : (
<> <>
<ArrowRightCircleIcon className="size-5" /> <ArrowRightCircleIcon className="size-5" />
Load more Load more
</> </>
)} )}
</button> </button>
</div> </div>
) : null} ) : null}
</div> </div>
); );
} }
function Empty() { function Empty() {
return ( return (
<div className="flex flex-col py-10 gap-10"> <div className="flex flex-col py-10 gap-10">
<div className="text-center flex flex-col items-center justify-center"> <div className="text-center flex flex-col items-center justify-center">
<div className="size-24 bg-blue-100 flex flex-col items-center justify-end overflow-hidden dark:bg-blue-900 rounded-full mb-8"> <div className="size-24 bg-blue-100 flex flex-col items-center justify-end overflow-hidden dark:bg-blue-900 rounded-full mb-8">
<div className="w-12 h-16 bg-gradient-to-b from-blue-500 dark:from-blue-200 to-blue-50 dark:to-blue-900 rounded-t-lg" /> <div className="w-12 h-16 bg-gradient-to-b from-blue-500 dark:from-blue-200 to-blue-50 dark:to-blue-900 rounded-t-lg" />
</div> </div>
<p className="text-lg font-medium">Your newsfeed is empty</p> <p className="text-lg font-medium">Your newsfeed is empty</p>
<p className="leading-tight text-neutral-700 dark:text-neutral-300"> <p className="leading-tight text-neutral-700 dark:text-neutral-300">
Here are few suggestions to get started. Here are few suggestions to get started.
</p> </p>
</div> </div>
<div className="flex flex-col px-3 gap-2"> <div className="flex flex-col px-3 gap-2">
<Link <Link
to="/trending/notes" to="/trending/notes"
className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3" className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3"
> >
<ArrowRightIcon className="size-5" /> <ArrowRightIcon className="size-5" />
Show trending notes Show trending notes
</Link> </Link>
<Link <Link
to="/trending/users" to="/trending/users"
className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3" className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3"
> >
<ArrowRightIcon className="size-5" /> <ArrowRightIcon className="size-5" />
Discover trending users Discover trending users
</Link> </Link>
</div> </div>
</div> </div>
); );
} }

View File

@ -11,86 +11,86 @@ import { Suspense } from "react";
import { Await } from "@tanstack/react-router"; import { Await } from "@tanstack/react-router";
export const Route = createFileRoute("/users/$pubkey")({ export const Route = createFileRoute("/users/$pubkey")({
beforeLoad: async ({ context }) => { beforeLoad: async ({ context }) => {
const ark = context.ark; const ark = context.ark;
const settings = await ark.get_settings(); const settings = await ark.get_settings();
return { settings }; return { settings };
}, },
loader: async ({ params, context }) => { loader: async ({ params, context }) => {
const ark = context.ark; const ark = context.ark;
return { data: defer(ark.get_events_from(params.pubkey, 50)) }; return { data: defer(ark.get_events_by(params.pubkey, 50)) };
}, },
component: Screen, component: Screen,
}); });
function Screen() { function Screen() {
const { pubkey } = Route.useParams(); const { pubkey } = Route.useParams();
const { data } = Route.useLoaderData(); const { data } = Route.useLoaderData();
const renderItem = (event: Event) => { const renderItem = (event: Event) => {
if (!event) return; if (!event) return;
switch (event.kind) { switch (event.kind) {
case Kind.Repost: case Kind.Repost:
return <RepostNote key={event.id} event={event} />; return <RepostNote key={event.id} event={event} />;
default: { default: {
const isConversation = const isConversation =
event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention") event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention")
.length > 0; .length > 0;
const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0; const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0;
if (isConversation) { if (isConversation) {
return <Conversation key={event.id} event={event} className="mb-3" />; return <Conversation key={event.id} event={event} className="mb-3" />;
} }
if (isQuote) { if (isQuote) {
return <Quote key={event.id} event={event} className="mb-3" />; return <Quote key={event.id} event={event} className="mb-3" />;
} }
return <TextNote key={event.id} event={event} className="mb-3" />; return <TextNote key={event.id} event={event} className="mb-3" />;
} }
} }
}; };
return ( return (
<Container withDrag> <Container withDrag>
<Box className="px-0 scrollbar-none bg-black/5 dark:bg-white/5 backdrop-blur-sm"> <Box className="px-0 scrollbar-none bg-black/5 dark:bg-white/5 backdrop-blur-sm">
<WindowVirtualizer> <WindowVirtualizer>
<User.Provider pubkey={pubkey}> <User.Provider pubkey={pubkey}>
<User.Root> <User.Root>
<User.Cover className="h-44 w-full object-cover" /> <User.Cover className="h-44 w-full object-cover" />
<div className="relative -mt-8 flex flex-col px-3"> <div className="relative -mt-8 flex flex-col px-3">
<User.Avatar className="size-14 rounded-full" /> <User.Avatar className="size-14 rounded-full" />
<div className="mb-4 inline-flex items-center justify-between"> <div className="mb-4 inline-flex items-center justify-between">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<User.Name className="text-lg font-semibold leading-tight" /> <User.Name className="text-lg font-semibold leading-tight" />
<User.NIP05 /> <User.NIP05 />
</div> </div>
<User.Button className="h-9 w-24 rounded-full inline-flex items-center justify-center bg-black text-sm font-medium text-white hover:bg-neutral-900 dark:bg-neutral-900" /> <User.Button className="h-9 w-24 rounded-full inline-flex items-center justify-center bg-black text-sm font-medium text-white hover:bg-neutral-900 dark:bg-neutral-900" />
</div> </div>
<User.About /> <User.About />
</div> </div>
</User.Root> </User.Root>
</User.Provider> </User.Provider>
<div className="px-3 mt-5"> <div className="px-3 mt-5">
<div className="mb-3"> <div className="mb-3">
<h3 className="text-lg font-semibold">Latest notes</h3> <h3 className="text-lg font-semibold">Latest notes</h3>
</div> </div>
<Suspense <Suspense
fallback={ fallback={
<div className="flex h-20 w-full items-center justify-center gap-1.5 text-sm font-medium"> <div className="flex h-20 w-full items-center justify-center gap-1.5 text-sm font-medium">
<Spinner className="size-5" /> <Spinner className="size-5" />
Loading... Loading...
</div> </div>
} }
> >
<Await promise={data}> <Await promise={data}>
{(events) => events.map((event) => renderItem(event))} {(events) => events.map((event) => renderItem(event))}
</Await> </Await>
</Suspense> </Suspense>
</div> </div>
</WindowVirtualizer> </WindowVirtualizer>
</Box> </Box>
</Container> </Container>
); );
} }

View File

@ -8,65 +8,65 @@ import { useInfiniteQuery } from "@tanstack/react-query";
import { useRouteContext } from "@tanstack/react-router"; import { useRouteContext } from "@tanstack/react-router";
export function EventList({ id }: { id: string }) { export function EventList({ id }: { id: string }) {
const { ark } = useRouteContext({ strict: false }); const { ark } = useRouteContext({ strict: false });
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } = const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({ useInfiniteQuery({
queryKey: ["events", id], queryKey: ["events", id],
initialPageParam: 0, initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => { queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events_from(id, FETCH_LIMIT, pageParam); const events = await ark.get_events_by(id, FETCH_LIMIT, pageParam);
return events; return events;
}, },
getNextPageParam: (lastPage) => { getNextPageParam: (lastPage) => {
const lastEvent = lastPage?.at(-1); const lastEvent = lastPage?.at(-1);
return lastEvent ? lastEvent.created_at - 1 : null; return lastEvent ? lastEvent.created_at - 1 : null;
}, },
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });
const renderItem = (event: Event) => { const renderItem = (event: Event) => {
if (!event) return; if (!event) return;
switch (event.kind) { switch (event.kind) {
case Kind.Repost: case Kind.Repost:
return <RepostNote key={event.id} event={event} />; return <RepostNote key={event.id} event={event} />;
default: default:
return <TextNote key={event.id} event={event} />; return <TextNote key={event.id} event={event} />;
} }
}; };
return ( return (
<div> <div>
{isLoading ? ( {isLoading ? (
<div className="flex h-20 w-full flex-col items-center justify-center gap-1"> <div className="flex h-20 w-full flex-col items-center justify-center gap-1">
<Spinner className="size-5" /> <Spinner className="size-5" />
</div> </div>
) : !data.length ? ( ) : !data.length ? (
<div className="flex items-center gap-2 rounded-xl bg-neutral-50 p-5 dark:bg-neutral-950"> <div className="flex items-center gap-2 rounded-xl bg-neutral-50 p-5 dark:bg-neutral-950">
<InfoIcon className="size-6" /> <InfoIcon className="size-6" />
<p>Empty newsfeed.</p> <p>Empty newsfeed.</p>
</div> </div>
) : ( ) : (
data.map((item) => renderItem(item)) data.map((item) => renderItem(item))
)} )}
<div className="flex h-20 items-center justify-center"> <div className="flex h-20 items-center justify-center">
{hasNextPage ? ( {hasNextPage ? (
<button <button
type="button" type="button"
onClick={() => fetchNextPage()} onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage} disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800" className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800"
> >
{isFetchingNextPage ? ( {isFetchingNextPage ? (
<Spinner className="size-5" /> <Spinner className="size-5" />
) : ( ) : (
<> <>
<ArrowRightCircleIcon className="size-5" /> <ArrowRightCircleIcon className="size-5" />
Load more Load more
</> </>
)} )}
</button> </button>
) : null} ) : null}
</div> </div>
</div> </div>
); );
} }

File diff suppressed because it is too large Load Diff

View File

@ -127,10 +127,12 @@ fn main() {
nostr::metadata::zap_event, nostr::metadata::zap_event,
nostr::metadata::friend_to_friend, nostr::metadata::friend_to_friend,
nostr::event::get_event, nostr::event::get_event,
nostr::event::get_events_from, nostr::event::get_thread,
nostr::event::get_events, nostr::event::get_events_by,
nostr::event::get_events_from_interests, nostr::event::get_local_events,
nostr::event::get_event_thread, nostr::event::get_global_events,
nostr::event::get_hashtag_events,
nostr::event::get_group_events,
nostr::event::publish, nostr::event::publish,
nostr::event::repost, nostr::event::repost,
commands::folder::show_in_folder, commands::folder::show_in_folder,

View File

@ -30,7 +30,7 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<String, Stri
Some(id) => { Some(id) => {
let filter = Filter::new().id(id); let filter = Filter::new().id(id);
match &client match client
.get_events_of(vec![filter], Some(Duration::from_secs(10))) .get_events_of(vec![filter], Some(Duration::from_secs(10)))
.await .await
{ {
@ -49,7 +49,24 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<String, Stri
} }
#[tauri::command] #[tauri::command]
pub async fn get_events_from( pub async fn get_thread(id: &str, state: State<'_, Nostr>) -> Result<Vec<Event>, String> {
let client = &state.client;
match EventId::from_hex(id) {
Ok(event_id) => {
let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id);
match client.get_events_of(vec![filter], None).await {
Ok(events) => Ok(events),
Err(err) => Err(err.to_string()),
}
}
Err(_) => Err("Event ID is not valid".into()),
}
}
#[tauri::command]
pub async fn get_events_by(
public_key: &str, public_key: &str,
limit: usize, limit: usize,
as_of: Option<&str>, as_of: Option<&str>,
@ -57,32 +74,31 @@ pub async fn get_events_from(
) -> Result<Vec<Event>, String> { ) -> Result<Vec<Event>, String> {
let client = &state.client; let client = &state.client;
if let Ok(author) = PublicKey::from_str(public_key) { match PublicKey::from_str(public_key) {
let until = match as_of { Ok(author) => {
Some(until) => Timestamp::from_str(until).unwrap(), let until = match as_of {
None => Timestamp::now(), Some(until) => Timestamp::from_str(until).unwrap(),
}; None => Timestamp::now(),
let filter = Filter::new() };
.kinds(vec![Kind::TextNote, Kind::Repost]) let filter = Filter::new()
.author(author) .kinds(vec![Kind::TextNote, Kind::Repost])
.limit(limit) .author(author)
.until(until); .limit(limit)
.until(until);
match client.get_events_of(vec![filter], None).await { match client.get_events_of(vec![filter], None).await {
Ok(events) => Ok(events), Ok(events) => Ok(events),
Err(err) => Err(err.to_string()), Err(err) => Err(err.to_string()),
}
} }
} else { Err(err) => Err(err.to_string()),
Err("Public Key is not valid, please check again.".into())
} }
} }
#[tauri::command] #[tauri::command]
pub async fn get_events( pub async fn get_local_events(
limit: usize, limit: usize,
until: Option<&str>, until: Option<&str>,
contacts: Option<Vec<&str>>,
global: bool,
state: State<'_, Nostr>, state: State<'_, Nostr>,
) -> Result<Vec<Event>, String> { ) -> Result<Vec<Event>, String> {
let client = &state.client; let client = &state.client;
@ -91,66 +107,57 @@ pub async fn get_events(
None => Timestamp::now(), None => Timestamp::now(),
}; };
match global { match client
true => { .get_contact_list_public_keys(Some(Duration::from_secs(10)))
.await
{
Ok(contacts) => {
let filter = Filter::new() let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost]) .kinds(vec![Kind::TextNote, Kind::Repost])
.limit(limit) .limit(limit)
.authors(contacts)
.until(as_of); .until(as_of);
match client match client
.get_events_of(vec![filter], Some(Duration::from_secs(15))) .get_events_of(vec![filter], Some(Duration::from_secs(8)))
.await .await
{ {
Ok(events) => Ok(events), Ok(events) => Ok(events),
Err(err) => Err(err.to_string()), Err(err) => Err(err.to_string()),
} }
} }
false => { Err(err) => Err(err.to_string()),
let authors = match contacts {
Some(val) => {
let c: Vec<PublicKey> = val
.into_iter()
.map(|key| PublicKey::from_str(key).unwrap())
.collect();
Some(c)
}
None => {
match client
.get_contact_list_public_keys(Some(Duration::from_secs(10)))
.await
{
Ok(val) => Some(val),
Err(_) => None,
}
}
};
match authors {
Some(val) => {
if val.is_empty() {
Err("Get local events but contact list is empty".into())
} else {
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(limit)
.authors(val.clone())
.until(as_of);
match client.get_events_of(vec![filter], None).await {
Ok(events) => Ok(events),
Err(err) => Err(err.to_string()),
}
}
}
None => Err("Get local events but contact list is empty".into()),
}
}
} }
} }
#[tauri::command] #[tauri::command]
pub async fn get_events_from_interests( pub async fn get_global_events(
limit: usize,
until: Option<&str>,
state: State<'_, Nostr>,
) -> Result<Vec<Event>, String> {
let client = &state.client;
let as_of = match until {
Some(until) => Timestamp::from_str(until).unwrap(),
None => Timestamp::now(),
};
let 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>, hashtags: Vec<&str>,
limit: usize, limit: usize,
until: Option<&str>, until: Option<&str>,
@ -174,19 +181,30 @@ pub async fn get_events_from_interests(
} }
#[tauri::command] #[tauri::command]
pub async fn get_event_thread(id: &str, state: State<'_, Nostr>) -> Result<Vec<Event>, String> { pub async fn get_group_events(
list: Vec<&str>,
limit: usize,
until: Option<&str>,
state: State<'_, Nostr>,
) -> Result<Vec<Event>, String> {
let client = &state.client; let client = &state.client;
let as_of = match until {
Some(until) => Timestamp::from_str(until).unwrap(),
None => Timestamp::now(),
};
let authors: Vec<PublicKey> = list
.into_iter()
.map(|hex| PublicKey::from_hex(hex).unwrap())
.collect();
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(limit)
.until(as_of)
.authors(authors);
match EventId::from_hex(id) { match client.get_events_of(vec![filter], None).await {
Ok(event_id) => { Ok(events) => Ok(events),
let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id); Err(err) => Err(err.to_string()),
match client.get_events_of(vec![filter], None).await {
Ok(events) => Ok(events),
Err(err) => Err(err.to_string()),
}
}
Err(_) => Err("Event ID is not valid".into()),
} }
} }
@ -210,9 +228,8 @@ pub async fn repost(raw: &str, state: State<'_, Nostr>) -> Result<EventId, Strin
let client = &state.client; let client = &state.client;
let event = Event::from_json(raw).unwrap(); let event = Event::from_json(raw).unwrap();
if let Ok(event_id) = client.repost(&event, None).await { match client.repost(&event, None).await {
Ok(event_id) Ok(event_id) => Ok(event_id),
} else { Err(err) => Err(err.to_string()),
Err("Repost failed".into())
} }
} }