mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-29 16:30:55 +00:00
feat: improve tauri commands
This commit is contained in:
parent
cac774a0c1
commit
9b5867f80c
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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
@ -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,
|
||||||
|
@ -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())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user