feat: nip-13

This commit is contained in:
Alejandro Gómez
2026-04-08 17:19:55 +02:00
parent 891d152a51
commit 6fcd5df671
3 changed files with 54 additions and 1 deletions

View File

@@ -52,6 +52,8 @@ import {
getFavoriteConfig,
FALLBACK_FAVORITE_CONFIG,
} from "@/config/favorite-lists";
import { getPowDifficulty } from "@/lib/nip13-helpers";
import { Label } from "@/components/ui/label";
/**
* Universal event properties and utilities shared across all kind renderers
@@ -489,6 +491,7 @@ export function BaseEventContainer({
};
}) {
const { locale } = useGrimoire();
const addWindow = useAddWindow();
const { canSign, signer, pubkey } = useAccount();
const { settings } = useSettings();
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
@@ -527,6 +530,8 @@ export function BaseEventContainer({
[signer, pubkey, event],
);
const powDifficulty = getPowDifficulty(event);
const relativeTime = formatTimestamp(
event.created_at,
"relative",
@@ -568,6 +573,14 @@ export function BaseEventContainer({
>
{relativeTime}
</span>
{powDifficulty !== undefined && (
<Label
className="text-[10px] tabular-nums cursor-crosshair hover:text-foreground"
onClick={() => addWindow("nip", { number: 13 }, "NIP 13")}
>
PoW {powDifficulty}
</Label>
)}
</div>
<EventMenu
event={event}

View File

@@ -9,15 +9,22 @@ interface LabelProps {
* - md: px-3 py-1
*/
size?: "sm" | "md";
onClick?: (e: React.MouseEvent) => void;
}
/**
* Label/Badge component with dotted border styling
* Used for tags, language indicators, and metadata labels
*/
export function Label({ children, className, size = "sm" }: LabelProps) {
export function Label({
children,
className,
size = "sm",
onClick,
}: LabelProps) {
return (
<span
onClick={onClick}
className={cn(
"truncate line-clamp-1 border border-muted border-dotted text-muted-foreground text-xs",
size === "sm" && "px-2 py-0.5",

33
src/lib/nip13-helpers.ts Normal file
View File

@@ -0,0 +1,33 @@
import type { NostrEvent } from "@/types/nostr";
import { getOrComputeCachedValue } from "applesauce-core/helpers";
const PowDifficultySymbol = Symbol("powDifficulty");
/**
* Count leading zero bits in a hex string (NIP-13 difficulty).
*/
function countLeadingZeroBits(hex: string): number {
let count = 0;
for (let i = 0; i < hex.length; i++) {
const nibble = parseInt(hex[i], 16);
if (nibble === 0) {
count += 4;
} else {
count += Math.clz32(nibble) - 28;
break;
}
}
return count;
}
/**
* Get the PoW difficulty of an event, or undefined if it has no nonce tag.
* Cached on the event object via applesauce helpers.
*/
export function getPowDifficulty(event: NostrEvent): number | undefined {
return getOrComputeCachedValue(event, PowDifficultySymbol, () => {
const nonceTag = event.tags.find((t) => t[0] === "nonce");
if (!nonceTag) return undefined;
return countLeadingZeroBits(event.id);
});
}