diff --git a/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx b/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx index 6d56e4dbfcc..f9638ecd2b0 100644 --- a/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx +++ b/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx @@ -33,6 +33,8 @@ import { MODAL_SEARCH_SHELL_CLASS, } from "@/shared/ui/modalSearchStyles"; import { Tabs, TabsList, TabsTrigger } from "@/shared/ui/tabs"; + +import { ChannelRowIcon } from "@/features/channels/ui/ChannelRowIcon"; import { DropdownMenu, DropdownMenuContent, @@ -790,9 +792,10 @@ function ChannelCard({ >
{channel.name}
diff --git a/desktop/src/features/channels/ui/ChannelRowIcon.test.mjs b/desktop/src/features/channels/ui/ChannelRowIcon.test.mjs new file mode 100644 index 00000000000..e1fc3bd95f9 --- /dev/null +++ b/desktop/src/features/channels/ui/ChannelRowIcon.test.mjs @@ -0,0 +1,132 @@ +import assert from "node:assert/strict"; +import { after, afterEach, before, test } from "node:test"; + +import { JSDOM } from "jsdom"; + +const dom = new JSDOM("", { + url: "http://localhost", +}); + +before(() => { + Object.assign(globalThis, { + document: dom.window.document, + HTMLElement: dom.window.HTMLElement, + IS_REACT_ACT_ENVIRONMENT: true, + window: dom.window, + }); + dom.window.matchMedia = () => ({ + matches: false, + addEventListener() {}, + removeEventListener() {}, + }); +}); + +afterEach(async () => { + const { cleanup } = await import("@testing-library/react"); + cleanup(); +}); + +after(() => dom.window.close()); + +function makeChannel(overrides) { + return { + id: "channel-1", + name: "general", + description: null, + memberCount: 1, + isMember: true, + archivedAt: null, + channelType: "stream", + visibility: "open", + participantPubkeys: [], + ...overrides, + }; +} + +test("getChannelRowIconKind returns 'lock' for private channels regardless of type", async () => { + const { getChannelRowIconKind } = await import("./ChannelRowIcon.tsx"); + assert.equal( + getChannelRowIconKind(makeChannel({ visibility: "private" })), + "lock", + ); + // A private forum still locks — the sidebar behaves the same way and the + // two surfaces must not diverge (issue #6120). + assert.equal( + getChannelRowIconKind( + makeChannel({ visibility: "private", channelType: "forum" }), + ), + "lock", + ); +}); + +test("getChannelRowIconKind returns 'forum' for public forum channels", async () => { + const { getChannelRowIconKind } = await import("./ChannelRowIcon.tsx"); + assert.equal( + getChannelRowIconKind( + makeChannel({ channelType: "forum", visibility: "open" }), + ), + "forum", + ); +}); + +test("getChannelRowIconKind returns 'hash' for public streams", async () => { + const { getChannelRowIconKind } = await import("./ChannelRowIcon.tsx"); + assert.equal( + getChannelRowIconKind( + makeChannel({ channelType: "stream", visibility: "open" }), + ), + "hash", + ); +}); + +test("ChannelRowIcon renders a Lock svg for private channels", async () => { + const { createElement } = await import("react"); + const { render } = await import("@testing-library/react"); + const { ChannelRowIcon } = await import("./ChannelRowIcon.tsx"); + + const { container } = render( + createElement(ChannelRowIcon, { + channel: makeChannel({ visibility: "private" }), + }), + ); + + // Lucide icons render an inline svg with the relevant lucide-* class. + assert.match(container.innerHTML, /lucide-lock/); + assert.doesNotMatch(container.innerHTML, /lucide-file-text|lucide-hash/); +}); + +test("ChannelRowIcon renders a FileText svg for public forum channels", async () => { + const { createElement } = await import("react"); + const { render } = await import("@testing-library/react"); + const { ChannelRowIcon } = await import("./ChannelRowIcon.tsx"); + + const { container } = render( + createElement(ChannelRowIcon, { + channel: makeChannel({ + channelType: "forum", + visibility: "open", + }), + }), + ); + + assert.match(container.innerHTML, /lucide-file-text/); + assert.doesNotMatch(container.innerHTML, /lucide-lock|lucide-hash/); +}); + +test("ChannelRowIcon renders a Hash svg for public stream channels", async () => { + const { createElement } = await import("react"); + const { render } = await import("@testing-library/react"); + const { ChannelRowIcon } = await import("./ChannelRowIcon.tsx"); + + const { container } = render( + createElement(ChannelRowIcon, { + channel: makeChannel({ + channelType: "stream", + visibility: "open", + }), + }), + ); + + assert.match(container.innerHTML, /lucide-hash/); + assert.doesNotMatch(container.innerHTML, /lucide-lock|lucide-file-text/); +}); \ No newline at end of file diff --git a/desktop/src/features/channels/ui/ChannelRowIcon.tsx b/desktop/src/features/channels/ui/ChannelRowIcon.tsx new file mode 100644 index 00000000000..535382298a1 --- /dev/null +++ b/desktop/src/features/channels/ui/ChannelRowIcon.tsx @@ -0,0 +1,41 @@ +import { FileText, Hash, Lock } from "lucide-react"; + +import type { Channel } from "@/shared/api/types"; +import { cn } from "@/shared/lib/cn"; + +export type ChannelRowIconKind = "hash" | "lock" | "forum"; + +/** + * Pure discriminator used by `