Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default function RootLayout({
>
<ClientProviders>
{children}
<Toaster />
<Toaster theme="dark" />
</ClientProviders>
</body>
</html>
Expand Down
5 changes: 5 additions & 0 deletions components/ClientProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { ReactNode, useEffect } from 'react';
import NostrProvider from '@/components/NostrProvider'
import dynamic from 'next/dynamic';
import { migrateStorageItems } from '@/utils/storageUtils';
import useRelays from '@/hooks/useRelays';

const DynamicNostrLoginProvider = dynamic(
() => import('@nostrify/react/login').then((mod) => mod.NostrLoginProvider),
{ ssr: false }
);

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

import { AppProvider } from './AppProvider';
import { AppConfig } from '@/context/AppContext';

Expand Down Expand Up @@ -45,6 +47,9 @@ export default function ClientProviders({ children }: { children: ReactNode }) {
migrateStorageItems();
}, []);

// Load user-configured relays (no hardcoded defaults)
const { relays } = useRelays();

return (
<AppProvider storageKey="nostr:app-config" defaultConfig={defaultConfig} presetRelays={presetRelays}>
<DynamicNostrLoginProvider storageKey='nostr:login'>
Expand Down
9 changes: 7 additions & 2 deletions components/NostrProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@ const NostrProvider: React.FC<NostrProviderProps> = (props) => {

const { config, presetRelays } = useAppContext(); // Keep presetRelays even if not used directly here

// Create NPool instance only once
const pool = useRef<NPool | undefined>(undefined);
// Keep relays in a ref so routers use the latest without recreating pool
const relaysRef = useRef<string[]>(relays);
useEffect(() => {
relaysRef.current = relays;
}, [relays]);

// NPool instance created once
const pool = useRef<NPool | undefined>(undefined);
// Use ref for relayUrls to ensure the pool always has the latest config
const currentRelayUrls = useRef<string[]>(config.relayUrls);

Expand Down
6 changes: 3 additions & 3 deletions components/chat/ChatMessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export default function ChatMessages({
<MessageContentRenderer content={message.content} />
</div>
</div>
<div className="flex justify-end mt-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<div className="flex justify-end mt-1 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity duration-200">
<button
onClick={() => startEditingMessage(index)}
className="p-1 rounded-full bg-white/10 hover:bg-white/20 transition-colors"
Expand All @@ -116,7 +116,7 @@ export default function ChatMessages({
<p className="text-sm font-medium">{getTextFromContent(message.content)}</p>
</div>
</div>
<div className="mt-1.5 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<div className="mt-1.5 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity duration-200">
<button
onClick={() => retryMessage(index)}
className="flex items-center gap-1.5 text-xs text-red-400 hover:text-red-300 bg-black/50 hover:bg-black/70 rounded-md px-3 py-1.5 transition-colors cursor-pointer"
Expand Down Expand Up @@ -156,7 +156,7 @@ export default function ChatMessages({
<div className="max-w-[95%] text-gray-100 py-2 px-0.5">
<MessageContentRenderer content={message.content} />
</div>
<div className="mt-1.5 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center gap-2">
<div className="mt-1.5 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity duration-200 flex items-center gap-2">
<button
onClick={() => copyMessageContent(index, message.content)}
className="flex items-center gap-1.5 text-xs text-gray-400 hover:text-white bg-black/50 hover:bg-black/70 rounded-md px-3 py-1.5 transition-colors cursor-pointer"
Expand Down
107 changes: 95 additions & 12 deletions components/settings/GeneralTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,16 @@ const GeneralTab: React.FC<GeneralTabProps> = ({
const [showNsec, setShowNsec] = useState<boolean>(false);
const [nsecValue, setNsecValue] = useState<string>('');
const [showNsecWarning, setShowNsecWarning] = useState<boolean>(false);
const [relays, setRelays] = useState<string[]>([]);
const [newRelayInput, setNewRelayInput] = useState<string>('');

const toast = (message: string) => {
alert(message); // Placeholder for a proper toast notification
};

useEffect(() => {
setBaseUrls(loadBaseUrlsList());
setRelays(loadRelays());
}, []); // Empty dependency array to run only once on mount

useEffect(() => {
Expand Down Expand Up @@ -103,6 +106,13 @@ const GeneralTab: React.FC<GeneralTabProps> = ({

const normalizeUrl = (url: string) => url.endsWith('/') ? url : `${url}/`;

const isValidRelay = (url: string) => {
try {
const u = new URL(url.trim());
return u.protocol === 'wss:';
} catch { return false; }
};

const handleRadioChange = (url: string) => {
const normalizedUrl = normalizeUrl(url);
setBaseUrl(normalizedUrl);
Expand Down Expand Up @@ -140,7 +150,7 @@ const GeneralTab: React.FC<GeneralTabProps> = ({
</p>
<button
onClick={handleCloseNsecWarning}
className="absolute top-2 right-2 text-red-400 hover:text-red-500 transition-colors"
className="absolute top-2 right-2 text-red-400 hover:text-red-500 transition-colors cursor-pointer"
type="button"
title="Dismiss warning"
>
Expand All @@ -162,9 +172,9 @@ const GeneralTab: React.FC<GeneralTabProps> = ({

{/* Base URL */}
<div className="mb-6">
<h3 className="text-sm font-medium text-white/80 mb-2">Base URL</h3>
<h3 className="text-sm font-medium text-white/80">Base URL</h3>
<p className="text-xs text-white/50 mb-2">Choose your preferred Routstr API base URL</p>
<div className="bg-white/5 border border-white/10 rounded-md p-4">
<p className="text-sm text-white mb-3">Choose your preferred Routstr API base URL</p>
<div className="max-h-48 overflow-y-auto space-y-2 mb-4">
{baseUrls.map((url, index) => (
<div className="flex items-center justify-between" key={index}>
Expand All @@ -181,7 +191,7 @@ const GeneralTab: React.FC<GeneralTabProps> = ({
</div>
<button
onClick={() => handleRemoveBaseUrl(url)}
className="text-red-400 hover:text-red-500 transition-colors"
className="text-red-400 hover:text-red-500 transition-colors cursor-pointer"
type="button"
>
<XCircle className="h-4 w-4" />
Expand All @@ -192,7 +202,7 @@ const GeneralTab: React.FC<GeneralTabProps> = ({
<div className="flex items-center gap-2">
<input
type="text"
className="flex-grow bg-white/5 border border-white/10 rounded-md px-3 py-2 text-sm text-white focus:border-white/30 focus:outline-none"
className="flex-grow bg-white/5 border border-white/10 rounded-md px-2 py-1.5 text-xs text-white focus:border-white/30 focus:outline-none"
placeholder="Add new base URL"
value={newBaseUrlInput}
onChange={(e) => setNewBaseUrlInput(e.target.value)}
Expand All @@ -204,10 +214,83 @@ const GeneralTab: React.FC<GeneralTabProps> = ({
/>
<button
onClick={handleAddBaseUrl}
className="bg-white/10 hover:bg-white/20 text-white px-3 py-2 rounded-md text-sm transition-colors flex items-center gap-1"
className="bg-white/10 hover:bg-white/20 text-white px-2.5 py-1.5 rounded-md text-xs transition-colors flex items-center gap-1 cursor-pointer"
type="button"
>
<Plus className="h-3.5 w-3.5" /> Add
</button>
</div>
</div>
</div>

{/* Nostr Relays */}
<div className="mb-6">
<h3 className="text-sm font-medium text-white/80">Nostr Relays</h3>
<p className="text-xs text-white/50 mb-2">Manage relays used for Nostr features</p>
<div className="bg-white/5 border border-white/10 rounded-md p-4">
<div className="max-h-48 overflow-y-auto space-y-2 mb-4">
{relays.length === 0 ? (
<div className="text-sm text-white/50">No relays configured.</div>
) : (
relays.map((r) => (
<div key={r} className="flex items-center justify-between">
<span className="text-sm text-white break-all">{r}</span>
<button
onClick={() => {
const next = relays.filter((x) => x !== r);
setRelays(next);
saveRelays(next);
}}
className="text-red-400 hover:text-red-500 transition-colors cursor-pointer"
type="button"
>
<XCircle className="h-4 w-4" />
</button>
</div>
))
)}
</div>
<div className="flex items-center gap-2">
<input
type="text"
className="flex-grow bg-white/5 border border-white/10 rounded-md px-2 py-1.5 text-xs text-white focus:border-white/30 focus:outline-none"
placeholder="wss://relay.example.com"
value={newRelayInput}
onChange={(e) => setNewRelayInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && isValidRelay(newRelayInput)) {
const trimmed = newRelayInput.trim();
if (!relays.includes(trimmed)) {
const next = [...relays, trimmed];
setRelays(next);
saveRelays(next);
}
setNewRelayInput('');
}
}}
/>
<button
onClick={() => {
if (!isValidRelay(newRelayInput)) return;
const trimmed = newRelayInput.trim();
if (!relays.includes(trimmed)) {
const next = [...relays, trimmed];
setRelays(next);
saveRelays(next);
}
setNewRelayInput('');
}}
className="bg-white/10 hover:bg-white/20 text-white px-2.5 py-1.5 rounded-md text-xs transition-colors flex items-center gap-1 cursor-pointer"
type="button"
>
<Plus className="h-3.5 w-3.5" /> Add
</button>
<button
onClick={() => { setRelays([...DEFAULT_RELAYS]); saveRelays([...DEFAULT_RELAYS]); }}
className="px-2.5 py-1.5 rounded-md text-xs border border-white/20 text-white/80 hover:bg-white/10 cursor-pointer"
type="button"
>
<Plus className="h-4 w-4" /> Add
Reset
</button>
</div>
</div>
Expand All @@ -230,7 +313,7 @@ const GeneralTab: React.FC<GeneralTabProps> = ({
</div>
<button
onClick={() => setIsModelSelectorOpen(!isModelSelectorOpen)}
className="bg-white/10 hover:bg-white/20 text-white px-3 py-2 rounded-md text-sm transition-colors flex items-center gap-1"
className="bg-white/10 hover:bg-white/20 text-white px-3 py-2 rounded-md text-sm transition-colors flex items-center gap-1 cursor-pointer"
type="button"
>
{isModelSelectorOpen ? (
Expand Down Expand Up @@ -268,7 +351,7 @@ const GeneralTab: React.FC<GeneralTabProps> = ({
<button
key={model.id}
className={`w-full text-left px-3 py-2 text-sm hover:bg-white/10 transition-colors border-b border-white/5 last:border-b-0 ${selectedModel?.id === model.id ? 'bg-white/10 text-white' : 'text-white/80'
}`}
} cursor-pointer`}
onClick={() => {
handleModelChange(model.id);
setIsModelSelectorOpen(false);
Expand Down Expand Up @@ -307,7 +390,7 @@ const GeneralTab: React.FC<GeneralTabProps> = ({
</div>
<button
onClick={() => setIsFavoritesManagerOpen(!isFavoritesManagerOpen)}
className="text-white/70 hover:text-white px-2 py-1 rounded text-xs border border-white/20 transition-colors flex items-center gap-1"
className="text-white/70 hover:text-white px-2 py-1 rounded text-xs border border-white/20 transition-colors flex items-center gap-1 cursor-pointer"
type="button"
>
{isFavoritesManagerOpen ? (
Expand Down Expand Up @@ -343,7 +426,7 @@ const GeneralTab: React.FC<GeneralTabProps> = ({
</div>
<button
onClick={() => toggleFavoriteModel(modelId)}
className="text-white/40 hover:text-red-400 transition-colors"
className="text-white/40 hover:text-red-400 transition-colors cursor-pointer"
type="button"
title="Remove from favorites"
>
Expand Down Expand Up @@ -402,7 +485,7 @@ const GeneralTab: React.FC<GeneralTabProps> = ({
className={`p-1 rounded-sm transition-colors ${isFavorite
? 'text-yellow-400 hover:text-yellow-300'
: 'text-white/30 hover:text-yellow-400'
}`}
} cursor-pointer`}
type="button"
title={isFavorite ? 'Remove from favorites' : 'Add to favorites'}
>
Expand Down
9 changes: 6 additions & 3 deletions context/NostrContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import {
getPublicKey,
isNostrExtensionAvailable,
createPool,
getDefaultRelays,
decodePrivateKey,
getPublicKeyFromPrivateKey,
signEventWithPrivateKey
} from '@/lib/nostr';
import { loadRelays } from '@/utils/storageUtils';
import type { Event } from 'nostr-tools';

type NostrContextType = {
Expand Down Expand Up @@ -74,7 +74,8 @@ export function NostrProvider({ children }: { children: ReactNode }) {
// Cleanup on unmount
return () => {
if (pool) {
pool.close(getDefaultRelays());
const relays = loadRelays();
if (relays.length > 0) pool.close(relays);
}
};
}, []);
Expand Down Expand Up @@ -148,7 +149,9 @@ export function NostrProvider({ children }: { children: ReactNode }) {

// Publish to relays
if (signedEvent) {
await Promise.any(pool.publish(getDefaultRelays(), signedEvent));
const relays = loadRelays();
if (relays.length === 0) return signedEvent; // no relays configured
await Promise.any(pool.publish(relays, signedEvent));
return signedEvent;
}

Expand Down
61 changes: 61 additions & 0 deletions hooks/useRelays.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"use client";

import { useCallback, useEffect, useRef, useState } from 'react';
import { loadRelays, saveRelays } from '@/utils/storageUtils';

export interface UseRelaysResult {
relays: string[];
addRelay: (url: string) => void;
removeRelay: (url: string) => void;
setRelays: (urls: string[]) => void;
}

export function useRelays(initialDefaults?: readonly string[]): UseRelaysResult {
const [relays, setRelaysState] = useState<string[]>(() => loadRelays());
const seededRef = useRef(false);

// Seed once on mount if storage empty and defaults provided
useEffect(() => {
if (seededRef.current) return;
seededRef.current = true;
const stored = loadRelays();
if ((stored?.length ?? 0) === 0 && initialDefaults && initialDefaults.length > 0) {
const cleaned = Array.from(new Set(initialDefaults.map((u) => u.trim()).filter(Boolean)));
setRelaysState(cleaned);
saveRelays(cleaned);
} else if (stored && stored.length > 0) {
setRelaysState(stored);
}
}, [initialDefaults]);

// Persist on change
useEffect(() => {
saveRelays(relays);
}, [relays]);

const setRelays = useCallback((urls: string[]) => {
const cleaned = Array.from(new Set(urls.map((u) => u.trim()).filter(Boolean)));
setRelaysState((prev) => {
if (prev.length === cleaned.length && prev.every((v, i) => v === cleaned[i])) return prev;
return cleaned;
});
}, []);

const addRelay = useCallback((url: string) => {
const trimmed = url.trim();
if (!trimmed) return;
setRelaysState((prev) => {
if (prev.includes(trimmed)) return prev;
return [...prev, trimmed];
});
}, []);

const removeRelay = useCallback((url: string) => {
setRelaysState((prev) => prev.filter((r) => r !== url));
}, []);

return { relays, addRelay, removeRelay, setRelays };
}

export default useRelays;

10 changes: 3 additions & 7 deletions lib/nostr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,6 @@ export const createPool = (): SimplePool => {

// Get a list of default relays
export const getDefaultRelays = (): string[] => {
return [
'wss://relay.damus.io',
'wss://relay.nostr.band',
'wss://nos.lol',
'wss://nostr.mutinywallet.com'
];
};
// Deprecated: relays are now user-configurable. This returns an empty list to avoid hardcoded connections.
return [];
};
Loading