From a14aeaeb557744cec59bb523717b454cad5f41af Mon Sep 17 00:00:00 2001 From: reya Date: Sat, 13 Apr 2024 08:30:58 +0700 Subject: [PATCH] feat: add empty state and polish trending column --- apps/desktop2/src/components/repost.tsx | 4 +- apps/desktop2/src/components/suggest.tsx | 56 ----- apps/desktop2/src/components/text.tsx | 5 +- apps/desktop2/src/routes/antenas.tsx | 50 +++-- apps/desktop2/src/routes/auth/new/backup.tsx | 9 +- apps/desktop2/src/routes/auth/settings.tsx | 8 +- apps/desktop2/src/routes/foryou.tsx | 54 +++-- .../routes/{global.lazy.tsx => global.tsx} | 79 ++++--- apps/desktop2/src/routes/group.tsx | 50 +++-- apps/desktop2/src/routes/newsfeed.tsx | 210 ++++-------------- apps/desktop2/src/routes/trending.lazy.tsx | 71 ------ apps/desktop2/src/routes/trending.notes.tsx | 67 ++++++ apps/desktop2/src/routes/trending.tsx | 69 ++++++ apps/desktop2/src/routes/trending.users.tsx | 71 ++++++ packages/ark/src/ark.ts | 9 +- packages/icons/src/article.tsx | 19 +- packages/icons/src/groupFeeds.tsx | 31 +-- src-tauri/resources/official_columns.json | 2 +- src-tauri/src/nostr/event.rs | 97 ++++---- 19 files changed, 505 insertions(+), 456 deletions(-) delete mode 100644 apps/desktop2/src/components/suggest.tsx rename apps/desktop2/src/routes/{global.lazy.tsx => global.tsx} (53%) delete mode 100644 apps/desktop2/src/routes/trending.lazy.tsx create mode 100644 apps/desktop2/src/routes/trending.notes.tsx create mode 100644 apps/desktop2/src/routes/trending.tsx create mode 100644 apps/desktop2/src/routes/trending.users.tsx diff --git a/apps/desktop2/src/components/repost.tsx b/apps/desktop2/src/components/repost.tsx index 97ee0ac7..1a0b4206 100644 --- a/apps/desktop2/src/components/repost.tsx +++ b/apps/desktop2/src/components/repost.tsx @@ -13,7 +13,7 @@ export function RepostNote({ event: Event; className?: string; }) { - const { ark } = useRouteContext({ strict: false }); + const { ark, settings } = useRouteContext({ strict: false }); const { t } = useTranslation(); const { isLoading, @@ -104,7 +104,7 @@ export function RepostNote({
- + {settings.zap ? : null}
diff --git a/apps/desktop2/src/components/suggest.tsx b/apps/desktop2/src/components/suggest.tsx deleted file mode 100644 index 2844fa4d..00000000 --- a/apps/desktop2/src/components/suggest.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { LoaderIcon } from "@lume/icons"; -import { useQuery } from "@tanstack/react-query"; -import { useTranslation } from "react-i18next"; -import { User } from "@lume/ui"; - -export function Suggest() { - const { t } = useTranslation(); - const { isLoading, isError, data } = useQuery({ - queryKey: ["trending-users"], - queryFn: async ({ signal }: { signal: AbortSignal }) => { - const res = await fetch("https://api.nostr.band/v0/trending/profiles", { - signal, - }); - if (!res.ok) { - throw new Error("Failed to fetch trending users from nostr.band API."); - } - return res.json(); - }, - }); - - return ( -
-
- Suggested Follows -
- {isLoading ? ( -
- -
- ) : isError ? ( -
- {t("suggestion.error")} -
- ) : ( - data?.profiles.map((item: { pubkey: string }) => ( -
- - -
-
-
- - -
- -
- -
-
-
-
- )) - )} -
- ); -} diff --git a/apps/desktop2/src/components/text.tsx b/apps/desktop2/src/components/text.tsx index bd02f43e..3c8b70c9 100644 --- a/apps/desktop2/src/components/text.tsx +++ b/apps/desktop2/src/components/text.tsx @@ -1,6 +1,7 @@ import { Event } from "@lume/types"; import { Note } from "@lume/ui"; import { cn } from "@lume/utils"; +import { useRouteContext } from "@tanstack/react-router"; export function TextNote({ event, @@ -9,6 +10,8 @@ export function TextNote({ event: Event; className?: string; }) { + const { settings } = useRouteContext({ strict: false }); + return ( - + {settings.zap ? : null} diff --git a/apps/desktop2/src/routes/antenas.tsx b/apps/desktop2/src/routes/antenas.tsx index 365b6cfd..d00c9c33 100644 --- a/apps/desktop2/src/routes/antenas.tsx +++ b/apps/desktop2/src/routes/antenas.tsx @@ -1,12 +1,10 @@ import { RepostNote } from "@/components/repost"; -import { Suggest } from "@/components/suggest"; import { TextNote } from "@/components/text"; -import { LoaderIcon, ArrowRightCircleIcon, InfoIcon } from "@lume/icons"; +import { LoaderIcon, ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons"; import { ColumnRouteSearch, Event, Kind } from "@lume/types"; import { Column } from "@lume/ui"; import { useInfiniteQuery } from "@tanstack/react-query"; -import { createFileRoute } from "@tanstack/react-router"; -import { useTranslation } from "react-i18next"; +import { Link, createFileRoute } from "@tanstack/react-router"; import { Virtualizer } from "virtua"; export const Route = createFileRoute("/antenas")({ @@ -23,7 +21,6 @@ export const Route = createFileRoute("/antenas")({ export function Screen() { const { label, name, account } = Route.useSearch(); const { ark } = Route.useRouteContext(); - const { t } = useTranslation(); const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useInfiniteQuery({ queryKey: [name, account], @@ -59,16 +56,7 @@ export function Screen() { ) : !data.length ? ( -
-
- -
-

{t("emptyFeedTitle")}

-

{t("emptyFeedSubtitle")}

-
-
- -
+ ) : ( {data.map((item) => renderItem(item))} @@ -97,3 +85,35 @@ export function Screen() { ); } + +function Empty() { + return ( +
+
+
+
+
+

Your newsfeed is empty

+

+ Here are few suggestions to get started. +

+
+
+ + + Show trending notes + + + + Discover trending users + +
+
+ ); +} diff --git a/apps/desktop2/src/routes/auth/new/backup.tsx b/apps/desktop2/src/routes/auth/new/backup.tsx index e90f21e4..293c6105 100644 --- a/apps/desktop2/src/routes/auth/new/backup.tsx +++ b/apps/desktop2/src/routes/auth/new/backup.tsx @@ -7,13 +7,18 @@ import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import * as Checkbox from "@radix-ui/react-checkbox"; import { CheckIcon } from "@lume/icons"; +import { AppRouteSearch } from "@lume/types"; export const Route = createFileRoute("/auth/new/backup")({ + validateSearch: (search: Record): AppRouteSearch => { + return { + account: search.account, + }; + }, component: Screen, }); function Screen() { - // @ts-ignore, magic!!! const { account } = Route.useSearch(); const { t } = useTranslation(); @@ -32,7 +37,7 @@ function Screen() { } else { return navigate({ to: "/auth/settings", - search: { account, new: true }, + search: { account }, }); } } diff --git a/apps/desktop2/src/routes/auth/settings.tsx b/apps/desktop2/src/routes/auth/settings.tsx index c08cbfc0..da5f23f1 100644 --- a/apps/desktop2/src/routes/auth/settings.tsx +++ b/apps/desktop2/src/routes/auth/settings.tsx @@ -42,28 +42,28 @@ function Screen() { await requestPermission(); setNewSettings((prev) => ({ ...prev, - notification: !settings.notification, + notification: !newSettings.notification, })); }; const toggleAutoUpdate = () => { setNewSettings((prev) => ({ ...prev, - autoUpdate: !settings.autoUpdate, + autoUpdate: !newSettings.autoUpdate, })); }; const toggleEnhancedPrivacy = () => { setNewSettings((prev) => ({ ...prev, - enhancedPrivacy: !settings.enhancedPrivacy, + enhancedPrivacy: !newSettings.enhancedPrivacy, })); }; const toggleZap = () => { setNewSettings((prev) => ({ ...prev, - zap: !settings.zap, + zap: !newSettings.zap, })); }; diff --git a/apps/desktop2/src/routes/foryou.tsx b/apps/desktop2/src/routes/foryou.tsx index 6247dee7..fcbde498 100644 --- a/apps/desktop2/src/routes/foryou.tsx +++ b/apps/desktop2/src/routes/foryou.tsx @@ -1,12 +1,10 @@ import { RepostNote } from "@/components/repost"; -import { Suggest } from "@/components/suggest"; import { TextNote } from "@/components/text"; -import { LoaderIcon, ArrowRightCircleIcon, InfoIcon } from "@lume/icons"; +import { LoaderIcon, ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons"; import { ColumnRouteSearch, Event, Kind } from "@lume/types"; import { Column } from "@lume/ui"; import { useInfiniteQuery } from "@tanstack/react-query"; -import { createFileRoute, redirect } from "@tanstack/react-router"; -import { useTranslation } from "react-i18next"; +import { Link, createFileRoute, redirect } from "@tanstack/react-router"; import { Virtualizer } from "virtua"; export const Route = createFileRoute("/foryou")({ @@ -41,7 +39,6 @@ export const Route = createFileRoute("/foryou")({ export function Screen() { const { label, name, account } = Route.useSearch(); const { ark, interests } = Route.useRouteContext(); - const { t } = useTranslation(); const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useInfiniteQuery({ queryKey: [name, account], @@ -84,20 +81,7 @@ export function Screen() {
) : !data.length ? ( -
-
- -
-

- {t("global.emptyFeedTitle")} -

-

- {t("global.emptyFeedSubtitle")} -

-
-
- -
+ ) : ( {data.map((item) => renderItem(item))} @@ -127,3 +111,35 @@ export function Screen() { ); } + +function Empty() { + return ( +
+
+
+
+
+

Your newsfeed is empty

+

+ Here are few suggestions to get started. +

+
+
+ + + Show trending notes + + + + Discover trending users + +
+
+ ); +} diff --git a/apps/desktop2/src/routes/global.lazy.tsx b/apps/desktop2/src/routes/global.tsx similarity index 53% rename from apps/desktop2/src/routes/global.lazy.tsx rename to apps/desktop2/src/routes/global.tsx index 46ce1108..e096052b 100644 --- a/apps/desktop2/src/routes/global.lazy.tsx +++ b/apps/desktop2/src/routes/global.tsx @@ -1,23 +1,32 @@ import { RepostNote } from "@/components/repost"; -import { Suggest } from "@/components/suggest"; import { TextNote } from "@/components/text"; -import { LoaderIcon, ArrowRightCircleIcon, InfoIcon } from "@lume/icons"; -import { Event, Kind } from "@lume/types"; +import { LoaderIcon, ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons"; +import { ColumnRouteSearch, Event, Kind } from "@lume/types"; import { Column } from "@lume/ui"; import { useInfiniteQuery } from "@tanstack/react-query"; -import { createLazyFileRoute } from "@tanstack/react-router"; -import { useTranslation } from "react-i18next"; +import { Link, createFileRoute } from "@tanstack/react-router"; import { Virtualizer } from "virtua"; -export const Route = createLazyFileRoute("/global")({ +export const Route = createFileRoute("/global")({ + validateSearch: (search: Record): ColumnRouteSearch => { + return { + account: search.account, + label: search.label, + name: search.name, + }; + }, + beforeLoad: async ({ context }) => { + const ark = context.ark; + const settings = await ark.get_settings(); + + return { settings }; + }, component: Screen, }); export function Screen() { - // @ts-ignore, just work!!! - const { id, name, account } = Route.useSearch(); + const { label, name, account } = Route.useSearch(); const { ark } = Route.useRouteContext(); - const { t } = useTranslation(); const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useInfiniteQuery({ queryKey: ["global", account], @@ -46,7 +55,7 @@ export function Screen() { return ( - + {isLoading ? (
@@ -55,32 +64,18 @@ export function Screen() {
) : !data.length ? ( -
-
- -
-

- {t("global.emptyFeedTitle")} -

-

- {t("global.emptyFeedSubtitle")} -

-
-
- -
+ ) : ( {data.map((item) => renderItem(item))} )} - {data?.length && hasNextPage ? (
) : !data.length ? ( -
-
- -
-

- {t("global.emptyFeedTitle")} -

-

- {t("global.emptyFeedSubtitle")} -

-
-
- -
+ ) : ( {data.map((item) => renderItem(item))} )} - {data?.length && hasNextPage ? (
-
- ) : !data.length ? ( -
-
- -
-

{t("emptyFeedTitle")}

-

{t("emptyFeedSubtitle")}

-
-
- -
- ) : ( - - {data.map((item) => renderItem(item))} - - )} -
-
- ); -} diff --git a/apps/desktop2/src/routes/trending.notes.tsx b/apps/desktop2/src/routes/trending.notes.tsx new file mode 100644 index 00000000..8f3be65d --- /dev/null +++ b/apps/desktop2/src/routes/trending.notes.tsx @@ -0,0 +1,67 @@ +import { RepostNote } from "@/components/repost"; +import { TextNote } from "@/components/text"; +import { LoaderIcon } from "@lume/icons"; +import { Event, Kind } from "@lume/types"; +import { Await, createFileRoute } from "@tanstack/react-router"; +import { Virtualizer } from "virtua"; +import { fetch } from "@tauri-apps/plugin-http"; +import { defer } from "@tanstack/react-router"; +import { Suspense } from "react"; + +export const Route = createFileRoute("/trending/notes")({ + loader: async ({ abortController }) => { + try { + return { + data: defer( + fetch("https://api.nostr.band/v0/trending/notes", { + signal: abortController.signal, + }) + .then((res) => res.json()) + .then((res) => res.notes.map((item) => item.event) as Event[]), + ), + }; + } catch (e) { + throw new Error(String(e)); + } + }, + component: Screen, +}); + +export function Screen() { + const { data } = Route.useLoaderData(); + + const renderItem = (event: Event) => { + if (!event) return; + switch (event.kind) { + case Kind.Repost: + return ; + default: + return ; + } + }; + + return ( +
+ + + +
+ } + > + + {(notes) => notes.map((event) => renderItem(event))} + + + +
+ ); +} diff --git a/apps/desktop2/src/routes/trending.tsx b/apps/desktop2/src/routes/trending.tsx new file mode 100644 index 00000000..31c696ff --- /dev/null +++ b/apps/desktop2/src/routes/trending.tsx @@ -0,0 +1,69 @@ +import { ArticleIcon, GroupFeedsIcon } from "@lume/icons"; +import { ColumnRouteSearch } from "@lume/types"; +import { Column } from "@lume/ui"; +import { cn } from "@lume/utils"; +import { Link, Outlet } from "@tanstack/react-router"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/trending")({ + validateSearch: (search: Record): ColumnRouteSearch => { + return { + account: search.account, + label: search.label, + name: search.name, + }; + }, + beforeLoad: async ({ context }) => { + const ark = context.ark; + const settings = await ark.get_settings(); + + return { settings }; + }, + component: Screen, +}); + +export function Screen() { + const { label, name } = Route.useSearch(); + + return ( + + +
+ + {({ isActive }) => ( +
+ + Notes +
+ )} + + + {({ isActive }) => ( +
+ + Users +
+ )} + +
+
+ + + +
+ ); +} diff --git a/apps/desktop2/src/routes/trending.users.tsx b/apps/desktop2/src/routes/trending.users.tsx new file mode 100644 index 00000000..026926ed --- /dev/null +++ b/apps/desktop2/src/routes/trending.users.tsx @@ -0,0 +1,71 @@ +import { LoaderIcon } from "@lume/icons"; +import { User } from "@lume/ui"; +import { Await, defer } from "@tanstack/react-router"; +import { createFileRoute } from "@tanstack/react-router"; +import { Suspense } from "react"; + +export const Route = createFileRoute("/trending/users")({ + loader: async ({ abortController }) => { + try { + return { + data: defer( + fetch("https://api.nostr.band/v0/trending/profiles", { + signal: abortController.signal, + }).then((res) => res.json()), + ), + }; + } catch (e) { + throw new Error(String(e)); + } + }, + component: Screen, +}); + +export function Screen() { + const { data } = Route.useLoaderData(); + + return ( +
+ + +
+ } + > + + {(users) => + users.profiles.map((item) => ( +
+ + +
+
+
+ + +
+ +
+ +
+
+
+
+ )) + } +
+ + + ); +} diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts index d93dc3c7..7e37c8ad 100644 --- a/packages/ark/src/ark.ts +++ b/packages/ark/src/ark.ts @@ -140,6 +140,8 @@ export class Ark { ) { try { let until: string = undefined; + let isGlobal = global ?? false; + if (asOf && asOf > 0) until = asOf.toString(); const dedup = true; @@ -150,7 +152,7 @@ export class Ark { limit, until, contacts, - global, + global: isGlobal, }); if (dedup) { @@ -175,8 +177,9 @@ export class Ark { .sort((a, b) => b.created_at - a.created_at); } - return nostrEvents.sort((a, b) => b.created_at - a.created_at); - } catch { + return nostrEvents; + } catch (e) { + console.error(String(e)); return []; } } diff --git a/packages/icons/src/article.tsx b/packages/icons/src/article.tsx index 6bc96cd5..6eeb0783 100644 --- a/packages/icons/src/article.tsx +++ b/packages/icons/src/article.tsx @@ -1,22 +1,17 @@ -import { SVGProps } from 'react'; +import { SVGProps } from "react"; -export function ArticleIcon(props: JSX.IntrinsicAttributes & SVGProps) { +export function ArticleIcon( + props: JSX.IntrinsicAttributes & SVGProps, +) { return ( - + + d="M20.248 15.25H17.25a2 2 0 0 0-2 2v2.998m4.998-4.998c.002-.026.002-.052.002-.078V5.75a2 2 0 0 0-2-2H5.75a2 2 0 0 0-2 2v12.5a2 2 0 0 0 2 2h9.422c.026 0 .052 0 .078-.002m4.998-4.998a2 2 0 0 1-.584 1.336l-3.078 3.078a2 2 0 0 1-1.336.584M8.75 8.75h6.5m-6.5 4h2.5" + /> ); } diff --git a/packages/icons/src/groupFeeds.tsx b/packages/icons/src/groupFeeds.tsx index be3d05a0..6414860f 100644 --- a/packages/icons/src/groupFeeds.tsx +++ b/packages/icons/src/groupFeeds.tsx @@ -1,24 +1,17 @@ import { SVGProps } from "react"; export function GroupFeedsIcon( - props: JSX.IntrinsicAttributes & SVGProps, + props: JSX.IntrinsicAttributes & SVGProps, ) { - return ( - - - - ); + return ( + + + + ); } diff --git a/src-tauri/resources/official_columns.json b/src-tauri/resources/official_columns.json index 5714c362..f61ba5a8 100644 --- a/src-tauri/resources/official_columns.json +++ b/src-tauri/resources/official_columns.json @@ -32,7 +32,7 @@ { "label": "gxtcIbgD8YNPbeI5o92I8", "name": "Trending", - "content": "/trending", + "content": "/trending/notes", "logo": "", "cover": "/trending.png", "coverRetina": "/trending@2x.png", diff --git a/src-tauri/src/nostr/event.rs b/src-tauri/src/nostr/event.rs index 5898694d..e7784145 100644 --- a/src-tauri/src/nostr/event.rs +++ b/src-tauri/src/nostr/event.rs @@ -76,7 +76,7 @@ pub async fn get_events( limit: usize, until: Option<&str>, contacts: Option>, - global: Option, + global: bool, state: State<'_, Nostr>, ) -> Result, String> { let client = &state.client; @@ -84,18 +84,34 @@ pub async fn get_events( Some(until) => Timestamp::from_str(until).unwrap(), None => Timestamp::now(), }; - let authors = match contacts { - Some(val) => { - let c: Vec = val - .into_iter() - .map(|key| PublicKey::from_str(key).unwrap()) - .collect(); - Some(c) + + match global { + true => { + let filter = Filter::new() + .kinds(vec![Kind::TextNote, Kind::Repost]) + .limit(limit) + .until(as_of); + + if let Ok(events) = client + .get_events_of(vec![filter], Some(Duration::from_secs(15))) + .await + { + println!("total global events: {}", events.len()); + Ok(events) + } else { + Err("Get events failed".into()) + } } - None => match global { - Some(val) => match val { - true => None, - false => { + false => { + let authors = match contacts { + Some(val) => { + let c: Vec = val + .into_iter() + .map(|key| PublicKey::from_str(key).unwrap()) + .collect(); + Some(c) + } + None => { match client .get_contact_list_public_keys(Some(Duration::from_secs(10))) .await @@ -104,38 +120,33 @@ pub async fn get_events( Err(_) => None, } } - }, - None => { - match client - .get_contact_list_public_keys(Some(Duration::from_secs(10))) - .await - { - Ok(val) => Some(val), - Err(_) => None, - } - } - }, - }; - let filter = match authors { - Some(val) => Filter::new() - .kinds(vec![Kind::TextNote, Kind::Repost]) - .authors(val) - .limit(limit) - .until(as_of), - None => Filter::new() - .kinds(vec![Kind::TextNote, Kind::Repost]) - .limit(limit) - .until(as_of), - }; + }; - if let Ok(events) = client - .get_events_of(vec![filter], Some(Duration::from_secs(15))) - .await - { - println!("total events: {}", events.len()); - Ok(events) - } else { - Err("Get text event failed".into()) + 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) + .until(as_of); + + if let Ok(events) = client + .get_events_of(vec![filter], Some(Duration::from_secs(15))) + .await + { + println!("total local events: {}", events.len()); + Ok(events) + } else { + Err("Get events failed".into()) + } + } + } + None => Err("Get local events but contact list is empty".into()), + } + } } }