Merge branch 'main' of gitlab.com:soapbox-pub/ditto
This commit is contained in:
@@ -17,6 +17,7 @@ import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useBlobbonautProfile } from '@/hooks/useBlobbonautProfile';
|
||||
import { useNostrPublish } from '@/hooks/useNostrPublish';
|
||||
import { toast } from '@/hooks/useToast';
|
||||
import { fetchFreshEvent } from '@/lib/fetchFreshEvent';
|
||||
|
||||
import type { BlobbiCompanion } from '@/blobbi/core/lib/blobbi';
|
||||
import {
|
||||
@@ -52,18 +53,14 @@ export function useBlobbiSleepToggle(): UseBlobbiSleepToggleResult {
|
||||
pubkey: string,
|
||||
dTag: string,
|
||||
): Promise<BlobbiCompanion | null> => {
|
||||
const events = await nostr.query([{
|
||||
kinds: [KIND_BLOBBI_STATE],
|
||||
authors: [pubkey],
|
||||
'#d': [dTag],
|
||||
}]);
|
||||
const event = await fetchFreshEvent(
|
||||
nostr,
|
||||
{ kinds: [KIND_BLOBBI_STATE], authors: [pubkey], '#d': [dTag] },
|
||||
{ eoseTimeout: 1000 },
|
||||
);
|
||||
|
||||
const validEvents = events
|
||||
.filter(isValidBlobbiEvent)
|
||||
.sort((a, b) => b.created_at - a.created_at);
|
||||
|
||||
if (validEvents.length === 0) return null;
|
||||
return parseBlobbiEvent(validEvents[0]) ?? null;
|
||||
if (!event || !isValidBlobbiEvent(event)) return null;
|
||||
return parseBlobbiEvent(event) ?? null;
|
||||
}, [nostr]);
|
||||
|
||||
/** Optimistically update the TanStack cache so the companion reacts immediately. */
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { fetchFreshEvents } from '@/lib/fetchFreshEvent';
|
||||
import {
|
||||
KIND_BLOBBI_STATE,
|
||||
isValidBlobbiEvent,
|
||||
@@ -51,46 +52,34 @@ export function useBlobbisCollection(dList: string[] | undefined) {
|
||||
// Main query to fetch all companions from relays
|
||||
const query = useQuery({
|
||||
queryKey: ['blobbi-collection', user?.pubkey, queryKeyDTags],
|
||||
queryFn: async ({ signal }) => {
|
||||
queryFn: async () => {
|
||||
if (!user?.pubkey || !sortedDList || sortedDList.length === 0) {
|
||||
console.log('[useBlobbisCollection] No pubkey or empty dList, returning empty');
|
||||
return { companionsByD: {}, companions: [] };
|
||||
}
|
||||
|
||||
// Log the dList we're about to query
|
||||
console.log('[Blobbi] dList:', sortedDList);
|
||||
|
||||
// Chunk the d-list for relay compatibility
|
||||
const chunks = chunkArray(sortedDList, CHUNK_SIZE);
|
||||
console.log('[useBlobbisCollection] Splitting into', chunks.length, 'chunk(s)');
|
||||
|
||||
// Query all chunks in parallel
|
||||
// Fetch all chunks, using a relaxed eoseTimeout (1000ms) so slower
|
||||
// relays have time to respond and we get the freshest events.
|
||||
const allEvents: NostrEvent[] = [];
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const filter = {
|
||||
kinds: [KIND_BLOBBI_STATE],
|
||||
authors: [user.pubkey],
|
||||
'#d': chunk,
|
||||
// IMPORTANT: No limit - fetch ALL pets matching the d-tags
|
||||
};
|
||||
|
||||
// Log the filter immediately before query
|
||||
console.log('[Blobbi] 31124 query filter:', JSON.stringify(filter, null, 2));
|
||||
|
||||
const events = await nostr.query([filter], { signal });
|
||||
const events = await fetchFreshEvents(
|
||||
nostr,
|
||||
[{
|
||||
kinds: [KIND_BLOBBI_STATE],
|
||||
authors: [user.pubkey],
|
||||
'#d': chunk,
|
||||
}],
|
||||
{ eoseTimeout: 1000 },
|
||||
);
|
||||
allEvents.push(...events);
|
||||
|
||||
console.log('[useBlobbisCollection] Chunk returned', events.length, 'events');
|
||||
}
|
||||
|
||||
console.log('[useBlobbisCollection] Total events received:', allEvents.length);
|
||||
|
||||
// Filter to valid events
|
||||
const validEvents = allEvents.filter(isValidBlobbiEvent);
|
||||
|
||||
console.log('[useBlobbisCollection] Valid events:', validEvents.length);
|
||||
|
||||
// Group events by d-tag and keep only the newest per d
|
||||
const eventsByD = new Map<string, NostrEvent>();
|
||||
|
||||
@@ -116,11 +105,6 @@ export function useBlobbisCollection(dList: string[] | undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[useBlobbisCollection] Parsed companions:', {
|
||||
count: companions.length,
|
||||
dTags: Object.keys(companionsByD),
|
||||
});
|
||||
|
||||
return { companionsByD, companions };
|
||||
},
|
||||
enabled: !!user?.pubkey && !!sortedDList && sortedDList.length > 0,
|
||||
|
||||
@@ -892,13 +892,15 @@ export function parseBlobbiEvent(event: NostrEvent): BlobbiCompanion | undefined
|
||||
const isLegacy = isLegacyBlobbiEvent(event);
|
||||
|
||||
// Concise, structured debug log
|
||||
console.log('[Blobbi]', {
|
||||
d: d.length > 30 ? `${d.slice(0, 20)}...` : d,
|
||||
name,
|
||||
isLegacy,
|
||||
hasSeed: !!seed,
|
||||
traits: `${visualTraits.baseColor} ${visualTraits.pattern} ${visualTraits.size}`,
|
||||
});
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('[Blobbi]', {
|
||||
d: d.length > 30 ? `${d.slice(0, 20)}...` : d,
|
||||
name,
|
||||
isLegacy,
|
||||
hasSeed: !!seed,
|
||||
traits: `${visualTraits.baseColor} ${visualTraits.pattern} ${visualTraits.size}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Parse task progress tags: ["task", "name:value"]
|
||||
const tasks: BlobbiTaskProgress[] = [];
|
||||
|
||||
@@ -201,7 +201,6 @@ export function ComposeBox({
|
||||
|
||||
// Poll mode state
|
||||
const [mode, setMode] = useState<'post' | 'poll'>(initialMode);
|
||||
const [pollQuestion, setPollQuestion] = useState('');
|
||||
const [pollOptions, setPollOptions] = useState([
|
||||
{ id: pollOptionId(), label: '' },
|
||||
{ id: pollOptionId(), label: '' },
|
||||
@@ -233,7 +232,6 @@ export function ComposeBox({
|
||||
setTrayOpen(false);
|
||||
setInternalPreviewMode(false);
|
||||
setMode(initialMode);
|
||||
setPollQuestion('');
|
||||
setPollOptions([{ id: pollOptionId(), label: '' }, { id: pollOptionId(), label: '' }]);
|
||||
setPollType('singlechoice');
|
||||
setPollDuration(7);
|
||||
@@ -982,7 +980,8 @@ export function ComposeBox({
|
||||
|
||||
const handlePollSubmit = async () => {
|
||||
const filledOptions = pollOptions.filter((o) => o.label.trim());
|
||||
if (!pollQuestion.trim() || filledOptions.length < 2 || !user || isPollPending) return;
|
||||
const finalContent = content.trim();
|
||||
if (!finalContent || filledOptions.length < 2 || !user || isPollPending) return;
|
||||
|
||||
const tags: string[][] = [];
|
||||
for (const opt of filledOptions) {
|
||||
@@ -992,10 +991,27 @@ export function ComposeBox({
|
||||
if (pollDuration > 0) {
|
||||
tags.push(['endsAt', String(Math.floor(Date.now() / 1000) + pollDuration * 86_400)]);
|
||||
}
|
||||
tags.push(['alt', `Poll: ${pollQuestion.trim()}`]);
|
||||
|
||||
// NIP-92: Add imeta tags for media URLs in content
|
||||
const mediaUrlMatches = finalContent.matchAll(new RegExp(IMETA_MEDIA_URL_REGEX.source, 'gi'));
|
||||
const processedUrls = new Set<string>();
|
||||
for (const match of mediaUrlMatches) {
|
||||
const url = match[0];
|
||||
if (processedUrls.has(url)) continue;
|
||||
processedUrls.add(url);
|
||||
const fileTags = uploadedFileGroups.get(url);
|
||||
if (fileTags) {
|
||||
tags.push(['imeta', ...fileTags.map(tag => `${tag[0]} ${tag[1]}`)]);
|
||||
} else {
|
||||
const ext = match[1].toLowerCase();
|
||||
tags.push(['imeta', `url ${url}`, `m ${mimeFromExt(ext)}`]);
|
||||
}
|
||||
}
|
||||
|
||||
tags.push(['alt', `Poll: ${finalContent}`]);
|
||||
|
||||
try {
|
||||
await createEvent({ kind: 1068, content: pollQuestion.trim(), tags });
|
||||
await createEvent({ kind: 1068, content: finalContent, tags });
|
||||
resetComposeState();
|
||||
queryClient.invalidateQueries({ queryKey: ['feed'] });
|
||||
toast({ title: 'Poll published!' });
|
||||
@@ -1006,7 +1022,7 @@ export function ComposeBox({
|
||||
};
|
||||
|
||||
const pollFilledCount = pollOptions.filter((o) => o.label.trim()).length;
|
||||
const isPollValid = pollQuestion.trim().length > 0 && pollFilledCount >= 2;
|
||||
const isPollValid = content.trim().length > 0 && pollFilledCount >= 2;
|
||||
|
||||
const isExpanded = forceExpanded || expanded || content.length > 0 || !compact;
|
||||
|
||||
@@ -1014,7 +1030,7 @@ export function ComposeBox({
|
||||
if (!user && compact) return null;
|
||||
|
||||
return (
|
||||
<div className={cn("px-4 py-3 bg-background/85")}>
|
||||
<div className={cn("px-4 py-3 bg-background/85 rounded-2xl")}>
|
||||
{/* Preview toggle at top when not controlled and has previewable content */}
|
||||
{hasPreviewableContent && controlledPreviewMode === undefined && (
|
||||
<div className="flex items-center justify-end mb-3">
|
||||
@@ -1062,31 +1078,83 @@ export function ComposeBox({
|
||||
)}
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
{mode === 'poll' ? (
|
||||
/* ── Inline poll builder ─────────────────────────────── */
|
||||
<div className="pt-2.5 pb-1 space-y-3">
|
||||
{!previewMode ? (
|
||||
/* ── Edit mode — Textarea ────────────────────────────── */
|
||||
<div className="relative">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
onFocus={expand}
|
||||
onPaste={handlePaste}
|
||||
placeholder={mode === 'poll' ? 'Ask a question…' : placeholder}
|
||||
className={cn(
|
||||
'w-full bg-transparent text-foreground placeholder:text-muted-foreground resize-none outline-none text-lg pt-2.5 pb-2 opacity-85 break-words overflow-hidden transition-[min-height] duration-200 ease-in-out',
|
||||
isExpanded ? 'min-h-[100px]' : 'min-h-[44px]',
|
||||
)}
|
||||
rows={1}
|
||||
disabled={!user}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<MentionAutocomplete
|
||||
textareaRef={textareaRef}
|
||||
content={content}
|
||||
onInsertMention={handleInsertMention}
|
||||
/>
|
||||
<EmojiShortcodeAutocomplete
|
||||
textareaRef={textareaRef}
|
||||
content={content}
|
||||
onInsertEmoji={handleInsertShortcodeEmoji}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
/* Preview mode - Show how post will look */
|
||||
mockEvent && (() => {
|
||||
const imetaMap = parseImetaMap(mockEvent.tags);
|
||||
const videos = extractVideoUrls(mockEvent.content);
|
||||
const imetaAudios = Array.from(imetaMap.values())
|
||||
.filter((e) => e.mime?.startsWith('audio/'))
|
||||
.map((e) => e.url);
|
||||
const audios = imetaAudios.length > 0 ? imetaAudios : extractAudioUrls(mockEvent.content);
|
||||
const webxdcApps = Array.from(imetaMap.values()).filter(
|
||||
(entry) => entry.mime === 'application/x-webxdc' || entry.mime === 'application/vnd.webxdc+zip',
|
||||
);
|
||||
return (
|
||||
<div className="pt-2.5 pb-2 min-h-[100px]">
|
||||
<div className="text-lg opacity-85">
|
||||
<NoteContent event={mockEvent} className="text-foreground" />
|
||||
</div>
|
||||
<NoteMedia
|
||||
videos={videos}
|
||||
audios={audios}
|
||||
imetaMap={imetaMap}
|
||||
webxdcApps={webxdcApps}
|
||||
event={mockEvent}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
|
||||
{/* Poll options + settings — shown below the normal textarea/preview */}
|
||||
{mode === 'poll' && (
|
||||
<div className="space-y-3 pt-1">
|
||||
{/* Back to post link — hidden when poll mode is the only mode */}
|
||||
{initialMode !== 'poll' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMode('post')}
|
||||
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors -mt-0.5"
|
||||
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<ChevronLeft className="size-3.5" />
|
||||
Back to post
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Question */}
|
||||
<textarea
|
||||
value={pollQuestion}
|
||||
onChange={(e) => setPollQuestion(e.target.value)}
|
||||
placeholder="Ask a question…"
|
||||
rows={2}
|
||||
maxLength={280}
|
||||
className="w-full bg-transparent text-foreground placeholder:text-muted-foreground resize-none outline-none text-lg pb-1 opacity-85 break-words"
|
||||
/>
|
||||
|
||||
{/* Options */}
|
||||
<div className="space-y-1.5">
|
||||
{pollOptions.map((opt, idx) => (
|
||||
@@ -1167,66 +1235,6 @@ export function ComposeBox({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : !previewMode ? (
|
||||
/* ── Edit mode — Textarea ────────────────────────────── */
|
||||
<div className="relative">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
onFocus={expand}
|
||||
onPaste={handlePaste}
|
||||
placeholder={placeholder}
|
||||
className={cn(
|
||||
'w-full bg-transparent text-foreground placeholder:text-muted-foreground resize-none outline-none text-lg pt-2.5 pb-2 opacity-85 break-words overflow-hidden transition-[min-height] duration-200 ease-in-out',
|
||||
isExpanded ? 'min-h-[100px]' : 'min-h-[44px]',
|
||||
)}
|
||||
rows={1}
|
||||
disabled={!user}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<MentionAutocomplete
|
||||
textareaRef={textareaRef}
|
||||
content={content}
|
||||
onInsertMention={handleInsertMention}
|
||||
/>
|
||||
<EmojiShortcodeAutocomplete
|
||||
textareaRef={textareaRef}
|
||||
content={content}
|
||||
onInsertEmoji={handleInsertShortcodeEmoji}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
/* Preview mode - Show how post will look */
|
||||
mockEvent && (() => {
|
||||
const imetaMap = parseImetaMap(mockEvent.tags);
|
||||
const videos = extractVideoUrls(mockEvent.content);
|
||||
const imetaAudios = Array.from(imetaMap.values())
|
||||
.filter((e) => e.mime?.startsWith('audio/'))
|
||||
.map((e) => e.url);
|
||||
const audios = imetaAudios.length > 0 ? imetaAudios : extractAudioUrls(mockEvent.content);
|
||||
const webxdcApps = Array.from(imetaMap.values()).filter(
|
||||
(entry) => entry.mime === 'application/x-webxdc' || entry.mime === 'application/vnd.webxdc+zip',
|
||||
);
|
||||
return (
|
||||
<div className="pt-2.5 pb-2 min-h-[100px]">
|
||||
<div className="text-lg opacity-85">
|
||||
<NoteContent event={mockEvent} className="text-foreground" />
|
||||
</div>
|
||||
<NoteMedia
|
||||
videos={videos}
|
||||
audios={audios}
|
||||
imetaMap={imetaMap}
|
||||
webxdcApps={webxdcApps}
|
||||
event={mockEvent}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
|
||||
{/* Content warning input */}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { ChevronLeft, ChevronRight, X, Download } from 'lucide-react';
|
||||
import { Blurhash } from 'react-blurhash';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -125,8 +126,8 @@ export function ImageGallery({
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Lightbox */}
|
||||
{lightboxIndex !== null && lightboxIndex !== undefined && (
|
||||
{/* Lightbox — portaled to document.body to escape stacking contexts (e.g. the center column z-0) */}
|
||||
{lightboxIndex !== null && lightboxIndex !== undefined && createPortal(
|
||||
<Lightbox
|
||||
images={images}
|
||||
currentIndex={lightboxIndex}
|
||||
@@ -135,7 +136,8 @@ export function ImageGallery({
|
||||
onPrev={goPrev}
|
||||
topBarLeft={lightboxTopBarLeft}
|
||||
bottomBar={lightboxBottomBar}
|
||||
/>
|
||||
/>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { NostrEvent } from '@nostrify/nostrify';
|
||||
|
||||
import { useCurrentUser } from './useCurrentUser';
|
||||
import { useLocalStorage } from './useLocalStorage';
|
||||
import { fetchFreshEvents } from '@/lib/fetchFreshEvent';
|
||||
import {
|
||||
KIND_BLOBBONAUT_PROFILE,
|
||||
BLOBBONAUT_PROFILE_KINDS,
|
||||
@@ -76,7 +77,7 @@ export function useBlobbonautProfile() {
|
||||
// Main query to fetch the profile from relays
|
||||
const query = useQuery({
|
||||
queryKey: ['blobbonaut-profile', user?.pubkey],
|
||||
queryFn: async ({ signal }) => {
|
||||
queryFn: async () => {
|
||||
if (!user?.pubkey) {
|
||||
return null;
|
||||
}
|
||||
@@ -84,14 +85,18 @@ export function useBlobbonautProfile() {
|
||||
// Query with all possible d-tag values (canonical + legacy)
|
||||
const dValues = getBlobbonautQueryDValues(user.pubkey);
|
||||
|
||||
// Query BOTH current (11125) and legacy (31125) kinds for migration support
|
||||
const filter = {
|
||||
kinds: [...BLOBBONAUT_PROFILE_KINDS],
|
||||
authors: [user.pubkey],
|
||||
'#d': dValues,
|
||||
};
|
||||
|
||||
const events = await nostr.query([filter], { signal });
|
||||
// Query BOTH current (11125) and legacy (31125) kinds for migration support.
|
||||
// Use a relaxed eoseTimeout (1000ms) so slower relays have time to respond
|
||||
// and we get the freshest profile across all relays.
|
||||
const events = await fetchFreshEvents(
|
||||
nostr,
|
||||
[{
|
||||
kinds: [...BLOBBONAUT_PROFILE_KINDS],
|
||||
authors: [user.pubkey],
|
||||
'#d': dValues,
|
||||
}],
|
||||
{ eoseTimeout: 1000 },
|
||||
);
|
||||
|
||||
// Filter to valid events
|
||||
const validEvents = events.filter(isValidBlobbonautEvent);
|
||||
|
||||
@@ -566,7 +566,7 @@ export class NostrBatcher {
|
||||
|
||||
req(
|
||||
filters: NostrFilter[],
|
||||
opts?: { signal?: AbortSignal },
|
||||
opts?: { signal?: AbortSignal; relays?: string[]; eoseTimeout?: number },
|
||||
): AsyncIterable<import('@nostrify/types').NostrRelayEVENT | import('@nostrify/types').NostrRelayEOSE | import('@nostrify/types').NostrRelayCLOSED> {
|
||||
return this.pool.req(filters, opts);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import type { NostrEvent, NostrFilter, NPool } from '@nostrify/nostrify';
|
||||
|
||||
interface FetchFreshEventOpts {
|
||||
/**
|
||||
* Override the pool-level eoseTimeout for this query. When set, uses
|
||||
* `nostr.req()` directly with this value instead of `nostr.query()`,
|
||||
* giving slower relays more time to respond.
|
||||
*
|
||||
* The default pool eoseTimeout is 300ms (resolves quickly after the
|
||||
* fastest relay). Set to eg. 1000 for accuracy-sensitive queries where
|
||||
* you need the absolute freshest event across all relays.
|
||||
*/
|
||||
eoseTimeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the freshest version of a replaceable/addressable event directly from
|
||||
* relays, bypassing any local cache.
|
||||
@@ -24,13 +37,9 @@ import type { NostrEvent, NostrFilter, NPool } from '@nostrify/nostrify';
|
||||
export async function fetchFreshEvent(
|
||||
nostr: NPool,
|
||||
filter: NostrFilter,
|
||||
opts?: FetchFreshEventOpts,
|
||||
): Promise<NostrEvent | null> {
|
||||
const signal = AbortSignal.timeout(10_000);
|
||||
|
||||
const events = await nostr.query(
|
||||
[{ ...filter, limit: 1 }],
|
||||
{ signal },
|
||||
);
|
||||
const events = await fetchFreshEvents(nostr, [{ ...filter, limit: 1 }], opts);
|
||||
|
||||
if (events.length === 0) return null;
|
||||
|
||||
@@ -39,3 +48,42 @@ export async function fetchFreshEvent(
|
||||
current.created_at > latest.created_at ? current : latest,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches events from relays, bypassing any local cache. Like
|
||||
* {@link fetchFreshEvent} but accepts multiple filters and returns all
|
||||
* matching events (not just one).
|
||||
*
|
||||
* When `opts.eoseTimeout` is set, uses `nostr.req()` directly with that
|
||||
* timeout, overriding the pool-level eoseTimeout. Otherwise falls back to
|
||||
* the standard `nostr.query()` path.
|
||||
*/
|
||||
export async function fetchFreshEvents(
|
||||
nostr: NPool,
|
||||
filters: NostrFilter[],
|
||||
opts?: FetchFreshEventOpts,
|
||||
): Promise<NostrEvent[]> {
|
||||
const signal = AbortSignal.timeout(10_000);
|
||||
|
||||
if (opts?.eoseTimeout !== undefined) {
|
||||
// Use req() directly so we can pass a custom eoseTimeout,
|
||||
// overriding the pool-level value (typically 300ms).
|
||||
const events: NostrEvent[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for await (const msg of nostr.req(filters, { signal, eoseTimeout: opts.eoseTimeout })) {
|
||||
if (msg[0] === 'EOSE' || msg[0] === 'CLOSED') break;
|
||||
if (msg[0] === 'EVENT') {
|
||||
const event = msg[2];
|
||||
if (!seen.has(event.id)) {
|
||||
seen.add(event.id);
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
return nostr.query(filters, { signal });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user