diff --git a/web/src/lib/RxNostrHelper.ts b/web/src/lib/RxNostrHelper.ts index 3793d29f..7df76b55 100644 --- a/web/src/lib/RxNostrHelper.ts +++ b/web/src/lib/RxNostrHelper.ts @@ -44,7 +44,10 @@ export async function fetchLastEvent(filter: LazyFilter): Promise { +export async function fetchEvents( + filters: LazyFilter[], + relays: string[] | undefined = undefined +): Promise { const { promise, resolve } = Promise.withResolvers(); const events: Event[] = []; const req = createRxBackwardReq(); @@ -68,7 +71,11 @@ export async function fetchEvents(filters: LazyFilter[]): Promise { resolve(events); } }); - req.emit(filters); + if (relays !== undefined && relays.length > 0) { + req.emit(filters, { relays }); + } else { + req.emit(filters); + } req.over(); return await promise; } diff --git a/web/src/lib/i18n/locales/en.json b/web/src/lib/i18n/locales/en.json index f658314a..2bba1fbb 100644 --- a/web/src/lib/i18n/locales/en.json +++ b/web/src/lib/i18n/locales/en.json @@ -147,7 +147,9 @@ "search": "Search", "options": "Search options", "mine": "My notes", - "proxy": "Include external SNS notes" + "proxy": "Include external SNS notes", + "notes": "Notes", + "users": "Users" }, "public_chat": { "create_channel": "Create channel", diff --git a/web/src/lib/i18n/locales/ja.json b/web/src/lib/i18n/locales/ja.json index bf5eed0a..e5e86611 100644 --- a/web/src/lib/i18n/locales/ja.json +++ b/web/src/lib/i18n/locales/ja.json @@ -146,7 +146,9 @@ "search": "検索", "options": "検索オプション", "mine": "自分の投稿", - "proxy": "外部 SNS の投稿を含める" + "proxy": "外部 SNS の投稿を含める", + "notes": "投稿", + "users": "ユーザー" }, "public_chat": { "create_channel": "チャンネルを作成", diff --git a/web/src/lib/timelines/SearchTimeline.ts b/web/src/lib/timelines/SearchTimeline.ts new file mode 100644 index 00000000..9bba4202 --- /dev/null +++ b/web/src/lib/timelines/SearchTimeline.ts @@ -0,0 +1,95 @@ +import { createRxBackwardReq, uniq, type LazyFilter } from 'rx-nostr'; +import { filter, tap } from 'rxjs'; +import { get, writable } from 'svelte/store'; +import { authorActionReqEmit } from '$lib/author/Action'; +import { minTimelineLength, searchRelays } from '$lib/Constants'; +import { EventItem } from '$lib/Items'; +import { fetchEvents } from '$lib/RxNostrHelper'; +import { referencesReqEmit, rxNostr } from './MainTimeline'; +import type { Timeline } from './Timeline'; +import { oldestCreatedAt } from './TimelineHelper'; + +export class SearchTimeline implements Timeline { + items = writable([]); + #completed = false; + + constructor(public readonly filter: LazyFilter) {} + + subscribe(): void { + console.debug('[search timeline subscribe]', this.filter); + } + + unsubscribe(): void { + console.debug('[search timeline unsubscribe]', this.filter); + this.items.set([]); + } + + async load(): Promise { + if (this.#completed) { + return; + } + + console.debug('[search timeline load]', this.filter); + const $items = get(this.items); + const firstLength = $items.length; + const filterBase = { ...this.filter }; + const { promise, resolve } = Promise.withResolvers(); + const req = createRxBackwardReq(); + rxNostr + .use(req) + .pipe( + uniq(), + filter(({ event }) => !$items.some((item) => item.event.id === event.id)), + tap(({ event }) => { + referencesReqEmit(event); + authorActionReqEmit(event); + }) + ) + .subscribe({ + next: ({ event }) => { + console.debug('[search next]', event); + const item = new EventItem(event); + const index = $items.findIndex( + (x) => x.event.created_at < item.event.created_at + ); + if (index < 0) { + $items.push(item); + } else { + $items.splice(index, 0, item); + } + this.items.set($items); + }, + complete: () => { + console.debug('[search complete]', firstLength, $items.length); + resolve(); + } + }); + const until = oldestCreatedAt($items); + req.emit([{ ...filterBase, until, since: until - 15 * 60 }], { relays: searchRelays }); + req.over(); + await promise; + + const length = $items.length - firstLength; + if (length < minTimelineLength) { + const limit = minTimelineLength - length; + const events = await fetchEvents( + [{ ...filterBase, until: oldestCreatedAt($items), limit }], + searchRelays + ); + const _items = events + .filter((event) => !$items.some((item) => item.event.id === event.id)) + .splice(0, limit) + .map((event) => new EventItem(event)); + if (_items.length === 0) { + this.#completed = true; + } + $items.push(..._items); + this.items.set($items); + } + console.log('[search loaded]', firstLength, $items.length); + } + + get completed() { + return this.#completed; + } +} diff --git a/web/src/routes/(app)/search/+page.svelte b/web/src/routes/(app)/search/+page.svelte index 0e795aa1..11cc49f1 100644 --- a/web/src/routes/(app)/search/+page.svelte +++ b/web/src/routes/(app)/search/+page.svelte @@ -1,18 +1,21 @@ @@ -143,12 +217,6 @@ -{#if query === ''} -
- -
-{/if} - {#if hashtags.length > 0}
{#each hashtags as hashtag} @@ -161,9 +229,30 @@
{/if} -
- -
+{#if query === ''} +
+ +
+{:else} + + +
+ +
+
+ {#if filter.search} + +
+ +
+
+ {/if} +
+{/if}