feat(toolbar): show open-ticket count badge on ModTools button

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 <LayoutItemCountView> 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.
This commit is contained in:
simoleo89
2026-05-20 22:10:40 +02:00
committed by simoleo89
parent 0ad284fa9c
commit 18effe33eb
+16 -4
View File
@@ -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 =>
<ToolbarItemView icon="youtube" onClick={ openYouTubePlayer } className="tb-icon" />
</motion.div> }
{ isMod &&
<motion.div variants={ itemVariants }>
<motion.div variants={ itemVariants } className="relative">
<ToolbarItemView icon="modtools" onClick={ () => CreateLinkEvent('mod-tools/toggle') } className="tb-icon" />
{ (openTicketsCount > 0) &&
<LayoutItemCountView count={ openTicketsCount } className="pointer-events-none absolute -right-1 -top-1 z-10" /> }
</motion.div> }
{ isMod &&
<motion.div variants={ itemVariants }>
@@ -370,8 +380,10 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
<ToolbarItemView icon="youtube" onClick={ openYouTubePlayer } className="tb-icon" />
</motion.div> }
{ isMod &&
<motion.div variants={ itemVariants }>
<motion.div variants={ itemVariants } className="relative">
<ToolbarItemView icon="modtools" onClick={ () => CreateLinkEvent('mod-tools/toggle') } className="tb-icon" />
{ (openTicketsCount > 0) &&
<LayoutItemCountView count={ openTicketsCount } className="pointer-events-none absolute -right-1 -top-1 z-10" /> }
</motion.div> }
{ isMod &&
<motion.div variants={ itemVariants }>