@@ -60,6 +60,7 @@ import {
6060 PERSONAL_CHANNEL_NAME ,
6161 useTaskChannels ,
6262} from "@posthog/ui/features/canvas/hooks/useTaskChannels" ;
63+ import { useIsChannelUnread } from "@posthog/ui/features/canvas/hooks/useUnreadChannels" ;
6364import { copyChannelLink } from "@posthog/ui/features/canvas/utils/copyChannelLink" ;
6465import { useSidebarStore } from "@posthog/ui/features/sidebar/sidebarStore" ;
6566import { toast } from "@posthog/ui/primitives/toast" ;
@@ -291,7 +292,14 @@ function ChannelMenu({
291292
292293// One channel in the list: a "# name" row that navigates to the channel home.
293294// No expansion — the channel's surfaces live in the in-channel top nav.
294- function ChannelSection ( { channel } : { channel : Channel } ) {
295+ function ChannelSection ( {
296+ channel,
297+ isUnread,
298+ } : {
299+ channel : Channel ;
300+ /** Bolds the name: activity here the viewer hasn't seen. */
301+ isUnread ?: boolean ;
302+ } ) {
295303 const navigate = useNavigate ( ) ;
296304 const pathname = useRouterState ( { select : ( s ) => s . location . pathname } ) ;
297305 const base = `/website/${ channel . id } ` ;
@@ -340,10 +348,26 @@ function ChannelSection({ channel }: { channel: Channel }) {
340348 } }
341349 className = "w-full min-w-0 justify-start gap-2 data-selected:bg-fill-selected data-selected:text-gray-12"
342350 >
343- < HashIcon size = { 14 } className = "shrink-0 text-gray-9" />
351+ < HashIcon
352+ size = { 14 }
353+ weight = { isUnread ? "bold" : undefined }
354+ className = { cn (
355+ "shrink-0" ,
356+ isUnread || isActive
357+ ? "text-foreground"
358+ : "text-muted-foreground group-hover/button:text-foreground" ,
359+ ) }
360+ />
344361 < span
345362 className = { cn (
346- "truncate font-medium text-[13px] text-gray-12 group-hover/chan:pr-8" ,
363+ "truncate text-[13px] group-hover/chan:pr-8" ,
364+ // Bold is unread's alone; full contrast is shared with the
365+ // channel you're in. Either way there's no hover brighten
366+ // left to do, so those rows skip it.
367+ isUnread ? "font-bold" : "font-medium" ,
368+ isUnread || isActive
369+ ? "text-foreground"
370+ : "text-muted-foreground group-hover/button:text-foreground" ,
347371 menuOpen && "pr-8" ,
348372 ) }
349373 >
@@ -498,6 +522,7 @@ function PersonalChannelRow() {
498522 useTaskChannels ( ) ;
499523 // The "+" dropdown (New task / New canvas), mirroring a shared channel row.
500524 const [ newMenuOpen , setNewMenuOpen ] = useState ( false ) ;
525+ const isUnread = useIsChannelUnread ( ) ( PERSONAL_CHANNEL_NAME ) ;
501526
502527 const meFolder = channels . find ( ( c ) => c . name === PERSONAL_CHANNEL_NAME ) ;
503528 const createAndOpenCanvas = useCreateAndOpenDashboard ( meFolder ?. id ) ;
@@ -560,16 +585,33 @@ function PersonalChannelRow() {
560585 onClick = { ( ) => void open ( ) }
561586 className = "w-full min-w-0 justify-start gap-2 data-selected:bg-fill-selected data-selected:text-gray-12"
562587 >
563- < HashIcon size = { 14 } className = "shrink-0 text-gray-9" />
564- < span className = "truncate font-medium text-[13px] text-gray-12" >
588+ < HashIcon
589+ size = { 14 }
590+ weight = { isUnread ? "bold" : undefined }
591+ className = { cn (
592+ "shrink-0" ,
593+ isUnread || isActive
594+ ? "text-foreground"
595+ : "text-muted-foreground group-hover/button:text-foreground" ,
596+ ) }
597+ />
598+ < span
599+ className = { cn (
600+ "truncate text-[13px]" ,
601+ isUnread ? "font-bold" : "font-medium" ,
602+ isUnread || isActive
603+ ? "text-foreground"
604+ : "text-muted-foreground group-hover/button:text-foreground" ,
605+ ) }
606+ >
565607 { PERSONAL_CHANNEL_NAME }
566608 </ span >
567609 { /* The lock and the hover "+" share the right edge, so fade the lock
568610 out as the "+" comes in. */ }
569611 < LockSimpleIcon
570612 size = { 12 }
571613 className = { cn (
572- "ml-auto shrink-0 text-gray-9 transition-opacity" ,
614+ "ml-auto shrink-0 text-chrome-foreground transition-opacity" ,
573615 newMenuOpen
574616 ? "opacity-0"
575617 : "opacity-100 group-hover/chan:opacity-0" ,
@@ -655,7 +697,12 @@ function ChannelGroup({
655697 return (
656698 < Collapsible . Root
657699 open = { isOpen }
658- onOpenChange = { ( ) => toggleSection ( sectionId ) }
700+ // The store only exposes a toggle, so drive it from the requested value:
701+ // an event for the state we're already in is then a no-op rather than an
702+ // inversion.
703+ onOpenChange = { ( open ) => {
704+ if ( open !== isOpen ) toggleSection ( sectionId ) ;
705+ } }
659706 className = { className }
660707 >
661708 { /* MenuLabel carries the sidebar's label styling; `render` keeps it a
@@ -703,6 +750,8 @@ export function ChannelsList() {
703750 const { channels : allChannels , isLoading } = useChannels ( ) ;
704751 const { starredRefToShortcutId } = useChannelStars ( ) ;
705752
753+ const isUnread = useIsChannelUnread ( ) ;
754+
706755 // The "me" folder renders as the pinned personal row, not a shared channel.
707756 const channels = allChannels . filter ( ( c ) => c . name !== PERSONAL_CHANNEL_NAME ) ;
708757 const starred = channels . filter ( ( c ) => starredRefToShortcutId . has ( c . path ) ) ;
@@ -733,7 +782,11 @@ export function ChannelsList() {
733782 { starred . length > 0 && (
734783 < ChannelGroup sectionId = { STARRED_SECTION_ID } label = "Starred" >
735784 { starred . map ( ( channel ) => (
736- < ChannelSection key = { channel . id } channel = { channel } />
785+ < ChannelSection
786+ key = { channel . id }
787+ channel = { channel }
788+ isUnread = { isUnread ( channel . name ) }
789+ />
737790 ) ) }
738791 </ ChannelGroup >
739792 ) }
@@ -745,7 +798,11 @@ export function ChannelsList() {
745798 </ Empty >
746799 ) }
747800 { others . map ( ( channel ) => (
748- < ChannelSection key = { channel . id } channel = { channel } />
801+ < ChannelSection
802+ key = { channel . id }
803+ channel = { channel }
804+ isUnread = { isUnread ( channel . name ) }
805+ />
749806 ) ) }
750807 </ ChannelGroup >
751808 </ Flex >
0 commit comments