allow user to select people list for home feed

This commit is contained in:
hzrd149 2023-08-25 08:52:53 -05:00
parent 1740c813a9
commit f6f465611d
26 changed files with 211 additions and 324 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Allow user to select people list for home feed

View File

@ -4,12 +4,10 @@ import { Spinner } from "@chakra-ui/react";
import { ErrorBoundary } from "./components/error-boundary";
import Layout from "./components/layout";
import HomeView from "./views/home";
import HomeView from "./views/home/index";
import SettingsView from "./views/settings";
import LoginView from "./views/login";
import ProfileView from "./views/profile";
import FollowingTab from "./views/home/following-tab";
import GlobalTab from "./views/home/global-tab";
import HashTagView from "./views/hashtag";
import UserView from "./views/user";
import UserNotesTab from "./views/user/notes";
@ -137,11 +135,6 @@ const router = createHashRouter([
{
path: "",
element: <HomeView />,
children: [
{ path: "", element: <FollowingTab /> },
{ path: "following", element: <FollowingTab /> },
{ path: "global", element: <GlobalTab /> },
],
},
],
},

View File

@ -37,13 +37,15 @@ export class Subject<Value> implements Connectable<Value> {
});
}
subscribe(listener: ListenerFn<Value>, ctx?: Object) {
subscribe(listener: ListenerFn<Value>, ctx?: Object, initCall = true) {
if (!this.findListener(listener, ctx)) {
this.listeners.push([listener, ctx]);
if (this.value !== undefined) {
if (ctx) listener.call(ctx, this.value);
else listener(this.value);
if (initCall) {
if (this.value !== undefined) {
if (ctx) listener.call(ctx, this.value);
else listener(this.value);
}
}
}
return this;

View File

@ -1,46 +1,57 @@
import { Select, SelectProps } from "@chakra-ui/react";
import { usePeopleListContext } from "./people-list-provider";
import useUserLists from "../../hooks/use-user-lists";
import { useCurrentAccount } from "../../hooks/use-current-account";
import { getListName } from "../../helpers/nostr/lists";
import { getEventCoordinate } from "../../helpers/nostr/events";
import {
Button,
ButtonProps,
Menu,
MenuButton,
MenuDivider,
MenuItemOption,
MenuList,
MenuOptionGroup,
} from "@chakra-ui/react";
import { Kind } from "nostr-tools";
function UserListOptions() {
const account = useCurrentAccount()!;
const lists = useUserLists(account?.pubkey);
return (
<>
{lists.map((list) => (
<option key={getEventCoordinate(list)} value={getEventCoordinate(list)}>
{getListName(list)}
</option>
))}
</>
);
}
import { usePeopleListContext } from "../../providers/people-list-provider";
import useUserLists from "../../hooks/use-user-lists";
import { useCurrentAccount } from "../../hooks/use-current-account";
import { PEOPLE_LIST_KIND, getListName } from "../../helpers/nostr/lists";
import { getEventCoordinate } from "../../helpers/nostr/events";
export default function PeopleListSelection({
hideGlobalOption = false,
...props
}: {
hideGlobalOption?: boolean;
} & Omit<SelectProps, "value" | "onChange" | "children">) {
const account = useCurrentAccount()!;
const { list, setList } = usePeopleListContext();
} & Omit<ButtonProps, "children">) {
const account = useCurrentAccount();
const lists = useUserLists(account?.pubkey);
const { list, setList, listEvent } = usePeopleListContext();
const handleSelect = (value: string | string[]) => {
console.log(value);
if (typeof value === "string") {
setList(value);
}
};
return (
<Select
value={list}
onChange={(e) => {
setList(e.target.value === "global" ? undefined : e.target.value);
}}
{...props}
>
{account && <option value={`${Kind.Contacts}:${account.pubkey}`}>Following</option>}
{!hideGlobalOption && <option value="global">Global</option>}
{account && <UserListOptions />}
</Select>
<Menu>
<MenuButton as={Button} {...props}>
{listEvent ? getListName(listEvent) : list === "global" ? "Global" : "Following"}
</MenuButton>
<MenuList zIndex={100}>
<MenuOptionGroup value={list} onChange={handleSelect} type="radio">
{account && <MenuItemOption value={`${Kind.Contacts}:${account.pubkey}`}>Following</MenuItemOption>}
{!hideGlobalOption && <MenuItemOption value="global">Global</MenuItemOption>}
{account && <MenuDivider />}
{lists
.filter((l) => l.kind === PEOPLE_LIST_KIND)
.map((list) => (
<MenuItemOption key={getEventCoordinate(list)} value={getEventCoordinate(list)} isTruncated maxW="90vw">
{getListName(list)}
</MenuItemOption>
))}
</MenuOptionGroup>
</MenuList>
</Menu>
);
}

View File

@ -1,15 +1,20 @@
import { Button, ButtonProps } from "@chakra-ui/react";
import { Button, ButtonProps, useDisclosure } from "@chakra-ui/react";
import { RelayIcon } from "../icons";
import { useRelaySelectionContext } from "../../providers/relay-selection-provider";
import RelaySelectionModal from "./relay-selection-modal";
export default function RelaySelectionButton({ ...props }: ButtonProps) {
const { openModal, relays } = useRelaySelectionContext();
const relaysModal = useDisclosure();
const { setSelected, relays } = useRelaySelectionContext();
return (
<>
<Button leftIcon={<RelayIcon />} onClick={openModal} {...props}>
<Button leftIcon={<RelayIcon />} onClick={relaysModal.onOpen} {...props}>
{relays.length} {relays.length === 1 ? "Relay" : "Relays"}
</Button>
{relaysModal.isOpen && (
<RelaySelectionModal selected={relays} onSubmit={setSelected} onClose={relaysModal.onClose} />
)}
</>
);
}

View File

@ -40,7 +40,7 @@ function UsersLists({ pubkey }: { pubkey: string }) {
const [isLoading, setLoading] = useState(false);
const newListModal = useDisclosure();
const lists = useUserLists(pubkey);
const lists = useUserLists(account.pubkey);
const inLists = lists.filter((list) => getPubkeysFromList(list).some((p) => p.pubkey === pubkey));

View File

@ -7,12 +7,9 @@ function useSubject<Value extends unknown>(subject?: Subject<Value>): Value | un
function useSubject<Value extends unknown>(subject?: Subject<Value>) {
const [_, setValue] = useState(subject?.value);
useEffect(() => {
const handler = (value: Value) => setValue(value);
setValue(subject?.value);
subject?.subscribe(handler);
subject?.subscribe(setValue, undefined, false);
return () => {
subject?.unsubscribe(handler);
subject?.unsubscribe(setValue, undefined);
};
}, [subject, setValue]);

View File

@ -3,12 +3,17 @@ import { useReadRelayUrls } from "./use-client-relays";
import useSubject from "./use-subject";
import useTimelineLoader from "./use-timeline-loader";
export default function useUserLists(pubkey: string, additionalRelays: string[] = []) {
export default function useUserLists(pubkey?: string, additionalRelays: string[] = []) {
const readRelays = useReadRelayUrls(additionalRelays);
const timeline = useTimelineLoader(`${pubkey}-lists`, readRelays, {
authors: [pubkey],
kinds: [PEOPLE_LIST_KIND, NOTE_LIST_KIND],
});
const timeline = useTimelineLoader(
`${pubkey}-lists`,
readRelays,
{
authors: pubkey ? [pubkey] : [],
kinds: [PEOPLE_LIST_KIND, NOTE_LIST_KIND],
},
{ enabled: !!pubkey },
);
return useSubject(timeline.timeline);
}

View File

@ -1,12 +1,14 @@
import { PropsWithChildren, createContext, useContext, useMemo, useState } from "react";
import { Kind } from "nostr-tools";
import { useCurrentAccount } from "../../hooks/use-current-account";
import { getPubkeysFromList } from "../../helpers/nostr/lists";
import useReplaceableEvent from "../../hooks/use-replaceable-event";
import { useCurrentAccount } from "../hooks/use-current-account";
import { getPubkeysFromList } from "../helpers/nostr/lists";
import useReplaceableEvent from "../hooks/use-replaceable-event";
import { NostrEvent } from "../types/nostr-event";
export type PeopleListContextType = {
list?: string;
listEvent?: NostrEvent;
people: { pubkey: string; relay?: string }[] | undefined;
setList: (list: string | undefined) => void;
};
@ -26,9 +28,10 @@ export default function PeopleListProvider({ children }: PropsWithChildren) {
() => ({
people,
list: listCord,
listEvent,
setList,
}),
[listCord, setList, people],
[listCord, setList, people, listEvent],
);
return <PeopleListContext.Provider value={context}>{children}</PeopleListContext.Provider>;

View File

@ -1,20 +1,17 @@
import { PropsWithChildren, createContext, useCallback, useContext, useMemo } from "react";
import { useReadRelayUrls } from "../hooks/use-client-relays";
import { useDisclosure } from "@chakra-ui/react";
import RelaySelectionModal from "../components/relay-selection/relay-selection-modal";
import { unique } from "../helpers/array";
import { useLocation, useNavigate } from "react-router-dom";
import { useReadRelayUrls } from "../hooks/use-client-relays";
import { unique } from "../helpers/array";
type RelaySelectionContextType = {
relays: string[];
setSelected: (relays: string[]) => void;
openModal: () => void;
};
export const RelaySelectionContext = createContext<RelaySelectionContextType>({
relays: [],
setSelected: () => {},
openModal: () => {},
});
export function useRelaySelectionContext() {
@ -34,7 +31,6 @@ export default function RelaySelectionProvider({
overrideDefault,
additionalDefaults,
}: RelaySelectionProviderProps) {
const relaysModal = useDisclosure();
const { state } = useLocation();
const navigate = useNavigate();
@ -44,19 +40,22 @@ export default function RelaySelectionProvider({
if (overrideDefault) return overrideDefault;
if (additionalDefaults) return unique([...userReadRelays, ...additionalDefaults]);
return userReadRelays;
}, [state?.relays, overrideDefault, userReadRelays, additionalDefaults]);
}, [state?.relays, overrideDefault, userReadRelays.join("|"), additionalDefaults]);
const setSelected = useCallback((relays: string[]) => {
navigate(".", { state: { relays }, replace: true });
}, []);
return (
<RelaySelectionContext.Provider value={{ relays, setSelected, openModal: relaysModal.onOpen }}>
{children}
{relaysModal.isOpen && (
<RelaySelectionModal selected={relays} onSubmit={setSelected} onClose={relaysModal.onClose} />
)}
</RelaySelectionContext.Provider>
const setSelected = useCallback(
(relays: string[]) => {
navigate(".", { state: { relays }, replace: true });
},
[navigate],
);
const context = useMemo(
() => ({
relays,
setSelected,
}),
[relays.join("|"), setSelected],
);
return <RelaySelectionContext.Provider value={context}>{children}</RelaySelectionContext.Provider>;
}

View File

@ -18,8 +18,8 @@ export function TrustProvider({
const parentTrust = useContext(TrustContext);
const account = useCurrentAccount();
const contacts = useUserContactList(account?.pubkey)
const following = contacts ? getPubkeysFromList(contacts).map(p => p.pubkey) : []
const contactList = useUserContactList(account?.pubkey);
const following = contactList ? getPubkeysFromList(contactList).map((p) => p.pubkey) : [];
const isEventTrusted = trust || (!!event && (event.pubkey === account?.pubkey || following.includes(event.pubkey)));

View File

@ -1,11 +1,12 @@
import db from "./db";
import { fetchWithCorsFallback } from "../helpers/cors";
import { isHex } from "../helpers/nip19";
export type RelayInformationDocument = {
name: string;
description: string;
icon?: string;
pubkey: string;
pubkey?: string;
contact: string;
supported_nips?: number[];
software: string;
@ -13,6 +14,13 @@ export type RelayInformationDocument = {
payments_url?: string;
};
function sanitizeInfo(info: RelayInformationDocument) {
if (info.pubkey && !isHex(info.pubkey)) {
delete info.pubkey;
}
return info;
}
async function fetchInfo(relay: string) {
const url = new URL(relay);
url.protocol = url.protocol === "ws:" ? "http" : "https";
@ -21,6 +29,8 @@ async function fetchInfo(relay: string) {
(res) => res.json() as Promise<RelayInformationDocument>,
);
sanitizeInfo(infoDoc);
memoryCache.set(relay, infoDoc);
await db.put("relayInfo", infoDoc, relay);

View File

@ -1,65 +0,0 @@
import { useCallback } from "react";
import { Flex, FormControl, FormLabel, Switch } from "@chakra-ui/react";
import { useSearchParams } from "react-router-dom";
import { Kind } from "nostr-tools";
import { isReply, truncatedId } from "../../helpers/nostr/events";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import { useCurrentAccount } from "../../hooks/use-current-account";
import RequireCurrentAccount from "../../providers/require-current-account";
import { NostrEvent } from "../../types/nostr-event";
import TimelinePage, { useTimelinePageEventFilter } from "../../components/timeline-page";
import TimelineViewTypeButtons from "../../components/timeline-page/timeline-view-type";
import useUserContactList from "../../hooks/use-user-contact-list";
import { getPubkeysFromList } from "../../helpers/nostr/lists";
function FollowingTabBody() {
const account = useCurrentAccount()!;
const contacts = useUserContactList(account.pubkey);
const [search, setSearch] = useSearchParams();
const showReplies = search.has("replies");
const onToggle = () => {
showReplies ? setSearch({}) : setSearch({ replies: "show" });
};
const timelinePageEventFilter = useTimelinePageEventFilter();
const eventFilter = useCallback(
(event: NostrEvent) => {
if (!showReplies && isReply(event)) return false;
return timelinePageEventFilter(event);
},
[showReplies, timelinePageEventFilter],
);
const following = contacts ? getPubkeysFromList(contacts).map((p) => p.pubkey) : [];
const readRelays = useReadRelayUrls();
const timeline = useTimelineLoader(
`${truncatedId(account.pubkey)}-following`,
readRelays,
{ authors: following, kinds: [Kind.Text, Kind.Repost, 2] },
{ enabled: following.length > 0, eventFilter },
);
const header = (
<Flex px="2">
<FormControl display="flex" alignItems="center">
<FormLabel htmlFor="show-replies" mb="0">
Show Replies
</FormLabel>
<Switch id="show-replies" isChecked={showReplies} onChange={onToggle} />
</FormControl>
<TimelineViewTypeButtons />
</Flex>
);
return <TimelinePage timeline={timeline} header={header} pt="4" pb="8" />;
}
export default function FollowingTab() {
return (
<RequireCurrentAccount>
<FollowingTabBody />
</RequireCurrentAccount>
);
}

View File

@ -1,65 +0,0 @@
import { useCallback } from "react";
import { Flex, FormControl, FormLabel, Switch, useDisclosure } from "@chakra-ui/react";
import { isReply } from "../../helpers/nostr/events";
import { useAppTitle } from "../../hooks/use-app-title";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { NostrEvent } from "../../types/nostr-event";
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
import RelaySelectionProvider, { useRelaySelectionRelays } from "../../providers/relay-selection-provider";
import useRelaysChanged from "../../hooks/use-relays-changed";
import TimelinePage, { useTimelinePageEventFilter } from "../../components/timeline-page";
import TimelineViewTypeButtons from "../../components/timeline-page/timeline-view-type";
import { useSearchParams } from "react-router-dom";
import { safeUrl } from "../../helpers/parse";
function GlobalPage() {
const readRelays = useRelaySelectionRelays();
const { isOpen: showReplies, onToggle } = useDisclosure();
useAppTitle("global");
const timelineEventFilter = useTimelinePageEventFilter();
const eventFilter = useCallback(
(event: NostrEvent) => {
if (!showReplies && isReply(event)) return false;
return timelineEventFilter(event);
},
[showReplies, timelineEventFilter],
);
const timeline = useTimelineLoader(`global`, readRelays, { kinds: [1] }, { eventFilter });
useRelaysChanged(readRelays, () => timeline.reset());
const header = (
<Flex gap="2" pr="2">
<RelaySelectionButton />
<FormControl display="flex" alignItems="center">
<Switch id="show-replies" isChecked={showReplies} onChange={onToggle} mr="2" />
<FormLabel htmlFor="show-replies" mb="0">
Show Replies
</FormLabel>
</FormControl>
<TimelineViewTypeButtons />
</Flex>
);
return <TimelinePage timeline={timeline} header={header} pt="4" pb="8" />;
}
export default function GlobalTab() {
const [params] = useSearchParams();
// wrap the global page with another relay selection so it dose not effect the rest of the app
let relays = ["wss://welcome.nostr.wine"];
const setRelay = params.get("relay");
if (setRelay) {
const url = safeUrl(setRelay);
relays = [setRelay];
}
return (
<RelaySelectionProvider overrideDefault={relays}>
<GlobalPage />
</RelaySelectionProvider>
);
}

View File

@ -1,40 +1,55 @@
import { Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react";
import { Outlet, useMatches, useNavigate } from "react-router-dom";
import { useCallback } from "react";
import { Flex } from "@chakra-ui/react";
import { Kind } from "nostr-tools";
const tabs = [
{ label: "Following", path: "/following" },
{ label: "Global", path: "/global" },
];
import { isReply, truncatedId } from "../../helpers/nostr/events";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { useCurrentAccount } from "../../hooks/use-current-account";
import { NostrEvent } from "../../types/nostr-event";
import TimelinePage, { useTimelinePageEventFilter } from "../../components/timeline-page";
import TimelineViewTypeButtons from "../../components/timeline-page/timeline-view-type";
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
import RelaySelectionProvider, { useRelaySelectionContext } from "../../providers/relay-selection-provider";
function HomePage() {
const timelinePageEventFilter = useTimelinePageEventFilter();
const eventFilter = useCallback(
(event: NostrEvent) => {
if (isReply(event)) return false;
return timelinePageEventFilter(event);
},
[timelinePageEventFilter],
);
const { relays } = useRelaySelectionContext();
const { people, list } = usePeopleListContext();
const kinds = [Kind.Text, Kind.Repost, 2];
const query = people && people.length > 0 ? { authors: people.map((p) => p.pubkey), kinds } : { kinds };
const timeline = useTimelineLoader(`${list}-home-feed`, relays, query, {
enabled: !!people && people.length > 0,
eventFilter,
});
const header = (
<Flex gap="2" wrap="wrap" px={["2", 0]}>
<PeopleListSelection />
<RelaySelectionButton ml="auto" />
<TimelineViewTypeButtons />
</Flex>
);
return <TimelinePage timeline={timeline} header={header} pt="2" pb="8" />;
}
export default function HomeView() {
const navigate = useNavigate();
const matches = useMatches();
const activeTab = tabs.indexOf(tabs.find((t) => matches[matches.length - 1].pathname === t.path) ?? tabs[0]);
return (
<Tabs
display="flex"
flexDirection="column"
flexGrow="1"
overflow="hidden"
isLazy
index={activeTab}
onChange={(v) => navigate(tabs[v].path)}
colorScheme="brand"
>
<TabList>
{tabs.map(({ label }) => (
<Tab key={label}>{label}</Tab>
))}
</TabList>
<TabPanels overflow="hidden" h="full">
{tabs.map(({ label }) => (
<TabPanel key={label} p={0} overflow="hidden" h="full" display="flex" flexDirection="column">
<Outlet />
</TabPanel>
))}
</TabPanels>
</Tabs>
<PeopleListProvider>
<RelaySelectionProvider>
<HomePage />
</RelaySelectionProvider>
</PeopleListProvider>
);
}

View File

@ -40,7 +40,7 @@ export default function ListCard({ cord, event: maybeEvent }: { cord?: string; e
<Text>{people.length} people</Text>
<AvatarGroup overflow="hidden" mb="2">
{people.map(({ pubkey, relay }) => (
<UserAvatarLink pubkey={pubkey} relay={relay} />
<UserAvatarLink key={pubkey} pubkey={pubkey} relay={relay} />
))}
</AvatarGroup>
</>

View File

@ -15,7 +15,6 @@ import { DraftNostrEvent } from "../../types/nostr-event";
import RequireCurrentAccount from "../../providers/require-current-account";
import { Message } from "./message";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { truncatedId } from "../../helpers/nostr/events";
import { useCurrentAccount } from "../../hooks/use-current-account";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import IntersectionObserverProvider from "../../providers/intersection-observer";
@ -31,7 +30,7 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
const readRelays = useReadRelayUrls();
const timeline = useTimelineLoader(`${truncatedId(pubkey)}-${truncatedId(account.pubkey)}-messages`, readRelays, [
const timeline = useTimelineLoader(`${pubkey}-${account.pubkey}-messages`, readRelays, [
{
kinds: [Kind.EncryptedDirectMessage],
"#p": [account.pubkey],

View File

@ -1,12 +1,12 @@
import { Badge } from "@chakra-ui/react";
import { Badge, BadgeProps } from "@chakra-ui/react";
import { ParsedStream } from "../../../helpers/nostr/stream";
export default function StreamStatusBadge({ stream }: { stream: ParsedStream }) {
export default function StreamStatusBadge({ stream, ...props }: { stream: ParsedStream } & Omit<BadgeProps, 'children'>) {
switch (stream.status) {
case "live":
return <Badge colorScheme="green">live</Badge>;
return <Badge colorScheme="green" {...props}>live</Badge>;
case "ended":
return <Badge colorScheme="red">ended</Badge>;
return <Badge colorScheme="red" {...props}>ended</Badge>;
}
return null;
}

View File

@ -1,32 +1,16 @@
import { useRef } from "react";
import { memo, useRef } from "react";
import dayjs from "dayjs";
import { Box, Card, CardBody, CardProps, Flex, Heading, LinkBox, LinkOverlay, Tag, Text } from "@chakra-ui/react";
import { ParsedStream } from "../../../helpers/nostr/stream";
import {
Badge,
Card,
CardBody,
CardFooter,
CardProps,
Divider,
Flex,
Heading,
Image,
LinkBox,
LinkOverlay,
Spacer,
Tag,
Text,
} from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { UserAvatar } from "../../../components/user-avatar";
import { UserLink } from "../../../components/user-link";
import dayjs from "dayjs";
import StreamStatusBadge from "./status-badge";
import { EventRelays } from "../../../components/note/note-relays";
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
import useEventNaddr from "../../../hooks/use-event-naddr";
import StreamDebugButton from "./stream-debug-button";
export default function StreamCard({ stream, ...props }: CardProps & { stream: ParsedStream }) {
function StreamCard({ stream, ...props }: CardProps & { stream: ParsedStream }) {
const { title, image } = stream;
// if there is a parent intersection observer, register this card
@ -36,9 +20,17 @@ export default function StreamCard({ stream, ...props }: CardProps & { stream: P
const naddr = useEventNaddr(stream.event, stream.relays);
return (
<Card {...props} ref={ref}>
<Card {...props} ref={ref} position="relative">
<LinkBox as={CardBody} p="2" display="flex" flexDirection="column" gap="2">
{image && <Image src={image} alt={title} borderRadius="lg" />}
<StreamStatusBadge stream={stream} position="absolute" top="4" left="4" />
<Box
backgroundImage={image}
backgroundPosition="center"
backgroundRepeat="no-repeat"
backgroundSize="cover"
aspectRatio={16 / 9}
borderRadius="lg"
/>
<Flex gap="2" alignItems="center">
<UserAvatar pubkey={stream.host} size="sm" noProxy />
<Heading size="sm">
@ -59,13 +51,7 @@ export default function StreamCard({ stream, ...props }: CardProps & { stream: P
)}
{stream.starts && <Text>Started: {dayjs.unix(stream.starts).fromNow()}</Text>}
</LinkBox>
<Divider />
<CardFooter p="2" display="flex" gap="2" alignItems="center">
<StreamStatusBadge stream={stream} />
<Spacer />
<EventRelays event={stream.event} />
<StreamDebugButton stream={stream} variant="ghost" size="sm" />
</CardFooter>
</Card>
);
}
export default memo(StreamCard);

View File

@ -1,34 +1,20 @@
import { useCallback, useState } from "react";
import { Code, Flex, Select, SimpleGrid } from "@chakra-ui/react";
import { Divider, Flex, Heading, Select, SimpleGrid } from "@chakra-ui/react";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import IntersectionObserverProvider from "../../providers/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import useSubject from "../../hooks/use-subject";
import StreamCard from "./components/stream-card";
import { STREAM_KIND, parseStreamEvent } from "../../helpers/nostr/stream";
import { NostrEvent } from "../../types/nostr-event";
import { STREAM_KIND } from "../../helpers/nostr/stream";
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
import RelaySelectionProvider, { useRelaySelectionRelays } from "../../providers/relay-selection-provider";
import useRelaysChanged from "../../hooks/use-relays-changed";
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
import PeopleListProvider, { usePeopleListContext } from "../../components/people-list-selection/people-list-provider";
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
import useParsedStreams from "../../hooks/use-parsed-streams";
function StreamsPage() {
const relays = useRelaySelectionRelays();
const [filterStatus, setFilterStatus] = useState<string>("live");
const eventFilter = useCallback(
(event: NostrEvent) => {
try {
const parsed = parseStreamEvent(event);
return parsed.status === filterStatus;
} catch (e) {}
return false;
},
[filterStatus],
);
const { people, list } = usePeopleListContext();
const query =
@ -39,7 +25,7 @@ function StreamsPage() {
]
: { kinds: [STREAM_KIND] };
const timeline = useTimelineLoader(`${list}-streams`, relays, query, { eventFilter });
const timeline = useTimelineLoader(`${list}-streams`, relays, query);
useRelaysChanged(relays, () => timeline.reset());
@ -48,19 +34,25 @@ function StreamsPage() {
const events = useSubject(timeline.timeline);
const streams = useParsedStreams(events);
const liveStreams = streams.filter((stream) => stream.status === "live");
const endedStreams = streams.filter((stream) => stream.status === "ended");
return (
<Flex p="2" gap="2" overflow="hidden" direction="column">
<Flex gap="2" wrap="wrap">
<PeopleListSelection w={["full", "xs"]} />
<Select w={["full", "xs"]} value={filterStatus} onChange={(e) => setFilterStatus(e.target.value)}>
<option value="live">Live</option>
<option value="ended">Ended</option>
</Select>
<RelaySelectionButton ml="auto" />
</Flex>
<IntersectionObserverProvider callback={callback}>
<SimpleGrid columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing="2">
{streams.map((stream) => (
{liveStreams.map((stream) => (
<StreamCard key={stream.event.id} stream={stream} />
))}
</SimpleGrid>
<Heading>Ended</Heading>
<Divider />
<SimpleGrid columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing="2">
{endedStreams.map((stream) => (
<StreamCard key={stream.event.id} stream={stream} />
))}
</SimpleGrid>

View File

@ -1,6 +1,7 @@
import { MenuItem, useDisclosure } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { nip19 } from "nostr-tools";
import { useCopyToClipboard } from "react-use";
import { MenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
import { ChatIcon, ClipboardIcon, CodeIcon, ExternalLinkIcon, RelayIcon, SpyIcon } from "../../../components/icons";
@ -10,7 +11,6 @@ import { getUserDisplayName } from "../../../helpers/user-metadata";
import { useUserRelays } from "../../../hooks/use-user-relays";
import { RelayMode } from "../../../classes/relay";
import UserDebugModal from "../../../components/debug-modals/user-debug-modal";
import { useCopyToClipboard } from "react-use";
import { useSharableProfileId } from "../../../hooks/use-shareable-profile-id";
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
import { truncatedId } from "../../../helpers/nostr/events";

View File

@ -29,7 +29,7 @@ export default function UserFollowersTab() {
const contextRelays = useAdditionalRelayContext();
const readRelays = useReadRelayUrls(contextRelays);
const timeline = useTimelineLoader(`${truncatedId(pubkey)}-followers`, readRelays, {
const timeline = useTimelineLoader(`${pubkey}-followers`, readRelays, {
"#p": [pubkey],
kinds: [Kind.Contacts],
});

View File

@ -56,7 +56,7 @@ export default function UserLikesTab() {
const contextRelays = useAdditionalRelayContext();
const readRelays = useReadRelayUrls(contextRelays);
const timeline = useTimelineLoader(`${truncatedId(pubkey)}-likes`, readRelays, { authors: [pubkey], kinds: [7] });
const timeline = useTimelineLoader(`${pubkey}-likes`, readRelays, { authors: [pubkey], kinds: [7] });
const likes = useSubject(timeline.timeline);

View File

@ -56,7 +56,7 @@ const UserRelaysTab = () => {
const userRelays = useUserRelays(pubkey);
const readRelays = useReadRelayUrls(userRelays.map((r) => r.url));
const timeline = useTimelineLoader(`${truncatedId(pubkey)}-relay-reviews`, readRelays, {
const timeline = useTimelineLoader(`${pubkey}-relay-reviews`, readRelays, {
authors: [pubkey],
kinds: [1985],
"#l": ["review/relay"],

View File

@ -1,8 +1,9 @@
import { Flex, Text } from "@chakra-ui/react";
import { useOutletContext } from "react-router-dom";
import { NoteLink } from "../../components/note-link";
import { UserLink } from "../../components/user-link";
import { filterTagsByContentRefs, truncatedId } from "../../helpers/nostr/events";
import { filterTagsByContentRefs } from "../../helpers/nostr/events";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { isETag, isPTag, NostrEvent } from "../../types/nostr-event";
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
@ -39,7 +40,7 @@ export default function UserReportsTab() {
const { pubkey } = useOutletContext() as { pubkey: string };
const contextRelays = useAdditionalRelayContext();
const timeline = useTimelineLoader(`${truncatedId(pubkey)}-reports`, contextRelays, {
const timeline = useTimelineLoader(`${pubkey}-reports`, contextRelays, {
authors: [pubkey],
kinds: [1984],
});

View File

@ -8,7 +8,6 @@ import { NoteLink } from "../../components/note-link";
import { UserAvatarLink } from "../../components/user-avatar-link";
import { UserLink } from "../../components/user-link";
import { readablizeSats } from "../../helpers/bolt11";
import { truncatedId } from "../../helpers/nostr/events";
import { isProfileZap, isNoteZap, parseZapEvent, totalZaps } from "../../helpers/zaps";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { NostrEvent } from "../../types/nostr-event";
@ -90,12 +89,7 @@ const UserZapsTab = () => {
[filter],
);
const timeline = useTimelineLoader(
`${truncatedId(pubkey)}-zaps`,
relays,
{ "#p": [pubkey], kinds: [9735] },
{ eventFilter },
);
const timeline = useTimelineLoader(`${pubkey}-zaps`, relays, { "#p": [pubkey], kinds: [9735] }, { eventFilter });
const events = useSubject(timeline.timeline);
const zaps = useMemo(() => {