diff --git a/src/components/ArticleView.tsx b/src/components/ArticleView.tsx index 6274ac7..d96dd60 100644 --- a/src/components/ArticleView.tsx +++ b/src/components/ArticleView.tsx @@ -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) { +
+ +
+ diff --git a/src/components/ClientTag.tsx b/src/components/ClientTag.tsx new file mode 100644 index 0000000..daf0ccf --- /dev/null +++ b/src/components/ClientTag.tsx @@ -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 ( +
+ + Published with + + {clientInfo.name} + +
+ ); +} diff --git a/src/hooks/useNostrPublish.ts b/src/hooks/useNostrPublish.ts index b9a3bc2..f749861 100644 --- a/src/hooks/useNostrPublish.ts +++ b/src/hooks/useNostrPublish.ts @@ -14,9 +14,11 @@ export function useNostrPublish(): UseMutationResult { 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({ diff --git a/src/lib/parseClientTag.test.ts b/src/lib/parseClientTag.test.ts new file mode 100644 index 0000000..352fd46 --- /dev/null +++ b/src/lib/parseClientTag.test.ts @@ -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(); + }); +}); diff --git a/src/lib/parseClientTag.ts b/src/lib/parseClientTag.ts new file mode 100644 index 0000000..8b2b312 --- /dev/null +++ b/src/lib/parseClientTag.ts @@ -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:", "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, + }; +}