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 && (