diff --git a/.changeset/warm-pianos-divide.md b/.changeset/warm-pianos-divide.md
new file mode 100644
index 000000000..541679d46
--- /dev/null
+++ b/.changeset/warm-pianos-divide.md
@@ -0,0 +1,5 @@
+---
+"nostrudel": minor
+---
+
+Add badge activity tab
diff --git a/src/helpers/nostr/badges.ts b/src/helpers/nostr/badges.ts
index 431f3939d..6d18ad9a0 100644
--- a/src/helpers/nostr/badges.ts
+++ b/src/helpers/nostr/badges.ts
@@ -30,7 +30,7 @@ export function getBadgeThumbnails(event: NostrEvent) {
.filter(Boolean);
}
-export function getBadgeAwardPubkey(event: NostrEvent) {
+export function getBadgeAwardPubkeys(event: NostrEvent) {
return getPubkeysFromList(event);
}
export function getBadgeAwardBadge(event: NostrEvent) {
@@ -39,20 +39,22 @@ export function getBadgeAwardBadge(event: NostrEvent) {
return badgeCord;
}
export function validateBadgeAwardEvent(event: NostrEvent) {
- getBadgeAwardPubkey(event);
+ getBadgeAwardPubkeys(event);
getBadgeAwardBadge(event);
return true;
}
export function parseProfileBadges(profileBadges: NostrEvent) {
+ const badgesAdded = new Set();
const badgeAwardSets: { badgeCord: string; awardEventId: string; relay?: string }[] = [];
let lastBadgeTag: ATag | undefined;
for (const tag of profileBadges.tags) {
if (isATag(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] });
+ badgesAdded.add(lastBadgeTag[1]);
lastBadgeTag = undefined;
}
}
diff --git a/src/views/badges/badge-details.tsx b/src/views/badges/badge-details.tsx
index 5ae194884..05781e00d 100644
--- a/src/views/badges/badge-details.tsx
+++ b/src/views/badges/badge-details.tsx
@@ -1,19 +1,30 @@
import { useNavigate, useParams } from "react-router-dom";
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 { useDeleteEventContext } from "../../providers/delete-event-provider";
import useReplaceableEvent from "../../hooks/use-replaceable-event";
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 BadgeAwardCard from "./components/award-card";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import IntersectionObserverProvider from "../../providers/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
-import { useCurrentAccount } from "../../hooks/use-current-account";
import useSubject from "../../hooks/use-subject";
import { NostrEvent } from "../../types/nostr-event";
import { getEventCoordinate } from "../../helpers/nostr/events";
@@ -21,11 +32,51 @@ import { UserAvatarLink } from "../../components/user-avatar-link";
import { UserLink } from "../../components/user-link";
import Timestamp from "../../components/timestamp";
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 (
+
+
+ {awards.map((award) => (
+
+ ))}
+
+
+ );
+}
+
+function BadgeUsersTab({ timeline }: { timeline: TimelineLoader }) {
+ const awards = useSubject(timeline.timeline);
+ const callback = useTimelineCurserIntersectionCallback(timeline);
+
+ const pubkeys = new Set();
+ for (const award of awards) {
+ for (const { pubkey } of getBadgeAwardPubkeys(award)) {
+ pubkeys.add(pubkey);
+ }
+ }
+
+ return (
+
+
+ {Array.from(pubkeys).map((pubkey) => (
+
+
+
+
+ ))}
+
+
+ );
+}
function BadgeDetailsPage({ badge }: { badge: NostrEvent }) {
const navigate = useNavigate();
- const { deleteEvent } = useDeleteEventContext();
- const account = useCurrentAccount();
const image = getBadgeImage(badge);
const description = getBadgeDescription(badge);
@@ -37,12 +88,8 @@ function BadgeDetailsPage({ badge }: { badge: NostrEvent }) {
kinds: [Kind.BadgeAward],
});
- const awards = useSubject(awardsTimeline.timeline);
- const callback = useTimelineCurserIntersectionCallback(awardsTimeline);
-
if (!badge) return ;
- const isAuthor = account?.pubkey === badge.pubkey;
return (
@@ -59,16 +106,13 @@ function BadgeDetailsPage({ badge }: { badge: NostrEvent }) {
- {isAuthor && (
-
- )}
-
- {image && }
+
+ {image && (
+
+ )}
{getBadgeName(badge)}
@@ -89,22 +133,20 @@ function BadgeDetailsPage({ badge }: { badge: NostrEvent }) {
- {awards.length > 0 && (
- <>
-
- Awarded to
-
- {awards.map((award) => (
- <>
- {getBadgeAwardPubkey(award).map(({ pubkey }) => (
-
- ))}
- >
- ))}
-
-
- >
- )}
+
+
+ Activity
+ Users
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/views/badges/components/award-card.tsx b/src/views/badges/components/award-card.tsx
deleted file mode 100644
index 2a18da964..000000000
--- a/src/views/badges/components/award-card.tsx
+++ /dev/null
@@ -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 & { award: NostrEvent; pubkey: string }) {
- const ref = useRef(null);
- useRegisterIntersectionEntity(ref, getEventUID(award));
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/views/badges/components/badge-award-card.tsx b/src/views/badges/components/badge-award-card.tsx
new file mode 100644
index 000000000..385ecd7ac
--- /dev/null
+++ b/src/views/badges/components/badge-award-card.tsx
@@ -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(null);
+ useRegisterIntersectionEntity(ref, badge && getEventUID(badge));
+
+ if (!badge) return null;
+
+ const naddr = getSharableEventAddress(badge);
+ return (
+
+ {showImage && (
+
+
+
+ )}
+
+
+
+
+ Awarded
+
+ {getBadgeName(badge)}
+
+ To
+
+
+
+ {getBadgeAwardPubkeys(award).map(({ pubkey }) => (
+
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/views/badges/components/badge-card.tsx b/src/views/badges/components/badge-card.tsx
index 77c800b45..798049641 100644
--- a/src/views/badges/components/badge-card.tsx
+++ b/src/views/badges/components/badge-card.tsx
@@ -7,10 +7,12 @@ import { UserLink } from "../../../components/user-link";
import { getSharableEventAddress } from "../../../helpers/nip19";
import { NostrEvent } from "../../../types/nostr-event";
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 { getBadgeImage, getBadgeName } from "../../../helpers/nostr/badges";
import Timestamp from "../../../components/timestamp";
+import useEventCount from "../../../hooks/use-event-count";
+import { Kind } from "nostr-tools";
function BadgeCard({ badge, ...props }: Omit & { badge: NostrEvent }) {
const naddr = getSharableEventAddress(badge);
@@ -21,9 +23,13 @@ function BadgeCard({ badge, ...props }: Omit & { badge: N
const ref = useRef(null);
useRegisterIntersectionEntity(ref, getEventUID(badge));
+ const timesAwarded = useEventCount({ kinds: [Kind.BadgeAward], "#a": [getEventCoordinate(badge)] });
+
return (
- {image && navigate(`/badges/${naddr}`)} />}
+ {image && (
+ navigate(`/badges/${naddr}`)} borderRadius="lg" />
+ )}
@@ -43,6 +49,7 @@ function BadgeCard({ badge, ...props }: Omit & { badge: N
Updated:
+ Times Awarded: {timesAwarded}
);
diff --git a/src/views/badges/index.tsx b/src/views/badges/index.tsx
index 01a99082c..f07293228 100644
--- a/src/views/badges/index.tsx
+++ b/src/views/badges/index.tsx
@@ -1,18 +1,5 @@
-import { useRef } from "react";
-import {
- 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 { Button, Flex, Heading, Image, Link, Spacer } from "@chakra-ui/react";
+import { Link as RouterLink } from "react-router-dom";
import { Kind } from "nostr-tools";
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 { useReadRelayUrls } from "../../hooks/use-client-relays";
import useSubject from "../../hooks/use-subject";
-import { NostrEvent, isPTag } from "../../types/nostr-event";
-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 IntersectionObserverProvider from "../../providers/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
-
-function BadgeAwardCard({ award }: { award: NostrEvent }) {
- const badge = useReplaceableEvent(getBadgeAwardBadge(award));
-
- // if there is a parent intersection observer, register this card
- const ref = useRef(null);
- useRegisterIntersectionEntity(ref, badge && getEventUID(badge));
-
- if (!badge) return null;
-
- const naddr = getSharableEventAddress(badge);
- return (
-
-
-
-
-
- {getBadgeName(badge)}
-
-
-
-
-
-
-
- Awarded:
-
-
-
-
- {award.tags.filter(isPTag).map((t) => (
-
-
-
-
- ))}
-
-
-
- );
-}
+import BadgeAwardCard from "./components/badge-award-card";
function BadgesPage() {
const { filter, listId } = usePeopleListContext();
const readRelays = useReadRelayUrls();
const timeline = useTimelineLoader(`${listId}-lists`, readRelays, {
- ...filter,
+ "#p": filter?.authors,
kinds: [Kind.BadgeAward],
});