@@ -870,30 +846,17 @@ export function TopbarSearch({
)
) : isSearchLoading && searchableResults.length === 0 ? (
) : searchQuery.error instanceof Error && searchableResults.length === 0 ? (
- {currentChannelSearchAction}
-
+
No {scopeChannel ? "messages" : "matches"} for{" "}
{trimmedQuery}
{scopeLabel ? (
@@ -911,7 +874,6 @@ export function TopbarSearch({
data-testid="search-results-list"
role="listbox"
>
- {currentChannelSearchAction}
{renderSearchResultSections(searchResultSections)}
diff --git a/desktop/src/features/search/ui/topbarSearchSuggestions.test.mjs b/desktop/src/features/search/ui/topbarSearchSuggestions.test.mjs
index b22c32a140..06470942b7 100644
--- a/desktop/src/features/search/ui/topbarSearchSuggestions.test.mjs
+++ b/desktop/src/features/search/ui/topbarSearchSuggestions.test.mjs
@@ -1,10 +1,7 @@
import assert from "node:assert/strict";
import test from "node:test";
-import {
- getSearchActionMenuIndex,
- getSuggestedChannels,
-} from "./topbarSearchSuggestions.ts";
+import { getSuggestedChannels } from "./topbarSearchSuggestions.ts";
function channel(id, options = {}) {
return {
@@ -72,30 +69,3 @@ test("archived and unjoined non-DM conversations are omitted", () => {
["dm"],
);
});
-
-test("the scope action follows unread rows while suggestions are visible", () => {
- assert.equal(
- getSearchActionMenuIndex({
- hasScopeAction: true,
- isShowingSuggestions: true,
- unreadResultCount: 2,
- }),
- 2,
- );
- assert.equal(
- getSearchActionMenuIndex({
- hasScopeAction: true,
- isShowingSuggestions: true,
- unreadResultCount: 0,
- }),
- 0,
- );
- assert.equal(
- getSearchActionMenuIndex({
- hasScopeAction: false,
- isShowingSuggestions: true,
- unreadResultCount: 2,
- }),
- null,
- );
-});
diff --git a/desktop/src/features/search/ui/topbarSearchSuggestions.ts b/desktop/src/features/search/ui/topbarSearchSuggestions.ts
index 61cb1a5b5f..329df44237 100644
--- a/desktop/src/features/search/ui/topbarSearchSuggestions.ts
+++ b/desktop/src/features/search/ui/topbarSearchSuggestions.ts
@@ -64,16 +64,3 @@ export function getSuggestedSearchResults(
unreadResults: unreadChannels.map(toResult),
};
}
-
-export function getSearchActionMenuIndex({
- hasScopeAction,
- isShowingSuggestions,
- unreadResultCount,
-}: {
- hasScopeAction: boolean;
- isShowingSuggestions: boolean;
- unreadResultCount: number;
-}) {
- if (!hasScopeAction) return null;
- return isShowingSuggestions && unreadResultCount > 0 ? unreadResultCount : 0;
-}
diff --git a/desktop/src/features/search/ui/useSearchMenuKeyboardNavigation.ts b/desktop/src/features/search/ui/useSearchMenuKeyboardNavigation.ts
index b29258fece..d729b1b16d 100644
--- a/desktop/src/features/search/ui/useSearchMenuKeyboardNavigation.ts
+++ b/desktop/src/features/search/ui/useSearchMenuKeyboardNavigation.ts
@@ -3,9 +3,7 @@ import * as React from "react";
import type { SearchResult } from "@/features/search/ui/SearchResultItem";
export function useSearchMenuKeyboardNavigation({
- actionMenuIndex,
activeResults,
- onActivateAction,
onOpenResult,
onRemoveScope,
query,
@@ -13,9 +11,7 @@ export function useSearchMenuKeyboardNavigation({
selectedMenuIndex,
setSelectedMenuIndex,
}: {
- actionMenuIndex: number | null;
activeResults: SearchResult[];
- onActivateAction: () => void;
onOpenResult: (result: SearchResult) => void;
onRemoveScope: () => void;
query: string;
@@ -23,8 +19,7 @@ export function useSearchMenuKeyboardNavigation({
selectedMenuIndex: number;
setSelectedMenuIndex: React.Dispatch
>;
}) {
- const selectableCount =
- activeResults.length + (actionMenuIndex === null ? 0 : 1);
+ const selectableCount = activeResults.length;
React.useEffect(() => {
setSelectedMenuIndex((current) => {
@@ -64,22 +59,12 @@ export function useSearchMenuKeyboardNavigation({
if (event.key === "Enter" && !event.nativeEvent.isComposing) {
event.preventDefault();
- if (actionMenuIndex === selectedMenuIndex) {
- onActivateAction();
- return;
- }
- const resultIndex =
- actionMenuIndex !== null && selectedMenuIndex > actionMenuIndex
- ? selectedMenuIndex - 1
- : selectedMenuIndex;
- const result = activeResults[resultIndex];
+ const result = activeResults[selectedMenuIndex];
if (result) onOpenResult(result);
}
},
[
- actionMenuIndex,
activeResults,
- onActivateAction,
onOpenResult,
onRemoveScope,
query.length,
diff --git a/desktop/src/features/search/useSearchResults.ts b/desktop/src/features/search/useSearchResults.ts
index a8da5a57f9..b1e1cb1a8f 100644
--- a/desktop/src/features/search/useSearchResults.ts
+++ b/desktop/src/features/search/useSearchResults.ts
@@ -104,12 +104,14 @@ export function useSearchResults({
channels: memberChannels,
enabled,
limit = 12,
+ prioritizedChannelId,
scopeChannelId,
}: {
channelLabels?: Record;
channels: Channel[];
enabled: boolean;
limit?: number;
+ prioritizedChannelId?: string | null;
scopeChannelId?: string | null;
}) {
const [query, setQuery] = React.useState("");
@@ -255,13 +257,40 @@ export function useSearchResults({
unresolvedOperator: hasUnresolvedOperator,
minimumQueryLength,
});
+ const prioritizedChannelSearchQuery = useSearchMessagesQuery(ftsQuery, {
+ enabled:
+ enabled &&
+ !scopeChannelId &&
+ Boolean(prioritizedChannelId) &&
+ channelResolution.status === "none" &&
+ !hasUnresolvedOperator &&
+ !waitingOnFromResolution &&
+ ftsQuery.length >= minimumQueryLength,
+ limit,
+ channelId: prioritizedChannelId ?? undefined,
+ authors:
+ authorResolution.status === "resolved"
+ ? [authorResolution.value]
+ : undefined,
+ since: parsedQuery.since,
+ until: parsedQuery.until,
+ unresolvedOperator: hasUnresolvedOperator,
+ minimumQueryLength,
+ });
const messageResults = React.useMemo(() => {
if (hasUnresolvedOperator) {
return [];
}
- return dedupeSearchHits(searchQuery.data?.hits ?? []);
- }, [hasUnresolvedOperator, searchQuery.data?.hits]);
+ return dedupeSearchHits([
+ ...(prioritizedChannelSearchQuery.data?.hits ?? []),
+ ...(searchQuery.data?.hits ?? []),
+ ]);
+ }, [
+ hasUnresolvedOperator,
+ prioritizedChannelSearchQuery.data?.hits,
+ searchQuery.data?.hits,
+ ]);
const channelResults = React.useMemo(() => {
if (scopeChannelId || ftsQuery.length < MIN_SEARCH_QUERY_LENGTH) {
return [];
@@ -498,6 +527,7 @@ export function useSearchResults({
isWaitingOnFromResolution: waitingOnFromResolution,
messageResults,
query,
+ prioritizedChannelSearchQuery,
resultProfiles: resultProfilesQuery.data?.profiles,
results,
searchQuery,
diff --git a/desktop/tests/e2e/smoke.spec.ts b/desktop/tests/e2e/smoke.spec.ts
index 035ffc8be5..a9d4b6010c 100644
--- a/desktop/tests/e2e/smoke.spec.ts
+++ b/desktop/tests/e2e/smoke.spec.ts
@@ -441,9 +441,9 @@ test("Cmd+K bubbles unread conversations to the top and Enter opens the first",
page.getByTestId(`search-unread-count-${directMessageId}`),
).toHaveText("2");
await expect(firstUnread.getByText("Enter", { exact: true })).toBeVisible();
- await expect(
- page.getByTestId("search-current-channel-control"),
- ).toHaveAttribute("data-search-result-index", "1");
+ await expect(page.getByTestId("search-current-channel-control")).toHaveCount(
+ 0,
+ );
await page.keyboard.press("Enter");
await expect(page).toHaveURL(
@@ -452,146 +452,29 @@ test("Cmd+K bubbles unread conversations to the top and Enter opens the first",
await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler");
});
-test("global search offers an optional current-channel scope", async ({
+test("global search prioritizes messages from the current channel", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
- await expect(page).toHaveURL(
- /#\/channels\/9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50$/,
- );
await focusSidebarSearchWithShortcut(page);
- const scopeControl = page.getByTestId("search-current-channel-control");
const input = page.getByTestId("search-dialog-input");
- await expect
- .poll(() =>
- scopeControl
- .getByTestId("search-current-scope-label")
- .evaluate((element) => element.textContent),
- )
- .toBe("Search in #general");
- await expect(scopeControl).toContainText("Search in");
- await expect(scopeControl).toContainText("#general");
- await expect(scopeControl).toContainText("Search messages in this channel");
- await expect(page.getByTestId("search-dialog-input-row")).toHaveCSS(
- "border-bottom-width",
- "1px",
+ await expect(page.getByTestId("search-current-channel-control")).toHaveCount(
+ 0,
);
- await expect(scopeControl.locator("..")).toHaveCSS(
- "border-bottom-width",
- "0px",
- );
- await expect(scopeControl.locator("..")).toHaveCSS("padding-top", "14px");
- await expect(scopeControl.locator("..")).toHaveCSS("padding-bottom", "14px");
- const [controlBox, dialogBox] = await Promise.all([
- scopeControl.boundingBox(),
- page.getByTestId("search-results").boundingBox(),
- ]);
- expect(controlBox).not.toBeNull();
- expect(dialogBox).not.toBeNull();
- expect(controlBox?.width ?? 0).toBeGreaterThan((dialogBox?.width ?? 0) * 0.9);
- expect(controlBox?.width ?? 0).toBeLessThan(dialogBox?.width ?? 0);
- await expect(scopeControl).toHaveAttribute("aria-selected", "true");
- const firstRecentResult = page.locator(".search-result-row").first();
- await input.press("ArrowDown");
- await expect(firstRecentResult).toHaveAttribute("aria-selected", "true");
- await input.press("ArrowUp");
- await expect(scopeControl).toHaveAttribute("aria-selected", "true");
- await input.press("Enter");
-
- const scopeChip = page.getByTestId("search-channel-scope-chip");
- await expect(scopeChip).toHaveText(/#general/);
- await expect(input).toBeFocused();
- await input.fill("w");
- const relevantHeader = page.getByText("Most relevant", { exact: true });
- const firstScopedResult = page
- .locator('[data-search-section="messages"] .search-result-row')
- .first();
- await expect(page.getByText("Welcome to general")).toBeVisible();
- await expect(page.getByText(/Searching messages in/)).toHaveCount(0);
- await expect(relevantHeader).toBeVisible();
- await expect(firstScopedResult).toBeVisible();
- const contentStart = (element: HTMLElement) => {
- const styles = window.getComputedStyle(element);
- return (
- element.getBoundingClientRect().left +
- Number.parseFloat(styles.paddingLeft)
- );
- };
- const [inputStart, headerStart, resultStart] = await Promise.all([
- page.getByTestId("search-dialog-input-row").evaluate(contentStart),
- relevantHeader.evaluate(contentStart),
- firstScopedResult.evaluate(contentStart),
- ]);
- expect(Math.abs(inputStart - headerStart)).toBeLessThanOrEqual(1);
- expect(Math.abs(inputStart - resultStart)).toBeLessThanOrEqual(1);
-
- await input.fill("x");
- await expect(page.getByTestId("search-results")).toContainText(
- "No messages for x in #general.",
- );
-
- await scopeChip.click();
- await expect(scopeChip).toHaveCount(0);
- await expect(input).toBeFocused();
- await input.fill("shipped");
- await expect(page.getByTestId("search-results")).toContainText(
- "Engineering shipped the desktop build.",
- );
-});
-
-test("global search offers a conversation-specific scope in direct messages", async ({
- page,
-}) => {
- const directMessageId = "f48efb06-0c93-5025-aac9-2e646bb6bfa8";
-
- await page.goto("/");
- await page.getByTestId("channel-alice-tyler").click();
- await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler");
- await expect(page).toHaveURL(
- new RegExp(`#\\/channels\\/${directMessageId}$`),
- );
-
- await focusSidebarSearchWithShortcut(page);
-
- const scopeControl = page.getByTestId("search-current-channel-control");
- await expect
- .poll(() =>
- scopeControl
- .getByTestId("search-current-scope-label")
- .evaluate((element) => element.textContent),
- )
- .toBe("Search conversation with alice");
- await expect(scopeControl).toContainText("Search conversation with alice");
- await expect(scopeControl).toContainText(
- "Search messages in this conversation.",
- );
- await expect(scopeControl).not.toContainText("channel");
- await expect(page.getByTestId("search-dialog-input-row")).toHaveCSS(
- "border-bottom-width",
- "1px",
- );
- await expect(scopeControl.locator("..")).toHaveCSS(
- "border-bottom-width",
- "0px",
- );
- await expect(scopeControl.locator("..")).toHaveCSS("padding-top", "14px");
- await expect(scopeControl.locator("..")).toHaveCSS("padding-bottom", "14px");
-
- await scopeControl.click();
-
- const scopeChip = page.getByTestId("search-channel-scope-chip");
- const input = page.getByTestId("search-dialog-input");
- await expect(scopeChip).toHaveText(/^alice$/);
- await expect(scopeChip).not.toContainText("#");
- await expect(input).toBeFocused();
- await input.fill("a");
- await expect(page.getByTestId("search-results")).toContainText(
- "No messages for a in alice.",
+ await input.fill("welcome");
+
+ const currentChannelSection = page.locator(
+ '[data-search-section="current-channel-messages"]',
);
+ await expect(currentChannelSection).toContainText("In this conversation");
+ await expect(currentChannelSection).toContainText("Welcome to #general");
+ await expect(
+ currentChannelSection.locator(".search-result-row").first(),
+ ).toHaveAttribute("aria-selected", "true");
await expect
.poll(() =>
page.evaluate(() => {
@@ -600,28 +483,27 @@ test("global search offers a conversation-specific scope in direct messages", as
window as Window & {
__BUZZ_E2E_COMMAND_LOG__?: Array<{
command: string;
- payload: unknown;
+ payload: { channelId?: string; q?: string };
}>;
}
).__BUZZ_E2E_COMMAND_LOG__ ?? [];
- return calls.findLast((entry) => entry.command === "search_messages")
- ?.payload;
+ return calls
+ .filter(
+ (entry) =>
+ entry.command === "search_messages" &&
+ entry.payload.q === "welcome",
+ )
+ .map((entry) => entry.payload.channelId ?? null);
}),
)
- .toMatchObject({
- channelId: directMessageId,
- q: "a",
- });
+ .toEqual(
+ expect.arrayContaining([null, "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50"]),
+ );
- await page.keyboard.press("Escape");
- await page.keyboard.press("ControlOrMeta+f");
- await expect(page.getByTestId("search-results")).toBeVisible();
- await expect(page.getByTestId("search-channel-scope-chip")).toHaveText(
- /^alice$/,
- );
- await expect(page.getByTestId("search-current-channel-control")).toHaveCount(
- 0,
+ await input.fill("shipped");
+ await expect(page.getByTestId("search-results")).toContainText(
+ "Engineering shipped the desktop build.",
);
});
@@ -710,15 +592,14 @@ test("global search exposes a larger scrollable result window", async ({
await page.getByTestId("search-dialog-input").fill("deep history message");
const resultRows = page.locator(
- '[data-search-section="messages"] .search-result-row',
+ '[data-search-section="current-channel-messages"] .search-result-row',
);
await expect(resultRows).toHaveCount(40);
const resultList = page.getByTestId("search-results-list");
await expect(resultList).toBeVisible();
- const scopeControl = page.getByTestId("search-current-channel-control");
- await expect(
- resultList.getByTestId("search-current-channel-control"),
- ).toBeVisible();
+ await expect(page.getByTestId("search-current-channel-control")).toHaveCount(
+ 0,
+ );
const dimensions = await resultList.evaluate((element) => ({
clientHeight: element.clientHeight,
scrollHeight: element.scrollHeight,
@@ -727,12 +608,11 @@ test("global search exposes a larger scrollable result window", async ({
await resultList.evaluate((element) => {
element.scrollTop = element.scrollHeight;
});
- await expect(scopeControl).not.toBeInViewport();
await resultList.evaluate((element) => {
element.scrollTop = 0;
});
- for (let index = 0; index < 14; index += 1) {
+ for (let index = 0; index < 13; index += 1) {
await page.keyboard.press("ArrowDown");
}
await expect(resultRows.nth(13)).toHaveAttribute("aria-selected", "true");