- {['Profile', 'Contacts', 'Bookmarks', 'Relay Lists', 'Mute Lists', 'Communities', 'Interests', 'Emojis', 'DM Relays', 'Search Relays'].map((item) => (
+ {['Profile', 'Contacts', 'Bookmarks', 'Relay Lists', 'Mute Lists', 'Communities', 'Interests', 'Emojis', 'DM Relays', 'Search Relays', 'Blossom Servers'].map((item) => (
{item}
@@ -298,7 +298,7 @@ export function ProfileSync() {
This tool syncs profile metadata (kind 0), contact lists (kind 3), mute lists (kind 10000),
pinned notes (kind 10001), relay lists (kind 10002), bookmarks (kind 10003), communities
(kind 10004), search relays (kind 10007), interests (kind 10015), emoji lists (kind 10030),
- and DM relays (kind 10050) between relays.
+ DM relays (kind 10050), and Blossom server lists (kind 10063) between relays.
From c9637174c60f39cf52e18d9e99246cc6b2324d1a Mon Sep 17 00:00:00 2001
From: Andrii Dvorzhak <20891922+advorzhak@users.noreply.github.com>
Date: Sun, 14 Jun 2026 13:43:07 +0300
Subject: [PATCH 2/8] fix: honor target relay in profile sync with NIP-42 auth
and fallback
- Previously, useSyncProfile attempted to publish via the global nostr pool first, ignoring the user-selected target relay.
- Updated to exclusively use a dedicated NPool configured with an eventRouter targeting the selected relay.
- Implemented NIP-42 AUTH callback support in NRelay1 to automatically authenticate with paid/protected relays.
- Added fallback logic to the default relay if the target relay fails.
- Added comprehensive unit tests for successful sync, fallback, failure, NIP-42 auth, and input validation.
- Enhanced logging to clearly indicate which relay is receiving each event.
---
package-lock.json | 20 ---
src/hooks/useSyncProfile.test.tsx | 227 ++++++++++++++++++++++++++++++
src/hooks/useSyncProfile.ts | 118 +++++++++++-----
3 files changed, 306 insertions(+), 59 deletions(-)
create mode 100644 src/hooks/useSyncProfile.test.tsx
diff --git a/package-lock.json b/package-lock.json
index 0f99c46..41dffc1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2767,7 +2767,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2781,7 +2780,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2795,7 +2793,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2809,7 +2806,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2823,7 +2819,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2837,7 +2832,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2851,7 +2845,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2865,7 +2858,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2879,7 +2871,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2893,7 +2884,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2907,7 +2897,6 @@
"cpu": [
"loong64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2921,7 +2910,6 @@
"cpu": [
"ppc64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2935,7 +2923,6 @@
"cpu": [
"riscv64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2949,7 +2936,6 @@
"cpu": [
"riscv64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2963,7 +2949,6 @@
"cpu": [
"s390x"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2977,7 +2962,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2991,7 +2975,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3005,7 +2988,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3019,7 +3001,6 @@
"cpu": [
"ia32"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3033,7 +3014,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
diff --git a/src/hooks/useSyncProfile.test.tsx b/src/hooks/useSyncProfile.test.tsx
new file mode 100644
index 0000000..9721dad
--- /dev/null
+++ b/src/hooks/useSyncProfile.test.tsx
@@ -0,0 +1,227 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { renderHook, act } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { useSyncProfile } from './useSyncProfile';
+import type { NostrEvent } from '@nostrify/nostrify';
+import * as Nostrify from '@nostrify/nostrify';
+
+vi.mock('@/hooks/useAppContext');
+vi.mock('@/hooks/useCurrentUser');
+vi.mock('@/hooks/useToast');
+vi.mock('@nostrify/nostrify', () => ({
+ NPool: vi.fn(),
+ NRelay1: vi.fn(),
+}));
+
+import { useAppContext } from '@/hooks/useAppContext';
+import { useCurrentUser } from '@/hooks/useCurrentUser';
+import { useToast } from '@/hooks/useToast';
+
+const mockToast = vi.fn();
+const mockAppContext = {
+ config: {
+ theme: 'light',
+ relayUrl: 'wss://default.relay.test',
+ },
+ updateConfig: vi.fn(),
+ presetRelays: [],
+};
+
+const mockUser = {
+ pubkey: 'testpubkey123',
+ signer: {
+ signEvent: vi.fn().mockImplementation((event: any) =>
+ Promise.resolve({ ...event, pubkey: 'testpubkey123', id: 'mockid', sig: 'mocksig' })
+ ),
+ },
+};
+
+const mockEvent1: NostrEvent = {
+ id: 'event1id123456789',
+ pubkey: 'testpubkey123',
+ created_at: 1234567890,
+ kind: 0,
+ tags: [],
+ content: 'test content',
+ sig: 'mocksig',
+};
+
+describe('useSyncProfile', () => {
+ let queryClient: QueryClient;
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ queryClient = new QueryClient({
+ defaultOptions: {
+ mutations: { retry: false },
+ queries: { retry: false },
+ },
+ });
+
+ (useAppContext as any).mockReturnValue(mockAppContext);
+ (useCurrentUser as any).mockReturnValue({ user: mockUser });
+ (useToast as any).mockReturnValue({ toast: mockToast });
+ });
+
+ afterEach(() => {
+ queryClient.clear();
+ });
+
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+ );
+
+ it('should successfully publish events to target relay', async () => {
+ const mockEventResult = vi.fn().mockResolvedValue(undefined);
+ (Nostrify.NPool as any).mockImplementation((opts: any) => ({
+ event: mockEventResult,
+ }));
+ (Nostrify.NRelay1 as any).mockImplementation(() => ({}));
+
+ const { result } = renderHook(() => useSyncProfile(), { wrapper });
+
+ await act(async () => {
+ await result.current.mutateAsync({
+ events: [mockEvent1],
+ targetRelay: 'wss://target.relay.test',
+ });
+ });
+
+ expect(Nostrify.NPool).toHaveBeenCalledWith(
+ expect.objectContaining({
+ eventRouter: expect.any(Function),
+ reqRouter: expect.any(Function),
+ })
+ );
+
+ expect(mockEventResult).toHaveBeenCalledWith(
+ mockEvent1,
+ expect.objectContaining({ signal: expect.any(AbortSignal) })
+ );
+
+ expect(mockToast).toHaveBeenCalledWith(
+ expect.objectContaining({ title: 'Sync Complete' })
+ );
+ });
+
+ it('should fall back to default relay when target relay fails', async () => {
+ let callCount = 0;
+ const mockEventResult = vi.fn().mockImplementation(() => {
+ callCount++;
+ if (callCount === 1) {
+ return Promise.reject(new Error('Target relay timeout'));
+ }
+ return Promise.resolve(undefined);
+ });
+
+ (Nostrify.NPool as any).mockImplementation(() => ({
+ event: mockEventResult,
+ }));
+ (Nostrify.NRelay1 as any).mockImplementation(() => ({}));
+
+ const { result } = renderHook(() => useSyncProfile(), { wrapper });
+
+ await act(async () => {
+ await result.current.mutateAsync({
+ events: [mockEvent1],
+ targetRelay: 'wss://target.relay.test',
+ });
+ });
+
+ expect(Nostrify.NPool).toHaveBeenCalledTimes(2);
+ expect(mockToast).toHaveBeenCalledWith(
+ expect.objectContaining({ title: 'Sync Complete' })
+ );
+ });
+
+ it('should fail if both target and default relay fail', async () => {
+ const mockEventResult = vi.fn().mockRejectedValue(new Error('Relay connection failed'));
+ (Nostrify.NPool as any).mockImplementation(() => ({
+ event: mockEventResult,
+ }));
+ (Nostrify.NRelay1 as any).mockImplementation(() => ({}));
+
+ const { result } = renderHook(() => useSyncProfile(), { wrapper });
+
+ await act(async () => {
+ await result.current.mutateAsync({
+ events: [mockEvent1],
+ targetRelay: 'wss://target.relay.test',
+ });
+ });
+
+ expect(Nostrify.NPool).toHaveBeenCalledTimes(2);
+ expect(mockToast).toHaveBeenCalledWith(
+ expect.objectContaining({ title: 'Sync Failed' })
+ );
+ });
+
+ it('should configure NRelay1 with NIP-42 AUTH callback', async () => {
+ let capturedRelayUrl = '';
+ let capturedAuthCallback: ((challenge: string) => Promise
) | undefined;
+
+ (Nostrify.NRelay1 as any).mockImplementation((url: string, opts: any) => {
+ capturedRelayUrl = url;
+ if (opts?.auth) {
+ capturedAuthCallback = opts.auth;
+ }
+ return { url, opts };
+ });
+
+ const mockEventResult = vi.fn().mockResolvedValue(undefined);
+ (Nostrify.NPool as any).mockImplementation((opts: any) => {
+ if (opts.open) {
+ opts.open('wss://target.relay.test');
+ }
+ return { event: mockEventResult };
+ });
+
+ const { result } = renderHook(() => useSyncProfile(), { wrapper });
+
+ await act(async () => {
+ await result.current.mutateAsync({
+ events: [mockEvent1],
+ targetRelay: 'wss://target.relay.test',
+ });
+ });
+
+ expect(capturedRelayUrl).toBe('wss://target.relay.test');
+ expect(capturedAuthCallback).toBeDefined();
+
+ if (capturedAuthCallback) {
+ const authEvent = await capturedAuthCallback('test-challenge-123');
+ expect(authEvent.kind).toBe(22242);
+ expect(authEvent.tags).toContainEqual(['relay', 'wss://target.relay.test']);
+ expect(authEvent.tags).toContainEqual(['challenge', 'test-challenge-123']);
+ expect(mockUser.signer.signEvent).toHaveBeenCalled();
+ }
+ });
+
+ it('should reject invalid target relay URLs', async () => {
+ const { result } = renderHook(() => useSyncProfile(), { wrapper });
+
+ await expect(
+ act(async () => {
+ await result.current.mutateAsync({
+ events: [mockEvent1],
+ targetRelay: 'invalid-url',
+ });
+ })
+ ).rejects.toThrow('Invalid target relay URL');
+ });
+
+ it('should throw error if no events are provided', async () => {
+ const { result } = renderHook(() => useSyncProfile(), { wrapper });
+
+ await expect(
+ act(async () => {
+ await result.current.mutateAsync({
+ events: [],
+ targetRelay: 'wss://target.relay.test',
+ });
+ })
+ ).rejects.toThrow('No events to sync');
+ });
+});
diff --git a/src/hooks/useSyncProfile.ts b/src/hooks/useSyncProfile.ts
index e0be05d..8f81d00 100644
--- a/src/hooks/useSyncProfile.ts
+++ b/src/hooks/useSyncProfile.ts
@@ -1,8 +1,9 @@
import { useMutation } from '@tanstack/react-query';
-import { useNostr } from '@nostrify/react';
import { NPool, NRelay1 } from '@nostrify/nostrify';
import type { NostrEvent } from '@nostrify/nostrify';
import { useToast } from '@/hooks/useToast';
+import { useAppContext } from '@/hooks/useAppContext';
+import { useCurrentUser } from '@/hooks/useCurrentUser';
export interface SyncProfileData {
events: NostrEvent[];
@@ -10,60 +11,99 @@ export interface SyncProfileData {
}
export function useSyncProfile() {
- const { nostr } = useNostr();
const { toast } = useToast();
+ const { config } = useAppContext();
+ const { user } = useCurrentUser();
return useMutation({
mutationFn: async ({ events, targetRelay }: SyncProfileData) => {
- console.log('๐ Starting sync:', { eventCount: events.length, targetRelay });
+ console.log('๐ Starting sync:', { eventCount: events.length, targetRelay, defaultRelay: config.relayUrl });
if (events.length === 0) {
throw new Error('No events to sync');
}
- // Try using global nostr instance first (like we did for queries)
- console.log('๐งช Testing global nostr for publishing...');
+ if (!targetRelay || (!targetRelay.startsWith('wss://') && !targetRelay.startsWith('ws://'))) {
+ throw new Error('Invalid target relay URL');
+ }
+
let successCount = 0;
let errorCount = 0;
- const results: Array<{ event: NostrEvent; success: boolean; error?: unknown }> = [];
+ const results: Array<{ event: NostrEvent; success: boolean; usedRelay: string; error?: unknown }> = [];
- // Try global nostr first
- for (const event of events) {
- try {
- console.log(`๐ค Publishing event ${event.kind} (${event.id.slice(0, 8)}...) via global nostr`);
- await nostr.event(event, { signal: AbortSignal.timeout(15000) });
- console.log(`โ
Successfully published event ${event.kind}`);
- results.push({ event, success: true });
- successCount++;
- } catch (globalError) {
- console.warn(`โ ๏ธ Global nostr failed for event ${event.kind}:`, globalError);
-
- // Fallback to custom pool for this specific event
- try {
- console.log(`๐ Falling back to custom pool for event ${event.kind}`);
- const customPool = new NPool({
- open(url: string) {
- console.log('๐ Opening custom connection for sync to:', url);
- return new NRelay1(url);
- },
- reqRouter() {
- return new Map([[targetRelay, []]]);
- },
- eventRouter() {
- return [targetRelay];
+ const createPool = (relayUrl: string) => {
+ return new NPool({
+ open(url: string) {
+ console.log(`๐ Opening connection to relay: ${url}`);
+ return new NRelay1(url, {
+ auth: async (challenge: string) => {
+ if (!user?.signer) {
+ throw new Error('No signer available for relay authentication (NIP-42)');
+ }
+ console.log(`๐ Responding to NIP-42 AUTH challenge from ${url}`);
+ const event: Omit = {
+ kind: 22242,
+ created_at: Math.floor(Date.now() / 1000),
+ tags: [
+ ['relay', url],
+ ['challenge', challenge],
+ ],
+ content: '',
+ };
+ return await user.signer.signEvent(event);
},
});
+ },
+ reqRouter() {
+ return new Map([[relayUrl, []]]);
+ },
+ eventRouter() {
+ return [relayUrl];
+ },
+ });
+ };
+
+ for (const event of events) {
+ let eventSuccess = false;
+ let eventError: unknown = null;
+ let usedRelay = '';
- await customPool.event(event, { signal: AbortSignal.timeout(15000) });
- console.log(`โ
Custom pool succeeded for event ${event.kind}`);
- results.push({ event, success: true });
- successCount++;
- } catch (customError) {
- console.error(`โ Both methods failed for event ${event.kind}:`, { globalError, customError });
- results.push({ event, success: false, error: customError });
- errorCount++;
+ // 1. Try target relay first
+ try {
+ console.log(`๐ค Publishing event ${event.kind} (${event.id.slice(0, 8)}...) to TARGET relay: ${targetRelay}`);
+ const targetPool = createPool(targetRelay);
+ await targetPool.event(event, { signal: AbortSignal.timeout(15000) });
+ console.log(`โ
Successfully published event ${event.kind} to TARGET relay: ${targetRelay}`);
+ eventSuccess = true;
+ usedRelay = targetRelay;
+ } catch (targetError) {
+ console.warn(`โ ๏ธ TARGET relay failed for event ${event.kind} (${targetRelay}):`, targetError);
+
+ // 2. Fallback to default relay if different from target
+ if (config.relayUrl !== targetRelay) {
+ try {
+ console.log(`๐ Falling back to DEFAULT relay for event ${event.kind}: ${config.relayUrl}`);
+ const fallbackPool = createPool(config.relayUrl);
+ await fallbackPool.event(event, { signal: AbortSignal.timeout(15000) });
+ console.log(`โ
Successfully published event ${event.kind} to DEFAULT relay: ${config.relayUrl}`);
+ eventSuccess = true;
+ usedRelay = config.relayUrl;
+ } catch (fallbackError) {
+ console.error(`โ Both TARGET (${targetRelay}) and DEFAULT (${config.relayUrl}) relays failed for event ${event.kind}:`, { targetError, fallbackError });
+ eventError = fallbackError;
+ }
+ } else {
+ eventError = targetError;
}
}
+
+ if (eventSuccess) {
+ successCount++;
+ } else {
+ errorCount++;
+ }
+
+ results.push({ event, success: eventSuccess, usedRelay, error: eventError });
}
console.log('๐ Sync results:', { successCount, errorCount, total: events.length });
@@ -92,7 +132,7 @@ export function useSyncProfile() {
} else {
toast({
title: "Sync Failed",
- description: `Failed to sync any events to the target relay.`,
+ description: `Failed to sync any events.`,
variant: "destructive",
});
}
From c9f7c278698d96872131bf9f5c6c4f2167575410 Mon Sep 17 00:00:00 2001
From: Andrii Dvorzhak <20891922+advorzhak@users.noreply.github.com>
Date: Sun, 14 Jun 2026 14:29:56 +0300
Subject: [PATCH 3/8] fix: optimize sync concurrency and handle read-only
logins safely
- Added early user validation to prevent errors for read-only logins.
- Moved NPool creation outside the event loop to reuse connections.
- Replaced sequential event publishing with Promise.allSettled for concurrent execution.
- Fixed unused parameter warning in test mock.
---
src/hooks/useSyncProfile.test.tsx | 6 +++---
src/hooks/useSyncProfile.ts | 32 +++++++++++++++++++++++--------
2 files changed, 27 insertions(+), 11 deletions(-)
diff --git a/src/hooks/useSyncProfile.test.tsx b/src/hooks/useSyncProfile.test.tsx
index 9721dad..514e8e9 100644
--- a/src/hooks/useSyncProfile.test.tsx
+++ b/src/hooks/useSyncProfile.test.tsx
@@ -171,9 +171,9 @@ describe('useSyncProfile', () => {
});
const mockEventResult = vi.fn().mockResolvedValue(undefined);
- (Nostrify.NPool as any).mockImplementation((opts: any) => {
- if (opts.open) {
- opts.open('wss://target.relay.test');
+ (Nostrify.NPool as any).mockImplementation((_opts: any) => {
+ if (_opts.open) {
+ _opts.open('wss://target.relay.test');
}
return { event: mockEventResult };
});
diff --git a/src/hooks/useSyncProfile.ts b/src/hooks/useSyncProfile.ts
index 8f81d00..188d929 100644
--- a/src/hooks/useSyncProfile.ts
+++ b/src/hooks/useSyncProfile.ts
@@ -23,6 +23,10 @@ export function useSyncProfile() {
throw new Error('No events to sync');
}
+ if (!user) {
+ throw new Error('User must be logged in to sync profile');
+ }
+
if (!targetRelay || (!targetRelay.startsWith('wss://') && !targetRelay.startsWith('ws://'))) {
throw new Error('Invalid target relay URL');
}
@@ -63,7 +67,10 @@ export function useSyncProfile() {
});
};
- for (const event of events) {
+ const targetPool = createPool(targetRelay);
+ const fallbackPool = config.relayUrl !== targetRelay ? createPool(config.relayUrl) : null;
+
+ const publishPromises = events.map(async (event) => {
let eventSuccess = false;
let eventError: unknown = null;
let usedRelay = '';
@@ -71,7 +78,6 @@ export function useSyncProfile() {
// 1. Try target relay first
try {
console.log(`๐ค Publishing event ${event.kind} (${event.id.slice(0, 8)}...) to TARGET relay: ${targetRelay}`);
- const targetPool = createPool(targetRelay);
await targetPool.event(event, { signal: AbortSignal.timeout(15000) });
console.log(`โ
Successfully published event ${event.kind} to TARGET relay: ${targetRelay}`);
eventSuccess = true;
@@ -80,10 +86,9 @@ export function useSyncProfile() {
console.warn(`โ ๏ธ TARGET relay failed for event ${event.kind} (${targetRelay}):`, targetError);
// 2. Fallback to default relay if different from target
- if (config.relayUrl !== targetRelay) {
+ if (fallbackPool) {
try {
console.log(`๐ Falling back to DEFAULT relay for event ${event.kind}: ${config.relayUrl}`);
- const fallbackPool = createPool(config.relayUrl);
await fallbackPool.event(event, { signal: AbortSignal.timeout(15000) });
console.log(`โ
Successfully published event ${event.kind} to DEFAULT relay: ${config.relayUrl}`);
eventSuccess = true;
@@ -97,13 +102,24 @@ export function useSyncProfile() {
}
}
- if (eventSuccess) {
- successCount++;
+ return { event, success: eventSuccess, usedRelay, error: eventError };
+ });
+
+ const settledResults = await Promise.allSettled(publishPromises);
+
+ for (const result of settledResults) {
+ if (result.status === 'fulfilled') {
+ const res = result.value;
+ if (res.success) {
+ successCount++;
+ } else {
+ errorCount++;
+ }
+ results.push(res);
} else {
errorCount++;
+ console.error('Unexpected promise rejection during event publishing:', result.reason);
}
-
- results.push({ event, success: eventSuccess, usedRelay, error: eventError });
}
console.log('๐ Sync results:', { successCount, errorCount, total: events.length });
From 9d7aad3e50c21c6267ff31878ec130a1ceb40765 Mon Sep 17 00:00:00 2001
From: Andrii Dvorzhak <20891922+advorzhak@users.noreply.github.com>
Date: Sun, 14 Jun 2026 15:50:35 +0300
Subject: [PATCH 4/8] fix: add missing DialogTitle components for accessibility
---
src/components/auth/LoginDialog.tsx | 6 ++++--
src/components/ui/command.tsx | 3 ++-
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/src/components/auth/LoginDialog.tsx b/src/components/auth/LoginDialog.tsx
index 7eb2bd3..9a31ef1 100644
--- a/src/components/auth/LoginDialog.tsx
+++ b/src/components/auth/LoginDialog.tsx
@@ -6,7 +6,7 @@ import { QRCodeSVG } from 'qrcode.react';
import { Shield, Upload, AlertTriangle, UserPlus, KeyRound, Sparkles, Link, Loader2, Copy, Check, ChevronDown, ChevronUp, ExternalLink } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
-import { Dialog, DialogContent, DialogHeader, DialogDescription } from "@/components/ui/dialog";
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { useLoginActions, generateNostrConnectParams, generateNostrConnectURI, NostrConnectParams } from '@/hooks/useLoginActions';
@@ -266,7 +266,9 @@ const LoginDialog: React.FC = ({ isOpen, onClose, onLogin, onS
className={cn("max-w-[95vw] sm:max-w-md max-h-[90vh] max-h-[90dvh] p-0 overflow-hidden rounded-2xl overflow-y-scroll")}
>
-
+
+ Sign In or Sign Up
+
Sign up or log in to continue
diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx
index 69d63a4..6cd0c11 100644
--- a/src/components/ui/command.tsx
+++ b/src/components/ui/command.tsx
@@ -4,7 +4,7 @@ import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
-import { Dialog, DialogContent } from "@/components/ui/dialog"
+import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef,
@@ -27,6 +27,7 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (