Skip to content

Commit

Permalink
Add badge activity tab
Browse files Browse the repository at this point in the history
  • Loading branch information
hzrd149 committed Oct 13, 2023
1 parent ed1cb04 commit 56fc982
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 138 deletions.
5 changes: 5 additions & 0 deletions .changeset/warm-pianos-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nostrudel": minor
---

Add badge activity tab
8 changes: 5 additions & 3 deletions src/helpers/nostr/badges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}
}
Expand Down
110 changes: 76 additions & 34 deletions src/views/badges/badge-details.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,82 @@
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";
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 (
<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 }) {
const navigate = useNavigate();
const { deleteEvent } = useDeleteEventContext();
const account = useCurrentAccount();

const image = getBadgeImage(badge);
const description = getBadgeDescription(badge);
Expand All @@ -37,12 +88,8 @@ function BadgeDetailsPage({ badge }: { badge: NostrEvent }) {
kinds: [Kind.BadgeAward],
});

const awards = useSubject(awardsTimeline.timeline);
const callback = useTimelineCurserIntersectionCallback(awardsTimeline);

if (!badge) return <Spinner />;

const isAuthor = account?.pubkey === badge.pubkey;
return (
<VerticalPageLayout>
<Flex gap="2" alignItems="center" wrap="wrap">
Expand All @@ -59,16 +106,13 @@ function BadgeDetailsPage({ badge }: { badge: NostrEvent }) {

<EventRelays event={badge} />

{isAuthor && (
<Button colorScheme="red" onClick={() => deleteEvent(badge).then(() => navigate("/lists"))}>
Delete
</Button>
)}
<BadgeMenu aria-label="More options" badge={badge} />
</Flex>

<Flex direction={{ base: "column", lg: "row" }} gap="2">
{image && <Image src={image.src} maxW="3in" mr="2" mb="2" mx={{ base: "auto", lg: "initial" }} />}
<Flex direction={{ base: "column", lg: "row" }} gap="4">
{image && (
<Image src={image.src} maxW="3in" mr="2" mb="2" mx={{ base: "auto", lg: "initial" }} borderRadius="lg" />
)}
<Flex direction="column">
<Heading size="md">{getBadgeName(badge)}</Heading>
<Text>
Expand All @@ -89,22 +133,20 @@ function BadgeDetailsPage({ badge }: { badge: NostrEvent }) {
</Flex>
</Flex>

{awards.length > 0 && (
<>
<IntersectionObserverProvider callback={callback}>
<Heading size="lg">Awarded to</Heading>
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
{awards.map((award) => (
<>
{getBadgeAwardPubkey(award).map(({ pubkey }) => (
<BadgeAwardCard award={award} pubkey={pubkey} />
))}
</>
))}
</SimpleGrid>
</IntersectionObserverProvider>
</>
)}
<Tabs colorScheme="primary" isLazy>
<TabList>
<Tab>Activity</Tab>
<Tab>Users</Tab>
</TabList>
<TabPanels>
<TabPanel px="0">
<BadgeActivityTab timeline={awardsTimeline} />
</TabPanel>
<TabPanel>
<BadgeUsersTab timeline={awardsTimeline} />
</TabPanel>
</TabPanels>
</Tabs>
</VerticalPageLayout>
);
}
Expand Down
32 changes: 0 additions & 32 deletions src/views/badges/components/award-card.tsx

This file was deleted.

54 changes: 54 additions & 0 deletions src/views/badges/components/badge-award-card.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
11 changes: 9 additions & 2 deletions src/views/badges/components/badge-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<CardProps, "children"> & { badge: NostrEvent }) {
const naddr = getSharableEventAddress(badge);
Expand All @@ -21,9 +23,13 @@ function BadgeCard({ badge, ...props }: Omit<CardProps, "children"> & { badge: N
const ref = useRef<HTMLDivElement | null>(null);
useRegisterIntersectionEntity(ref, getEventUID(badge));

const timesAwarded = useEventCount({ kinds: [Kind.BadgeAward], "#a": [getEventCoordinate(badge)] });

return (
<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">
<Heading size="md">
<Link as={RouterLink} to={`/badges/${naddr}`}>
Expand All @@ -43,6 +49,7 @@ function BadgeCard({ badge, ...props }: Omit<CardProps, "children"> & { badge: N
<Text>
Updated: <Timestamp timestamp={badge.created_at} />
</Text>
<Text>Times Awarded: {timesAwarded}</Text>
</CardBody>
</Card>
);
Expand Down
Loading

0 comments on commit 56fc982

Please sign in to comment.