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,
+ };
+}