mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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:
@@ -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}
|
||||
|
||||
@@ -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("/");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user