diff --git a/web/src/lib/RxNostrHelper.ts b/web/src/lib/RxNostrHelper.ts index 3793d29fc..7df76b55b 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/Search.ts b/web/src/lib/Search.ts index 6ae0893b1..734472a90 100644 --- a/web/src/lib/Search.ts +++ b/web/src/lib/Search.ts @@ -1,4 +1,4 @@ -import { nip19, type Filter, Kind } from 'nostr-tools'; +import { nip19, type Filter } from 'nostr-tools'; import { get } from 'svelte/store'; import { authorActionReqEmit } from './author/Action'; import { hashtagsRegexp, reverseChronological, searchRelays } from './Constants'; @@ -60,10 +60,6 @@ export class Search { } console.debug('[search matches]', fromPubkeys, toPubkeys, hashtags, kinds, since, until); - if (kinds.length === 0) { - kinds.push(Kind.Text); - } - const $pubkey = get(pubkey); if (mine && !fromPubkeys.includes($pubkey)) { fromPubkeys.push($pubkey); diff --git a/web/src/lib/i18n/locales/en.json b/web/src/lib/i18n/locales/en.json index f658314a7..2bba1fbb6 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 bf5eed0ae..e5e86611e 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 000000000..9bba4202a --- /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/lib/timelines/TimelineHelper.ts b/web/src/lib/timelines/TimelineHelper.ts index b1569cd8e..f42fcec49 100644 --- a/web/src/lib/timelines/TimelineHelper.ts +++ b/web/src/lib/timelines/TimelineHelper.ts @@ -10,3 +10,9 @@ export function insertIntoAscendingTimeline(event: NostrEvent, items: EventItem[ items.splice(index, 0, item); } } + +export function oldestCreatedAt(items: EventItem[]): number { + return items.length > 0 + ? items[items.length - 1].event.created_at + : Math.floor(Date.now() / 1000); +} diff --git a/web/src/routes/(app)/channels/[nevent=note]/+page.svelte b/web/src/routes/(app)/channels/[nevent=note]/+page.svelte index 0b4218fec..bfdad789a 100644 --- a/web/src/routes/(app)/channels/[nevent=note]/+page.svelte +++ b/web/src/routes/(app)/channels/[nevent=note]/+page.svelte @@ -31,6 +31,7 @@ import PinChannel from './PinChannel.svelte'; import ChannelTitle from '$lib/components/ChannelTitle.svelte'; import MuteButton from '$lib/components/MuteButton.svelte'; + import { oldestCreatedAt } from '$lib/timelines/TimelineHelper'; let slug = $page.params.nevent; let channelId: string; @@ -148,9 +149,6 @@ $channelIdStore = undefined; }); - const oldestCreatedAt = (): number => - items.length > 0 ? items[items.length - 1].event.created_at : Math.floor(Date.now() / 1000); - async function load() { console.log('[channel page load]', slug, channelId); if (channelId === undefined) { @@ -189,7 +187,7 @@ resolve(); } }); - const until = oldestCreatedAt(); + const until = oldestCreatedAt(items); req.emit([{ ...filterBase, until, since: until - 15 * 60 }]); req.over(); await promise; @@ -197,7 +195,9 @@ const length = items.length - firstLength; if (length < minTimelineLength) { const limit = minTimelineLength - length; - const events = await fetchEvents([{ ...filterBase, until: oldestCreatedAt(), limit }]); + const events = await fetchEvents([ + { ...filterBase, until: oldestCreatedAt(items), limit } + ]); items.push( ...events .filter((event) => !items.some((item) => item.event.id === event.id)) diff --git a/web/src/routes/(app)/search/+page.svelte b/web/src/routes/(app)/search/+page.svelte index b30619969..11cc49f17 100644 --- a/web/src/routes/(app)/search/+page.svelte +++ b/web/src/routes/(app)/search/+page.svelte @@ -1,18 +1,21 @@ @@ -139,12 +217,6 @@ -{#if query === ''} -
- -
-{/if} - {#if hashtags.length > 0}
{#each hashtags as hashtag} @@ -157,9 +229,30 @@
{/if} -
- -
+{#if query === ''} +
+ +
+{:else} + + +
+ +
+
+ {#if filter.search} + +
+ +
+
+ {/if} +
+{/if}