fix(desktop): restore channel browsing and add recent sorting (#2065)

Signed-off-by: Pavlenex <36959754+pavlenex@users.noreply.github.com>
This commit is contained in:
Pavlenex
2026-07-18 10:10:02 -07:00
committed by GitHub
parent 66a0f7bcb9
commit 2cb7753fa2
6 changed files with 90 additions and 27 deletions
@@ -10,6 +10,10 @@ import {
import type { Channel } from "@/shared/api/types";
import { scoreChannelMatch } from "@/features/channels/lib/channelSearchScore";
import {
type ChannelSortMode,
sortChannelsForSidebar,
} from "@/features/sidebar/lib/channelSortPreference";
import { ListSortDescending } from "@/shared/ui/icons";
import {
Dialog,
@@ -45,10 +49,11 @@ import {
} from "@/features/sidebar/ui/CreateChannelFormFields";
type BrowserTab = "all" | "joined" | "archived";
type ChannelSort = "alphabetical" | "members";
type ChannelSort = ChannelSortMode | "members";
const CHANNEL_SORT_OPTIONS: { label: string; value: ChannelSort }[] = [
{ label: "Alphabetical", value: "alphabetical" },
{ label: "Alphabetical", value: "alpha" },
{ label: "Recent", value: "recent" },
{ label: "Most members", value: "members" },
];
@@ -102,7 +107,7 @@ export function ChannelBrowserDialog({
}: ChannelBrowserDialogProps) {
const [query, setQuery] = React.useState("");
const [activeTab, setActiveTab] = React.useState<BrowserTab>("all");
const [sort, setSort] = React.useState<ChannelSort>("alphabetical");
const [sort, setSort] = React.useState<ChannelSort>("alpha");
const [selectedIndex, setSelectedIndex] = React.useState<number | null>(null);
const [joiningChannelId, setJoiningChannelId] = React.useState<string | null>(
null,
@@ -132,7 +137,7 @@ export function ChannelBrowserDialog({
const isForumMode = channelTypeFilter === "forum";
const canCreate = Boolean(onCreateChannel);
const createKind = isForumMode ? "forum" : "stream";
const browseTitle = isForumMode ? "Add a forum" : "Add a channel";
const browseTitle = isForumMode ? "Add a forum" : "Browse channels";
const searchPlaceholder = canCreate
? isForumMode
? "Search or create a forum"
@@ -206,23 +211,30 @@ export function ChannelBrowserDialog({
const isSearching = deferredQuery.length > 0;
const orderedVisibleChannels = React.useMemo(() => {
return [...visibleChannels].sort((a, b) => {
// While searching, best match wins so the channel you meant floats to
// the top; ties fall back to the user's chosen sort below.
if (isSearching) {
const scoreA = matchScoreById.get(a.id) ?? Number.POSITIVE_INFINITY;
const scoreB = matchScoreById.get(b.id) ?? Number.POSITIVE_INFINITY;
if (scoreA !== scoreB) return scoreA - scoreB;
}
const sorted =
sort === "members"
? [...visibleChannels].sort(
(a, b) =>
b.memberCount - a.memberCount ||
a.name.localeCompare(b.name, undefined, {
sensitivity: "base",
}),
)
: sortChannelsForSidebar(visibleChannels, sort);
if (sort === "members" && b.memberCount !== a.memberCount) {
return b.memberCount - a.memberCount;
}
if (!isSearching) return sorted;
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
});
return sorted.sort(
(a, b) =>
(matchScoreById.get(a.id) ?? Number.POSITIVE_INFINITY) -
(matchScoreById.get(b.id) ?? Number.POSITIVE_INFINITY),
);
}, [isSearching, matchScoreById, sort, visibleChannels]);
const selectedSortLabel =
CHANNEL_SORT_OPTIONS.find((option) => option.value === sort)?.label ??
"Alphabetical";
const allTabLabel = isForumMode ? "All forums" : "All channels";
// Whether an exact name match already exists — if so we don't offer to
@@ -320,7 +332,7 @@ export function ChannelBrowserDialog({
if (!open) {
setQuery("");
setActiveTab("all");
setSort("alphabetical");
setSort("alpha");
setSelectedIndex(null);
setJoiningChannelId(null);
setMode("browse");
@@ -500,11 +512,7 @@ export function ChannelBrowserDialog({
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label={`Sort ${entityLabel}s: ${
sort === "alphabetical"
? "Alphabetical"
: "Most members"
}`}
aria-label={`Sort ${entityLabel}s: ${selectedSortLabel}`}
data-testid="channel-browser-sort"
size="icon-xs"
type="button"
@@ -10,6 +10,7 @@ import {
type LucideIcon,
} from "lucide-react";
import { HashSearch } from "@/shared/ui/icons";
import {
resolveUserLabel,
resolveUserSecondaryLabel,
@@ -24,7 +25,7 @@ export type SearchResult =
kind: "action";
action: {
description?: string;
id: "create-agent" | "create-channel";
id: "browse-channels" | "create-agent" | "create-channel";
title: string;
};
}
@@ -69,6 +70,9 @@ export function resultIcon(
channelLookup: ReadonlyMap<string, Channel>,
) {
if (result.kind === "action") {
if (result.action.id === "browse-channels") {
return HashSearch;
}
return result.action.id === "create-agent" ? Bot : Plus;
}
@@ -34,6 +34,7 @@ type TopbarSearchProps = {
onOpenChannel: (channelId: string) => void;
onOpenResult: (hit: SearchHit) => void;
onOpenUser?: (user: UserSearchResult) => void | Promise<void>;
onBrowseChannels?: () => void | Promise<void>;
onCreateAgent?: () => void | Promise<void>;
onCreateChannel?: () => void | Promise<void>;
suggestionChannels?: Channel[];
@@ -393,6 +394,7 @@ export function TopbarSearch({
onOpenChannel,
onOpenResult,
onOpenUser,
onBrowseChannels,
onCreateAgent,
onCreateChannel,
suggestionChannels,
@@ -426,6 +428,16 @@ export function TopbarSearch({
const suggestionActionResults = React.useMemo(() => {
const actions: SearchResult[] = [];
if (onBrowseChannels) {
actions.push({
kind: "action",
action: {
id: "browse-channels",
title: "Browse channels",
},
});
}
if (onCreateChannel) {
actions.push({
kind: "action",
@@ -447,7 +459,7 @@ export function TopbarSearch({
}
return actions;
}, [onCreateAgent, onCreateChannel]);
}, [onBrowseChannels, onCreateAgent, onCreateChannel]);
const suggestionResults = React.useMemo(
() => [...suggestedResults, ...suggestionActionResults],
[suggestedResults, suggestionActionResults],
@@ -513,7 +525,11 @@ export function TopbarSearch({
if (result.kind === "action") {
setSelectedMenuIndex(0);
if (result.action.id === "create-channel") {
if (result.action.id === "browse-channels") {
openAfterExit(() => {
void onBrowseChannels?.();
});
} else if (result.action.id === "create-channel") {
openAfterExit(() => {
void onCreateChannel?.();
});
@@ -528,6 +544,7 @@ export function TopbarSearch({
onOpenResult(result.hit);
},
[
onBrowseChannels,
onCreateAgent,
onCreateChannel,
onOpenChannel,
@@ -554,6 +554,7 @@ export function AppSidebar({
<AppSidebarPinnedHeader
channelLabels={dmChannelLabels}
currentPubkey={currentPubkey}
onBrowseChannels={onBrowseChannels}
onCreateAgent={onCreateAgent}
onCreateChannel={handleOpenCreateChannel}
onOpenDm={onOpenDm}
@@ -720,7 +721,7 @@ export function AppSidebar({
}
actionsTestId="section-actions-channels"
listTestId="stream-list"
quickCreateLabel="Add channel"
quickCreateLabel="Browse channels"
onQuickCreateClick={onBrowseChannels}
showQuickCreate
onMarkAllRead={onMarkAllChannelsRead}
@@ -24,6 +24,7 @@ type SidebarSelectedView =
type AppSidebarPinnedHeaderProps = {
channelLabels: Record<string, string>;
currentPubkey?: string;
onBrowseChannels?: () => void;
onCreateAgent: () => void;
onCreateChannel: () => void;
onOpenDm: (input: { pubkeys: string[] }) => Promise<void>;
@@ -47,6 +48,7 @@ type AppSidebarPrimaryMenuProps = {
export function AppSidebarPinnedHeader({
channelLabels,
currentPubkey,
onBrowseChannels,
onCreateAgent,
onCreateChannel,
onOpenDm,
@@ -69,6 +71,7 @@ export function AppSidebarPinnedHeader({
onOpenChannel={onSelectChannel}
onOpenResult={onOpenSearchResult}
onOpenUser={(user) => onOpenDm({ pubkeys: [user.pubkey] })}
onBrowseChannels={onBrowseChannels}
onCreateAgent={onCreateAgent}
onCreateChannel={onCreateChannel}
suggestionChannels={suggestionChannels}
+30
View File
@@ -57,6 +57,9 @@ test("channel browser shows channels not yet joined", async ({ page }) => {
await openChannelBrowser(page);
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
await expect(
page.getByRole("heading", { name: "Browse channels" }),
).toBeVisible();
// "design" and "sales" are open channels the mock user is NOT a member of
await expect(page.getByTestId("browse-channel-design")).toBeVisible();
@@ -108,6 +111,33 @@ test("channel browser sorts alphabetically or by member count", async ({
);
});
test("channel browser sorts by recent activity", async ({ page }) => {
await page.goto("/");
await openChannelBrowser(page);
const rows = page.locator('[data-testid^="browse-channel-"]');
await page.getByTestId("channel-browser-sort").click();
await page.getByTestId("channel-browser-sort-recent").click();
await expect(rows).toHaveText([
/#all-replies/,
/#deep-history/,
/#general/,
/#agents/,
/#sales/,
/#engineering/,
/#design/,
/#random/,
/#secret-projects/,
/#welcome-everyone/,
]);
await expect(page.getByTestId("channel-browser-sort")).toHaveAttribute(
"aria-label",
"Sort channels: Recent",
);
});
test("channel browser search filters by name", async ({ page }) => {
await page.goto("/");