mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-10-03 16:09:52 +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 { useBreakpointValue } from "../../providers/breakpoint-provider";
|
||||||
import SearchModal from "../search-modal";
|
import SearchModal from "../search-modal";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import ChatWindows from "../chat-windows";
|
// import ChatWindows from "../chat-windows";
|
||||||
|
|
||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
const isMobile = useBreakpointValue({ base: true, md: false });
|
const isMobile = useBreakpointValue({ base: true, md: false });
|
||||||
@@ -66,7 +66,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
{isGhost && <GhostToolbar />}
|
{isGhost && <GhostToolbar />}
|
||||||
{searchModal.isOpen && <SearchModal isOpen onClose={searchModal.onClose} />}
|
{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 { NostrEvent } from "../../../types/nostr-event";
|
||||||
import ReactionButton from "./reaction-button";
|
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 useEventReactions from "../../../hooks/use-event-reactions";
|
||||||
import { useBreakpointValue } from "../../../providers/breakpoint-provider";
|
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) {
|
export default function NoteZapButton({ event, allowComment, showEventPreview, ...props }: NoteZapButtonProps) {
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
const { metadata } = useUserLNURLMetadata(event.pubkey);
|
const { metadata } = useUserLNURLMetadata(event.pubkey);
|
||||||
const zaps = useEventZaps(event.id);
|
const zaps = useEventZaps(getEventUID(event));
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
|
||||||
const hasZapped = !!account && zaps.some((zap) => zap.request.pubkey === account.pubkey);
|
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 { NostrEvent } from "../types/nostr-event";
|
||||||
import { groupReactions } from "../helpers/nostr/reactions";
|
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 UserAvatarLink from "./user-avatar-link";
|
||||||
import { UserLink } from "./user-link";
|
import { UserLink } from "./user-link";
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { Kind } from "nostr-tools";
|
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 dayjs from "dayjs";
|
||||||
|
import { getEventCoordinate, isReplaceable } from "./events";
|
||||||
|
|
||||||
export type ReactionGroup = { emoji: string; url?: string; name?: string; count: number; pubkeys: string[] };
|
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);
|
return Array.from(Object.values(groups)).sort((a, b) => b.pubkeys.length - a.pubkeys.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function draftEventReaction(reacted: NostrEvent, emoji = "+", url?: string) {
|
export function draftEventReaction(event: NostrEvent, emoji = "+", url?: string) {
|
||||||
// only keep the e, and p tags on the parent event
|
const tags: Tag[] = [
|
||||||
const inheritedTags = reacted.tags.filter((tag) => tag.length >= 2 && (tag[0] === "e" || tag[0] === "p"));
|
["e", event.id],
|
||||||
|
["p", event.pubkey],
|
||||||
|
];
|
||||||
const draft: DraftNostrEvent = {
|
const draft: DraftNostrEvent = {
|
||||||
kind: Kind.Reaction,
|
kind: Kind.Reaction,
|
||||||
content: url ? ":" + emoji + ":" : emoji,
|
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(),
|
created_at: dayjs().unix(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,16 +1,17 @@
|
|||||||
import { memo, useRef } from "react";
|
import { memo, useRef } from "react";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
AvatarGroup,
|
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Card,
|
Card,
|
||||||
CardBody,
|
CardBody,
|
||||||
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardProps,
|
CardProps,
|
||||||
Flex,
|
|
||||||
Heading,
|
Heading,
|
||||||
Link,
|
Link,
|
||||||
|
LinkBox,
|
||||||
LinkProps,
|
LinkProps,
|
||||||
|
SimpleGrid,
|
||||||
Text,
|
Text,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { Kind, nip19 } from "nostr-tools";
|
import { Kind, nip19 } from "nostr-tools";
|
||||||
@@ -29,15 +30,20 @@ import { getSharableEventAddress } from "../../../helpers/nip19";
|
|||||||
import { NostrEvent } from "../../../types/nostr-event";
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
|
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
|
||||||
import { createCoordinate } from "../../../services/replaceable-event-requester";
|
import { createCoordinate } from "../../../services/replaceable-event-requester";
|
||||||
import { NoteLink } from "../../../components/note-link";
|
|
||||||
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||||
import ListFavoriteButton from "./list-favorite-button";
|
import ListFavoriteButton from "./list-favorite-button";
|
||||||
import { getEventUID } from "../../../helpers/nostr/events";
|
import { getEventUID } from "../../../helpers/nostr/events";
|
||||||
import ListMenu from "./list-menu";
|
import ListMenu from "./list-menu";
|
||||||
import Timestamp from "../../../components/timestamp";
|
|
||||||
import { COMMUNITY_DEFINITION_KIND } from "../../../helpers/nostr/communities";
|
import { COMMUNITY_DEFINITION_KIND } from "../../../helpers/nostr/communities";
|
||||||
import { getArticleTitle } from "../../../helpers/nostr/long-form";
|
import { getArticleTitle } from "../../../helpers/nostr/long-form";
|
||||||
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
|
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">) {
|
function ArticleLinkLoader({ pointer, ...props }: { pointer: nip19.AddressPointer } & Omit<LinkProps, "children">) {
|
||||||
const article = useReplaceableEvent(pointer);
|
const article = useReplaceableEvent(pointer);
|
||||||
@@ -64,62 +70,33 @@ export function ListCardContent({ list, ...props }: Omit<CardProps, "children">
|
|||||||
const references = getReferencesFromList(list);
|
const references = getReferencesFromList(list);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<SimpleGrid spacing="2" columns={4}>
|
||||||
<Text>
|
|
||||||
Updated: <Timestamp timestamp={list.created_at} />
|
|
||||||
</Text>
|
|
||||||
{people.length > 0 && (
|
{people.length > 0 && (
|
||||||
<>
|
<Text>
|
||||||
<Text>People ({people.length}):</Text>
|
<User01 boxSize={5} /> {people.length}
|
||||||
<AvatarGroup overflow="hidden" mb="2" max={16} size="sm">
|
</Text>
|
||||||
{people.map(({ pubkey, relay }) => (
|
|
||||||
<UserAvatarLink key={pubkey} pubkey={pubkey} relay={relay} />
|
|
||||||
))}
|
|
||||||
</AvatarGroup>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{notes.length > 0 && (
|
{notes.length > 0 && (
|
||||||
<Flex gap="2" overflow="hidden" wrap="wrap">
|
<Text>
|
||||||
<Text>Notes ({notes.length}):</Text>
|
<NotesIcon boxSize={5} /> {notes.length}
|
||||||
{notes.slice(0, 4).map(({ id, relay }) => (
|
</Text>
|
||||||
<NoteLink key={id} noteId={id} />
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
)}
|
)}
|
||||||
{references.length > 0 && (
|
{references.length > 0 && (
|
||||||
<Flex gap="2" overflow="hidden" wrap="wrap">
|
<Text>
|
||||||
<Text>References ({references.length})</Text>
|
<Link01 boxSize={5} /> {references.length}
|
||||||
{references.slice(0, 3).map(({ url, petname }) => (
|
</Text>
|
||||||
<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>
|
|
||||||
)}
|
)}
|
||||||
{articles.length > 0 && (
|
{articles.length > 0 && (
|
||||||
<Flex overflow="hidden" direction="column" wrap="wrap">
|
<Text>
|
||||||
<Text>Articles ({articles.length}):</Text>
|
<File02 /> {articles.length}
|
||||||
{articles.slice(0, 4).map((pointer) => (
|
</Text>
|
||||||
<ArticleLinkLoader key={JSON.stringify(pointer)} pointer={pointer} isTruncated />
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
)}
|
)}
|
||||||
</>
|
{communities.length > 0 && (
|
||||||
|
<Text>
|
||||||
|
<CommunityIcon boxSize={5} /> {communities.length}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</SimpleGrid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,12 +112,12 @@ function ListCardRender({
|
|||||||
useRegisterIntersectionEntity(ref, getEventUID(list));
|
useRegisterIntersectionEntity(ref, getEventUID(list));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card ref={ref} variant="outline" {...props}>
|
<Card as={LinkBox} ref={ref} variant="outline" {...props}>
|
||||||
<CardHeader display="flex" gap="2" alignItems="center" p="2" pb="0">
|
<CardHeader display="flex" gap="2" p="4" alignItems="center">
|
||||||
<Heading size="md" isTruncated>
|
<Heading size="md" isTruncated>
|
||||||
<Link as={RouterLink} to={`/lists/${link}`}>
|
<HoverLinkOverlay as={RouterLink} to={`/lists/${link}`}>
|
||||||
{getListName(list)}
|
{getListName(list)}
|
||||||
</Link>
|
</HoverLinkOverlay>
|
||||||
</Heading>
|
</Heading>
|
||||||
{!hideCreator && (
|
{!hideCreator && (
|
||||||
<>
|
<>
|
||||||
@@ -149,14 +126,19 @@ function ListCardRender({
|
|||||||
<UserLink pubkey={list.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
|
<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} />
|
<ListFavoriteButton list={list} />
|
||||||
<ListMenu list={list} aria-label="list menu" />
|
<ListMenu list={list} aria-label="list menu" />
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</CardHeader>
|
</CardFooter>
|
||||||
<CardBody p="2">
|
|
||||||
<ListCardContent list={list} />
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -18,16 +18,16 @@ import {
|
|||||||
import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
||||||
import UserCard from "./components/user-card";
|
import UserCard from "./components/user-card";
|
||||||
import OpenGraphCard from "../../components/open-graph-card";
|
import OpenGraphCard from "../../components/open-graph-card";
|
||||||
import NoteCard from "./components/note-card";
|
|
||||||
import { TrustProvider } from "../../providers/trust";
|
import { TrustProvider } from "../../providers/trust";
|
||||||
import ListMenu from "./components/list-menu";
|
import ListMenu from "./components/list-menu";
|
||||||
import ListFavoriteButton from "./components/list-favorite-button";
|
import ListFavoriteButton from "./components/list-favorite-button";
|
||||||
import ListFeedButton from "./components/list-feed-button";
|
import ListFeedButton from "./components/list-feed-button";
|
||||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
import { COMMUNITY_DEFINITION_KIND } from "../../helpers/nostr/communities";
|
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 { encodePointer } from "../../helpers/nip19";
|
||||||
import { DecodeResult } from "nostr-tools/lib/types/nip19";
|
import { DecodeResult } from "nostr-tools/lib/types/nip19";
|
||||||
|
import useSingleEvent from "../../hooks/use-single-event";
|
||||||
|
|
||||||
function useListCoordinate() {
|
function useListCoordinate() {
|
||||||
const { addr } = useParams() as { addr: string };
|
const { addr } = useParams() as { addr: string };
|
||||||
@@ -43,6 +43,12 @@ function useListCoordinate() {
|
|||||||
return parsed.data;
|
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() {
|
export default function ListDetailsView() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const coordinate = useListCoordinate();
|
const coordinate = useListCoordinate();
|
||||||
@@ -67,6 +73,7 @@ export default function ListDetailsView() {
|
|||||||
const references = getReferencesFromList(list);
|
const references = getReferencesFromList(list);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<TrustProvider trust>
|
||||||
<VerticalPageLayout overflow="hidden" h="full">
|
<VerticalPageLayout overflow="hidden" h="full">
|
||||||
<Flex gap="2" alignItems="center">
|
<Flex gap="2" alignItems="center">
|
||||||
<Button onClick={() => navigate(-1)} leftIcon={<ChevronLeftIcon />}>
|
<Button onClick={() => navigate(-1)} leftIcon={<ChevronLeftIcon />}>
|
||||||
@@ -103,20 +110,17 @@ export default function ListDetailsView() {
|
|||||||
{notes.length > 0 && (
|
{notes.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Heading size="lg">Notes</Heading>
|
<Heading size="lg">Notes</Heading>
|
||||||
<TrustProvider trust>
|
|
||||||
<Flex gap="2" direction="column">
|
<Flex gap="2" direction="column">
|
||||||
{notes.map(({ id, relay }) => (
|
{notes.map(({ id, relay }) => (
|
||||||
<NoteCard id={id} relay={relay} />
|
<BookmarkedEvent id={id} relay={relay} />
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
</TrustProvider>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{references.length > 0 && (
|
{references.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Heading size="lg">References</Heading>
|
<Heading size="lg">References</Heading>
|
||||||
<TrustProvider trust>
|
|
||||||
<Flex gap="2" direction="column">
|
<Flex gap="2" direction="column">
|
||||||
{references.map(({ url, petname }) => (
|
{references.map(({ url, petname }) => (
|
||||||
<>
|
<>
|
||||||
@@ -125,7 +129,6 @@ export default function ListDetailsView() {
|
|||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
</TrustProvider>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -152,5 +155,6 @@ export default function ListDetailsView() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</VerticalPageLayout>
|
</VerticalPageLayout>
|
||||||
|
</TrustProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,7 @@ import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-
|
|||||||
import { Kind } from "nostr-tools";
|
import { Kind } from "nostr-tools";
|
||||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
|
import UserName from "../../components/user-name";
|
||||||
|
|
||||||
export default function UserListsTab() {
|
export default function UserListsTab() {
|
||||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||||
@@ -24,30 +25,36 @@ export default function UserListsTab() {
|
|||||||
const timeline = useTimelineLoader(
|
const timeline = useTimelineLoader(
|
||||||
pubkey + "-lists",
|
pubkey + "-lists",
|
||||||
readRelays,
|
readRelays,
|
||||||
|
[
|
||||||
{
|
{
|
||||||
authors: [pubkey],
|
authors: [pubkey],
|
||||||
kinds: [PEOPLE_LIST_KIND, NOTE_LIST_KIND],
|
kinds: [PEOPLE_LIST_KIND, NOTE_LIST_KIND],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"#p": [pubkey],
|
||||||
|
kinds: [PEOPLE_LIST_KIND],
|
||||||
|
},
|
||||||
|
],
|
||||||
{ eventFilter },
|
{ eventFilter },
|
||||||
);
|
);
|
||||||
|
|
||||||
const lists = useSubject(timeline.timeline);
|
const lists = useSubject(timeline.timeline);
|
||||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
|
||||||
const peopleLists = lists.filter((event) => event.kind === PEOPLE_LIST_KIND);
|
const peopleLists = lists.filter((event) => event.pubkey === pubkey && event.kind === PEOPLE_LIST_KIND);
|
||||||
const noteLists = lists.filter((event) => event.kind === NOTE_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 (
|
return (
|
||||||
<IntersectionObserverProvider callback={callback}>
|
|
||||||
<VerticalPageLayout>
|
<VerticalPageLayout>
|
||||||
|
<IntersectionObserverProvider callback={callback}>
|
||||||
<Heading size="md" mt="2">
|
<Heading size="md" mt="2">
|
||||||
Special lists
|
Special lists
|
||||||
</Heading>
|
</Heading>
|
||||||
<Divider />
|
|
||||||
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
|
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
|
||||||
<ListCard cord={`${Kind.Contacts}:${pubkey}`} />
|
<ListCard cord={`${Kind.Contacts}:${pubkey}`} hideCreator />
|
||||||
<ListCard cord={`${MUTE_LIST_KIND}:${pubkey}`} />
|
<ListCard cord={`${MUTE_LIST_KIND}:${pubkey}`} hideCreator />
|
||||||
<ListCard cord={`${PIN_LIST_KIND}:${pubkey}`} />
|
<ListCard cord={`${PIN_LIST_KIND}:${pubkey}`} hideCreator />
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
{peopleLists.length > 0 && (
|
{peopleLists.length > 0 && (
|
||||||
@@ -55,10 +62,9 @@ export default function UserListsTab() {
|
|||||||
<Heading size="md" mt="2">
|
<Heading size="md" mt="2">
|
||||||
People lists
|
People lists
|
||||||
</Heading>
|
</Heading>
|
||||||
<Divider />
|
|
||||||
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
|
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
|
||||||
{peopleLists.map((event) => (
|
{peopleLists.map((event) => (
|
||||||
<ListCard key={getEventUID(event)} list={event} />
|
<ListCard key={getEventUID(event)} list={event} hideCreator />
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</>
|
</>
|
||||||
@@ -69,15 +75,25 @@ export default function UserListsTab() {
|
|||||||
<Heading size="md" mt="2">
|
<Heading size="md" mt="2">
|
||||||
Bookmark lists
|
Bookmark lists
|
||||||
</Heading>
|
</Heading>
|
||||||
<Divider />
|
|
||||||
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
|
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
|
||||||
{noteLists.map((event) => (
|
{noteLists.map((event) => (
|
||||||
<ListCard key={getEventUID(event)} list={event} />
|
<ListCard key={getEventUID(event)} list={event} hideCreator />
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</VerticalPageLayout>
|
|
||||||
</IntersectionObserverProvider>
|
</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