11import {
2+ CaretDownIcon ,
3+ CaretRightIcon ,
24 ChartBarIcon ,
35 DotsThreeIcon ,
6+ EyeIcon ,
7+ EyeSlashIcon ,
48 FileTextIcon ,
59 LinkIcon ,
610 LockSimpleIcon ,
@@ -40,6 +44,10 @@ import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events";
4044import { CreateChannelModal } from "@posthog/ui/features/canvas/components/CreateChannelModal" ;
4145import { RenameChannelModal } from "@posthog/ui/features/canvas/components/RenameChannelModal" ;
4246import { trackAndCreateCanvas } from "@posthog/ui/features/canvas/createCanvasAnalytics" ;
47+ import {
48+ useChannelHides ,
49+ useChannelHideToggle ,
50+ } from "@posthog/ui/features/canvas/hooks/useChannelHides" ;
4351import {
4452 useChannelStars ,
4553 useChannelStarToggle ,
@@ -76,9 +84,10 @@ type ChannelActionItem = {
7684 separatorBefore ?: boolean ;
7785} ;
7886
79- // The channel actions (star, copy link, rename, delete) plus the rename-modal
80- // state they drive. Single source of truth so the dropdown and context menus
81- // stay in lockstep — add an action here and both surfaces pick it up.
87+ // The channel actions (star, hide, copy link, rename, delete) plus the
88+ // rename-modal state they drive. Single source of truth so the dropdown and
89+ // context menus stay in lockstep — add an action here and both surfaces pick
90+ // it up.
8291function useChannelActions ( channel : Channel ) : {
8392 actions : ChannelActionItem [ ] ;
8493 renameOpen : boolean ;
@@ -96,6 +105,8 @@ function useChannelActions(channel: Channel): {
96105 const pathname = useRouterState ( { select : ( s ) => s . location . pathname } ) ;
97106 const { deleteChannel, isDeleting } = useChannelMutations ( ) ;
98107 const { isStarred, toggleStar, removeStar } = useChannelStarToggle ( channel ) ;
108+ const { isHidden, toggleHidden, removeHidden } =
109+ useChannelHideToggle ( channel ) ;
99110
100111 // Runs the actual delete once confirmed. Returns whether it succeeded so the
101112 // dialog can stay open (and show the toast) on failure.
@@ -120,6 +131,7 @@ function useChannelActions(channel: Channel): {
120131
121132 await deleteChannel ( channel . id ) ;
122133 removeStar ( ) ;
134+ removeHidden ( ) ;
123135 track ( ANALYTICS_EVENTS . CHANNEL_ACTION , {
124136 action_type : "delete" ,
125137 surface : "sidebar" ,
@@ -159,6 +171,19 @@ function useChannelActions(channel: Channel): {
159171 toggleStar ( ) ;
160172 } ,
161173 } ,
174+ {
175+ key : "hide" ,
176+ label : isHidden ? "Unhide context" : "Hide context" ,
177+ icon : isHidden ? < EyeIcon size = { 14 } /> : < EyeSlashIcon size = { 14 } /> ,
178+ onSelect : ( ) => {
179+ track ( ANALYTICS_EVENTS . CHANNEL_ACTION , {
180+ action_type : isHidden ? "unhide" : "hide" ,
181+ surface : "sidebar" ,
182+ channel_id : channel . id ,
183+ } ) ;
184+ toggleHidden ( ) ;
185+ } ,
186+ } ,
162187 {
163188 key : "copy-link" ,
164189 label : "Copy link" ,
@@ -532,17 +557,26 @@ function PersonalChannelRow() {
532557
533558// The channel list — the Channels space sidebar body. The private "#me"
534559// channel is pinned at the top; starred channels surface in their own section
535- // so the ones you use most stay in reach; the rest sit under a "Channels"
536- // label with the "New" channel button.
560+ // so the ones you use most stay in reach; the rest sit under a "Contexts"
561+ // label with the "New" channel button; channels the user has hidden collapse
562+ // into a "Hidden" group at the bottom.
537563export function ChannelsList ( ) {
538564 const { channels : allChannels , isLoading } = useChannels ( ) ;
539565 const { starredRefToShortcutId } = useChannelStars ( ) ;
566+ const { hiddenRefToShortcutId } = useChannelHides ( ) ;
540567 const [ modalOpen , setModalOpen ] = useState ( false ) ;
568+ // The "Hidden" group is collapsed by default — hidden channels are ones the
569+ // user chose to get out of the way.
570+ const [ hiddenExpanded , setHiddenExpanded ] = useState ( false ) ;
541571
542572 // The "me" folder renders as the pinned personal row, not a shared channel.
543573 const channels = allChannels . filter ( ( c ) => c . name !== PERSONAL_CHANNEL_NAME ) ;
544- const starred = channels . filter ( ( c ) => starredRefToShortcutId . has ( c . path ) ) ;
545- const others = channels . filter ( ( c ) => ! starredRefToShortcutId . has ( c . path ) ) ;
574+ // Hiding wins over starring: a hidden channel leaves both the starred and
575+ // main lists and only surfaces under the collapsed "Hidden" group.
576+ const hidden = channels . filter ( ( c ) => hiddenRefToShortcutId . has ( c . path ) ) ;
577+ const visible = channels . filter ( ( c ) => ! hiddenRefToShortcutId . has ( c . path ) ) ;
578+ const starred = visible . filter ( ( c ) => starredRefToShortcutId . has ( c . path ) ) ;
579+ const others = visible . filter ( ( c ) => ! starredRefToShortcutId . has ( c . path ) ) ;
546580
547581 // Fire CHANNELS_SPACE_VIEWED once per space mount, after channels first load
548582 // (so the counts are accurate). The sidebar stays mounted while navigating
@@ -554,8 +588,9 @@ export function ChannelsList() {
554588 track ( ANALYTICS_EVENTS . CHANNELS_SPACE_VIEWED , {
555589 channel_count : channels . length ,
556590 starred_count : starred . length ,
591+ hidden_count : hidden . length ,
557592 } ) ;
558- } , [ isLoading , channels . length , starred . length ] ) ;
593+ } , [ isLoading , channels . length , starred . length , hidden . length ] ) ;
559594
560595 return (
561596 // One shared provider groups every row tooltip so that once one shows,
@@ -608,6 +643,36 @@ export function ChannelsList() {
608643 < ChannelSection key = { channel . id } channel = { channel } />
609644 ) ) }
610645 </ div >
646+
647+ { hidden . length > 0 && (
648+ < Box className = "mt-3" >
649+ < MenuLabel className = "uppercase" >
650+ < button
651+ type = "button"
652+ aria-expanded = { hiddenExpanded }
653+ onClick = { ( ) => setHiddenExpanded ( ( open ) => ! open ) }
654+ className = "flex w-full items-center gap-2"
655+ >
656+ { hiddenExpanded ? (
657+ < CaretDownIcon size = { 14 } className = "text-gray-9" />
658+ ) : (
659+ < CaretRightIcon size = { 14 } className = "text-gray-9" />
660+ ) }
661+ < EyeSlashIcon size = { 14 } className = "text-gray-9" />
662+ Hidden
663+ < span className = "ml-0.5 text-gray-9" > { hidden . length } </ span >
664+ </ button >
665+ </ MenuLabel >
666+ </ Box >
667+ ) }
668+
669+ { hidden . length > 0 && hiddenExpanded && (
670+ < div className = "pl-2" >
671+ { hidden . map ( ( channel ) => (
672+ < ChannelSection key = { channel . id } channel = { channel } />
673+ ) ) }
674+ </ div >
675+ ) }
611676 </ Flex >
612677
613678 < CreateChannelModal open = { modalOpen } onOpenChange = { setModalOpen } />
0 commit comments