diff --git a/packages/ui/src/features/canvas/components/ChannelBackRow.test.tsx b/packages/ui/src/features/canvas/components/ChannelBackRow.test.tsx index 705d724870..c572ba1cda 100644 --- a/packages/ui/src/features/canvas/components/ChannelBackRow.test.tsx +++ b/packages/ui/src/features/canvas/components/ChannelBackRow.test.tsx @@ -22,6 +22,10 @@ vi.mock("@posthog/ui/features/canvas/hooks/useChannelStars", () => ({ toggleStar: mocks.toggleStar, }), })); +vi.mock("@posthog/ui/features/canvas/hooks/usePersonalSpaceName", () => ({ + usePersonalSpaceName: () => "me", + useSpaceDisplayName: (name: string | undefined) => name, +})); import { useChannelPaneStore } from "@posthog/ui/features/canvas/stores/channelPaneStore"; import { ChannelBackRow } from "./ChannelBackRow"; diff --git a/packages/ui/src/features/canvas/components/ChannelBackRow.tsx b/packages/ui/src/features/canvas/components/ChannelBackRow.tsx index 0e91183d06..05ff75a571 100644 --- a/packages/ui/src/features/canvas/components/ChannelBackRow.tsx +++ b/packages/ui/src/features/canvas/components/ChannelBackRow.tsx @@ -14,6 +14,7 @@ import { useChannels, } from "@posthog/ui/features/canvas/hooks/useChannels"; import { useChannelsLayout } from "@posthog/ui/features/canvas/hooks/useChannelsLayout"; +import { useSpaceDisplayName } from "@posthog/ui/features/canvas/hooks/usePersonalSpaceName"; import { PERSONAL_CHANNEL_NAME } from "@posthog/ui/features/canvas/hooks/useTaskChannels"; import { showChannelList } from "@posthog/ui/features/canvas/stores/channelPaneStore"; import { track } from "@posthog/ui/shell/analytics"; @@ -55,6 +56,9 @@ export function ChannelBackRow({ channelId }: { channelId: string }) { const spacesLayout = useChannelsLayout(); const { channels, isLoading } = useChannels(); const current = channels.find((c) => c.id === channelId); + // Display only: the personal space reads as the user's name (the star/glyph + // logic below still keys off the raw "me" name). + const currentLabel = useSpaceDisplayName(current?.name); const showStar = current != null && current.name !== PERSONAL_CHANNEL_NAME; const glyph = channelGlyph(current?.name, { size: 14, @@ -101,7 +105,7 @@ export function ChannelBackRow({ channelId }: { channelId: string }) { )} {current ? ( - current.name + currentLabel ) : isLoading ? ( // A placeholder word here would read as a real channel named // "channel"; a skeleton says "still loading" honestly. diff --git a/packages/ui/src/features/canvas/components/ChannelBreadcrumb.tsx b/packages/ui/src/features/canvas/components/ChannelBreadcrumb.tsx index 670f071518..1e58d73540 100644 --- a/packages/ui/src/features/canvas/components/ChannelBreadcrumb.tsx +++ b/packages/ui/src/features/canvas/components/ChannelBreadcrumb.tsx @@ -13,8 +13,16 @@ import { useNavigate, useRouterState } from "@tanstack/react-router"; import { type ReactNode, useState } from "react"; interface ChannelBreadcrumbProps { - /** The channel (root) segment label. */ + /** + * The channel's raw name, used for the root segment's glyph (e.g. the "me" + * space's lock). Also the default label unless `channelLabel` overrides it. + */ channelName: string; + /** + * What the root segment reads as, when it differs from the raw name — the + * personal space shows the user's name but keeps "me" for its glyph/identity. + */ + channelLabel?: string; /** * When provided, the "# channel" segment links to the channel home, like the * sidebar channel row and the channel-view header. @@ -50,6 +58,7 @@ interface ChannelBreadcrumbProps { // the channel home. export function ChannelBreadcrumb({ channelName, + channelLabel, channelId, middle, leafIcon, @@ -80,7 +89,7 @@ export function ChannelBreadcrumb({ space: spacesLayout, className: "shrink-0 text-muted-foreground/80", })} - label={channelName} + label={channelLabel ?? channelName} strong // Nowhere to go from the space's own index, and no channelId means no // route at all — either way the segment stops responding. diff --git a/packages/ui/src/features/canvas/components/ChannelHeader.tsx b/packages/ui/src/features/canvas/components/ChannelHeader.tsx index 40f65fb29e..1e624c8aa5 100644 --- a/packages/ui/src/features/canvas/components/ChannelHeader.tsx +++ b/packages/ui/src/features/canvas/components/ChannelHeader.tsx @@ -10,6 +10,7 @@ import { import { useChannels } from "@posthog/ui/features/canvas/hooks/useChannels"; import { useChannelsLayout } from "@posthog/ui/features/canvas/hooks/useChannelsLayout"; import { useMarkChannelSeen } from "@posthog/ui/features/canvas/hooks/useMarkChannelSeen"; +import { useSpaceDisplayName } from "@posthog/ui/features/canvas/hooks/usePersonalSpaceName"; import { Text } from "@radix-ui/themes"; import { useNavigate, useRouterState } from "@tanstack/react-router"; @@ -34,6 +35,9 @@ export function ChannelHeader({ const channelsLayout = useChannelsLayout(); const { channels } = useChannels(); const channelName = channels.find((c) => c.id === channelId)?.name; + // Display only — the personal space reads as the user's name, every other + // space as itself. `channelName` stays the raw name for identity below. + const displayName = useSpaceDisplayName(channelName); // Every channel surface renders this header, so mark the channel read here. useMarkChannelSeen(channelName); @@ -45,6 +49,7 @@ export function ChannelHeader({ return ( ({ starredPaths: [] as string[], channelsLayout: true, navigate: vi.fn(), + // The personal row's display label; "me" until a signed-in user resolves. + personalName: "me", })); vi.mock("@posthog/ui/shell/analytics", () => ({ track: vi.fn() })); @@ -40,6 +42,11 @@ vi.mock("@posthog/ui/features/canvas/hooks/useTaskChannels", async () => { >("@posthog/ui/features/canvas/hooks/useTaskChannels"); return { ...actual, useTaskChannels: () => ({ channels: [] }) }; }); +vi.mock("@posthog/ui/features/canvas/hooks/usePersonalSpaceName", () => ({ + usePersonalSpaceName: () => mocks.personalName, + useSpaceDisplayName: (name: string | undefined) => + name === "me" ? mocks.personalName : name, +})); vi.mock("@posthog/ui/features/canvas/components/RenameChannelModal", () => ({ RenameChannelModal: () => null, })); @@ -73,6 +80,7 @@ describe("ChannelsList", () => { mocks.channels = [ME, ENG, DESIGN]; mocks.starredPaths = []; mocks.channelsLayout = true; + mocks.personalName = "me"; // The pane store is module state: reset to its resting value so a test that // slides the slider can't hand the next one a pre-focused search box. showChannelPane(); @@ -93,6 +101,16 @@ describe("ChannelsList", () => { expect(me.parentElement?.textContent).toMatch(/me(⌘|Ctrl)/); }); + it("shows the personal row as the user's name with a 'your space' tag", () => { + mocks.personalName = "Shy"; + renderList(); + // The name replaces the raw "me", and the quiet tag keeps it legible as the + // personal space (like Slack's "you" next to your own DM). + expect(screen.getByText("Shy")).toBeTruthy(); + expect(screen.getByText("your space")).toBeTruthy(); + expect(screen.queryByText("me")).toBeNull(); + }); + // "Starred" and "Spaces" are headings over the rows. Spaces receive a small // Slack-style inset; the alpha keeps its deeper tree indentation. describe("group headings", () => { diff --git a/packages/ui/src/features/canvas/components/ChannelsList.tsx b/packages/ui/src/features/canvas/components/ChannelsList.tsx index 1b648d1959..aa3a6c3da9 100644 --- a/packages/ui/src/features/canvas/components/ChannelsList.tsx +++ b/packages/ui/src/features/canvas/components/ChannelsList.tsx @@ -64,6 +64,7 @@ import { } from "@posthog/ui/features/canvas/hooks/useChannels"; import { useChannelsLayout } from "@posthog/ui/features/canvas/hooks/useChannelsLayout"; import { useCreateAndOpenDashboard } from "@posthog/ui/features/canvas/hooks/useDashboards"; +import { usePersonalSpaceName } from "@posthog/ui/features/canvas/hooks/usePersonalSpaceName"; import { useStarredChannelSlots } from "@posthog/ui/features/canvas/hooks/useStarredChannelSlots"; import { PERSONAL_CHANNEL_NAME, @@ -697,6 +698,10 @@ function PersonalChannelRow({ hotkeySlot }: { hotkeySlot?: number }) { // The "+" dropdown (New task / New canvas), mirroring a shared channel row. const [newMenuOpen, setNewMenuOpen] = useState(false); const isUnread = useIsChannelUnread()(PERSONAL_CHANNEL_NAME); + // Shown as the user's own name, so it's clear these are their tasks. The + // channel is still "me" for every identity/routing purpose (below). + const personalName = usePersonalSpaceName(); + const hasResolvedName = personalName !== PERSONAL_CHANNEL_NAME; const meFolder = channels.find((c) => c.name === PERSONAL_CHANNEL_NAME); const createAndOpenCanvas = useCreateAndOpenDashboard(meFolder?.id); @@ -759,8 +764,15 @@ function PersonalChannelRow({ hotkeySlot }: { hotkeySlot?: number }) { : "text-muted-foreground group-hover/button:text-foreground", )} > - {PERSONAL_CHANNEL_NAME} + {personalName} + {hasResolvedName && ( + // A quiet "your space" tag, like Slack's "you" next to your own DM, + // so the renamed row still reads as the personal space. + + your space + + )} {hotkeySlot != null && ( {formatHotkey(`mod+${hotkeySlot}`)} @@ -919,6 +931,7 @@ export function ChannelsList() { const channelsLayout = useChannelsLayout(); const isUnread = useIsChannelUnread(); + const personalName = usePersonalSpaceName(); const [query, setQuery] = useState(""); const normalizedQuery = channelsLayout ? query.trim().toLowerCase() : ""; @@ -935,7 +948,9 @@ export function ChannelsList() { // stand between you and the row you already named, and an empty "Starred" // heading reads as a result that isn't there. const searchResults = channels.filter((c) => matches(c.name)); - const meMatches = matches(PERSONAL_CHANNEL_NAME); + // The personal row now reads as the user's name, so search it by that too — + // otherwise typing your own name wouldn't surface your own space. + const meMatches = matches(PERSONAL_CHANNEL_NAME) || matches(personalName); const noMatches = normalizedQuery !== "" && !meMatches && !searchResults.length; diff --git a/packages/ui/src/features/canvas/hooks/usePersonalSpaceName.ts b/packages/ui/src/features/canvas/hooks/usePersonalSpaceName.ts new file mode 100644 index 0000000000..23adb37599 --- /dev/null +++ b/packages/ui/src/features/canvas/hooks/usePersonalSpaceName.ts @@ -0,0 +1,33 @@ +import { useMeQuery } from "@posthog/ui/features/auth/useMeQuery"; +import { PERSONAL_CHANNEL_NAME } from "@posthog/ui/features/canvas/hooks/useTaskChannels"; + +/** + * Display label for the personal ("me") space. We show the signed-in user's + * own name so it's obvious these are *their* tasks rather than a space literally + * called "me", which readers kept misreading. + * + * This only changes what's rendered — the channel is still identified, routed, + * matched, and provisioned under {@link PERSONAL_CHANNEL_NAME} everywhere. Falls + * back to that raw name until the (cached, fast) user record loads, or if the + * user has no name set. + */ +export function usePersonalSpaceName(): string { + const { data: user } = useMeQuery(); + const name = [user?.first_name, user?.last_name] + .map((part) => part?.trim()) + .filter(Boolean) + .join(" "); + return name || PERSONAL_CHANNEL_NAME; +} + +/** + * Maps a raw channel name to what should be shown for it, swapping the personal + * channel's "me" for the user's name (see {@link usePersonalSpaceName}) and + * leaving every other space's name untouched. + */ +export function useSpaceDisplayName( + rawName: string | undefined, +): string | undefined { + const personalName = usePersonalSpaceName(); + return rawName === PERSONAL_CHANNEL_NAME ? personalName : rawName; +}