From fbcda88cd3c26b26c6ec0330ff5bd1331ee76a27 Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 29 May 2026 08:31:18 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=86=99=20Update=20Rare-Value=20page=20and?= =?UTF-8?q?=20fix=20the=20loading=20of=20the=20json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/rare-values/RareValuesView.tsx | 94 +++++++++++++++---- src/hooks/radio/useRadio.ts | 4 +- .../rooms/widgets/useChatCommandSelector.ts | 26 +---- src/hooks/soundboard/useSoundboard.ts | 4 +- 4 files changed, 84 insertions(+), 44 deletions(-) diff --git a/src/components/rare-values/RareValuesView.tsx b/src/components/rare-values/RareValuesView.tsx index c03f787..266c32b 100644 --- a/src/components/rare-values/RareValuesView.tsx +++ b/src/components/rare-values/RareValuesView.tsx @@ -1,10 +1,12 @@ import { AddLinkEventTracker, GetRoomEngine, GetSessionDataManager, ILinkEventTracker, IRareValue, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; -import { FC, useEffect, useMemo, useState } from 'react'; +import { FC, useEffect, useMemo, useRef, useState } from 'react'; import { LocalizeFormattedNumber, LocalizeText } from '../../api'; import { Column, Flex, LayoutCurrencyIcon, LayoutImage, Text } from '../../common'; import { useRareValues } from '../../hooks'; import { NitroCard, NitroInput } from '../../layout'; +const PAGE_SIZE = 50; + interface RareValueRow { spriteId: number; @@ -17,7 +19,10 @@ export const RareValuesView: FC<{}> = () => { const [ isVisible, setIsVisible ] = useState(false); const [ searchValue, setSearchValue ] = useState(''); + const [ visibleCount, setVisibleCount ] = useState(PAGE_SIZE); const { values = null, loaded = false } = useRareValues(); + const scrollRef = useRef(null); + const sentinelRef = useRef(null); useEffect(() => { @@ -79,35 +84,90 @@ export const RareValuesView: FC<{}> = () => return rows.filter(row => row.name.toLocaleLowerCase().includes(query)); }, [ rows, searchValue ]); + useEffect(() => + { + setVisibleCount(PAGE_SIZE); + if(scrollRef.current) scrollRef.current.scrollTop = 0; + }, [ filtered ]); + + useEffect(() => + { + if(!isVisible) return; + if(visibleCount >= filtered.length) return; + + const sentinel = sentinelRef.current; + const root = scrollRef.current; + if(!sentinel || !root) return; + + const observer = new IntersectionObserver(entries => + { + if(entries.some(entry => entry.isIntersecting)) + { + setVisibleCount(prev => Math.min(prev + PAGE_SIZE, filtered.length)); + } + }, { root, rootMargin: '120px 0px' }); + + observer.observe(sentinel); + return () => observer.disconnect(); + }, [ isVisible, visibleCount, filtered.length ]); + if(!isVisible) return null; + const visibleRows = filtered.slice(0, visibleCount); + const hasMore = visibleCount < filtered.length; + return ( - + setIsVisible(false) } /> - - setSearchValue(event.target.value) } /> - + + + 🔍 + setSearchValue(event.target.value) } + className="grow !border-0 !bg-transparent !p-0 !shadow-none focus:!ring-0" /> + + { loaded && + + { filtered.length } { LocalizeText('rarevalues.title').toLowerCase() } + { hasMore && { visibleRows.length } / { filtered.length } } + } +
{ !loaded && - { LocalizeText('rarevalues.loading') } } +
+ { LocalizeText('rarevalues.loading') } +
} { (loaded && !filtered.length) && - { LocalizeText('rarevalues.empty') } } - { filtered.map(row => ( - - - { row.name } - - { LocalizeFormattedNumber(row.value.points) } +
+ { LocalizeText('rarevalues.empty') } +
} + { visibleRows.map((row, index) => ( + +
+ +
+ { row.name } + + { LocalizeFormattedNumber(row.value.points) }
)) } - + { hasMore && +
+ { LocalizeText('rarevalues.loading.more') } +
} +
diff --git a/src/hooks/radio/useRadio.ts b/src/hooks/radio/useRadio.ts index 88bff81..bafc320 100644 --- a/src/hooks/radio/useRadio.ts +++ b/src/hooks/radio/useRadio.ts @@ -31,7 +31,9 @@ const useRadioState = () => if(loadStartedRef.current) return; loadStartedRef.current = true; - const url = GetConfigurationValue('radio.stations.url') || 'configuration/radio-stations.json5'; + const url = GetConfigurationValue('radio.url') + || GetConfigurationValue('radio.stations.url') + || 'configuration/radio-stations.json5'; (async () => { diff --git a/src/hooks/rooms/widgets/useChatCommandSelector.ts b/src/hooks/rooms/widgets/useChatCommandSelector.ts index 31dd495..1c23877 100644 --- a/src/hooks/rooms/widgets/useChatCommandSelector.ts +++ b/src/hooks/rooms/widgets/useChatCommandSelector.ts @@ -32,18 +32,6 @@ const CLIENT_COMMANDS: { key: string; descriptionKey: string }[] = [ { key: 'nitro', descriptionKey: 'chatcmd.client.info' }, ]; -/** - * Server-pushed command cache. Lives in a Zustand store (instead of - * module-level `let` variables) so the React Compiler can analyze the - * surrounding hook cleanly, and so a future test can `setState({…})` - * a deterministic fixture without monkey-patching the module. - * - * The `isListenerRegistered` flag prevents the renderer from getting - * two AvailableCommandsEvent listeners — one from the module-level - * pre-mount registration (which captures the server's reply that lands - * during login, BEFORE any React widget mounts) and one from the - * in-hook `useMessageEvent` (which covers later rank-change refreshes). - */ interface ChatCommandStore { serverCommands: CommandDefinition[]; @@ -74,15 +62,9 @@ const ensureGlobalListener = (): void => GetCommunication().registerMessageEvent(event); useChatCommandStore.getState().markListenerRegistered(); } - catch - { - // Communication not ready yet — the in-hook useMessageEvent - // below covers later mounts. - } + catch {} }; -// Try once at module load so the server's response landing before any -// React mount still hits the cache. ensureGlobalListener(); export const useChatCommandSelector = (chatValue: string) => @@ -94,13 +76,9 @@ export const useChatCommandSelector = (chatValue: string) => useEffect(() => { - // Cover the case where the module-level registration failed - // because GetCommunication() wasn't ready at import time. ensureGlobalListener(); }, []); - // Late updates (rank change, etc.) — go through the store so all - // consumers see the same data. useMessageEvent(AvailableCommandsEvent, event => { const parser = event.getParser(); @@ -164,13 +142,11 @@ export const useChatCommandSelector = (chatValue: string) => setDismissed(true); }, []); - // Reset dismissed when chatValue changes to a new command start useEffect(() => { if(chatValue === ':' || chatValue === '') setDismissed(false); }, [ chatValue ]); - // Reset selectedIndex when filtered list changes useEffect(() => { setSelectedIndex(0); diff --git a/src/hooks/soundboard/useSoundboard.ts b/src/hooks/soundboard/useSoundboard.ts index e671e59..25624ce 100644 --- a/src/hooks/soundboard/useSoundboard.ts +++ b/src/hooks/soundboard/useSoundboard.ts @@ -65,7 +65,9 @@ const useSoundboardState = () => if(!enabled || serverSounds.length || fileLoadStartedRef.current) return; fileLoadStartedRef.current = true; - const url = GetConfigurationValue('soundboard.sounds.url') || 'configuration/soundboard-sounds.json5'; + const url = GetConfigurationValue('soundboard.url') + || GetConfigurationValue('soundboard.sounds.url') + || 'configuration/soundboard-sounds.json5'; (async () => {