Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 33 additions & 25 deletions desktop/src/features/channels/ui/ChannelBrowserDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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" },
];

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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;
}

if (sort === "members" && b.memberCount !== a.memberCount) {
return b.memberCount - a.memberCount;
}

return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
});
const sorted =
sort === "members"
? [...visibleChannels].sort(
(a, b) =>
b.memberCount - a.memberCount ||
a.name.localeCompare(b.name, undefined, {
sensitivity: "base",
}),
)
: sortChannelsForSidebar(visibleChannels, sort);

if (!isSearching) return sorted;

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
Expand Down Expand Up @@ -320,7 +332,7 @@ export function ChannelBrowserDialog({
if (!open) {
setQuery("");
setActiveTab("all");
setSort("alphabetical");
setSort("alpha");
setSelectedIndex(null);
setJoiningChannelId(null);
setMode("browse");
Expand Down Expand Up @@ -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"
Expand Down
6 changes: 5 additions & 1 deletion desktop/src/features/search/ui/SearchResultItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
type LucideIcon,
} from "lucide-react";

import { HashSearch } from "@/shared/ui/icons";
import {
resolveUserLabel,
resolveUserSecondaryLabel,
Expand All @@ -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;
};
}
Expand Down Expand Up @@ -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;
}

Expand Down
21 changes: 19 additions & 2 deletions desktop/src/features/search/ui/TopbarSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -393,6 +394,7 @@ export function TopbarSearch({
onOpenChannel,
onOpenResult,
onOpenUser,
onBrowseChannels,
onCreateAgent,
onCreateChannel,
suggestionChannels,
Expand Down Expand Up @@ -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",
Expand All @@ -447,7 +459,7 @@ export function TopbarSearch({
}

return actions;
}, [onCreateAgent, onCreateChannel]);
}, [onBrowseChannels, onCreateAgent, onCreateChannel]);
const suggestionResults = React.useMemo(
() => [...suggestedResults, ...suggestionActionResults],
[suggestedResults, suggestionActionResults],
Expand Down Expand Up @@ -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?.();
});
Expand All @@ -528,6 +544,7 @@ export function TopbarSearch({
onOpenResult(result.hit);
},
[
onBrowseChannels,
onCreateAgent,
onCreateChannel,
onOpenChannel,
Expand Down
3 changes: 2 additions & 1 deletion desktop/src/features/sidebar/ui/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,7 @@ export function AppSidebar({
<AppSidebarPinnedHeader
channelLabels={dmChannelLabels}
currentPubkey={currentPubkey}
onBrowseChannels={onBrowseChannels}
onCreateAgent={onCreateAgent}
onCreateChannel={handleOpenCreateChannel}
onOpenDm={onOpenDm}
Expand Down Expand Up @@ -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}
Expand Down
3 changes: 3 additions & 0 deletions desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand All @@ -47,6 +48,7 @@ type AppSidebarPrimaryMenuProps = {
export function AppSidebarPinnedHeader({
channelLabels,
currentPubkey,
onBrowseChannels,
onCreateAgent,
onCreateChannel,
onOpenDm,
Expand All @@ -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}
Expand Down
30 changes: 30 additions & 0 deletions desktop/tests/e2e/channel-browser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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("/");

Expand Down
Loading