mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-20 04:20:39 +02:00
Add badge activity tab
This commit is contained in:
5
.changeset/warm-pianos-divide.md
Normal file
5
.changeset/warm-pianos-divide.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add badge activity tab
|
@@ -30,7 +30,7 @@ export function getBadgeThumbnails(event: NostrEvent) {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBadgeAwardPubkey(event: NostrEvent) {
|
export function getBadgeAwardPubkeys(event: NostrEvent) {
|
||||||
return getPubkeysFromList(event);
|
return getPubkeysFromList(event);
|
||||||
}
|
}
|
||||||
export function getBadgeAwardBadge(event: NostrEvent) {
|
export function getBadgeAwardBadge(event: NostrEvent) {
|
||||||
@@ -39,20 +39,22 @@ export function getBadgeAwardBadge(event: NostrEvent) {
|
|||||||
return badgeCord;
|
return badgeCord;
|
||||||
}
|
}
|
||||||
export function validateBadgeAwardEvent(event: NostrEvent) {
|
export function validateBadgeAwardEvent(event: NostrEvent) {
|
||||||
getBadgeAwardPubkey(event);
|
getBadgeAwardPubkeys(event);
|
||||||
getBadgeAwardBadge(event);
|
getBadgeAwardBadge(event);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseProfileBadges(profileBadges: NostrEvent) {
|
export function parseProfileBadges(profileBadges: NostrEvent) {
|
||||||
|
const badgesAdded = new Set();
|
||||||
const badgeAwardSets: { badgeCord: string; awardEventId: string; relay?: string }[] = [];
|
const badgeAwardSets: { badgeCord: string; awardEventId: string; relay?: string }[] = [];
|
||||||
|
|
||||||
let lastBadgeTag: ATag | undefined;
|
let lastBadgeTag: ATag | undefined;
|
||||||
for (const tag of profileBadges.tags) {
|
for (const tag of profileBadges.tags) {
|
||||||
if (isATag(tag)) {
|
if (isATag(tag)) {
|
||||||
lastBadgeTag = tag;
|
lastBadgeTag = tag;
|
||||||
} else if (isETag(tag) && lastBadgeTag) {
|
} else if (isETag(tag) && lastBadgeTag && !badgesAdded.has(lastBadgeTag[1])) {
|
||||||
badgeAwardSets.push({ badgeCord: lastBadgeTag[1], awardEventId: tag[1], relay: tag[2] });
|
badgeAwardSets.push({ badgeCord: lastBadgeTag[1], awardEventId: tag[1], relay: tag[2] });
|
||||||
|
badgesAdded.add(lastBadgeTag[1]);
|
||||||
lastBadgeTag = undefined;
|
lastBadgeTag = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,19 +1,30 @@
|
|||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { Kind, nip19 } from "nostr-tools";
|
import { Kind, nip19 } from "nostr-tools";
|
||||||
import { Box, Button, Divider, Flex, Heading, Image, SimpleGrid, Spacer, Spinner, Text } from "@chakra-ui/react";
|
import {
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
Image,
|
||||||
|
SimpleGrid,
|
||||||
|
Spacer,
|
||||||
|
Spinner,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
TabPanel,
|
||||||
|
TabPanels,
|
||||||
|
Tabs,
|
||||||
|
Text,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
|
||||||
import { ChevronLeftIcon } from "../../components/icons";
|
import { ChevronLeftIcon } from "../../components/icons";
|
||||||
import { useDeleteEventContext } from "../../providers/delete-event-provider";
|
|
||||||
import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
||||||
import { EventRelays } from "../../components/note/note-relays";
|
import { EventRelays } from "../../components/note/note-relays";
|
||||||
import { getBadgeAwardPubkey, getBadgeDescription, getBadgeImage, getBadgeName } from "../../helpers/nostr/badges";
|
import { getBadgeAwardPubkeys, getBadgeDescription, getBadgeImage, getBadgeName } from "../../helpers/nostr/badges";
|
||||||
import BadgeMenu from "./components/badge-menu";
|
import BadgeMenu from "./components/badge-menu";
|
||||||
import BadgeAwardCard from "./components/award-card";
|
|
||||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||||
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";
|
||||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
|
||||||
import useSubject from "../../hooks/use-subject";
|
import useSubject from "../../hooks/use-subject";
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
import { getEventCoordinate } from "../../helpers/nostr/events";
|
import { getEventCoordinate } from "../../helpers/nostr/events";
|
||||||
@@ -21,11 +32,51 @@ import { UserAvatarLink } from "../../components/user-avatar-link";
|
|||||||
import { UserLink } from "../../components/user-link";
|
import { UserLink } from "../../components/user-link";
|
||||||
import Timestamp from "../../components/timestamp";
|
import Timestamp from "../../components/timestamp";
|
||||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
|
import BadgeAwardCard from "./components/badge-award-card";
|
||||||
|
import TimelineLoader from "../../classes/timeline-loader";
|
||||||
|
|
||||||
|
function BadgeActivityTab({ timeline }: { timeline: TimelineLoader }) {
|
||||||
|
const awards = useSubject(timeline.timeline);
|
||||||
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" gap="4">
|
||||||
|
<IntersectionObserverProvider callback={callback}>
|
||||||
|
{awards.map((award) => (
|
||||||
|
<BadgeAwardCard key={award.id} award={award} showImage={false} />
|
||||||
|
))}
|
||||||
|
</IntersectionObserverProvider>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BadgeUsersTab({ timeline }: { timeline: TimelineLoader }) {
|
||||||
|
const awards = useSubject(timeline.timeline);
|
||||||
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
|
||||||
|
const pubkeys = new Set<string>();
|
||||||
|
for (const award of awards) {
|
||||||
|
for (const { pubkey } of getBadgeAwardPubkeys(award)) {
|
||||||
|
pubkeys.add(pubkey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SimpleGrid spacing={4} columns={[1, 2, 2, 3, 4, 5, 6]}>
|
||||||
|
<IntersectionObserverProvider callback={callback}>
|
||||||
|
{Array.from(pubkeys).map((pubkey) => (
|
||||||
|
<Flex key={pubkey} gap="2" alignItems="center">
|
||||||
|
<UserAvatarLink pubkey={pubkey} size="md" />
|
||||||
|
<UserLink pubkey={pubkey} fontWeight="bold" isTruncated />
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</IntersectionObserverProvider>
|
||||||
|
</SimpleGrid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function BadgeDetailsPage({ badge }: { badge: NostrEvent }) {
|
function BadgeDetailsPage({ badge }: { badge: NostrEvent }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { deleteEvent } = useDeleteEventContext();
|
|
||||||
const account = useCurrentAccount();
|
|
||||||
|
|
||||||
const image = getBadgeImage(badge);
|
const image = getBadgeImage(badge);
|
||||||
const description = getBadgeDescription(badge);
|
const description = getBadgeDescription(badge);
|
||||||
@@ -37,12 +88,8 @@ function BadgeDetailsPage({ badge }: { badge: NostrEvent }) {
|
|||||||
kinds: [Kind.BadgeAward],
|
kinds: [Kind.BadgeAward],
|
||||||
});
|
});
|
||||||
|
|
||||||
const awards = useSubject(awardsTimeline.timeline);
|
|
||||||
const callback = useTimelineCurserIntersectionCallback(awardsTimeline);
|
|
||||||
|
|
||||||
if (!badge) return <Spinner />;
|
if (!badge) return <Spinner />;
|
||||||
|
|
||||||
const isAuthor = account?.pubkey === badge.pubkey;
|
|
||||||
return (
|
return (
|
||||||
<VerticalPageLayout>
|
<VerticalPageLayout>
|
||||||
<Flex gap="2" alignItems="center" wrap="wrap">
|
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||||
@@ -59,16 +106,13 @@ function BadgeDetailsPage({ badge }: { badge: NostrEvent }) {
|
|||||||
|
|
||||||
<EventRelays event={badge} />
|
<EventRelays event={badge} />
|
||||||
|
|
||||||
{isAuthor && (
|
|
||||||
<Button colorScheme="red" onClick={() => deleteEvent(badge).then(() => navigate("/lists"))}>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<BadgeMenu aria-label="More options" badge={badge} />
|
<BadgeMenu aria-label="More options" badge={badge} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex direction={{ base: "column", lg: "row" }} gap="2">
|
<Flex direction={{ base: "column", lg: "row" }} gap="4">
|
||||||
{image && <Image src={image.src} maxW="3in" mr="2" mb="2" mx={{ base: "auto", lg: "initial" }} />}
|
{image && (
|
||||||
|
<Image src={image.src} maxW="3in" mr="2" mb="2" mx={{ base: "auto", lg: "initial" }} borderRadius="lg" />
|
||||||
|
)}
|
||||||
<Flex direction="column">
|
<Flex direction="column">
|
||||||
<Heading size="md">{getBadgeName(badge)}</Heading>
|
<Heading size="md">{getBadgeName(badge)}</Heading>
|
||||||
<Text>
|
<Text>
|
||||||
@@ -89,22 +133,20 @@ function BadgeDetailsPage({ badge }: { badge: NostrEvent }) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{awards.length > 0 && (
|
<Tabs colorScheme="primary" isLazy>
|
||||||
<>
|
<TabList>
|
||||||
<IntersectionObserverProvider callback={callback}>
|
<Tab>Activity</Tab>
|
||||||
<Heading size="lg">Awarded to</Heading>
|
<Tab>Users</Tab>
|
||||||
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
|
</TabList>
|
||||||
{awards.map((award) => (
|
<TabPanels>
|
||||||
<>
|
<TabPanel px="0">
|
||||||
{getBadgeAwardPubkey(award).map(({ pubkey }) => (
|
<BadgeActivityTab timeline={awardsTimeline} />
|
||||||
<BadgeAwardCard award={award} pubkey={pubkey} />
|
</TabPanel>
|
||||||
))}
|
<TabPanel>
|
||||||
</>
|
<BadgeUsersTab timeline={awardsTimeline} />
|
||||||
))}
|
</TabPanel>
|
||||||
</SimpleGrid>
|
</TabPanels>
|
||||||
</IntersectionObserverProvider>
|
</Tabs>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</VerticalPageLayout>
|
</VerticalPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,32 +0,0 @@
|
|||||||
import { useRef } from "react";
|
|
||||||
import { Card, CardBody, CardProps, Flex, Heading } from "@chakra-ui/react";
|
|
||||||
|
|
||||||
import { UserAvatar } from "../../../components/user-avatar";
|
|
||||||
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon";
|
|
||||||
import { NostrEvent } from "../../../types/nostr-event";
|
|
||||||
import { UserLink } from "../../../components/user-link";
|
|
||||||
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
|
||||||
import { getEventUID } from "../../../helpers/nostr/events";
|
|
||||||
|
|
||||||
export default function BadgeAwardCard({
|
|
||||||
pubkey,
|
|
||||||
award,
|
|
||||||
...props
|
|
||||||
}: Omit<CardProps, "children"> & { award: NostrEvent; pubkey: string }) {
|
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
|
||||||
useRegisterIntersectionEntity(ref, getEventUID(award));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card {...props} ref={ref}>
|
|
||||||
<CardBody p="2" display="flex" alignItems="center" overflow="hidden" gap="2">
|
|
||||||
<UserAvatar pubkey={pubkey} />
|
|
||||||
<Flex direction="column" flex={1} overflow="hidden">
|
|
||||||
<Heading size="sm" whiteSpace="nowrap" isTruncated>
|
|
||||||
<UserLink pubkey={pubkey} />
|
|
||||||
</Heading>
|
|
||||||
<UserDnsIdentityIcon pubkey={pubkey} />
|
|
||||||
</Flex>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
54
src/views/badges/components/badge-award-card.tsx
Normal file
54
src/views/badges/components/badge-award-card.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
import { Card, Flex, Image, Link, LinkBox, SimpleGrid, Text } from "@chakra-ui/react";
|
||||||
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
|
||||||
|
import { getBadgeAwardBadge, getBadgeAwardPubkeys, getBadgeImage, getBadgeName } from "../../../helpers/nostr/badges";
|
||||||
|
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
|
||||||
|
import { NostrEvent, isPTag } from "../../../types/nostr-event";
|
||||||
|
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||||
|
import { getEventUID } from "../../../helpers/nostr/events";
|
||||||
|
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||||
|
import { UserLink } from "../../../components/user-link";
|
||||||
|
import Timestamp from "../../../components/timestamp";
|
||||||
|
import { UserAvatarLink } from "../../../components/user-avatar-link";
|
||||||
|
|
||||||
|
export default function BadgeAwardCard({ award, showImage = true }: { award: NostrEvent; showImage?: boolean }) {
|
||||||
|
const badge = useReplaceableEvent(getBadgeAwardBadge(award));
|
||||||
|
|
||||||
|
// if there is a parent intersection observer, register this card
|
||||||
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
|
useRegisterIntersectionEntity(ref, badge && getEventUID(badge));
|
||||||
|
|
||||||
|
if (!badge) return null;
|
||||||
|
|
||||||
|
const naddr = getSharableEventAddress(badge);
|
||||||
|
return (
|
||||||
|
<Card as={LinkBox} p="2" variant="outline" gap="2" flexDirection={["column", null, "row"]} ref={ref}>
|
||||||
|
{showImage && (
|
||||||
|
<Flex as={RouterLink} to={`/badges/${naddr}`} direction="column" overflow="hidden" gap="2" w="40" mx="auto">
|
||||||
|
<Image aspectRatio={1} src={getBadgeImage(badge)?.src ?? ""} w="40" />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
<Flex gap="2" direction="column" flex={1}>
|
||||||
|
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||||
|
<UserAvatarLink pubkey={award.pubkey} size="sm" />
|
||||||
|
<UserLink pubkey={award.pubkey} fontWeight="bold" />
|
||||||
|
<Text>Awarded</Text>
|
||||||
|
<Link as={RouterLink} to={`/badges/${naddr}`} fontWeight="bold">
|
||||||
|
{getBadgeName(badge)}
|
||||||
|
</Link>
|
||||||
|
<Text>To</Text>
|
||||||
|
<Timestamp timestamp={award.created_at} ml="auto" />
|
||||||
|
</Flex>
|
||||||
|
<Flex gap="4" wrap="wrap">
|
||||||
|
{getBadgeAwardPubkeys(award).map(({ pubkey }) => (
|
||||||
|
<Flex key={pubkey} gap="2" alignItems="center">
|
||||||
|
<UserAvatarLink pubkey={pubkey} size="sm" />
|
||||||
|
<UserLink pubkey={pubkey} fontWeight="bold" isTruncated />
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
@@ -7,10 +7,12 @@ import { UserLink } from "../../../components/user-link";
|
|||||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||||
import { NostrEvent } from "../../../types/nostr-event";
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||||
import { getEventUID } from "../../../helpers/nostr/events";
|
import { getEventCoordinate, getEventUID } from "../../../helpers/nostr/events";
|
||||||
import BadgeMenu from "./badge-menu";
|
import BadgeMenu from "./badge-menu";
|
||||||
import { getBadgeImage, getBadgeName } from "../../../helpers/nostr/badges";
|
import { getBadgeImage, getBadgeName } from "../../../helpers/nostr/badges";
|
||||||
import Timestamp from "../../../components/timestamp";
|
import Timestamp from "../../../components/timestamp";
|
||||||
|
import useEventCount from "../../../hooks/use-event-count";
|
||||||
|
import { Kind } from "nostr-tools";
|
||||||
|
|
||||||
function BadgeCard({ badge, ...props }: Omit<CardProps, "children"> & { badge: NostrEvent }) {
|
function BadgeCard({ badge, ...props }: Omit<CardProps, "children"> & { badge: NostrEvent }) {
|
||||||
const naddr = getSharableEventAddress(badge);
|
const naddr = getSharableEventAddress(badge);
|
||||||
@@ -21,9 +23,13 @@ function BadgeCard({ badge, ...props }: Omit<CardProps, "children"> & { badge: N
|
|||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
useRegisterIntersectionEntity(ref, getEventUID(badge));
|
useRegisterIntersectionEntity(ref, getEventUID(badge));
|
||||||
|
|
||||||
|
const timesAwarded = useEventCount({ kinds: [Kind.BadgeAward], "#a": [getEventCoordinate(badge)] });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card ref={ref} variant="outline" {...props}>
|
<Card ref={ref} variant="outline" {...props}>
|
||||||
{image && <Image src={image.src} cursor="pointer" onClick={() => navigate(`/badges/${naddr}`)} />}
|
{image && (
|
||||||
|
<Image src={image.src} cursor="pointer" onClick={() => navigate(`/badges/${naddr}`)} borderRadius="lg" />
|
||||||
|
)}
|
||||||
<CardHeader display="flex" alignItems="center" p="2" pb="0">
|
<CardHeader display="flex" alignItems="center" p="2" pb="0">
|
||||||
<Heading size="md">
|
<Heading size="md">
|
||||||
<Link as={RouterLink} to={`/badges/${naddr}`}>
|
<Link as={RouterLink} to={`/badges/${naddr}`}>
|
||||||
@@ -43,6 +49,7 @@ function BadgeCard({ badge, ...props }: Omit<CardProps, "children"> & { badge: N
|
|||||||
<Text>
|
<Text>
|
||||||
Updated: <Timestamp timestamp={badge.created_at} />
|
Updated: <Timestamp timestamp={badge.created_at} />
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text>Times Awarded: {timesAwarded}</Text>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
@@ -1,18 +1,5 @@
|
|||||||
import { useRef } from "react";
|
import { Button, Flex, Heading, Image, Link, Spacer } from "@chakra-ui/react";
|
||||||
import {
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
AvatarGroup,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Flex,
|
|
||||||
Heading,
|
|
||||||
Image,
|
|
||||||
Link,
|
|
||||||
LinkBox,
|
|
||||||
LinkOverlay,
|
|
||||||
Spacer,
|
|
||||||
Text,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { Navigate, Link as RouterLink } from "react-router-dom";
|
|
||||||
import { Kind } from "nostr-tools";
|
import { Kind } from "nostr-tools";
|
||||||
|
|
||||||
import { ExternalLinkIcon } from "../../components/icons";
|
import { ExternalLinkIcon } from "../../components/icons";
|
||||||
@@ -22,64 +9,15 @@ import PeopleListProvider, { usePeopleListContext } from "../../providers/people
|
|||||||
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
||||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||||
import useSubject from "../../hooks/use-subject";
|
import useSubject from "../../hooks/use-subject";
|
||||||
import { NostrEvent, isPTag } from "../../types/nostr-event";
|
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||||
import { UserLink } from "../../components/user-link";
|
|
||||||
import { UserAvatar } from "../../components/user-avatar";
|
|
||||||
import { getBadgeAwardBadge, getBadgeImage, getBadgeName } from "../../helpers/nostr/badges";
|
|
||||||
import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
|
||||||
import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
|
|
||||||
import { getEventUID } from "../../helpers/nostr/events";
|
|
||||||
import { getSharableEventAddress } from "../../helpers/nip19";
|
|
||||||
import { UserAvatarLink } from "../../components/user-avatar-link";
|
|
||||||
import Timestamp from "../../components/timestamp";
|
|
||||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||||
|
import BadgeAwardCard from "./components/badge-award-card";
|
||||||
function BadgeAwardCard({ award }: { award: NostrEvent }) {
|
|
||||||
const badge = useReplaceableEvent(getBadgeAwardBadge(award));
|
|
||||||
|
|
||||||
// if there is a parent intersection observer, register this card
|
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
|
||||||
useRegisterIntersectionEntity(ref, badge && getEventUID(badge));
|
|
||||||
|
|
||||||
if (!badge) return null;
|
|
||||||
|
|
||||||
const naddr = getSharableEventAddress(badge);
|
|
||||||
return (
|
|
||||||
<Card p="2" variant="outline" gap="2" flexDirection={["column", null, "row"]} ref={ref}>
|
|
||||||
<Flex as={LinkBox} direction="column" overflow="hidden" gap="2" w="40" mx="auto">
|
|
||||||
<Image aspectRatio={1} src={getBadgeImage(badge)?.src ?? ""} w="40" />
|
|
||||||
<Heading size="sm" isTruncated>
|
|
||||||
<LinkOverlay as={RouterLink} to={`/badges/${naddr}`}>
|
|
||||||
{getBadgeName(badge)}
|
|
||||||
</LinkOverlay>
|
|
||||||
</Heading>
|
|
||||||
</Flex>
|
|
||||||
<Flex gap="2" direction="column" flex={1}>
|
|
||||||
<Flex gap="2" alignItems="center">
|
|
||||||
<UserAvatar pubkey={award.pubkey} size="sm" />
|
|
||||||
<UserLink pubkey={award.pubkey} fontWeight="bold" />
|
|
||||||
<Text>Awarded:</Text>
|
|
||||||
<Spacer />
|
|
||||||
<Timestamp timestamp={award.created_at} />
|
|
||||||
</Flex>
|
|
||||||
<Flex gap="2" wrap="wrap">
|
|
||||||
{award.tags.filter(isPTag).map((t) => (
|
|
||||||
<Flex key={t[1]} gap="2" alignItems="center">
|
|
||||||
<UserAvatarLink pubkey={t[1]} size="sm" />
|
|
||||||
<UserLink pubkey={t[1]} fontWeight="bold" />
|
|
||||||
</Flex>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function BadgesPage() {
|
function BadgesPage() {
|
||||||
const { filter, listId } = usePeopleListContext();
|
const { filter, listId } = usePeopleListContext();
|
||||||
const readRelays = useReadRelayUrls();
|
const readRelays = useReadRelayUrls();
|
||||||
const timeline = useTimelineLoader(`${listId}-lists`, readRelays, {
|
const timeline = useTimelineLoader(`${listId}-lists`, readRelays, {
|
||||||
...filter,
|
"#p": filter?.authors,
|
||||||
kinds: [Kind.BadgeAward],
|
kinds: [Kind.BadgeAward],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user