diff --git a/.changeset/giant-actors-rule.md b/.changeset/giant-actors-rule.md new file mode 100644 index 000000000..3ced25fa6 --- /dev/null +++ b/.changeset/giant-actors-rule.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Add browse lists view diff --git a/src/app.tsx b/src/app.tsx index 38192236b..101f9867a 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -39,6 +39,7 @@ import ListView from "./views/lists/list"; import UserListsTab from "./views/user/lists"; import "./services/emoji-packs"; +import BrowseListView from "./views/lists/browse"; const StreamsView = React.lazy(() => import("./views/streams")); const StreamView = React.lazy(() => import("./views/streams/stream")); @@ -125,6 +126,7 @@ const router = createHashRouter([ path: "lists", children: [ { path: "", element: }, + { path: "browse", element: }, { path: ":addr", element: }, ], }, diff --git a/src/components/note/index.tsx b/src/components/note/index.tsx index 7f4e65b82..edc6e65c4 100644 --- a/src/components/note/index.tsx +++ b/src/components/note/index.tsx @@ -92,13 +92,13 @@ export const Note = React.memo(({ event, variant = "outline" }: NoteProps) => { aria-label="Open External" href={externalLink} size="sm" - variant="link" + variant="ghost" target="_blank" /> )} - - + + diff --git a/src/views/lists/browse/index.tsx b/src/views/lists/browse/index.tsx new file mode 100644 index 000000000..4f1a5bcf3 --- /dev/null +++ b/src/views/lists/browse/index.tsx @@ -0,0 +1,87 @@ +import { Flex, Select, SimpleGrid, Switch, useDisclosure } from "@chakra-ui/react"; +import PeopleListProvider, { usePeopleListContext } from "../../../providers/people-list-provider"; +import PeopleListSelection from "../../../components/people-list-selection/people-list-selection"; +import useTimelineLoader from "../../../hooks/use-timeline-loader"; +import { useReadRelayUrls } from "../../../hooks/use-client-relays"; +import { + MUTE_LIST_KIND, + NOTE_LIST_KIND, + PEOPLE_LIST_KIND, + getEventsFromList, + getListName, + getPubkeysFromList, +} from "../../../helpers/nostr/lists"; +import { useCallback, useState } from "react"; +import { NostrEvent } from "../../../types/nostr-event"; +import IntersectionObserverProvider from "../../../providers/intersection-observer"; +import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback"; +import useSubject from "../../../hooks/use-subject"; +import ListCard from "../components/list-card"; +import { getEventUID } from "../../../helpers/nostr/events"; + +function BrowseListPage() { + const { filter, list } = usePeopleListContext(); + const showEmpty = useDisclosure(); + const showMute = useDisclosure(); + const [listKind, setListKind] = useState(PEOPLE_LIST_KIND); + + const eventFilter = useCallback( + (event: NostrEvent) => { + if (event.kind !== listKind) return false; + if (!showEmpty.isOpen && getPubkeysFromList(event).length === 0 && getEventsFromList(event).length === 0) + return false; + + if ( + (!showMute.isOpen && event.kind === PEOPLE_LIST_KIND && getListName(event) === "mute") || + event.kind === MUTE_LIST_KIND + ) + return false; + return true; + }, + [showEmpty.isOpen, showMute.isOpen, listKind], + ); + const readRelays = useReadRelayUrls(); + const timeline = useTimelineLoader( + `${list}-lists`, + readRelays, + { ...filter, kinds: [PEOPLE_LIST_KIND, NOTE_LIST_KIND] }, + { enabled: !!filter, eventFilter }, + ); + + const lists = useSubject(timeline.timeline); + const callback = useTimelineCurserIntersectionCallback(timeline); + + return ( + + + + + + + Show Empty + + + Show Mute + + + + + {lists.map((event) => ( + + ))} + + + + ); +} + +export default function BrowseListView() { + return ( + + + + ); +} diff --git a/src/views/lists/components/list-card.tsx b/src/views/lists/components/list-card.tsx index d53da1892..34ab6eb39 100644 --- a/src/views/lists/components/list-card.tsx +++ b/src/views/lists/components/list-card.tsx @@ -12,6 +12,8 @@ import useReplaceableEvent from "../../../hooks/use-replaceable-event"; import { createCoordinate } from "../../../services/replaceable-event-requester"; import { EventRelays } from "../../../components/note/note-relays"; import { NoteLink } from "../../../components/note-link"; +import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer"; +import { useRef } from "react"; export default function ListCard({ cord, event: maybeEvent }: { cord?: string; event?: NostrEvent }) { const event = maybeEvent ?? (cord ? useReplaceableEvent(cord as string) : undefined); @@ -22,9 +24,13 @@ export default function ListCard({ cord, event: maybeEvent }: { cord?: string; e const link = event.kind === Kind.Contacts ? createCoordinate(Kind.Contacts, event.pubkey) : getSharableEventNaddr(event); + // if there is a parent intersection observer, register this card + const ref = useRef(null); + useRegisterIntersectionEntity(ref, event.id); + return ( - - + + {getListName(event)} @@ -51,7 +57,7 @@ export default function ListCard({ cord, event: maybeEvent }: { cord?: string; e {notes.length > 0 && ( <> Notes ({notes.length}): - + {notes.map(({ id, relay }) => ( ))} diff --git a/src/views/lists/index.tsx b/src/views/lists/index.tsx index 08a34affa..79ec3cd17 100644 --- a/src/views/lists/index.tsx +++ b/src/views/lists/index.tsx @@ -1,5 +1,5 @@ import { Button, Divider, Flex, Heading, Image, Link, SimpleGrid, Spacer, useDisclosure } from "@chakra-ui/react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, Link as RouterLink } from "react-router-dom"; import { Kind } from "nostr-tools"; import { useCurrentAccount } from "../../hooks/use-current-account"; @@ -24,6 +24,9 @@ function ListsPage() { return ( + {isAuthor && (