Skip to content

Commit

Permalink
show profile badges on user profile
Browse files Browse the repository at this point in the history
  • Loading branch information
hzrd149 committed Oct 9, 2023
1 parent d9353b0 commit 21a1a8a
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 24 deletions.
5 changes: 5 additions & 0 deletions .changeset/selfish-otters-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nostrudel": minor
---

Show profile badges on users profile
18 changes: 17 additions & 1 deletion src/helpers/nostr/badges.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NostrEvent, isATag, isPTag } from "../../types/nostr-event";
import { ATag, NostrEvent, isATag, isETag } from "../../types/nostr-event";
import { getPubkeysFromList } from "./lists";

export const PROFILE_BADGES_IDENTIFIER = "profile_badges";
Expand Down Expand Up @@ -43,3 +43,19 @@ export function validateBadgeAwardEvent(event: NostrEvent) {
getBadgeAwardBadge(event);
return true;
}

export function parseProfileBadges(profileBadges: NostrEvent) {
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) {
badgeAwardSets.push({ badgeCord: lastBadgeTag[1], awardEventId: tag[1], relay: tag[2] });
lastBadgeTag = undefined;
}
}

return badgeAwardSets;
}
14 changes: 14 additions & 0 deletions src/hooks/use-single-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useMemo } from "react";

import singleEventService from "../services/single-event";
import { useReadRelayUrls } from "./use-client-relays";
import useSubjects from "./use-subjects";

export default function useSingleEvents(ids?: string[], additionalRelays: string[] = []) {
const readRelays = useReadRelayUrls(additionalRelays);
const subjects = useMemo(() => {
return ids?.map((id) => singleEventService.requestEvent(id, readRelays)) ?? [];
}, [ids, readRelays.join("|")]);

return useSubjects(subjects);
}
30 changes: 30 additions & 0 deletions src/hooks/use-user-profile-badges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Kind } from "nostr-tools";

import useReplaceableEvent from "./use-replaceable-event";
import { PROFILE_BADGES_IDENTIFIER, parseProfileBadges } from "../helpers/nostr/badges";
import useReplaceableEvents from "./use-replaceable-events";
import useSingleEvents from "./use-single-events";
import { getEventCoordinate } from "../helpers/nostr/events";
import { NostrEvent } from "../types/nostr-event";

export default function useUserProfileBadges(pubkey: string, additionalRelays: string[] = []) {
const profileBadgesEvent = useReplaceableEvent({
pubkey,
kind: Kind.ProfileBadge,
identifier: PROFILE_BADGES_IDENTIFIER,
});
const parsed = profileBadgesEvent ? parseProfileBadges(profileBadgesEvent) : [];

const badges = useReplaceableEvents(parsed.map((b) => b.badgeCord));
const awardEvents = useSingleEvents(parsed.map((b) => b.awardEventId));

const final: { badge: NostrEvent; award: NostrEvent }[] = [];
for (const p of parsed) {
const badge = badges.find((e) => getEventCoordinate(e) === p.badgeCord);
const award = awardEvents.find((e) => e.id === p.awardEventId);

if (badge && award) final.push({ badge, award });
}

return final;
}
2 changes: 1 addition & 1 deletion src/views/badges/badge-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ function BadgeDetailsPage({ badge }: { badge: NostrEvent }) {
<UserLink fontWeight="bold" pubkey={badge.pubkey} />
</Text>
<Text>
Last Updated: <Timestamp timestamp={badge.created_at} />
Created: <Timestamp timestamp={badge.created_at} />
</Text>
{description && <Text pb="2">{description}</Text>}
</Flex>
Expand Down
47 changes: 25 additions & 22 deletions src/views/user/about.tsx → src/views/user/about/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,36 +23,37 @@ import {
import { useAsync } from "react-use";
import { nip19 } from "nostr-tools";

import { readablizeSats } from "../../helpers/bolt11";
import { getUserDisplayName } from "../../helpers/user-metadata";
import { getLudEndpoint } from "../../helpers/lnurl";
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
import { truncatedId } from "../../helpers/nostr/events";
import trustedUserStatsService from "../../services/trusted-user-stats";
import { parseAddress } from "../../services/dns-identity";
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import { embedNostrLinks, renderGenericUrl } from "../../components/embed-types";
import { readablizeSats } from "../../../helpers/bolt11";
import { getUserDisplayName } from "../../../helpers/user-metadata";
import { getLudEndpoint } from "../../../helpers/lnurl";
import { EmbedableContent, embedUrls } from "../../../helpers/embeds";
import { truncatedId } from "../../../helpers/nostr/events";
import trustedUserStatsService from "../../../services/trusted-user-stats";
import { parseAddress } from "../../../services/dns-identity";
import { useAdditionalRelayContext } from "../../../providers/additional-relay-context";
import { useUserMetadata } from "../../../hooks/use-user-metadata";
import { embedNostrLinks, renderGenericUrl } from "../../../components/embed-types";
import {
ChevronDownIcon,
ChevronUpIcon,
AtIcon,
ExternalLinkIcon,
KeyIcon,
LightningIcon,
} from "../../components/icons";
import { CopyIconButton } from "../../components/copy-icon-button";
import { QrIconButton } from "./components/share-qr-button";
import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
import { UserAvatar } from "../../components/user-avatar";
} from "../../../components/icons";
import { CopyIconButton } from "../../../components/copy-icon-button";
import { QrIconButton } from "../components/share-qr-button";
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon";
import { UserAvatar } from "../../../components/user-avatar";
import { ChatIcon } from "@chakra-ui/icons";
import { UserFollowButton } from "../../components/user-follow-button";
import UserZapButton from "./components/user-zap-button";
import { UserProfileMenu } from "./components/user-profile-menu";
import { useSharableProfileId } from "../../hooks/use-shareable-profile-id";
import useUserContactList from "../../hooks/use-user-contact-list";
import { getPubkeysFromList } from "../../helpers/nostr/lists";
import Timestamp from "../../components/timestamp";
import { UserFollowButton } from "../../../components/user-follow-button";
import UserZapButton from "../components/user-zap-button";
import { UserProfileMenu } from "../components/user-profile-menu";
import { useSharableProfileId } from "../../../hooks/use-shareable-profile-id";
import useUserContactList from "../../../hooks/use-user-contact-list";
import { getPubkeysFromList } from "../../../helpers/nostr/lists";
import Timestamp from "../../../components/timestamp";
import UserProfileBadges from "./user-profile-badges";

function buildDescriptionContent(description: string) {
let content: EmbedableContent = [description.trim()];
Expand Down Expand Up @@ -182,6 +183,8 @@ export default function UserAboutTab() {
)}
</Flex>

<UserProfileBadges pubkey={pubkey} />

<Accordion allowMultiple>
<AccordionItem>
<h2>
Expand Down
93 changes: 93 additions & 0 deletions src/views/user/about/user-profile-badges.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {
Button,
Flex,
FlexProps,
Heading,
Image,
Link,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Text,
Tooltip,
useDisclosure,
} from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";

import useUserProfileBadges from "../../../hooks/use-user-profile-badges";
import { getBadgeDescription, getBadgeImage, getBadgeName } from "../../../helpers/nostr/badges";
import { getEventCoordinate } from "../../../helpers/nostr/events";
import { NostrEvent } from "../../../types/nostr-event";
import { getSharableEventAddress } from "../../../helpers/nip19";
import { UserAvatarLink } from "../../../components/user-avatar-link";
import { UserLink } from "../../../components/user-link";
import Timestamp from "../../../components/timestamp";

function Badge({ pubkey, badge, award }: { pubkey: string; badge: NostrEvent; award: NostrEvent }) {
const naddr = getSharableEventAddress(badge);
const modal = useDisclosure();
const description = getBadgeDescription(badge);

return (
<>
<Link
as={RouterLink}
to={`/badges/${naddr}`}
onClick={(e) => {
e.preventDefault();
modal.onOpen();
}}
>
<Tooltip label={getBadgeName(badge)}>
<Image w="14" h="14" src={getBadgeImage(badge)?.src ?? ""} />
</Tooltip>
</Link>
<Modal isOpen={modal.isOpen} onClose={modal.onClose}>
<ModalOverlay />
<ModalContent>
<Image src={getBadgeImage(badge)?.src ?? ""} />
<ModalCloseButton />
<ModalHeader px="4" pt="2" pb="0">
{getBadgeName(badge)}
</ModalHeader>
<ModalBody px="4" py="2">
<Text>
Created by <UserAvatarLink pubkey={badge.pubkey} size="xs" />{" "}
<UserLink fontWeight="bold" pubkey={badge.pubkey} /> on <Timestamp timestamp={badge.created_at} />
</Text>
<Text>
Date Awarded: <Timestamp timestamp={award.created_at} />
</Text>
<Heading size="sm" mt="4">
Description:
</Heading>
{description && <Text>{description}</Text>}
</ModalBody>
<ModalFooter p="4">
<Button as={RouterLink} to={`/badges/${naddr}`}>
Details
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
}

export default function UserProfileBadges({ pubkey, ...props }: Omit<FlexProps, "children"> & { pubkey: string }) {
const profileBadges = useUserProfileBadges(pubkey);

if (profileBadges.length === 0) return null;

return (
<Flex gap="2" wrap="wrap" {...props}>
{profileBadges.map(({ badge, award }) => (
<Badge key={getEventCoordinate(badge)} pubkey={pubkey} badge={badge} award={award} />
))}
</Flex>
);
}

0 comments on commit 21a1a8a

Please sign in to comment.