From 18effe33eb4cc6b572eb299a226f908d32a69067 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 20 May 2026 22:10:40 +0200 Subject: [PATCH] feat(toolbar): show open-ticket count badge on ModTools button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a new CFH ticket arrives the moderator currently only finds out by opening the ModTools launcher and looking at the Report Tool counter. If the launcher is closed they have no signal — same treatment friend requests already get on the People button next door. Match the existing pattern: read `tickets` from `useModTools()` (useBetween-shared, no extra subscription cost), filter to state===1 (OPEN), and render a over the ToolbarItemView in absolute-positioned relative wrapper. Same positioning as the friend-requests badge (-right-1 -top-1 z-10 pointer-events-none). Gated on `isMod` so non-mods don't compute the filter or render the wrapper — and since useModTools is a useBetween singleton its event listeners only register once across the whole app regardless of consumer count. Applied to both toolbar layouts (desktop and mobile, lines ~272 and ~382) so the badge follows the user across breakpoints. --- src/components/toolbar/ToolbarView.tsx | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index 9b492c7..2e717f2 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -1,9 +1,9 @@ import { CreateLinkEvent, Dispose, DropBounce, EaseOut, JumpBy, Motions, NitroToolbarAnimateIconEvent, PerkAllowancesMessageEvent, PerkEnum, Queue, Wait, YouTubeRoomSettingsEvent } from '@nitrots/nitro-renderer'; import { AnimatePresence, motion, Variants } from 'framer-motion'; -import { FC, useEffect, useState } from 'react'; +import { FC, useEffect, useMemo, useState } from 'react'; import { GetConfigurationValue, MessengerIconState, OpenMessengerChat, setYoutubeRoomEnabled, VisitDesktop } from '../../api'; import { Flex, LayoutAvatarImageView, LayoutItemCountView } from '../../common'; -import { useAchievements, useFriends, useHasPermission, useInventoryUnseenTracker, useMessageEvent, useMessenger, useNitroEvent, useSessionInfo, useWiredTools } from '../../hooks'; +import { useAchievements, useFriends, useHasPermission, useInventoryUnseenTracker, useMessageEvent, useMessenger, useModTools, useNitroEvent, useSessionInfo, useWiredTools } from '../../hooks'; import { ToolbarItemView } from './ToolbarItemView'; import { ToolbarMeView } from './ToolbarMeView'; import { YouTubePlayerView } from './YouTubePlayerView'; @@ -50,6 +50,14 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => const { iconState = MessengerIconState.HIDDEN } = useMessenger(); const { openMonitor, showToolbarButton } = useWiredTools(); const isMod = useHasPermission('acc_supporttool'); + // Surface the open-ticket count on the toolbar ModTools button so a + // new CFH pings the mod even when the launcher itself is closed. + // useBetween-shared state — no extra subscription cost. + const { tickets = [] } = useModTools(); + const openTicketsCount = useMemo( + () => isMod ? tickets.filter(ticket => ticket && (ticket.state === 1)).length : 0, + [ isMod, tickets ] + ); const isVisible = (isToolbarOpen || !isInRoom); const visibilityVariant = isVisible ? 'visible' : 'hidden'; @@ -260,8 +268,10 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => } { isMod && - + CreateLinkEvent('mod-tools/toggle') } className="tb-icon" /> + { (openTicketsCount > 0) && + } } { isMod && @@ -370,8 +380,10 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => } { isMod && - + CreateLinkEvent('mod-tools/toggle') } className="tb-icon" /> + { (openTicketsCount > 0) && + } } { isMod &&