{(() => {
let resultIndex = 0;
@@ -840,6 +843,7 @@ export function TopbarSearch({
)}
) : null}
+ {currentChannelSearchAction}
{suggestedResults.length > 0 ? (
diff --git a/desktop/src/features/search/ui/topbarSearchSuggestions.test.mjs b/desktop/src/features/search/ui/topbarSearchSuggestions.test.mjs
index c6a8090605..b22c32a140 100644
--- a/desktop/src/features/search/ui/topbarSearchSuggestions.test.mjs
+++ b/desktop/src/features/search/ui/topbarSearchSuggestions.test.mjs
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
import test from "node:test";
import {
- getInitialSearchSelection,
+ getSearchActionMenuIndex,
getSuggestedChannels,
} from "./topbarSearchSuggestions.ts";
@@ -73,26 +73,29 @@ test("archived and unjoined non-DM conversations are omitted", () => {
);
});
-test("the first unread row is selected even when a scope action leads the menu", () => {
+test("the scope action follows unread rows while suggestions are visible", () => {
assert.equal(
- getInitialSearchSelection({
- hasLeadingAction: true,
- hasUnreadChannels: true,
+ getSearchActionMenuIndex({
+ hasScopeAction: true,
+ isShowingSuggestions: true,
+ unreadResultCount: 2,
}),
- 1,
+ 2,
);
assert.equal(
- getInitialSearchSelection({
- hasLeadingAction: true,
- hasUnreadChannels: false,
+ getSearchActionMenuIndex({
+ hasScopeAction: true,
+ isShowingSuggestions: true,
+ unreadResultCount: 0,
}),
0,
);
assert.equal(
- getInitialSearchSelection({
- hasLeadingAction: false,
- hasUnreadChannels: true,
+ getSearchActionMenuIndex({
+ hasScopeAction: false,
+ isShowingSuggestions: true,
+ unreadResultCount: 2,
}),
- 0,
+ null,
);
});
diff --git a/desktop/src/features/search/ui/topbarSearchSuggestions.ts b/desktop/src/features/search/ui/topbarSearchSuggestions.ts
index 733f4e9c32..61cb1a5b5f 100644
--- a/desktop/src/features/search/ui/topbarSearchSuggestions.ts
+++ b/desktop/src/features/search/ui/topbarSearchSuggestions.ts
@@ -2,7 +2,7 @@ import type { Channel } from "@/shared/api/types";
export const MAX_RECENT_SEARCH_SUGGESTIONS = 4;
-function getChannelActivityTime(channel: Channel) {
+export function getChannelActivityTime(channel: Channel) {
if (!channel.lastMessageAt) return 0;
const timestamp = Date.parse(channel.lastMessageAt);
@@ -65,12 +65,15 @@ export function getSuggestedSearchResults(
};
}
-export function getInitialSearchSelection({
- hasLeadingAction,
- hasUnreadChannels,
+export function getSearchActionMenuIndex({
+ hasScopeAction,
+ isShowingSuggestions,
+ unreadResultCount,
}: {
- hasLeadingAction: boolean;
- hasUnreadChannels: boolean;
+ hasScopeAction: boolean;
+ isShowingSuggestions: boolean;
+ unreadResultCount: number;
}) {
- return hasUnreadChannels && hasLeadingAction ? 1 : 0;
+ 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 66832ac7a9..b29258fece 100644
--- a/desktop/src/features/search/ui/useSearchMenuKeyboardNavigation.ts
+++ b/desktop/src/features/search/ui/useSearchMenuKeyboardNavigation.ts
@@ -3,9 +3,9 @@ import * as React from "react";
import type { SearchResult } from "@/features/search/ui/SearchResultItem";
export function useSearchMenuKeyboardNavigation({
+ actionMenuIndex,
activeResults,
- hasLeadingAction,
- onActivateLeadingAction,
+ onActivateAction,
onOpenResult,
onRemoveScope,
query,
@@ -13,9 +13,9 @@ export function useSearchMenuKeyboardNavigation({
selectedMenuIndex,
setSelectedMenuIndex,
}: {
+ actionMenuIndex: number | null;
activeResults: SearchResult[];
- hasLeadingAction: boolean;
- onActivateLeadingAction: () => void;
+ onActivateAction: () => void;
onOpenResult: (result: SearchResult) => void;
onRemoveScope: () => void;
query: string;
@@ -23,7 +23,8 @@ export function useSearchMenuKeyboardNavigation({
selectedMenuIndex: number;
setSelectedMenuIndex: React.Dispatch>;
}) {
- const selectableCount = activeResults.length + (hasLeadingAction ? 1 : 0);
+ const selectableCount =
+ activeResults.length + (actionMenuIndex === null ? 0 : 1);
React.useEffect(() => {
setSelectedMenuIndex((current) => {
@@ -63,19 +64,22 @@ export function useSearchMenuKeyboardNavigation({
if (event.key === "Enter" && !event.nativeEvent.isComposing) {
event.preventDefault();
- if (hasLeadingAction && selectedMenuIndex === 0) {
- onActivateLeadingAction();
+ if (actionMenuIndex === selectedMenuIndex) {
+ onActivateAction();
return;
}
- const result =
- activeResults[selectedMenuIndex - (hasLeadingAction ? 1 : 0)];
+ const resultIndex =
+ actionMenuIndex !== null && selectedMenuIndex > actionMenuIndex
+ ? selectedMenuIndex - 1
+ : selectedMenuIndex;
+ const result = activeResults[resultIndex];
if (result) onOpenResult(result);
}
},
[
+ actionMenuIndex,
activeResults,
- hasLeadingAction,
- onActivateLeadingAction,
+ onActivateAction,
onOpenResult,
onRemoveScope,
query.length,
diff --git a/desktop/src/features/sidebar/ui/AppSidebar.tsx b/desktop/src/features/sidebar/ui/AppSidebar.tsx
index 5937b6106b..ee6d886a51 100644
--- a/desktop/src/features/sidebar/ui/AppSidebar.tsx
+++ b/desktop/src/features/sidebar/ui/AppSidebar.tsx
@@ -477,6 +477,7 @@ export function AppSidebar({
searchFocusRequest={searchFocusRequests[0]}
scopeSearchFocusRequest={searchFocusRequests[1]}
suggestionChannels={channels}
+ unreadChannelCounts={unreadChannelCounts}
unreadChannelIds={unreadChannelIds}
/>
diff --git a/desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx b/desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx
index f793b61dcc..4a553ae40e 100644
--- a/desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx
+++ b/desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx
@@ -36,6 +36,7 @@ type AppSidebarPinnedHeaderProps = {
searchFocusRequest: number;
scopeSearchFocusRequest: number;
suggestionChannels: Channel[];
+ unreadChannelCounts: ReadonlyMap;
unreadChannelIds: ReadonlySet;
};
@@ -64,6 +65,7 @@ export function AppSidebarPinnedHeader({
searchFocusRequest,
scopeSearchFocusRequest,
suggestionChannels,
+ unreadChannelCounts,
unreadChannelIds,
}: AppSidebarPinnedHeaderProps) {
return (
@@ -85,6 +87,7 @@ export function AppSidebarPinnedHeader({
onCreateChannel={onCreateChannel}
scopeFocusRequest={scopeSearchFocusRequest}
suggestionChannels={suggestionChannels}
+ unreadChannelCounts={unreadChannelCounts}
unreadChannelIds={unreadChannelIds}
/>
diff --git a/desktop/tests/e2e/smoke.spec.ts b/desktop/tests/e2e/smoke.spec.ts
index dedc402cf8..035ffc8be5 100644
--- a/desktop/tests/e2e/smoke.spec.ts
+++ b/desktop/tests/e2e/smoke.spec.ts
@@ -404,39 +404,52 @@ test("Cmd+K bubbles unread conversations to the top and Enter opens the first",
page,
}) => {
await page.goto("/");
- await page.getByTestId("channel-random").click();
- await waitForMockLiveSubscription(page, "random");
+ await page.getByTestId("channel-alice-tyler").click();
+ await waitForMockLiveSubscription(page, "alice-tyler");
await page.getByTestId("channel-general").click();
await page.evaluate((pubkey) => {
- window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
- channelName: "random",
- content: "Unread shortcut from Cmd+K",
- kind: 40002,
- pubkey,
- });
+ for (const content of [
+ "First unread DM shortcut from Cmd+K",
+ "Second unread DM shortcut from Cmd+K",
+ ]) {
+ window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
+ channelName: "alice-tyler",
+ content,
+ kind: 40002,
+ pubkey,
+ });
+ }
}, TEST_IDENTITIES.alice.pubkey);
- await expect(page.getByTestId("channel-random")).toHaveCSS(
+ await expect(page.getByTestId("channel-alice-tyler")).toHaveCSS(
"font-weight",
"700",
);
await focusSidebarSearchWithShortcut(page);
+ const directMessageId = "f48efb06-0c93-5025-aac9-2e646bb6bfa8";
const unreadSection = page.locator('[data-search-section="unread"]');
const firstUnread = unreadSection.locator(".search-result-row").first();
await expect(unreadSection).toContainText("Unread");
await expect(firstUnread).toHaveAttribute(
"data-testid",
- "search-result-channel-9dae0116-799b-5071-a0a8-fdd30a91a35d",
+ `search-result-channel-${directMessageId}`,
);
await expect(firstUnread).toHaveAttribute("aria-selected", "true");
+ await expect(
+ 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 page.keyboard.press("Enter");
await expect(page).toHaveURL(
- /#\/channels\/9dae0116-799b-5071-a0a8-fdd30a91a35d$/,
+ new RegExp(`#\\/channels\\/${directMessageId}$`),
);
- await expect(page.getByTestId("chat-title")).toHaveText("random");
+ await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler");
});
test("global search offers an optional current-channel scope", async ({