From 9c5b3d42d22e0ef8e4b6c8eb2a27744467d2a853 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Mon, 7 Sep 2026 13:53:25 -0700 Subject: [PATCH] Rework search dialogs --- e2e/USER_STORIES.md | 4 +- e2e/specs/people.spec.ts | 52 ++++---- e2e/specs/rooms.spec.ts | 2 +- src/app/components/PrimaryNav.svelte | 7 +- src/app/components/RoomSearch.svelte | 160 ++--------------------- src/app/components/Search.svelte | 36 ++++++ src/app/components/SearchBody.svelte | 167 ++++++++++++++++++++++++ src/app/components/SpaceSearch.svelte | 179 +++----------------------- src/app/title.ts | 1 - src/routes/people/+page.svelte | 54 -------- 10 files changed, 266 insertions(+), 396 deletions(-) create mode 100644 src/app/components/Search.svelte create mode 100644 src/app/components/SearchBody.svelte delete mode 100644 src/routes/people/+page.svelte diff --git a/e2e/USER_STORIES.md b/e2e/USER_STORIES.md index a30d073c..8138c973 100644 --- a/e2e/USER_STORIES.md +++ b/e2e/USER_STORIES.md @@ -1083,9 +1083,9 @@ profile. Acceptance: -- Typing a name on the people search filters results as she types, each showing +- Typing a name in the search dialog filters results as she types, each showing avatar, display name, and about text. -- Scrolling to the bottom loads more matches. +- Only the ten best matches are listed. - Clicking a result opens that person's profile. ### US-075 — View someone's profile diff --git a/e2e/specs/people.spec.ts b/e2e/specs/people.spec.ts index 8de50828..93ef5b93 100644 --- a/e2e/specs/people.spec.ts +++ b/e2e/specs/people.spec.ts @@ -53,6 +53,13 @@ const sidebar = (page: Page) => page.locator("aside") // content in it. const dialog = (page: Page) => page.locator(".dialog").last() +// Search is a dialog the nav opens rather than a page of its own. +const openSearch = (page: Page) => page.locator('.primary-nav button[data-tip="Search"]').click() + +const searchTerm = (page: Page) => page.getByPlaceholder("Search your spaces...") + +const searchResults = (page: Page) => dialog(page).locator(".card.card-interactive") + const notes = (page: Page) => pageContent(page).locator(".cv.card") const readClipboard = (page: Page) => page.evaluate(() => navigator.clipboard.readText()) @@ -76,11 +83,8 @@ const seedRelayList = (space: SeededSpace, user: TestUser) => ) test("US-074 find a person", async ({seed, as}) => { - // Sixty of them: the people list starts at ten and adds another ten every second until the page - // is taller than the scroller's threshold — and it keeps counting while the profiles are still - // on their way, so by the time the first ones render the limit is already well past ten. - // "Scrolling loads more" is only a fact when there are far more matches than that first fill - // can reach. + // Sixty of them, so that the ten the dialog lists are visibly the best matches rather than + // everyone who matched. const searchers = Array.from({length: 60}, (_, i) => makeTestUser(`searcher-${i}`)) const searcherName = (i: number) => `Searcher ${String(i).padStart(2, "0")}` const searcherAvatar = (i: number) => `https://images.test/searcher-${i}.png` @@ -91,7 +95,7 @@ test("US-074 find a person", async ({seed, as}) => { "Answers to a whistle and to nothing else.", ].join("\n") - await seed(({relay, user}) => { + const scenario = await seed(({relay, user}) => { const space = relay("space") space.room("general", {name: "General"}) @@ -113,10 +117,11 @@ test("US-074 find a person", async ({seed, as}) => { } }) - const page = await as(users.alice, "/people", {context: DESKTOP}) - const term = page.getByPlaceholder("Search for people...") - const cards = page.locator(".card.card-interactive") + const page = await as(users.alice, spacePath(scenario.space("space").url), {context: DESKTOP}) + const term = searchTerm(page) + const cards = searchResults(page) + await openSearch(page) await term.fill("Searcher") // Which of them ranks first is fuse's business, so a result is described by what every @@ -128,15 +133,7 @@ test("US-074 find a person", async ({seed, as}) => { await expect(first).toContainText(/Searcher \d\d/) await expect(first).toContainText("Deck crew on the northern run.") - await expect.poll(() => cards.count()).toBeGreaterThanOrEqual(10) - - const shown = await cards.count() - - expect(shown).toBeLessThan(searchers.length) - - await cards.last().scrollIntoViewIfNeeded() - - await expect.poll(() => cards.count()).toBeGreaterThan(shown) + await expect.poll(() => cards.count()).toBe(10) // Narrowing the term filters the list rather than reordering it. await term.fill("Barnacle") @@ -267,7 +264,7 @@ test("US-076 follow and unfollow", async ({seed, as}) => { test("US-077 see web-of-trust standing build up", async ({seed, as}) => { const carolAvatar = "https://images.test/carol-avatar.png" - await seed(({relay, user}) => { + const scenario = await seed(({relay, user}) => { const space = relay("space") space.room("general", {name: "General"}) @@ -289,9 +286,9 @@ test("US-077 see web-of-trust standing build up", async ({seed, as}) => { // Every hop here is a link click rather than a navigation, so the follow alice publishes stays // in the client that published it. - const page = await as(users.alice, "/people", {context: DESKTOP}) - const term = page.getByPlaceholder("Search for people...") - const cards = page.locator(".card.card-interactive") + const page = await as(users.alice, spacePath(scenario.space("space").url), {context: DESKTOP}) + const term = searchTerm(page) + const cards = searchResults(page) const bobCard = cards.filter({hasText: "Bob Barnacle"}) const carolCard = cards.filter({hasText: "Carol Cutter"}) const reputation = () => sidebar(page).locator(".card.card-sm").filter({hasText: "Reputation"}) @@ -300,6 +297,7 @@ test("US-077 see web-of-trust standing build up", async ({seed, as}) => { const ring = async () => Number(await bobCard.locator("circle.wot-highlight").getAttribute("stroke-dashoffset")) + await openSearch(page) await term.fill("Barnacle") await expect(bobCard).toHaveCount(1) @@ -315,7 +313,7 @@ test("US-077 see web-of-trust standing build up", async ({seed, as}) => { await expect(reputation()).toContainText(/\b0 \/ 100\b/) await expect(reputation()).toContainText("This user is not well known in your network.") - await page.locator('.primary-nav a[href="/people"]').click() + await openSearch(page) await term.fill("Cutter") await expect(carolCard).toHaveCount(1) @@ -328,7 +326,7 @@ test("US-077 see web-of-trust standing build up", async ({seed, as}) => { await expect(page.getByRole("button", {name: "Unfollow", exact: true})).toBeVisible() - await page.locator('.primary-nav a[href="/people"]').click() + await openSearch(page) await term.fill("Barnacle") await expect(bobCard).toHaveCount(1) @@ -733,10 +731,10 @@ test("US-082 mute an account", async ({seed, as}) => { // Through the nav rather than a fresh navigation, so what is on screen is what the client that // just published the mute believes. const openBobsProfile = async () => { - await page.locator('.primary-nav a[href="/people"]').click() - await page.getByPlaceholder("Search for people...").fill("Barnacle") + await openSearch(page) + await searchTerm(page).fill("Barnacle") - const card = page.locator(".card.card-interactive").filter({hasText: "Bob Barnacle"}) + const card = searchResults(page).filter({hasText: "Bob Barnacle"}) await expect(card).toHaveCount(1) await viewProfile(card).click() diff --git a/e2e/specs/rooms.spec.ts b/e2e/specs/rooms.spec.ts index 05a3ebfa..7004edb4 100644 --- a/e2e/specs/rooms.spec.ts +++ b/e2e/specs/rooms.spec.ts @@ -683,7 +683,7 @@ test("US-027 find a past message and jump to it", async ({seed, as}) => { await pageBar(page).getByRole("button", {name: "Search"}).click() const term = page.getByPlaceholder("Search this room...") - const search = dialog(page, "Search Content") + const search = dialog(page, "Search") await term.fill("harbor") diff --git a/src/app/components/PrimaryNav.svelte b/src/app/components/PrimaryNav.svelte index ce1679c5..2d5e0fba 100644 --- a/src/app/components/PrimaryNav.svelte +++ b/src/app/components/PrimaryNav.svelte @@ -13,6 +13,7 @@ import MenuSettings from "@app/components/MenuSettings.svelte" import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte" import PrimaryNavSpaces from "@app/components/PrimaryNavSpaces.svelte" + import Search from "@app/components/Search.svelte" import {userSpaceUrls} from "@app/rooms" import {PLATFORM_RELAYS} from "@app/env" import {pushModal} from "@app/modal" @@ -32,6 +33,8 @@ const showSettingsMenu = () => pushModal(MenuSettings) + const showSearch = () => pushModal(Search) + const anySpaceNotifications = $derived( $userSpaceUrls.some(p => $notifications.has(makeSpacePath(p))), ) @@ -56,7 +59,7 @@ notification={$notifications.has("/chat")}> - + @@ -74,7 +77,7 @@ {#if PLATFORM_RELAYS.length === 1} {:else} - + {/if} diff --git a/src/app/components/RoomSearch.svelte b/src/app/components/RoomSearch.svelte index e4c223e3..fd5ec41a 100644 --- a/src/app/components/RoomSearch.svelte +++ b/src/app/components/RoomSearch.svelte @@ -1,26 +1,13 @@ - Search Content + Search - on {displayRelayUrl(url)} + in - - - {#if loading} - Searching... - {:else if eventsByAge.size === 0 && term} - No results found. - {:else} - {#each eventsByAge as [key, events] (key)} -

- {#if key === "day"} - Last 24 Hours - {:else if key === "week"} - Last 7 Days - {:else} - Older - {/if} -

- {#each events as event (event.id)} - - {/each} - {/each} - {/if} -
+
diff --git a/src/app/components/Search.svelte b/src/app/components/Search.svelte new file mode 100644 index 00000000..810ed81f --- /dev/null +++ b/src/app/components/Search.svelte @@ -0,0 +1,36 @@ + + + + + Search + across all your spaces + + + {#snippet badges(event)} + {@const url = getSpaceUrl(event)} + {#if url} + + + + {/if} + {/snippet} + + diff --git a/src/app/components/SearchBody.svelte b/src/app/components/SearchBody.svelte new file mode 100644 index 00000000..0212a5ec --- /dev/null +++ b/src/app/components/SearchBody.svelte @@ -0,0 +1,167 @@ + + + + + {#if people.length > 0} +

People

+ {#each people as pubkey (pubkey)} + + {/each} + {/if} + {#if loading} + Searching... + {:else if term && results.length === 0 && people.length === 0} + No results found. + {:else} + {#each eventsByAge as [key, events] (key)} +

+ {#if key === "day"} + Last 24 Hours + {:else if key === "week"} + Last 7 Days + {:else} + Older + {/if} +

+ {#each events as event (event.id)} + + {/each} + {/each} + {/if} +
diff --git a/src/app/components/SpaceSearch.svelte b/src/app/components/SpaceSearch.svelte index f5b0c775..f634e210 100644 --- a/src/app/components/SpaceSearch.svelte +++ b/src/app/components/SpaceSearch.svelte @@ -1,28 +1,14 @@ - Search Content + Search on {displayRelayUrl(url)} - - - {#if loading} - Searching... - {:else if eventsByAge.size === 0 && term} - No results found. - {:else} - {#each eventsByAge as [key, events] (key)} -

- {#if key === "day"} - Last 24 Hours - {:else if key === "week"} - Last 7 Days - {:else} - Older - {/if} -

- {#each events as event (event.id)} - {@const h = tagValue(tagSpec("h"), event.tags)} - - {/each} - {/each} - {/if} -
+ + {#snippet badges(event)} + {@const h = tagValue(tagSpec("h"), event.tags)} + {#if h} + + + + {/if} + {/snippet} +
diff --git a/src/app/title.ts b/src/app/title.ts index 41735fd5..2912846d 100644 --- a/src/app/title.ts +++ b/src/app/title.ts @@ -27,7 +27,6 @@ const staticTitles = new Map([ ["/spaces/[relay]/admin", "Space Administration"], ["/chat", "Messages"], ["/join", "Join Space"], - ["/people", "Find People"], ["/settings/about", "About"], ["/settings/profile", "Profile Settings"], ["/settings/content", "Content Settings"], diff --git a/src/routes/people/+page.svelte b/src/routes/people/+page.svelte deleted file mode 100644 index b3b55f60..00000000 --- a/src/routes/people/+page.svelte +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - {#snippet input()} - - {/snippet} - {#snippet content()} -
- {#each $search.searchValues(term).slice(0, limit) as pubkey (pubkey)} - - {/each} -
- {/snippet} -
-
-