mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-09 20:29:17 +02:00
show profile badges on user profile
This commit is contained in:
parent
d9353b08b3
commit
21a1a8a5a3
5
.changeset/selfish-otters-drum.md
Normal file
5
.changeset/selfish-otters-drum.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Show profile badges on users profile
|
@ -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";
|
||||
@ -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
src/hooks/use-single-events.ts
Normal file
14
src/hooks/use-single-events.ts
Normal file
@ -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
src/hooks/use-user-profile-badges.ts
Normal file
30
src/hooks/use-user-profile-badges.ts
Normal file
@ -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;
|
||||
}
|
@ -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>
|
||||
|
@ -23,16 +23,16 @@ 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,
|
||||
@ -40,19 +40,20 @@ import {
|
||||
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()];
|
||||
@ -182,6 +183,8 @@ export default function UserAboutTab() {
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<UserProfileBadges pubkey={pubkey} />
|
||||
|
||||
<Accordion allowMultiple>
|
||||
<AccordionItem>
|
||||
<h2>
|
93
src/views/user/about/user-profile-badges.tsx
Normal file
93
src/views/user/about/user-profile-badges.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user