mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-27 20:17:05 +02:00
Cleanup list card
This commit is contained in:
@@ -1,77 +0,0 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { Button, ButtonProps, IconButton, Image, useDisclosure } from "@chakra-ui/react";
|
||||
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import useEventReactions from "../hooks/use-event-reactions";
|
||||
import { DislikeIcon, LikeIcon } from "./icons";
|
||||
import { draftEventReaction, groupReactions } from "../helpers/nostr/reactions";
|
||||
import ReactionDetailsModal from "./reaction-details-modal";
|
||||
import { useSigningContext } from "../providers/signing-provider";
|
||||
import clientRelaysService from "../services/client-relays";
|
||||
import NostrPublishAction from "../classes/nostr-publish-action";
|
||||
import eventReactionsService from "../services/event-reactions";
|
||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||
|
||||
export function ReactionIcon({ emoji, url }: { emoji: string; url?: string }) {
|
||||
if (emoji === "+") return <LikeIcon />;
|
||||
if (emoji === "-") return <DislikeIcon />;
|
||||
if (url) return <Image src={url} title={emoji} alt={emoji} w="1em" h="1em" display="inline" />;
|
||||
return <span>{emoji}</span>;
|
||||
}
|
||||
|
||||
function ReactionGroupButton({
|
||||
emoji,
|
||||
url,
|
||||
count,
|
||||
...props
|
||||
}: Omit<ButtonProps, "leftIcon" | "children"> & { emoji: string; count: number; url?: string }) {
|
||||
if (count <= 1) {
|
||||
return <IconButton icon={<ReactionIcon emoji={emoji} url={url} />} aria-label="Reaction" {...props} />;
|
||||
}
|
||||
return (
|
||||
<Button leftIcon={<ReactionIcon emoji={emoji} url={url} />} title={emoji} {...props}>
|
||||
{count > 1 && count}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function EventReactionButtons({ event, max }: { event: NostrEvent; max?: number }) {
|
||||
const account = useCurrentAccount();
|
||||
const detailsModal = useDisclosure();
|
||||
const reactions = useEventReactions(event.id) ?? [];
|
||||
const grouped = useMemo(() => groupReactions(reactions), [reactions]);
|
||||
const { requestSignature } = useSigningContext();
|
||||
|
||||
const addReaction = useCallback(async (emoji = "+", url?: string) => {
|
||||
const draft = draftEventReaction(event, emoji, url);
|
||||
|
||||
const signed = await requestSignature(draft);
|
||||
if (signed) {
|
||||
const writeRelays = clientRelaysService.getWriteUrls();
|
||||
new NostrPublishAction("Reaction", writeRelays, signed);
|
||||
eventReactionsService.handleEvent(signed);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (grouped.length === 0) return null;
|
||||
|
||||
const clamped = Array.from(grouped);
|
||||
if (max !== undefined) clamped.length = max;
|
||||
|
||||
return (
|
||||
<>
|
||||
{clamped.map((group) => (
|
||||
<ReactionGroupButton
|
||||
key={group.emoji}
|
||||
emoji={group.emoji}
|
||||
url={group.url}
|
||||
count={group.pubkeys.length}
|
||||
onClick={() => addReaction(group.emoji, group.url)}
|
||||
colorScheme={account && group.pubkeys.includes(account?.pubkey) ? "primary" : undefined}
|
||||
/>
|
||||
))}
|
||||
<Button onClick={detailsModal.onOpen}>Show all</Button>
|
||||
{detailsModal.isOpen && <ReactionDetailsModal isOpen onClose={detailsModal.onClose} reactions={reactions} />}
|
||||
</>
|
||||
);
|
||||
}
|
37
src/components/event-reactions/common-hooks.tsx
Normal file
37
src/components/event-reactions/common-hooks.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useCallback } from "react";
|
||||
import { useToast } from "@chakra-ui/react";
|
||||
|
||||
import { ReactionGroup, draftEventReaction } from "../../helpers/nostr/reactions";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { useSigningContext } from "../../providers/signing-provider";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import clientRelaysService from "../../services/client-relays";
|
||||
import NostrPublishAction from "../../classes/nostr-publish-action";
|
||||
import eventReactionsService from "../../services/event-reactions";
|
||||
|
||||
export function useAddReaction(event: NostrEvent, grouped: ReactionGroup[]) {
|
||||
const account = useCurrentAccount();
|
||||
const toast = useToast();
|
||||
const { requestSignature } = useSigningContext();
|
||||
|
||||
return useCallback(
|
||||
async (emoji = "+", url?: string) => {
|
||||
try {
|
||||
const group = grouped.find((g) => g.emoji === emoji);
|
||||
if (account && group && group.pubkeys.includes(account?.pubkey)) return;
|
||||
|
||||
const draft = draftEventReaction(event, emoji, url);
|
||||
|
||||
const signed = await requestSignature(draft);
|
||||
if (signed) {
|
||||
const writeRelays = clientRelaysService.getWriteUrls();
|
||||
new NostrPublishAction("Reaction", writeRelays, signed);
|
||||
eventReactionsService.handleEvent(signed);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
},
|
||||
[grouped, account, toast, requestSignature],
|
||||
);
|
||||
}
|
41
src/components/event-reactions/event-reactions.tsx
Normal file
41
src/components/event-reactions/event-reactions.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useMemo } from "react";
|
||||
import { Button, useDisclosure } from "@chakra-ui/react";
|
||||
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import useEventReactions from "../../hooks/use-event-reactions";
|
||||
import { groupReactions } from "../../helpers/nostr/reactions";
|
||||
import ReactionDetailsModal from "../reaction-details-modal";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import ReactionGroupButton from "./reaction-group-button";
|
||||
import { useAddReaction } from "./common-hooks";
|
||||
|
||||
export default function EventReactionButtons({ event, max }: { event: NostrEvent; max?: number }) {
|
||||
const account = useCurrentAccount();
|
||||
const detailsModal = useDisclosure();
|
||||
const reactions = useEventReactions(event.id) ?? [];
|
||||
const grouped = useMemo(() => groupReactions(reactions), [reactions]);
|
||||
|
||||
const addReaction = useAddReaction(event, grouped);
|
||||
|
||||
if (grouped.length === 0) return null;
|
||||
|
||||
const clamped = Array.from(grouped);
|
||||
if (max !== undefined) clamped.length = max;
|
||||
|
||||
return (
|
||||
<>
|
||||
{clamped.map((group) => (
|
||||
<ReactionGroupButton
|
||||
key={group.emoji}
|
||||
emoji={group.emoji}
|
||||
url={group.url}
|
||||
count={group.pubkeys.length}
|
||||
onClick={() => addReaction(group.emoji, group.url)}
|
||||
colorScheme={account && group.pubkeys.includes(account?.pubkey) ? "primary" : undefined}
|
||||
/>
|
||||
))}
|
||||
<Button onClick={detailsModal.onOpen}>Show all</Button>
|
||||
{detailsModal.isOpen && <ReactionDetailsModal isOpen onClose={detailsModal.onClose} reactions={reactions} />}
|
||||
</>
|
||||
);
|
||||
}
|
18
src/components/event-reactions/reaction-group-button.tsx
Normal file
18
src/components/event-reactions/reaction-group-button.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Button, ButtonProps, IconButton } from "@chakra-ui/react";
|
||||
import ReactionIcon from "./reaction-icon";
|
||||
|
||||
export default function ReactionGroupButton({
|
||||
emoji,
|
||||
url,
|
||||
count,
|
||||
...props
|
||||
}: Omit<ButtonProps, "leftIcon" | "children"> & { emoji: string; count: number; url?: string }) {
|
||||
if (count <= 1) {
|
||||
return <IconButton icon={<ReactionIcon emoji={emoji} url={url} />} aria-label="Reaction" {...props} />;
|
||||
}
|
||||
return (
|
||||
<Button leftIcon={<ReactionIcon emoji={emoji} url={url} />} title={emoji} {...props}>
|
||||
{count > 1 && count}
|
||||
</Button>
|
||||
);
|
||||
}
|
9
src/components/event-reactions/reaction-icon.tsx
Normal file
9
src/components/event-reactions/reaction-icon.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Image } from "@chakra-ui/react";
|
||||
import { DislikeIcon, LikeIcon } from "../icons";
|
||||
|
||||
export default function ReactionIcon({ emoji, url }: { emoji: string; url?: string }) {
|
||||
if (emoji === "+") return <LikeIcon />;
|
||||
if (emoji === "-") return <DislikeIcon />;
|
||||
if (url) return <Image src={url} title={emoji} alt={emoji} w="1em" h="1em" display="inline" />;
|
||||
return <span>{emoji}</span>;
|
||||
}
|
28
src/components/event-reactions/simple-like-button.tsx
Normal file
28
src/components/event-reactions/simple-like-button.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import useEventReactions from "../../hooks/use-event-reactions";
|
||||
import { groupReactions } from "../../helpers/nostr/reactions";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import ReactionGroupButton from "./reaction-group-button";
|
||||
import { useAddReaction } from "./common-hooks";
|
||||
import { ButtonProps } from "@chakra-ui/react";
|
||||
|
||||
export default function SimpleLikeButton({ event, ...props }: Omit<ButtonProps, "children"> & { event: NostrEvent }) {
|
||||
const account = useCurrentAccount();
|
||||
const reactions = useEventReactions(event.id) ?? [];
|
||||
const grouped = useMemo(() => groupReactions(reactions), [reactions]);
|
||||
|
||||
const addReaction = useAddReaction(event, grouped);
|
||||
const group = grouped.find((g) => g.emoji === "+");
|
||||
|
||||
return (
|
||||
<ReactionGroupButton
|
||||
emoji="+"
|
||||
count={group?.pubkeys.length ?? 0}
|
||||
onClick={() => addReaction("+")}
|
||||
colorScheme={account && group?.pubkeys.includes(account?.pubkey) ? "primary" : undefined}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
@@ -12,7 +12,7 @@ import GhostToolbar from "./ghost-toolbar";
|
||||
import { useBreakpointValue } from "../../providers/breakpoint-provider";
|
||||
import SearchModal from "../search-modal";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import ChatWindows from "../chat-windows";
|
||||
// import ChatWindows from "../chat-windows";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const isMobile = useBreakpointValue({ base: true, md: false });
|
||||
@@ -66,7 +66,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
</Flex>
|
||||
{isGhost && <GhostToolbar />}
|
||||
{searchModal.isOpen && <SearchModal isOpen onClose={searchModal.onClose} />}
|
||||
{!isMobile && <ChatWindows />}
|
||||
{/* {!isMobile && <ChatWindows />} */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ import { ButtonGroup, ButtonGroupProps, Divider } from "@chakra-ui/react";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import ReactionButton from "./reaction-button";
|
||||
import EventReactionButtons from "../../event-reactions";
|
||||
import EventReactionButtons from "../../event-reactions/event-reactions";
|
||||
import useEventReactions from "../../../hooks/use-event-reactions";
|
||||
import { useBreakpointValue } from "../../../providers/breakpoint-provider";
|
||||
|
||||
|
@@ -21,7 +21,7 @@ export type NoteZapButtonProps = Omit<ButtonProps, "children"> & {
|
||||
export default function NoteZapButton({ event, allowComment, showEventPreview, ...props }: NoteZapButtonProps) {
|
||||
const account = useCurrentAccount();
|
||||
const { metadata } = useUserLNURLMetadata(event.pubkey);
|
||||
const zaps = useEventZaps(event.id);
|
||||
const zaps = useEventZaps(getEventUID(event));
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const hasZapped = !!account && zaps.some((zap) => zap.request.pubkey === account.pubkey);
|
||||
|
@@ -18,7 +18,7 @@ import { useMemo } from "react";
|
||||
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { groupReactions } from "../helpers/nostr/reactions";
|
||||
import { ReactionIcon } from "./event-reactions";
|
||||
import { ReactionIcon } from "./event-reactions/event-reactions";
|
||||
import UserAvatarLink from "./user-avatar-link";
|
||||
import { UserLink } from "./user-link";
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Kind } from "nostr-tools";
|
||||
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
||||
import { DraftNostrEvent, NostrEvent, Tag } from "../../types/nostr-event";
|
||||
import dayjs from "dayjs";
|
||||
import { getEventCoordinate, isReplaceable } from "./events";
|
||||
|
||||
export type ReactionGroup = { emoji: string; url?: string; name?: string; count: number; pubkeys: string[] };
|
||||
|
||||
@@ -20,14 +21,15 @@ export function groupReactions(reactions: NostrEvent[]) {
|
||||
return Array.from(Object.values(groups)).sort((a, b) => b.pubkeys.length - a.pubkeys.length);
|
||||
}
|
||||
|
||||
export function draftEventReaction(reacted: NostrEvent, emoji = "+", url?: string) {
|
||||
// only keep the e, and p tags on the parent event
|
||||
const inheritedTags = reacted.tags.filter((tag) => tag.length >= 2 && (tag[0] === "e" || tag[0] === "p"));
|
||||
|
||||
export function draftEventReaction(event: NostrEvent, emoji = "+", url?: string) {
|
||||
const tags: Tag[] = [
|
||||
["e", event.id],
|
||||
["p", event.pubkey],
|
||||
];
|
||||
const draft: DraftNostrEvent = {
|
||||
kind: Kind.Reaction,
|
||||
content: url ? ":" + emoji + ":" : emoji,
|
||||
tags: [...inheritedTags, ["e", reacted.id], ["p", reacted.pubkey]],
|
||||
tags: isReplaceable(event.kind) ? [...tags, ["a", getEventCoordinate(event)]] : tags,
|
||||
created_at: dayjs().unix(),
|
||||
};
|
||||
|
||||
|
@@ -1,16 +1,17 @@
|
||||
import { memo, useRef } from "react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import {
|
||||
AvatarGroup,
|
||||
ButtonGroup,
|
||||
Card,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardProps,
|
||||
Flex,
|
||||
Heading,
|
||||
Link,
|
||||
LinkBox,
|
||||
LinkProps,
|
||||
SimpleGrid,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import { Kind, nip19 } from "nostr-tools";
|
||||
@@ -29,15 +30,20 @@ import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
|
||||
import { createCoordinate } from "../../../services/replaceable-event-requester";
|
||||
import { NoteLink } from "../../../components/note-link";
|
||||
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||
import ListFavoriteButton from "./list-favorite-button";
|
||||
import { getEventUID } from "../../../helpers/nostr/events";
|
||||
import ListMenu from "./list-menu";
|
||||
import Timestamp from "../../../components/timestamp";
|
||||
import { COMMUNITY_DEFINITION_KIND } from "../../../helpers/nostr/communities";
|
||||
import { getArticleTitle } from "../../../helpers/nostr/long-form";
|
||||
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
|
||||
import { CommunityIcon, NotesIcon } from "../../../components/icons";
|
||||
import User01 from "../../../components/icons/user-01";
|
||||
import HoverLinkOverlay from "../../../components/hover-link-overlay";
|
||||
import NoteZapButton from "../../../components/note/note-zap-button";
|
||||
import Link01 from "../../../components/icons/link-01";
|
||||
import File02 from "../../../components/icons/file-02";
|
||||
import SimpleLikeButton from "../../../components/event-reactions/simple-like-button";
|
||||
|
||||
function ArticleLinkLoader({ pointer, ...props }: { pointer: nip19.AddressPointer } & Omit<LinkProps, "children">) {
|
||||
const article = useReplaceableEvent(pointer);
|
||||
@@ -64,62 +70,33 @@ export function ListCardContent({ list, ...props }: Omit<CardProps, "children">
|
||||
const references = getReferencesFromList(list);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text>
|
||||
Updated: <Timestamp timestamp={list.created_at} />
|
||||
</Text>
|
||||
<SimpleGrid spacing="2" columns={4}>
|
||||
{people.length > 0 && (
|
||||
<>
|
||||
<Text>People ({people.length}):</Text>
|
||||
<AvatarGroup overflow="hidden" mb="2" max={16} size="sm">
|
||||
{people.map(({ pubkey, relay }) => (
|
||||
<UserAvatarLink key={pubkey} pubkey={pubkey} relay={relay} />
|
||||
))}
|
||||
</AvatarGroup>
|
||||
</>
|
||||
<Text>
|
||||
<User01 boxSize={5} /> {people.length}
|
||||
</Text>
|
||||
)}
|
||||
{notes.length > 0 && (
|
||||
<Flex gap="2" overflow="hidden" wrap="wrap">
|
||||
<Text>Notes ({notes.length}):</Text>
|
||||
{notes.slice(0, 4).map(({ id, relay }) => (
|
||||
<NoteLink key={id} noteId={id} />
|
||||
))}
|
||||
</Flex>
|
||||
<Text>
|
||||
<NotesIcon boxSize={5} /> {notes.length}
|
||||
</Text>
|
||||
)}
|
||||
{references.length > 0 && (
|
||||
<Flex gap="2" overflow="hidden" wrap="wrap">
|
||||
<Text>References ({references.length})</Text>
|
||||
{references.slice(0, 3).map(({ url, petname }) => (
|
||||
<Link maxW="200" href={url} isExternal whiteSpace="pre" color="blue.500" isTruncated>
|
||||
{petname || url}
|
||||
</Link>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
{communities.length > 0 && (
|
||||
<Flex gap="2" overflow="hidden" wrap="wrap">
|
||||
<Text>Communities ({communities.length}):</Text>
|
||||
{communities.map((pointer) => (
|
||||
<Link
|
||||
key={JSON.stringify(pointer)}
|
||||
as={RouterLink}
|
||||
to={`/c/${pointer.identifier}/${nip19.npubEncode(pointer.pubkey)}`}
|
||||
color="blue.500"
|
||||
>
|
||||
{pointer.identifier}
|
||||
</Link>
|
||||
))}
|
||||
</Flex>
|
||||
<Text>
|
||||
<Link01 boxSize={5} /> {references.length}
|
||||
</Text>
|
||||
)}
|
||||
{articles.length > 0 && (
|
||||
<Flex overflow="hidden" direction="column" wrap="wrap">
|
||||
<Text>Articles ({articles.length}):</Text>
|
||||
{articles.slice(0, 4).map((pointer) => (
|
||||
<ArticleLinkLoader key={JSON.stringify(pointer)} pointer={pointer} isTruncated />
|
||||
))}
|
||||
</Flex>
|
||||
<Text>
|
||||
<File02 /> {articles.length}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
{communities.length > 0 && (
|
||||
<Text>
|
||||
<CommunityIcon boxSize={5} /> {communities.length}
|
||||
</Text>
|
||||
)}
|
||||
</SimpleGrid>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -135,12 +112,12 @@ function ListCardRender({
|
||||
useRegisterIntersectionEntity(ref, getEventUID(list));
|
||||
|
||||
return (
|
||||
<Card ref={ref} variant="outline" {...props}>
|
||||
<CardHeader display="flex" gap="2" alignItems="center" p="2" pb="0">
|
||||
<Card as={LinkBox} ref={ref} variant="outline" {...props}>
|
||||
<CardHeader display="flex" gap="2" p="4" alignItems="center">
|
||||
<Heading size="md" isTruncated>
|
||||
<Link as={RouterLink} to={`/lists/${link}`}>
|
||||
<HoverLinkOverlay as={RouterLink} to={`/lists/${link}`}>
|
||||
{getListName(list)}
|
||||
</Link>
|
||||
</HoverLinkOverlay>
|
||||
</Heading>
|
||||
{!hideCreator && (
|
||||
<>
|
||||
@@ -149,14 +126,19 @@ function ListCardRender({
|
||||
<UserLink pubkey={list.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
|
||||
</>
|
||||
)}
|
||||
<ButtonGroup size="xs" variant="ghost" ml="auto">
|
||||
</CardHeader>
|
||||
<CardBody py="0" px="4">
|
||||
<ListCardContent list={list} />
|
||||
</CardBody>
|
||||
<CardFooter p="2">
|
||||
<NoteZapButton event={list} size="sm" variant="ghost" />
|
||||
{/* TODO: reactions are tagging every user in list */}
|
||||
<SimpleLikeButton event={list} variant="ghost" size="sm" />
|
||||
<ButtonGroup size="sm" variant="ghost" ml="auto">
|
||||
<ListFavoriteButton list={list} />
|
||||
<ListMenu list={list} aria-label="list menu" />
|
||||
</ButtonGroup>
|
||||
</CardHeader>
|
||||
<CardBody p="2">
|
||||
<ListCardContent list={list} />
|
||||
</CardBody>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
@@ -18,16 +18,16 @@ import {
|
||||
import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
||||
import UserCard from "./components/user-card";
|
||||
import OpenGraphCard from "../../components/open-graph-card";
|
||||
import NoteCard from "./components/note-card";
|
||||
import { TrustProvider } from "../../providers/trust";
|
||||
import ListMenu from "./components/list-menu";
|
||||
import ListFavoriteButton from "./components/list-favorite-button";
|
||||
import ListFeedButton from "./components/list-feed-button";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import { COMMUNITY_DEFINITION_KIND } from "../../helpers/nostr/communities";
|
||||
import { EmbedEventPointer } from "../../components/embed-event";
|
||||
import { EmbedEvent, EmbedEventPointer } from "../../components/embed-event";
|
||||
import { encodePointer } from "../../helpers/nip19";
|
||||
import { DecodeResult } from "nostr-tools/lib/types/nip19";
|
||||
import useSingleEvent from "../../hooks/use-single-event";
|
||||
|
||||
function useListCoordinate() {
|
||||
const { addr } = useParams() as { addr: string };
|
||||
@@ -43,6 +43,12 @@ function useListCoordinate() {
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
function BookmarkedEvent({ id, relay }: { id: string; relay?: string }) {
|
||||
const event = useSingleEvent(id, relay ? [relay] : undefined);
|
||||
|
||||
return event ? <EmbedEvent event={event} /> : <>Loading {id}</>;
|
||||
}
|
||||
|
||||
export default function ListDetailsView() {
|
||||
const navigate = useNavigate();
|
||||
const coordinate = useListCoordinate();
|
||||
@@ -67,6 +73,7 @@ export default function ListDetailsView() {
|
||||
const references = getReferencesFromList(list);
|
||||
|
||||
return (
|
||||
<TrustProvider trust>
|
||||
<VerticalPageLayout overflow="hidden" h="full">
|
||||
<Flex gap="2" alignItems="center">
|
||||
<Button onClick={() => navigate(-1)} leftIcon={<ChevronLeftIcon />}>
|
||||
@@ -103,20 +110,17 @@ export default function ListDetailsView() {
|
||||
{notes.length > 0 && (
|
||||
<>
|
||||
<Heading size="lg">Notes</Heading>
|
||||
<TrustProvider trust>
|
||||
<Flex gap="2" direction="column">
|
||||
{notes.map(({ id, relay }) => (
|
||||
<NoteCard id={id} relay={relay} />
|
||||
<BookmarkedEvent id={id} relay={relay} />
|
||||
))}
|
||||
</Flex>
|
||||
</TrustProvider>
|
||||
</>
|
||||
)}
|
||||
|
||||
{references.length > 0 && (
|
||||
<>
|
||||
<Heading size="lg">References</Heading>
|
||||
<TrustProvider trust>
|
||||
<Flex gap="2" direction="column">
|
||||
{references.map(({ url, petname }) => (
|
||||
<>
|
||||
@@ -125,7 +129,6 @@ export default function ListDetailsView() {
|
||||
</>
|
||||
))}
|
||||
</Flex>
|
||||
</TrustProvider>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -152,5 +155,6 @@ export default function ListDetailsView() {
|
||||
</>
|
||||
)}
|
||||
</VerticalPageLayout>
|
||||
</TrustProvider>
|
||||
);
|
||||
}
|
||||
|
@@ -13,6 +13,7 @@ import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-
|
||||
import { Kind } from "nostr-tools";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import UserName from "../../components/user-name";
|
||||
|
||||
export default function UserListsTab() {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
@@ -24,30 +25,36 @@ export default function UserListsTab() {
|
||||
const timeline = useTimelineLoader(
|
||||
pubkey + "-lists",
|
||||
readRelays,
|
||||
[
|
||||
{
|
||||
authors: [pubkey],
|
||||
kinds: [PEOPLE_LIST_KIND, NOTE_LIST_KIND],
|
||||
},
|
||||
{
|
||||
"#p": [pubkey],
|
||||
kinds: [PEOPLE_LIST_KIND],
|
||||
},
|
||||
],
|
||||
{ eventFilter },
|
||||
);
|
||||
|
||||
const lists = useSubject(timeline.timeline);
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
const peopleLists = lists.filter((event) => event.kind === PEOPLE_LIST_KIND);
|
||||
const noteLists = lists.filter((event) => event.kind === NOTE_LIST_KIND);
|
||||
const peopleLists = lists.filter((event) => event.pubkey === pubkey && event.kind === PEOPLE_LIST_KIND);
|
||||
const noteLists = lists.filter((event) => event.pubkey === pubkey && event.kind === NOTE_LIST_KIND);
|
||||
const otherLists = lists.filter((event) => event.pubkey !== pubkey && event.kind === PEOPLE_LIST_KIND);
|
||||
|
||||
return (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<VerticalPageLayout>
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<Heading size="md" mt="2">
|
||||
Special lists
|
||||
</Heading>
|
||||
<Divider />
|
||||
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
|
||||
<ListCard cord={`${Kind.Contacts}:${pubkey}`} />
|
||||
<ListCard cord={`${MUTE_LIST_KIND}:${pubkey}`} />
|
||||
<ListCard cord={`${PIN_LIST_KIND}:${pubkey}`} />
|
||||
<ListCard cord={`${Kind.Contacts}:${pubkey}`} hideCreator />
|
||||
<ListCard cord={`${MUTE_LIST_KIND}:${pubkey}`} hideCreator />
|
||||
<ListCard cord={`${PIN_LIST_KIND}:${pubkey}`} hideCreator />
|
||||
</SimpleGrid>
|
||||
|
||||
{peopleLists.length > 0 && (
|
||||
@@ -55,10 +62,9 @@ export default function UserListsTab() {
|
||||
<Heading size="md" mt="2">
|
||||
People lists
|
||||
</Heading>
|
||||
<Divider />
|
||||
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
|
||||
{peopleLists.map((event) => (
|
||||
<ListCard key={getEventUID(event)} list={event} />
|
||||
<ListCard key={getEventUID(event)} list={event} hideCreator />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
@@ -69,15 +75,25 @@ export default function UserListsTab() {
|
||||
<Heading size="md" mt="2">
|
||||
Bookmark lists
|
||||
</Heading>
|
||||
<Divider />
|
||||
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
|
||||
{noteLists.map((event) => (
|
||||
<ListCard key={getEventUID(event)} list={event} />
|
||||
<ListCard key={getEventUID(event)} list={event} hideCreator />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)}
|
||||
</VerticalPageLayout>
|
||||
</IntersectionObserverProvider>
|
||||
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<Heading size="md" mt="2">
|
||||
Lists <UserName pubkey={pubkey} /> is in
|
||||
</Heading>
|
||||
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
|
||||
{otherLists.map((event) => (
|
||||
<ListCard key={getEventUID(event)} list={event} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</IntersectionObserverProvider>
|
||||
</VerticalPageLayout>
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user