mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-10-06 12:06:07 +02:00
More list features
This commit is contained in:
5
.changeset/fifty-lamps-itch.md
Normal file
5
.changeset/fifty-lamps-itch.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add more details to publish details modal
|
5
.changeset/old-news-lie.md
Normal file
5
.changeset/old-news-lie.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Filter relay reviews by list
|
@@ -27,7 +27,6 @@ export default function PeopleListSelection({
|
|||||||
const { list, setList, listEvent } = usePeopleListContext();
|
const { list, setList, listEvent } = usePeopleListContext();
|
||||||
|
|
||||||
const handleSelect = (value: string | string[]) => {
|
const handleSelect = (value: string | string[]) => {
|
||||||
console.log(value);
|
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
setList(value);
|
setList(value);
|
||||||
}
|
}
|
||||||
@@ -36,7 +35,7 @@ export default function PeopleListSelection({
|
|||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton as={Button} {...props}>
|
<MenuButton as={Button} {...props}>
|
||||||
{listEvent ? getListName(listEvent) : list === "global" ? "Global" : "Following"}
|
{listEvent ? getListName(listEvent) : list === "global" ? "Global" : "Loading..."}
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuList zIndex={100}>
|
<MenuList zIndex={100}>
|
||||||
<MenuOptionGroup value={list} onChange={handleSelect} type="radio">
|
<MenuOptionGroup value={list} onChange={handleSelect} type="radio">
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import { Alert, AlertDescription, AlertIcon, AlertTitle, Box, Flex, FlexProps, Progress } from "@chakra-ui/react";
|
import { Alert, AlertDescription, AlertIcon, AlertTitle, Box, Flex, FlexProps, Link, Progress } from "@chakra-ui/react";
|
||||||
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import NostrPublishAction from "../classes/nostr-publish-action";
|
import NostrPublishAction from "../classes/nostr-publish-action";
|
||||||
import useSubject from "../hooks/use-subject";
|
import useSubject from "../hooks/use-subject";
|
||||||
|
import { RelayPaidTag } from "../views/relays/components/relay-card";
|
||||||
|
|
||||||
export type PostResultsProps = {
|
export type PostResultsProps = {
|
||||||
pub: NostrPublishAction;
|
pub: NostrPublishAction;
|
||||||
@@ -16,7 +18,12 @@ export const PublishDetails = ({ pub }: PostResultsProps & Omit<FlexProps, "chil
|
|||||||
<Alert key={result.relay.url} status={result.status ? "success" : "warning"}>
|
<Alert key={result.relay.url} status={result.status ? "success" : "warning"}>
|
||||||
<AlertIcon />
|
<AlertIcon />
|
||||||
<Box>
|
<Box>
|
||||||
<AlertTitle>{result.relay.url}</AlertTitle>
|
<AlertTitle>
|
||||||
|
<Link as={RouterLink} to={`/r/${encodeURIComponent(result.relay.url)}`}>
|
||||||
|
{result.relay.url}
|
||||||
|
</Link>
|
||||||
|
<RelayPaidTag url={result.relay.url} />
|
||||||
|
</AlertTitle>
|
||||||
{result.message && <AlertDescription>{result.message}</AlertDescription>}
|
{result.message && <AlertDescription>{result.message}</AlertDescription>}
|
||||||
</Box>
|
</Box>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
@@ -1,18 +1,24 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { ButtonGroup, ButtonGroupProps, IconButton } from "@chakra-ui/react";
|
import { ButtonGroup, ButtonGroupProps, IconButton } from "@chakra-ui/react";
|
||||||
|
|
||||||
import { ImageGridTimelineIcon, TextTimelineIcon, TimelineHealthIcon } from "../icons";
|
import { ImageGridTimelineIcon, TextTimelineIcon, TimelineHealthIcon } from "../icons";
|
||||||
import { TimelineViewType } from "./index";
|
import { TimelineViewType } from "./index";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { searchParamsToJson } from "../../helpers/url";
|
||||||
|
|
||||||
export default function TimelineViewTypeButtons(props: ButtonGroupProps) {
|
export default function TimelineViewTypeButtons(props: ButtonGroupProps) {
|
||||||
const [params, setParams] = useSearchParams();
|
const [params, setParams] = useSearchParams();
|
||||||
const mode = (params.get("view") as TimelineViewType) ?? "timeline";
|
const mode = (params.get("view") as TimelineViewType) ?? "timeline";
|
||||||
|
|
||||||
const onChange = (type: TimelineViewType) => {
|
const onChange = useCallback(
|
||||||
setParams({ view: type }, { replace: true });
|
(type: TimelineViewType) => {
|
||||||
};
|
setParams((p) => ({ ...searchParamsToJson(p), view: type }), { replace: true });
|
||||||
|
},
|
||||||
|
[setParams],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonGroup>
|
<ButtonGroup {...props}>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Health"
|
aria-label="Health"
|
||||||
icon={<TimelineHealthIcon />}
|
icon={<TimelineHealthIcon />}
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import type { URLSearchParamsInit } from "react-router-dom";
|
||||||
|
|
||||||
export const convertToUrl = (url: string | URL) => (url instanceof URL ? url : new URL(url));
|
export const convertToUrl = (url: string | URL) => (url instanceof URL ? url : new URL(url));
|
||||||
|
|
||||||
export const IMAGE_EXT = [".svg", ".gif", ".png", ".jpg", ".jpeg", ".webp", ".avif"];
|
export const IMAGE_EXT = [".svg", ".gif", ".png", ".jpg", ".jpeg", ".webp", ".avif"];
|
||||||
@@ -52,3 +54,13 @@ export function replaceDomain(url: string | URL, replacementUrl: string | URL) {
|
|||||||
if (replacementUrl.password) newUrl.password = replacementUrl.password;
|
if (replacementUrl.password) newUrl.password = replacementUrl.password;
|
||||||
return newUrl;
|
return newUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function searchParamsToJson(params: URLSearchParams) {
|
||||||
|
const json: URLSearchParamsInit = {};
|
||||||
|
|
||||||
|
for (const [key, value] of params.entries()) {
|
||||||
|
json[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
@@ -1,37 +1,70 @@
|
|||||||
import { PropsWithChildren, createContext, useContext, useMemo, useState } from "react";
|
import { PropsWithChildren, createContext, useCallback, useContext, useMemo } from "react";
|
||||||
import { Kind } from "nostr-tools";
|
import { Kind } from "nostr-tools";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||||
import { getPubkeysFromList } from "../helpers/nostr/lists";
|
import { getPubkeysFromList } from "../helpers/nostr/lists";
|
||||||
import useReplaceableEvent from "../hooks/use-replaceable-event";
|
import useReplaceableEvent from "../hooks/use-replaceable-event";
|
||||||
import { NostrEvent } from "../types/nostr-event";
|
import { NostrEvent } from "../types/nostr-event";
|
||||||
|
import { NostrQuery } from "../types/nostr-query";
|
||||||
|
import { searchParamsToJson } from "../helpers/url";
|
||||||
|
|
||||||
|
export type ListId = "global" | string;
|
||||||
|
export type Person = { pubkey: string; relay?: string };
|
||||||
|
|
||||||
export type PeopleListContextType = {
|
export type PeopleListContextType = {
|
||||||
list?: string;
|
list: ListId;
|
||||||
listEvent?: NostrEvent;
|
listEvent?: NostrEvent;
|
||||||
people: { pubkey: string; relay?: string }[] | undefined;
|
people: Person[] | undefined;
|
||||||
setList: (list: string | undefined) => void;
|
setList: (list: ListId) => void;
|
||||||
|
filter: NostrQuery | undefined;
|
||||||
};
|
};
|
||||||
const PeopleListContext = createContext<PeopleListContextType>({ list: "following", setList: () => {}, people: [] });
|
const PeopleListContext = createContext<PeopleListContextType>({
|
||||||
|
setList: () => {},
|
||||||
|
people: undefined,
|
||||||
|
list: "global",
|
||||||
|
filter: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
export function usePeopleListContext() {
|
export function usePeopleListContext() {
|
||||||
return useContext(PeopleListContext);
|
return useContext(PeopleListContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PeopleListProvider({ children }: PropsWithChildren) {
|
export type PeopleListProviderProps = PropsWithChildren & {
|
||||||
|
initList?: "following" | "global";
|
||||||
|
};
|
||||||
|
export default function PeopleListProvider({ children, initList = "following" }: PeopleListProviderProps) {
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
const [listCord, setList] = useState(account ? `${Kind.Contacts}:${account.pubkey}` : undefined);
|
const [params, setParams] = useSearchParams({
|
||||||
const listEvent = useReplaceableEvent(listCord);
|
people: account && initList === "following" ? `${Kind.Contacts}:${account.pubkey}` : "global",
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = params.get("people") as ListId;
|
||||||
|
const setList = useCallback(
|
||||||
|
(value: ListId) => {
|
||||||
|
setParams((p) => ({ ...searchParamsToJson(p), people: value }));
|
||||||
|
},
|
||||||
|
[setParams],
|
||||||
|
);
|
||||||
|
|
||||||
|
const listEvent = useReplaceableEvent(list !== "global" ? list : undefined, [], true);
|
||||||
|
|
||||||
const people = listEvent && getPubkeysFromList(listEvent);
|
const people = listEvent && getPubkeysFromList(listEvent);
|
||||||
|
|
||||||
|
const filter = useMemo<NostrQuery | undefined>(() => {
|
||||||
|
if (list === "global") return {};
|
||||||
|
return people && { authors: people.map((p) => p.pubkey) };
|
||||||
|
}, [people, list]);
|
||||||
|
|
||||||
const context = useMemo(
|
const context = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
people,
|
people,
|
||||||
list: listCord,
|
list,
|
||||||
listEvent,
|
listEvent,
|
||||||
setList,
|
setList,
|
||||||
|
filter,
|
||||||
}),
|
}),
|
||||||
[listCord, setList, people, listEvent],
|
[list, setList, people, listEvent],
|
||||||
);
|
);
|
||||||
|
|
||||||
return <PeopleListContext.Provider value={context}>{children}</PeopleListContext.Provider>;
|
return <PeopleListContext.Provider value={context}>{children}</PeopleListContext.Provider>;
|
||||||
|
@@ -1,10 +1,9 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { Flex } from "@chakra-ui/react";
|
import { Flex } from "@chakra-ui/react";
|
||||||
import { Kind } from "nostr-tools";
|
import { Kind } from "nostr-tools";
|
||||||
|
|
||||||
import { isReply, truncatedId } from "../../helpers/nostr/events";
|
import { isReply } from "../../helpers/nostr/events";
|
||||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
import TimelinePage, { useTimelinePageEventFilter } from "../../components/timeline-page";
|
import TimelinePage, { useTimelinePageEventFilter } from "../../components/timeline-page";
|
||||||
import TimelineViewTypeButtons from "../../components/timeline-page/timeline-view-type";
|
import TimelineViewTypeButtons from "../../components/timeline-page/timeline-view-type";
|
||||||
@@ -12,6 +11,7 @@ import PeopleListSelection from "../../components/people-list-selection/people-l
|
|||||||
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
|
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
|
||||||
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
|
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
|
||||||
import RelaySelectionProvider, { useRelaySelectionContext } from "../../providers/relay-selection-provider";
|
import RelaySelectionProvider, { useRelaySelectionContext } from "../../providers/relay-selection-provider";
|
||||||
|
import { NostrRequestFilter } from "../../types/nostr-query";
|
||||||
|
|
||||||
function HomePage() {
|
function HomePage() {
|
||||||
const timelinePageEventFilter = useTimelinePageEventFilter();
|
const timelinePageEventFilter = useTimelinePageEventFilter();
|
||||||
@@ -24,12 +24,16 @@ function HomePage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { relays } = useRelaySelectionContext();
|
const { relays } = useRelaySelectionContext();
|
||||||
const { people, list } = usePeopleListContext();
|
const { list, filter } = usePeopleListContext();
|
||||||
|
|
||||||
const kinds = [Kind.Text, Kind.Repost, 2];
|
const kinds = [Kind.Text, Kind.Repost, 2];
|
||||||
const query = people && people.length > 0 ? { authors: people.map((p) => p.pubkey), kinds } : { kinds };
|
const query = useMemo<NostrRequestFilter>(() => {
|
||||||
|
if (filter === undefined) return { kinds };
|
||||||
|
return { ...filter, kinds };
|
||||||
|
}, [filter]);
|
||||||
|
|
||||||
const timeline = useTimelineLoader(`${list}-home-feed`, relays, query, {
|
const timeline = useTimelineLoader(`${list}-home-feed`, relays, query, {
|
||||||
enabled: !!people && people.length > 0,
|
enabled: !!filter,
|
||||||
eventFilter,
|
eventFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,23 +1,10 @@
|
|||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import {
|
import { AvatarGroup, Box, Card, CardBody, CardFooter, CardHeader, Flex, Heading, Link, Text } from "@chakra-ui/react";
|
||||||
AvatarGroup,
|
|
||||||
Box,
|
|
||||||
Card,
|
|
||||||
CardBody,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
Flex,
|
|
||||||
Heading,
|
|
||||||
Link,
|
|
||||||
Spacer,
|
|
||||||
Text,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { Kind } from "nostr-tools";
|
import { Kind } from "nostr-tools";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import { UserAvatarLink } from "../../../components/user-avatar-link";
|
import { UserAvatarLink } from "../../../components/user-avatar-link";
|
||||||
import { UserLink } from "../../../components/user-link";
|
import { UserLink } from "../../../components/user-link";
|
||||||
import EventVerificationIcon from "../../../components/event-verification-icon";
|
|
||||||
import { getEventsFromList, getListName, getPubkeysFromList } from "../../../helpers/nostr/lists";
|
import { getEventsFromList, getListName, getPubkeysFromList } from "../../../helpers/nostr/lists";
|
||||||
import { getSharableEventNaddr } from "../../../helpers/nip19";
|
import { getSharableEventNaddr } from "../../../helpers/nip19";
|
||||||
import { NostrEvent } from "../../../types/nostr-event";
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
@@ -44,13 +31,13 @@ export default function ListCard({ cord, event: maybeEvent }: { cord?: string; e
|
|||||||
{getListName(event)}
|
{getListName(event)}
|
||||||
</Link>
|
</Link>
|
||||||
</Heading>
|
</Heading>
|
||||||
|
<Flex gap="2">
|
||||||
|
<Text>Created by:</Text>
|
||||||
|
<UserAvatarLink pubkey={event.pubkey} size="xs" />
|
||||||
|
<UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
|
||||||
|
</Flex>
|
||||||
<Text>Updated: {dayjs.unix(event.created_at).fromNow()}</Text>
|
<Text>Updated: {dayjs.unix(event.created_at).fromNow()}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Spacer />
|
|
||||||
<Text>Created by:</Text>
|
|
||||||
<UserAvatarLink pubkey={event.pubkey} size="xs" />
|
|
||||||
<UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
|
|
||||||
<EventVerificationIcon event={event} />
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody p="2">
|
<CardBody p="2">
|
||||||
{people.length > 0 && (
|
{people.length > 0 && (
|
||||||
|
@@ -184,6 +184,18 @@ export function RelayShareButton({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function RelayPaidTag({ url }: { url: string }) {
|
||||||
|
const { info } = useRelayInfo(url);
|
||||||
|
|
||||||
|
return (
|
||||||
|
info?.payments_url && (
|
||||||
|
<Tag as="a" variant="solid" colorScheme="green" size="sm" ml="2" target="_blank" href={info.payments_url}>
|
||||||
|
Paid relay
|
||||||
|
</Tag>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function RelayCard({ url, ...props }: { url: string } & Omit<CardProps, "children">) {
|
export default function RelayCard({ url, ...props }: { url: string } & Omit<CardProps, "children">) {
|
||||||
const { info } = useRelayInfo(url);
|
const { info } = useRelayInfo(url);
|
||||||
return (
|
return (
|
||||||
@@ -193,11 +205,7 @@ export default function RelayCard({ url, ...props }: { url: string } & Omit<Card
|
|||||||
<RelayFavicon relay={url} size="xs" />
|
<RelayFavicon relay={url} size="xs" />
|
||||||
<Heading size="md" isTruncated>
|
<Heading size="md" isTruncated>
|
||||||
<RouterLink to={`/r/${encodeURIComponent(url)}`}>{url}</RouterLink>
|
<RouterLink to={`/r/${encodeURIComponent(url)}`}>{url}</RouterLink>
|
||||||
{info?.payments_url && (
|
<RelayPaidTag url={url} />
|
||||||
<Tag as="a" variant="solid" colorScheme="green" size="sm" ml="2" target="_blank" href={info.payments_url}>
|
|
||||||
Paid
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
</Heading>
|
</Heading>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody px="2" py="0" display="flex" flexDirection="column" gap="2">
|
<CardBody px="2" py="0" display="flex" flexDirection="column" gap="2">
|
||||||
|
@@ -21,6 +21,8 @@ import { ExternalLinkIcon } from "../../../components/icons";
|
|||||||
import RelayReviewForm from "./relay-review-form";
|
import RelayReviewForm from "./relay-review-form";
|
||||||
import RelayReviews from "./relay-reviews";
|
import RelayReviews from "./relay-reviews";
|
||||||
import RelayNotes from "./relay-notes";
|
import RelayNotes from "./relay-notes";
|
||||||
|
import PeopleListProvider from "../../../providers/people-list-provider";
|
||||||
|
import PeopleListSelection from "../../../components/people-list-selection/people-list-selection";
|
||||||
|
|
||||||
function RelayPage({ relay }: { relay: string }) {
|
function RelayPage({ relay }: { relay: string }) {
|
||||||
const { info } = useRelayInfo(relay);
|
const { info } = useRelayInfo(relay);
|
||||||
@@ -68,13 +70,15 @@ function RelayPage({ relay }: { relay: string }) {
|
|||||||
|
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
<TabPanel py="2" px="0">
|
<TabPanel py="2" px="0">
|
||||||
{showReviewForm.isOpen ? (
|
<Flex gap="2">
|
||||||
<RelayReviewForm onClose={showReviewForm.onClose} relay={relay} />
|
<PeopleListSelection />
|
||||||
) : (
|
{!showReviewForm.isOpen && (
|
||||||
<Button colorScheme="brand" ml="aut" mb="2" onClick={showReviewForm.onOpen}>
|
<Button colorScheme="brand" ml="auto" mb="2" onClick={showReviewForm.onOpen}>
|
||||||
Write review
|
Write review
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
</Flex>
|
||||||
|
{showReviewForm.isOpen && <RelayReviewForm onClose={showReviewForm.onClose} relay={relay} my="4" />}
|
||||||
<RelayReviews relay={relay} />
|
<RelayReviews relay={relay} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel py="2" px="0">
|
<TabPanel py="2" px="0">
|
||||||
@@ -94,5 +98,9 @@ export default function RelayView() {
|
|||||||
|
|
||||||
if (!safeUrl) return <>Bad relay url</>;
|
if (!safeUrl) return <>Bad relay url</>;
|
||||||
|
|
||||||
return <RelayPage relay={safeUrl} />;
|
return (
|
||||||
|
<PeopleListProvider initList="global">
|
||||||
|
<RelayPage relay={safeUrl} />
|
||||||
|
</PeopleListProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { Flex, Switch, useDisclosure } from "@chakra-ui/react";
|
import { Flex, Spacer } from "@chakra-ui/react";
|
||||||
|
import { Kind } from "nostr-tools";
|
||||||
|
|
||||||
import { isReply } from "../../../helpers/nostr/events";
|
import { isReply } from "../../../helpers/nostr/events";
|
||||||
import { useAppTitle } from "../../../hooks/use-app-title";
|
import { useAppTitle } from "../../../hooks/use-app-title";
|
||||||
@@ -7,27 +8,34 @@ import useTimelineLoader from "../../../hooks/use-timeline-loader";
|
|||||||
import { NostrEvent } from "../../../types/nostr-event";
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
import TimelinePage, { useTimelinePageEventFilter } from "../../../components/timeline-page";
|
import TimelinePage, { useTimelinePageEventFilter } from "../../../components/timeline-page";
|
||||||
import TimelineViewTypeButtons from "../../../components/timeline-page/timeline-view-type";
|
import TimelineViewTypeButtons from "../../../components/timeline-page/timeline-view-type";
|
||||||
|
import PeopleListSelection from "../../../components/people-list-selection/people-list-selection";
|
||||||
|
import { usePeopleListContext } from "../../../providers/people-list-provider";
|
||||||
|
import { NostrRequestFilter } from "../../../types/nostr-query";
|
||||||
|
|
||||||
export default function RelayNotes({ relay }: { relay: string }) {
|
export default function RelayNotes({ relay }: { relay: string }) {
|
||||||
useAppTitle(`${relay} - Notes`);
|
useAppTitle(`${relay} - Notes`);
|
||||||
|
|
||||||
const showReplies = useDisclosure();
|
const { filter } = usePeopleListContext();
|
||||||
|
const kinds = [Kind.Text];
|
||||||
|
const query = useMemo<NostrRequestFilter>(() => {
|
||||||
|
if (filter === undefined) return { kinds };
|
||||||
|
return { ...filter, kinds };
|
||||||
|
}, [filter]);
|
||||||
|
|
||||||
const timelineEventFilter = useTimelinePageEventFilter();
|
const timelineEventFilter = useTimelinePageEventFilter();
|
||||||
const eventFilter = useCallback(
|
const eventFilter = useCallback(
|
||||||
(event: NostrEvent) => {
|
(event: NostrEvent) => {
|
||||||
if (!showReplies.isOpen && isReply(event)) return false;
|
if (!isReply(event)) return false;
|
||||||
return timelineEventFilter(event);
|
return timelineEventFilter(event);
|
||||||
},
|
},
|
||||||
[showReplies.isOpen, timelineEventFilter],
|
[timelineEventFilter],
|
||||||
);
|
);
|
||||||
const timeline = useTimelineLoader(`${relay}-notes`, [relay], { kinds: [1] }, { eventFilter });
|
const timeline = useTimelineLoader(`${relay}-notes`, [relay], query, { eventFilter, enabled: !!filter });
|
||||||
|
|
||||||
const header = (
|
const header = (
|
||||||
<Flex gap="2" pr="2" justifyContent="space-between" alignItems="center">
|
<Flex gap="2" wrap="wrap" px={["2", 0]}>
|
||||||
<Switch isChecked={showReplies.isOpen} onChange={showReplies.onToggle} size="sm">
|
<PeopleListSelection />
|
||||||
Show Replies
|
<Spacer />
|
||||||
</Switch>
|
|
||||||
<TimelineViewTypeButtons />
|
<TimelineViewTypeButtons />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Button, Flex, Heading, Textarea, useToast } from "@chakra-ui/react";
|
import { Button, Flex, FlexProps, Heading, Textarea, useToast } from "@chakra-ui/react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
@@ -9,7 +9,11 @@ import { RELAY_REVIEW_LABEL, RELAY_REVIEW_LABEL_NAMESPACE, REVIEW_KIND } from ".
|
|||||||
import { useSigningContext } from "../../../providers/signing-provider";
|
import { useSigningContext } from "../../../providers/signing-provider";
|
||||||
import NostrPublishAction from "../../../classes/nostr-publish-action";
|
import NostrPublishAction from "../../../classes/nostr-publish-action";
|
||||||
|
|
||||||
export default function RelayReviewForm({ onClose, relay }: { onClose: () => void; relay: string }) {
|
export default function RelayReviewForm({
|
||||||
|
onClose,
|
||||||
|
relay,
|
||||||
|
...props
|
||||||
|
}: { onClose: () => void; relay: string } & Omit<FlexProps, "children">) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { requestSignature } = useSigningContext();
|
const { requestSignature } = useSigningContext();
|
||||||
const writeRelays = useWriteRelayUrls();
|
const writeRelays = useWriteRelayUrls();
|
||||||
@@ -44,7 +48,7 @@ export default function RelayReviewForm({ onClose, relay }: { onClose: () => voi
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex as="form" direction="column" onSubmit={onSubmit} gap="2" mb="2">
|
<Flex as="form" direction="column" onSubmit={onSubmit} gap="2" mb="2" {...props}>
|
||||||
<Flex gap="2">
|
<Flex gap="2">
|
||||||
<Heading size="md">Write review</Heading>
|
<Heading size="md">Write review</Heading>
|
||||||
<StarRating quality={getValues().quality} fontSize="1.5rem" onChange={(q) => setValue("quality", q)} />
|
<StarRating quality={getValues().quality} fontSize="1.5rem" onChange={(q) => setValue("quality", q)} />
|
||||||
|
@@ -6,15 +6,24 @@ import useSubject from "../../../hooks/use-subject";
|
|||||||
import useTimelineLoader from "../../../hooks/use-timeline-loader";
|
import useTimelineLoader from "../../../hooks/use-timeline-loader";
|
||||||
import RelayReviewNote from "../components/relay-review-note";
|
import RelayReviewNote from "../components/relay-review-note";
|
||||||
import { useAppTitle } from "../../../hooks/use-app-title";
|
import { useAppTitle } from "../../../hooks/use-app-title";
|
||||||
|
import { usePeopleListContext } from "../../../providers/people-list-provider";
|
||||||
|
|
||||||
export default function RelayReviews({ relay }: { relay: string }) {
|
export default function RelayReviews({ relay }: { relay: string }) {
|
||||||
useAppTitle(`${relay} - Reviews`);
|
useAppTitle(`${relay} - Reviews`);
|
||||||
const readRelays = useReadRelayUrls();
|
const readRelays = useReadRelayUrls();
|
||||||
const timeline = useTimelineLoader(`${relay}-reviews`, readRelays, {
|
|
||||||
kinds: [1985],
|
const { filter } = usePeopleListContext();
|
||||||
"#r": [relay],
|
const timeline = useTimelineLoader(
|
||||||
"#l": [RELAY_REVIEW_LABEL],
|
`${relay}-reviews`,
|
||||||
});
|
readRelays,
|
||||||
|
{
|
||||||
|
...filter,
|
||||||
|
kinds: [1985],
|
||||||
|
"#r": [relay],
|
||||||
|
"#l": [RELAY_REVIEW_LABEL],
|
||||||
|
},
|
||||||
|
{ enabled: !!filter },
|
||||||
|
);
|
||||||
|
|
||||||
const events = useSubject(timeline.timeline);
|
const events = useSubject(timeline.timeline);
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { Button, Flex } from "@chakra-ui/react";
|
import { Button, Flex } from "@chakra-ui/react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||||
@@ -6,15 +7,24 @@ import useSubject from "../../hooks/use-subject";
|
|||||||
import RelayReviewNote from "./components/relay-review-note";
|
import RelayReviewNote from "./components/relay-review-note";
|
||||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||||
import { useNavigate } from "react-router-dom";
|
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
|
||||||
|
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
||||||
|
|
||||||
export default function RelayReviewsView() {
|
function RelayReviewsPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const readRelays = useReadRelayUrls();
|
const readRelays = useReadRelayUrls();
|
||||||
const timeline = useTimelineLoader("relay-reviews", readRelays, {
|
|
||||||
kinds: [1985],
|
const { filter } = usePeopleListContext();
|
||||||
"#l": ["review/relay"],
|
const timeline = useTimelineLoader(
|
||||||
});
|
"relay-reviews",
|
||||||
|
readRelays,
|
||||||
|
{
|
||||||
|
...filter,
|
||||||
|
kinds: [1985],
|
||||||
|
"#l": ["review/relay"],
|
||||||
|
},
|
||||||
|
{ enabled: !!filter },
|
||||||
|
);
|
||||||
|
|
||||||
const reviews = useSubject(timeline.timeline);
|
const reviews = useSubject(timeline.timeline);
|
||||||
|
|
||||||
@@ -23,8 +33,9 @@ export default function RelayReviewsView() {
|
|||||||
return (
|
return (
|
||||||
<IntersectionObserverProvider<string> callback={callback}>
|
<IntersectionObserverProvider<string> callback={callback}>
|
||||||
<Flex direction="column" gap="2" py="2">
|
<Flex direction="column" gap="2" py="2">
|
||||||
<Flex>
|
<Flex gap="2">
|
||||||
<Button onClick={() => navigate(-1)}>Back</Button>
|
<Button onClick={() => navigate(-1)}>Back</Button>
|
||||||
|
<PeopleListSelection />
|
||||||
</Flex>
|
</Flex>
|
||||||
{reviews.map((event) => (
|
{reviews.map((event) => (
|
||||||
<RelayReviewNote key={event.id} event={event} />
|
<RelayReviewNote key={event.id} event={event} />
|
||||||
@@ -33,3 +44,11 @@ export default function RelayReviewsView() {
|
|||||||
</IntersectionObserverProvider>
|
</IntersectionObserverProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function RelayReviewsView() {
|
||||||
|
return (
|
||||||
|
<PeopleListProvider initList="global">
|
||||||
|
<RelayReviewsPage />
|
||||||
|
</PeopleListProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { Divider, Flex, Heading, Select, SimpleGrid } from "@chakra-ui/react";
|
import { useMemo } from "react";
|
||||||
|
import { Divider, Flex, Heading, SimpleGrid } from "@chakra-ui/react";
|
||||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||||
@@ -12,20 +13,23 @@ import PeopleListSelection from "../../components/people-list-selection/people-l
|
|||||||
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
|
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
|
||||||
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
||||||
import useParsedStreams from "../../hooks/use-parsed-streams";
|
import useParsedStreams from "../../hooks/use-parsed-streams";
|
||||||
|
import { NostrRequestFilter } from "../../types/nostr-query";
|
||||||
|
import { useAppTitle } from "../../hooks/use-app-title";
|
||||||
|
|
||||||
function StreamsPage() {
|
function StreamsPage() {
|
||||||
|
useAppTitle("Streams");
|
||||||
const relays = useRelaySelectionRelays();
|
const relays = useRelaySelectionRelays();
|
||||||
|
|
||||||
const { people, list } = usePeopleListContext();
|
const { filter, list } = usePeopleListContext();
|
||||||
const query =
|
const query = useMemo<NostrRequestFilter>(() => {
|
||||||
people && people.length > 0
|
if (list === "global" || !filter) return { kinds: [STREAM_KIND] };
|
||||||
? [
|
return [
|
||||||
{ authors: people.map((p) => p.pubkey), kinds: [STREAM_KIND] },
|
{ authors: filter.authors, kinds: [STREAM_KIND] },
|
||||||
{ "#p": people.map((p) => p.pubkey), kinds: [STREAM_KIND] },
|
{ "#p": filter.authors, kinds: [STREAM_KIND] },
|
||||||
]
|
];
|
||||||
: { kinds: [STREAM_KIND] };
|
}, [filter, list]);
|
||||||
|
|
||||||
const timeline = useTimelineLoader(`${list}-streams`, relays, query);
|
const timeline = useTimelineLoader(`${list}-streams`, relays, query, { enabled: !!filter });
|
||||||
|
|
||||||
useRelaysChanged(relays, () => timeline.reset());
|
useRelaysChanged(relays, () => timeline.reset());
|
||||||
|
|
||||||
@@ -40,7 +44,7 @@ function StreamsPage() {
|
|||||||
return (
|
return (
|
||||||
<Flex p="2" gap="2" overflow="hidden" direction="column">
|
<Flex p="2" gap="2" overflow="hidden" direction="column">
|
||||||
<Flex gap="2" wrap="wrap">
|
<Flex gap="2" wrap="wrap">
|
||||||
<PeopleListSelection w={["full", "xs"]} />
|
<PeopleListSelection />
|
||||||
<RelaySelectionButton ml="auto" />
|
<RelaySelectionButton ml="auto" />
|
||||||
</Flex>
|
</Flex>
|
||||||
<IntersectionObserverProvider callback={callback}>
|
<IntersectionObserverProvider callback={callback}>
|
||||||
|
@@ -33,8 +33,10 @@ import useSubject from "../../../hooks/use-subject";
|
|||||||
import RelaySelectionButton from "../../../components/relay-selection/relay-selection-button";
|
import RelaySelectionButton from "../../../components/relay-selection/relay-selection-button";
|
||||||
import RelaySelectionProvider from "../../../providers/relay-selection-provider";
|
import RelaySelectionProvider from "../../../providers/relay-selection-provider";
|
||||||
import StreamerCards from "../components/streamer-cards";
|
import StreamerCards from "../components/streamer-cards";
|
||||||
|
import { useAppTitle } from "../../../hooks/use-app-title";
|
||||||
|
|
||||||
function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode?: ChatDisplayMode }) {
|
function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode?: ChatDisplayMode }) {
|
||||||
|
useAppTitle(stream.title);
|
||||||
const vertical = useBreakpointValue({ base: true, lg: false });
|
const vertical = useBreakpointValue({ base: true, lg: false });
|
||||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
const scrollBox = useRef<HTMLDivElement | null>(null);
|
||||||
const scrollState = useScroll(scrollBox);
|
const scrollState = useScroll(scrollBox);
|
||||||
|
Reference in New Issue
Block a user