Implement NIP-89 client tag display and improved tag format

Co-authored-by: mroxso <24775431+mroxso@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-11-18 22:11:19 +00:00
parent 5343e55942
commit 7caf34f95a
5 changed files with 153 additions and 2 deletions

View File

@@ -10,6 +10,7 @@ import { ZapButton } from '@/components/ZapButton';
import { BookmarkButton } from '@/components/BookmarkButton';
import { ReadingTime } from '@/components/ReadingTime';
import { ArticleProgressBar } from '@/components/ArticleProgressBar';
import { ClientTag } from '@/components/ClientTag';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
@@ -291,6 +292,10 @@ export function ArticleView({ post }: ArticleViewProps) {
<Separator className="my-8" />
<div className="mb-8">
<ClientTag event={post} />
</div>
<CommentsSection root={post} />
</article>
</div>

View File

@@ -0,0 +1,30 @@
import type { NostrEvent } from '@nostrify/nostrify';
import { parseClientTag } from '@/lib/parseClientTag';
import { Badge } from '@/components/ui/badge';
import { Smartphone } from 'lucide-react';
interface ClientTagProps {
event: NostrEvent;
}
/**
* Displays the client tag information from a Nostr event according to NIP-89
* Shows the client name that was used to publish the event
*/
export function ClientTag({ event }: ClientTagProps) {
const clientInfo = parseClientTag(event);
if (!clientInfo) {
return null;
}
return (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Smartphone className="h-4 w-4" />
<span>Published with</span>
<Badge variant="secondary" className="font-normal">
{clientInfo.name}
</Badge>
</div>
);
}

View File

@@ -14,9 +14,11 @@ export function useNostrPublish(): UseMutationResult<NostrEvent> {
if (user) {
const tags = t.tags ?? [];
// Add the client tag if it doesn't exist
// Add the client tag if it doesn't exist (NIP-89)
// Format: ["client", "Client Name", "31990:pubkey:d-identifier", "wss://relay-hint"]
// The address and relay are optional but recommended for better discoverability
if (location.protocol === "https:" && !tags.some(([name]) => name === "client")) {
tags.push(["client", location.hostname]);
tags.push(["client", "zelo.news"]);
}
const event = await user.signer.signEvent({

View File

@@ -0,0 +1,75 @@
import { describe, it, expect } from 'vitest';
import { parseClientTag } from './parseClientTag';
import type { NostrEvent } from '@nostrify/nostrify';
describe('parseClientTag', () => {
it('should return undefined if no client tag exists', () => {
const event: NostrEvent = {
id: '123',
pubkey: 'abc',
created_at: 1234567890,
kind: 1,
tags: [],
content: 'test',
sig: 'sig',
};
const result = parseClientTag(event);
expect(result).toBeUndefined();
});
it('should parse a client tag with only name', () => {
const event: NostrEvent = {
id: '123',
pubkey: 'abc',
created_at: 1234567890,
kind: 1,
tags: [['client', 'zelo.news']],
content: 'test',
sig: 'sig',
};
const result = parseClientTag(event);
expect(result).toEqual({
name: 'zelo.news',
address: undefined,
relay: undefined,
});
});
it('should parse a full NIP-89 client tag', () => {
const event: NostrEvent = {
id: '123',
pubkey: 'abc',
created_at: 1234567890,
kind: 1,
tags: [
['client', 'My Client', '31990:pubkey123:identifier', 'wss://relay.example.com'],
],
content: 'test',
sig: 'sig',
};
const result = parseClientTag(event);
expect(result).toEqual({
name: 'My Client',
address: '31990:pubkey123:identifier',
relay: 'wss://relay.example.com',
});
});
it('should return undefined for empty client tag', () => {
const event: NostrEvent = {
id: '123',
pubkey: 'abc',
created_at: 1234567890,
kind: 1,
tags: [['client']],
content: 'test',
sig: 'sig',
};
const result = parseClientTag(event);
expect(result).toBeUndefined();
});
});

39
src/lib/parseClientTag.ts Normal file
View File

@@ -0,0 +1,39 @@
import type { NostrEvent } from '@nostrify/nostrify';
export interface ClientTagInfo {
name: string;
address?: string; // 31990:pubkey:d-identifier
relay?: string;
}
/**
* Extracts and parses the client tag from a Nostr event according to NIP-89
*
* Client tag format: ["client", "My Client", "31990:app-pubkey:<d-identifier>", "wss://relay"]
* - First element: tag name "client"
* - Second element: client name (required)
* - Third element: handler address (optional)
* - Fourth element: relay hint (optional)
*
* @param event - The Nostr event to extract the client tag from
* @returns ClientTagInfo if a client tag exists, undefined otherwise
*/
export function parseClientTag(event: NostrEvent): ClientTagInfo | undefined {
const clientTag = event.tags.find(([name]) => name === 'client');
if (!clientTag || clientTag.length < 2) {
return undefined;
}
const [, name, address, relay] = clientTag;
if (!name) {
return undefined;
}
return {
name,
address: address || undefined,
relay: relay || undefined,
};
}