From 9fcdac4edbcf2ee44f2ac326a4f6bf914777ba80 Mon Sep 17 00:00:00 2001 From: reya Date: Sat, 20 Jan 2024 14:51:13 +0700 Subject: [PATCH] feat: add suggest screen --- packages/ark/src/components/column/header.tsx | 6 + .../src/components/column/interestModal.tsx | 157 +++++++++++++++ packages/ark/src/components/user/about.tsx | 2 +- packages/icons/index.ts | 1 + packages/icons/src/editInterest.tsx | 24 +++ packages/lume-column-foryou/src/home.tsx | 25 ++- packages/lume-column-foryou/src/index.tsx | 20 +- packages/lume-column-timeline/src/home.tsx | 12 +- packages/lume-column-timeline/src/index.tsx | 26 +-- packages/storage/src/storage.ts | 23 +-- packages/ui/src/emptyFeed.tsx | 4 +- packages/ui/src/index.ts | 1 + packages/ui/src/onboarding/finish.tsx | 2 +- packages/ui/src/routes/suggest.tsx | 183 ++++++++++++++++++ 14 files changed, 447 insertions(+), 39 deletions(-) create mode 100644 packages/ark/src/components/column/interestModal.tsx create mode 100644 packages/icons/src/editInterest.tsx create mode 100644 packages/ui/src/routes/suggest.tsx diff --git a/packages/ark/src/components/column/header.tsx b/packages/ark/src/components/column/header.tsx index 456955dd..7d9846c9 100644 --- a/packages/ark/src/components/column/header.tsx +++ b/packages/ark/src/components/column/header.tsx @@ -9,6 +9,7 @@ import { import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import { useQueryClient } from "@tanstack/react-query"; import { ReactNode } from "react"; +import { InterestModal } from "./interestModal"; import { useColumnContext } from "./provider"; export function ColumnHeader({ @@ -71,6 +72,11 @@ export function ColumnHeader({ Refresh + {queryKey[0] === "foryou-9998" ? ( + + + + ) : null} + +
+ {topic.content.map((hashtag) => ( + + ))} +
+ + ))} + + +
+ + + Cancel + + +
+ + + + + + + ); +} diff --git a/packages/ark/src/components/user/about.tsx b/packages/ark/src/components/user/about.tsx index 401911bc..aa4cf0ca 100644 --- a/packages/ark/src/components/user/about.tsx +++ b/packages/ark/src/components/user/about.tsx @@ -31,7 +31,7 @@ export function UserAbout({ className }: { className?: string }) { return (
- {user.about || user.bio} + {user.about?.trim() || user.bio?.trim() || "No bio"}
); } diff --git a/packages/icons/index.ts b/packages/icons/index.ts index be1c7d44..aa9a30be 100644 --- a/packages/icons/index.ts +++ b/packages/icons/index.ts @@ -108,3 +108,4 @@ export * from "./src/composeFilled"; export * from "./src/settingsFilled"; export * from "./src/bellFilled"; export * from "./src/foryou"; +export * from "./src/editInterest"; diff --git a/packages/icons/src/editInterest.tsx b/packages/icons/src/editInterest.tsx new file mode 100644 index 00000000..4b9fdda8 --- /dev/null +++ b/packages/icons/src/editInterest.tsx @@ -0,0 +1,24 @@ +import { SVGProps } from "react"; + +export function EditInterestIcon( + props: JSX.IntrinsicAttributes & SVGProps, +) { + return ( + + + + ); +} diff --git a/packages/lume-column-foryou/src/home.tsx b/packages/lume-column-foryou/src/home.tsx index 11370895..a87c2632 100644 --- a/packages/lume-column-foryou/src/home.tsx +++ b/packages/lume-column-foryou/src/home.tsx @@ -1,6 +1,8 @@ import { TextNote, useArk } from "@lume/ark"; -import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons"; +import { InterestModal } from "@lume/ark/src/components/column/interestModal"; +import { ArrowRightCircleIcon, ForyouIcon, LoaderIcon } from "@lume/icons"; import { useStorage } from "@lume/storage"; +import { EmptyFeed } from "@lume/ui"; import { FETCH_LIMIT } from "@lume/utils"; import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; @@ -31,10 +33,14 @@ export function HomeRoute({ colKey }: { colKey: string }) { signal: AbortSignal; pageParam: number; }) => { + if (!storage.interests?.hashtags) return []; + const events = await ark.getInfiniteEvents({ filter: { kinds: [NDKKind.Text], - "#t": storage.interests.hashtags, + "#t": storage.interests.hashtags.map((item: string) => + item.replace("#", "").toLowerCase(), + ), }, limit: FETCH_LIMIT, pageParam, @@ -81,6 +87,21 @@ export function HomeRoute({ colKey }: { colKey: string }) { }; }, []); + if (!storage.interests?.hashtags?.length) { + return ( +
+ + + + Add interest + +
+ ); + } + return (
diff --git a/packages/lume-column-foryou/src/index.tsx b/packages/lume-column-foryou/src/index.tsx index b36a998a..87d08625 100644 --- a/packages/lume-column-foryou/src/index.tsx +++ b/packages/lume-column-foryou/src/index.tsx @@ -33,14 +33,18 @@ export function ForYou({ column }: { column: IColumn }) { title="For You" icon={} /> - + {storage.interests?.hashtags ? ( + + item.replace("#", "").toLowerCase(), + ), + since: since.current, + }} + onClick={refresh} + /> + ) : null} } /> } /> diff --git a/packages/lume-column-timeline/src/home.tsx b/packages/lume-column-timeline/src/home.tsx index 50b0d4bd..b62c60ad 100644 --- a/packages/lume-column-timeline/src/home.tsx +++ b/packages/lume-column-timeline/src/home.tsx @@ -1,10 +1,11 @@ import { RepostNote, TextNote, useArk } from "@lume/ark"; -import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons"; +import { ArrowRightCircleIcon, LoaderIcon, SearchIcon } from "@lume/icons"; import { EmptyFeed } from "@lume/ui"; import { FETCH_LIMIT } from "@lume/utils"; import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { useEffect, useMemo, useRef } from "react"; +import { Link } from "react-router-dom"; import { CacheSnapshot, VList, VListHandle } from "virtua"; export function HomeRoute({ colKey }: { colKey: string }) { @@ -30,6 +31,8 @@ export function HomeRoute({ colKey }: { colKey: string }) { signal: AbortSignal; pageParam: number; }) => { + if (!ark.account.contacts.length) return []; + const events = await ark.getInfiniteEvents({ filter: { kinds: [NDKKind.Text, NDKKind.Repost], @@ -94,6 +97,13 @@ export function HomeRoute({ colKey }: { colKey: string }) { return (
+ + + Find accounts to follow +
); } diff --git a/packages/lume-column-timeline/src/index.tsx b/packages/lume-column-timeline/src/index.tsx index 238e15b2..1cc4c821 100644 --- a/packages/lume-column-timeline/src/index.tsx +++ b/packages/lume-column-timeline/src/index.tsx @@ -1,7 +1,7 @@ import { Column, useArk } from "@lume/ark"; import { TimelineIcon } from "@lume/icons"; import { IColumn } from "@lume/types"; -import { EventRoute, UserRoute } from "@lume/ui"; +import { EventRoute, SuggestRoute, UserRoute } from "@lume/ui"; import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; import { useQueryClient } from "@tanstack/react-query"; import { useRef } from "react"; @@ -32,20 +32,24 @@ export function Timeline({ column }: { column: IColumn }) { title="Timeline" icon={} /> - + {ark.account.contacts.length ? ( + + ) : null} } /> } /> } /> + } + /> ); diff --git a/packages/storage/src/storage.ts b/packages/storage/src/storage.ts index 12a7c982..c6fe2541 100644 --- a/packages/storage/src/storage.ts +++ b/packages/storage/src/storage.ts @@ -57,6 +57,12 @@ export class LumeStorage { public async init() { const settings = await this.getAllSettings(); + const account = await this.getActiveAccount(); + + if (account) { + this.currentUser = account; + this.interests = await this.getInterests(); + } for (const item of settings) { if (item.value.length > 10) { @@ -65,20 +71,6 @@ export class LumeStorage { this.settings[item.key] = !!parseInt(item.value); } } - - const account = await this.getActiveAccount(); - - if (account) { - this.currentUser = account; - - const interests = await this.getInterests(); - if (interests) { - interests.hashtags = interests.hashtags.map((item: string) => - item.replace("#", "").toLowerCase(), - ); - this.interests = interests; - } - } } async #keyring_save(key: string, value: string) { @@ -430,7 +422,10 @@ export class LumeStorage { const results: { key: string; value: string }[] = await this.#db.select( "SELECT * FROM settings WHERE key = 'interests' ORDER BY id DESC LIMIT 1;", ); + if (!results.length) return null; + if (!results[0].value.length) return null; + return JSON.parse(results[0].value) as Interests; } diff --git a/packages/ui/src/emptyFeed.tsx b/packages/ui/src/emptyFeed.tsx index 1137d0f0..2238f088 100644 --- a/packages/ui/src/emptyFeed.tsx +++ b/packages/ui/src/emptyFeed.tsx @@ -15,7 +15,9 @@ export function EmptyFeed({ >
-

{text ? text : "No events yet"}

+

+ {text ? text : "This feed is empty"} +

{subtext ? subtext diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 03739e9d..fefe25a3 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -11,5 +11,6 @@ export * from "./replyList"; export * from "./emptyFeed"; export * from "./routes/event"; export * from "./routes/user"; +export * from "./routes/suggest"; export * from "./translateRegisterModal"; export * from "./user"; diff --git a/packages/ui/src/onboarding/finish.tsx b/packages/ui/src/onboarding/finish.tsx index e02ad9af..94c0d441 100644 --- a/packages/ui/src/onboarding/finish.tsx +++ b/packages/ui/src/onboarding/finish.tsx @@ -18,7 +18,7 @@ export function OnboardingFinishScreen() { await queryClient.refetchQueries({ queryKey: ["timeline-9999"] }); setLoading(false); - setOnboarding(false); + setOnboarding({ open: false, newUser: false }); }; return ( diff --git a/packages/ui/src/routes/suggest.tsx b/packages/ui/src/routes/suggest.tsx new file mode 100644 index 00000000..e44ca451 --- /dev/null +++ b/packages/ui/src/routes/suggest.tsx @@ -0,0 +1,183 @@ +import { User, useArk } from "@lume/ark"; +import { + ArrowLeftIcon, + ArrowRightIcon, + CancelIcon, + LoaderIcon, + PlusIcon, +} from "@lume/icons"; +import { cn } from "@lume/utils"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { nip19 } from "nostr-tools"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { toast } from "sonner"; +import { WindowVirtualizer } from "virtua"; + +const POPULAR_USERS = [ + "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6", + "npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m", + "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s", + "npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z", + "npub1az9xj85cmxv8e9j9y80lvqp97crsqdu2fpu3srwthd99qfu9qsgstam8y8", + "npub1a2cww4kn9wqte4ry70vyfwqyqvpswksna27rtxd8vty6c74era8sdcw83a", + "npub168ghgug469n4r2tuyw05dmqhqv5jcwm7nxytn67afmz8qkc4a4zqsu2dlc", + "npub133vj8ycevdle0cq8mtgddq0xtn34kxkwxvak983dx0u5vhqnycyqj6tcza", + "npub18ams6ewn5aj2n3wt2qawzglx9mr4nzksxhvrdc4gzrecw7n5tvjqctp424", + "npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac", + "npub1prya33fnqerq0fljwjtp77ehtu7jlsjt5ydhwveuwmqdsdm6k8esk42xcv", + "npub19mduaf5569jx9xz555jcx3v06mvktvtpu0zgk47n4lcpjsz43zzqhj6vzk", +]; + +const LUME_USERS = [ + "npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445", +]; + +export function SuggestRoute({ queryKey }: { queryKey: string[] }) { + const ark = useArk(); + const queryClient = useQueryClient(); + const navigate = useNavigate(); + + 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(); + }, + }); + + const [loading, setLoading] = useState(false); + const [follows, setFollows] = useState([]); + + // toggle follow state + const toggleFollow = (pubkey: string) => { + const arr = follows.includes(pubkey) + ? follows.filter((i) => i !== pubkey) + : [...follows, pubkey]; + setFollows(arr); + }; + + const submit = async () => { + try { + setLoading(true); + + if (!follows.length) return navigate("/"); + + const publish = await ark.newContactList({ + tags: follows.map((item) => { + if (item.startsWith("npub1")) + return ["p", nip19.decode(item).data as string]; + return ["p", item]; + }), + }); + + if (publish) { + await queryClient.refetchQueries({ queryKey: ["timeline-9999"] }); + } + + setLoading(false); + + return navigate("/"); + } catch (e) { + setLoading(false); + toast.error(String(e)); + } + }; + + return ( +

+ +
+ + +
+
+
+

Suggested Follows

+
+
+ {isLoading ? ( +
+ +
+ ) : isError ? ( +
+ Error. Cannot get trending users +
+ ) : ( + data?.profiles.map((item: { pubkey: string }) => ( +
+ + +
+
+
+ + +
+ +
+ +
+
+
+
+ )) + )} +
+
+ +
+
+
+
+ ); +}