From dc6ce7bf86fc8549033498d9ee7dbef750c72869 Mon Sep 17 00:00:00 2001 From: junaiddshaukat Date: Thu, 2 Apr 2026 23:04:09 +0500 Subject: [PATCH 1/3] feat: add space search bar to recent activity page (#59) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a search bar on the recent activity page that allows users to search across all content within the current space — messages, threads, classifieds, goals, and calendar events. Uses createSearch from Welshman with fuse.js for client-side fuzzy matching. When the search field is empty, the normal recent activity view is shown. Matching activity items are filtered inline; individual event matches can be clicked to navigate. --- src/routes/spaces/[relay]/recent/+page.svelte | 82 +++++++++++++++++-- 1 file changed, 73 insertions(+), 9 deletions(-) diff --git a/src/routes/spaces/[relay]/recent/+page.svelte b/src/routes/spaces/[relay]/recent/+page.svelte index cc888f00..15e2c5fe 100644 --- a/src/routes/spaces/[relay]/recent/+page.svelte +++ b/src/routes/spaces/[relay]/recent/+page.svelte @@ -15,10 +15,13 @@ getIdAndAddress, } from "@welshman/util" import type {TrustedEvent} from "@welshman/util" - import {repository} from "@welshman/app" + import {repository, createSearch} from "@welshman/app" import History from "@assets/icons/history.svg?dataurl" + import Magnifier from "@assets/icons/magnifier.svg?dataurl" + import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import {createScroller} from "@lib/html" import Icon from "@lib/components/Icon.svelte" + import Button from "@lib/components/Button.svelte" import PageContent from "@lib/components/PageContent.svelte" import SpaceBar from "@app/components/SpaceBar.svelte" import NoteItem from "@app/components/NoteItem.svelte" @@ -28,6 +31,7 @@ import CalendarEventItem from "@app/components/CalendarEventItem.svelte" import RecentConversation from "@app/components/RecentConversation.svelte" import {decodeRelay, deriveEventsForUrl, CONTENT_KINDS} from "@app/core/state" + import {goToEvent} from "@app/util/routes" const url = decodeRelay($page.params.relay!) const since = ago(3, MONTH) @@ -36,15 +40,17 @@ const content = deriveEventsForUrl(url, [{kinds: CONTENT_KINDS, since}]) const comments = deriveEventsForUrl(url, [{kinds: [COMMENT], since}]) + type ActivityItem = { + type: "message" | "content" + event: TrustedEvent + count: number + timestamp: number + } + const recentActivity = derived( [messages, content, comments], ([$messages, $content, $comments]) => { - const activity: Array<{ - type: "message" | "content" - event: TrustedEvent - count: number - timestamp: number - }> = [] + const activity: Array = [] const byRoom = groupBy(e => getTagValue("h", e.tags), $messages) for (const roomMessages of byRoom.values()) { @@ -88,9 +94,40 @@ }, ) + const allSpaceEvents = derived([messages, content], ([$messages, $content]) => [ + ...$messages, + ...$content, + ]) + + const searchIndex = $derived.by(() => + createSearch($allSpaceEvents, { + getValue: (event: TrustedEvent) => event.id, + fuseOptions: {keys: ["content", "tags.1"]}, + }), + ) + + let term = $state("") let limit = $state(20) let element: Element | undefined = $state() + const searchResults = $derived(term ? searchIndex.searchOptions(term) : []) + + const filteredActivity = $derived.by(() => { + if (!term) return $recentActivity + + const matchedIds = new Set(searchResults.map((e: TrustedEvent) => e.id)) + + return $recentActivity.filter(a => matchedIds.has(a.event.id)) + }) + + const clearSearch = () => { + term = "" + } + + const onSearchResultClick = (event: TrustedEvent) => { + goToEvent(event, {keepFocus: true}) + } + onMount(() => { const scroller = createScroller({ element: element!, @@ -112,10 +149,37 @@
- {#if $recentActivity.length === 0} + + + {#if term && searchResults.length > 0 && filteredActivity.length === 0} +

Search Results

+ {#each searchResults as event (event.id)} + + {/each} + {:else if term && searchResults.length === 0} +

No results found.

+ {:else if filteredActivity.length === 0}

No recent activity found!

{:else} - {#each $recentActivity.slice(0, limit) as { type, event, count = 0 } (event.id)} + {#each filteredActivity.slice(0, limit) as { type, event, count = 0 } (event.id)} {#if type === "message"} {:else if event.kind === THREAD} -- 2.52.0 From 195efaf889e1c508c1c38b9e92dad49a75a88b0a Mon Sep 17 00:00:00 2001 From: junaiddshaukat Date: Fri, 3 Apr 2026 03:15:48 +0500 Subject: [PATCH 2/3] refactor: switch to NIP-50 search with popover UI Address review feedback: - Replace client-side fuse.js search with NIP-50 relay-side search using request from @welshman/net with debounced queries and AbortController for cancellation - Move search from inline bar to a search icon in SpaceBar that opens a popover/dialog, matching the existing SpaceSearch pattern - Search results grouped by age (24h, 7d, older) with timestamps - Recent activity view remains untouched when search is closed --- src/routes/spaces/[relay]/recent/+page.svelte | 243 +++++++++++++----- 1 file changed, 184 insertions(+), 59 deletions(-) diff --git a/src/routes/spaces/[relay]/recent/+page.svelte b/src/routes/spaces/[relay]/recent/+page.svelte index 15e2c5fe..51ca871a 100644 --- a/src/routes/spaces/[relay]/recent/+page.svelte +++ b/src/routes/spaces/[relay]/recent/+page.svelte @@ -1,8 +1,23 @@