From 2b6f1f2368e61188e70dfa13459c572388c52304 Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Fri, 31 Jul 2026 13:53:01 +0200 Subject: [PATCH] feat(canvas): show personal space as the user's name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The personal "me" space read as a literal "me", which people kept misreading and didn't connect to their own tasks. Display the signed-in user's name instead, with a quiet "your space" tag (like Slack's "you" next to your own DM) so the row still reads as the personal space. Only the rendered label changes — the channel is still identified, routed, matched, and provisioned under PERSONAL_CHANNEL_NAME everywhere, and the personal glyph (lock) and star rules still key off the raw name. Sidebar search also matches the resolved name so typing your own name surfaces your space. Generated-By: PostHog Code Task-Id: 0c402083-78dc-48b1-8cf2-480b061785e5 --- .../canvas/components/ChannelBackRow.test.tsx | 4 +++ .../canvas/components/ChannelBackRow.tsx | 6 +++- .../canvas/components/ChannelBreadcrumb.tsx | 13 ++++++-- .../canvas/components/ChannelHeader.tsx | 5 +++ .../canvas/components/ChannelsList.test.tsx | 18 ++++++++++ .../canvas/components/ChannelsList.tsx | 19 +++++++++-- .../canvas/hooks/usePersonalSpaceName.ts | 33 +++++++++++++++++++ 7 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 packages/ui/src/features/canvas/hooks/usePersonalSpaceName.ts 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; +}