Skip to content

Commit 050a565

Browse files
committed
add all time option, 2000 limit
1 parent 8de057d commit 050a565

5 files changed

Lines changed: 423 additions & 33 deletions

File tree

src/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ const presetRelays = [
4040
{ url: 'wss://relay.nostr.band', name: 'Nostr.Band' },
4141
{ url: 'wss://relay.damus.io', name: 'Damus' },
4242
{ url: 'wss://relay.primal.net', name: 'Primal' },
43+
{ url: 'wss://nos.lol', name: 'nos.lol' },
44+
{ url: 'wss://relay.snort.social', name: 'Snort' },
45+
{ url: 'wss://nostr.wine', name: 'Nostr Wine' },
46+
{ url: 'wss://relay.nostr.info', name: 'Nostr.info' },
4347
];
4448

4549
export function App() {
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import { useState } from 'react';
2+
import { Check, ChevronsUpDown, Wifi, Plus, RotateCcw } from "lucide-react";
3+
import { cn } from "@/lib/utils";
4+
import { Button } from "@/components/ui/button";
5+
import {
6+
Command,
7+
CommandEmpty,
8+
CommandGroup,
9+
CommandInput,
10+
CommandItem,
11+
CommandList,
12+
} from "@/components/ui/command";
13+
import {
14+
Popover,
15+
PopoverContent,
16+
PopoverTrigger,
17+
} from "@/components/ui/popover";
18+
import { useAppContext } from "@/hooks/useAppContext";
19+
20+
interface NotesRelaySelectorProps {
21+
selectedRelay?: string;
22+
onSelectionChange: (relay?: string) => void;
23+
className?: string;
24+
placeholder?: string;
25+
}
26+
27+
export function NotesRelaySelector(props: NotesRelaySelectorProps) {
28+
const {
29+
selectedRelay,
30+
onSelectionChange,
31+
className,
32+
placeholder = "Choose a relay to search..."
33+
} = props;
34+
35+
const { config, presetRelays = [] } = useAppContext();
36+
const [open, setOpen] = useState(false);
37+
const [inputValue, setInputValue] = useState("");
38+
39+
// Function to normalize relay URL by adding wss:// if no protocol is present
40+
const normalizeRelayUrl = (url: string): string => {
41+
const trimmed = url.trim();
42+
if (!trimmed) return trimmed;
43+
44+
// Check if it already has a protocol
45+
if (trimmed.includes('://')) {
46+
return trimmed;
47+
}
48+
49+
// Add wss:// prefix
50+
return `wss://${trimmed}`;
51+
};
52+
53+
// Check if input value looks like a valid relay URL
54+
const isValidRelayInput = (value: string): boolean => {
55+
const trimmed = value.trim();
56+
if (!trimmed) return false;
57+
58+
// Basic validation - should contain at least a domain-like structure
59+
const normalized = normalizeRelayUrl(trimmed);
60+
try {
61+
new URL(normalized);
62+
return true;
63+
} catch {
64+
return false;
65+
}
66+
};
67+
68+
// Handle selecting a relay
69+
const handleSelectRelay = (url: string) => {
70+
const normalizedUrl = normalizeRelayUrl(url);
71+
onSelectionChange(normalizedUrl);
72+
setInputValue("");
73+
setOpen(false);
74+
};
75+
76+
// Handle resetting to default (current relay)
77+
const handleReset = () => {
78+
onSelectionChange(undefined);
79+
setOpen(false);
80+
};
81+
82+
// Get available preset relays (including current relay)
83+
const availablePresetRelays = presetRelays.filter(relay =>
84+
!inputValue ||
85+
relay.name.toLowerCase().includes(inputValue.toLowerCase()) ||
86+
relay.url.toLowerCase().includes(inputValue.toLowerCase())
87+
);
88+
89+
const getRelayDisplayName = (url: string): string => {
90+
const preset = presetRelays.find(relay => relay.url === url);
91+
return preset ? preset.name : url.replace(/^wss?:\/\//, '');
92+
};
93+
94+
const currentRelayName = getRelayDisplayName(config.relayUrl);
95+
const selectedRelayName = selectedRelay ? getRelayDisplayName(selectedRelay) : currentRelayName;
96+
const isUsingCustomRelay = selectedRelay && selectedRelay !== config.relayUrl;
97+
98+
return (
99+
<div className={cn("space-y-3", className)}>
100+
{/* Current selection display */}
101+
<div className="flex items-center justify-between">
102+
<div className="flex items-center gap-2">
103+
<Wifi className="h-4 w-4 text-muted-foreground" />
104+
<span className="text-sm">
105+
<span className="font-medium">Searching:</span>{' '}
106+
<span className={isUsingCustomRelay ? "text-blue-600 font-medium" : "text-muted-foreground"}>
107+
{selectedRelayName}
108+
</span>
109+
{!isUsingCustomRelay && (
110+
<span className="text-xs text-muted-foreground ml-1">(default)</span>
111+
)}
112+
</span>
113+
</div>
114+
{isUsingCustomRelay && (
115+
<Button
116+
variant="ghost"
117+
size="sm"
118+
onClick={handleReset}
119+
className="h-auto p-1"
120+
title="Reset to default relay"
121+
>
122+
<RotateCcw className="h-3 w-3" />
123+
</Button>
124+
)}
125+
</div>
126+
127+
{/* Relay selector */}
128+
<Popover open={open} onOpenChange={setOpen}>
129+
<PopoverTrigger asChild>
130+
<Button
131+
variant="outline"
132+
role="combobox"
133+
aria-expanded={open}
134+
className="w-full justify-between"
135+
>
136+
<div className="flex items-center gap-2">
137+
<Wifi className="h-4 w-4" />
138+
<span className="truncate">{placeholder}</span>
139+
</div>
140+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
141+
</Button>
142+
</PopoverTrigger>
143+
<PopoverContent className="w-[400px] p-0">
144+
<Command>
145+
<CommandInput
146+
placeholder="Search relays or type URL..."
147+
value={inputValue}
148+
onValueChange={setInputValue}
149+
/>
150+
<CommandList>
151+
<CommandEmpty>
152+
{inputValue && isValidRelayInput(inputValue) ? (
153+
<CommandItem
154+
onSelect={() => handleSelectRelay(inputValue)}
155+
className="cursor-pointer"
156+
>
157+
<Plus className="mr-2 h-4 w-4" />
158+
<div className="flex flex-col">
159+
<span className="font-medium">Use custom relay</span>
160+
<span className="text-xs text-muted-foreground">
161+
{normalizeRelayUrl(inputValue)}
162+
</span>
163+
</div>
164+
</CommandItem>
165+
) : (
166+
<div className="py-6 text-center text-sm text-muted-foreground">
167+
{inputValue ? "Invalid relay URL" : "No relay found."}
168+
</div>
169+
)}
170+
</CommandEmpty>
171+
172+
{availablePresetRelays.length > 0 && (
173+
<CommandGroup heading="Available Relays">
174+
{availablePresetRelays.map((relay) => (
175+
<CommandItem
176+
key={relay.url}
177+
value={relay.url}
178+
onSelect={() => handleSelectRelay(relay.url)}
179+
>
180+
<Check
181+
className={cn(
182+
"mr-2 h-4 w-4",
183+
selectedRelay === relay.url ? "opacity-100" : "opacity-0"
184+
)}
185+
/>
186+
<div className="flex flex-col">
187+
<span className="font-medium">{relay.name}</span>
188+
<span className="text-xs text-muted-foreground">{relay.url}</span>
189+
{relay.url === config.relayUrl && (
190+
<span className="text-xs text-blue-600">Current default</span>
191+
)}
192+
</div>
193+
</CommandItem>
194+
))}
195+
</CommandGroup>
196+
)}
197+
198+
{inputValue && isValidRelayInput(inputValue) && (
199+
<CommandGroup heading="Custom Relay">
200+
<CommandItem
201+
onSelect={() => handleSelectRelay(inputValue)}
202+
className="cursor-pointer"
203+
>
204+
<Plus className="mr-2 h-4 w-4" />
205+
<div className="flex flex-col">
206+
<span className="font-medium">Use custom relay</span>
207+
<span className="text-xs text-muted-foreground">
208+
{normalizeRelayUrl(inputValue)}
209+
</span>
210+
</div>
211+
</CommandItem>
212+
</CommandGroup>
213+
)}
214+
</CommandList>
215+
</Command>
216+
</PopoverContent>
217+
</Popover>
218+
219+
<div className="text-xs text-muted-foreground">
220+
Choose a specific relay to search for your notes. Different relays may have different historical data.
221+
</div>
222+
</div>
223+
);
224+
}

src/components/notes/NotesList.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@ import { useToast } from '@/hooks/useToast';
1414
import { formatDistanceToNow } from '@/lib/dateUtils';
1515
import { Clock, CheckCircle, RotateCcw, Repeat2, ChevronDown, Quote, Copy } from 'lucide-react';
1616
import { nip19 } from 'nostr-tools';
17+
import type { TimeRange } from '@/hooks/useMyNotes';
1718

1819
interface NotesListProps {
1920
notes: NostrEvent[];
2021
isLoading: boolean;
22+
timeRange?: TimeRange;
23+
isUsingCustomRelay?: boolean;
2124
}
2225

23-
export function NotesList({ notes, isLoading }: NotesListProps) {
26+
export function NotesList({ notes, isLoading, timeRange = 'month', isUsingCustomRelay = false }: NotesListProps) {
2427
const [quotedBoostNote, setQuotedBoostNote] = useState<NostrEvent | null>(null);
2528
const { getReviewProgress, markReviewed, resetProgress } = useSpacedRepetition();
2629
const { mutate: createEvent } = useNostrPublish();
@@ -75,8 +78,15 @@ export function NotesList({ notes, isLoading }: NotesListProps) {
7578
};
7679

7780
if (isLoading) {
81+
const loadingMessage = isUsingCustomRelay
82+
? 'Searching selected relay for notes...'
83+
: 'Loading notes...';
84+
7885
return (
7986
<div className="space-y-4">
87+
<div className="text-center py-4">
88+
<div className="text-sm text-muted-foreground">{loadingMessage}</div>
89+
</div>
8090
{Array.from({ length: 3 }).map((_, i) => (
8191
<Card key={i}>
8292
<CardHeader>
@@ -99,12 +109,22 @@ export function NotesList({ notes, isLoading }: NotesListProps) {
99109
}
100110

101111
if (notes.length === 0) {
112+
let emptyMessage = timeRange === 'all-time'
113+
? "No notes found. Write some notes in your favorite Nostr client to start revisiting!"
114+
: "No notes found from the last month. Write some notes in your favorite Nostr client to start revisiting!";
115+
116+
if (timeRange === 'all-time' && !isUsingCustomRelay) {
117+
emptyMessage = "No notes found on your default relay. Try selecting a different relay above to search for older notes that might be stored elsewhere.";
118+
} else if (timeRange === 'all-time' && isUsingCustomRelay) {
119+
emptyMessage = "No notes found on the selected relay. Try choosing a different relay to search for your notes.";
120+
}
121+
102122
return (
103123
<Card className="border-dashed">
104124
<CardContent className="py-12 px-8 text-center">
105125
<div className="max-w-sm mx-auto space-y-4">
106126
<p className="text-muted-foreground">
107-
No notes found from the last month. Write some notes in your favorite Nostr client to start revisiting!
127+
{emptyMessage}
108128
</p>
109129
</div>
110130
</CardContent>

src/hooks/useMyNotes.ts

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,80 @@
11
import { useQuery } from '@tanstack/react-query';
22
import { useNostr } from '@nostrify/react';
33
import { useCurrentUser } from './useCurrentUser';
4+
import { NPool, NRelay1 } from '@nostrify/nostrify';
45

5-
export function useMyNotes() {
6+
export type TimeRange = 'month' | 'all-time';
7+
8+
interface UseMyNotesOptions {
9+
timeRange?: TimeRange;
10+
specificRelay?: string;
11+
}
12+
13+
export function useMyNotes(options: UseMyNotesOptions = {}) {
14+
const { timeRange = 'month', specificRelay } = options;
615
const { nostr } = useNostr();
716
const { user } = useCurrentUser();
817

918
return useQuery({
10-
queryKey: ['my-notes', user?.pubkey],
19+
queryKey: ['my-notes', user?.pubkey, timeRange, specificRelay],
1120
queryFn: async (c) => {
1221
if (!user?.pubkey) {
1322
throw new Error('User not logged in');
1423
}
1524

16-
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(5000)]);
25+
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(15000)]);
1726

18-
// Calculate timestamp for 1 month ago
19-
const oneMonthAgo = Math.floor((Date.now() - (30 * 24 * 60 * 60 * 1000)) / 1000);
27+
// Build the filter based on time range
28+
const filter: {
29+
kinds: number[];
30+
authors: string[];
31+
limit: number;
32+
since?: number;
33+
} = {
34+
kinds: [1],
35+
authors: [user.pubkey],
36+
limit: timeRange === 'all-time' ? 2000 : 500,
37+
};
2038

21-
// Query for the user's text notes (kind 1) from the last month
22-
const events = await nostr.query([
23-
{
24-
kinds: [1],
25-
authors: [user.pubkey],
26-
since: oneMonthAgo,
27-
limit: 500,
28-
}
29-
], { signal });
39+
// Only add 'since' filter for month range
40+
if (timeRange === 'month') {
41+
// Calculate timestamp for 1 month ago
42+
const oneMonthAgo = Math.floor((Date.now() - (30 * 24 * 60 * 60 * 1000)) / 1000);
43+
filter.since = oneMonthAgo;
44+
}
3045

31-
// Sort by creation date (newest first)
32-
return events.sort((a, b) => b.created_at - a.created_at);
46+
// If a specific relay is chosen, query only that relay
47+
if (specificRelay) {
48+
// Create a temporary pool for the specific relay
49+
const specificPool = new NPool({
50+
open(url: string) {
51+
return new NRelay1(url);
52+
},
53+
reqRouter(filters) {
54+
const relayMap = new Map();
55+
relayMap.set(specificRelay, filters);
56+
return relayMap;
57+
},
58+
eventRouter() {
59+
return [];
60+
},
61+
});
62+
63+
try {
64+
// Query the specific relay
65+
const events = await specificPool.query([filter], { signal });
66+
67+
// Sort by creation date (newest first)
68+
return events.sort((a, b) => b.created_at - a.created_at);
69+
} finally {
70+
// Clean up the temporary pool
71+
specificPool.close();
72+
}
73+
} else {
74+
// Use the default single relay query (current relay from config)
75+
const events = await nostr.query([filter], { signal });
76+
return events.sort((a, b) => b.created_at - a.created_at);
77+
}
3378
},
3479
enabled: !!user?.pubkey,
3580
staleTime: 1000 * 60 * 5, // 5 minutes

0 commit comments

Comments
 (0)