From 6022911448f82d5b9beb0a81d12d47eeabb5ef33 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 26 May 2026 19:26:07 +0200 Subject: [PATCH 1/5] fix(toolbar): bump desktop layout breakpoint to 1700px to avoid icon clip The left-nav container is `max-w-[calc(50vw-242px)]` (reserves the chat frame width) and uses `overflow-x: clip`. With the full icon set (habbo, rooms, game, catalog, buildersclub, inventory, ME, wired-tools, camera, youtube, modtools, furnieditor, housekeeping) the icons exceed the available 528-608px around the 1540-1700px viewport range, so the last icons get silently clipped on the right. Raising the desktop breakpoint from 1540px to 1700px makes the client fall back to the mobile-scrollable layout (`.tb-bar-scroll`) below 1700px, which scrolls horizontally and doesn't clip. Above 1700px the desktop fixed-icon layout still applies, now with enough horizontal room for every icon even with mod+HK enabled. Touch devices are unaffected (already forced onto the mobile layout via `pointer: coarse`). --- src/components/toolbar/ToolbarView.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index edf59e6..8bec8e3 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -69,10 +69,10 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => toggleTimeoutRef.current = setTimeout(() => { toggleLockRef.current = false; }, TOGGLE_LOCK_MS); }, []); - const compactFramePosition = (isToolbarOpen && isInRoom) ? 'bottom-[90px] min-[1540px]:bottom-0' : 'bottom-0'; - const mobileOnlyClasses = isTouchLayout ? '' : 'min-[1540px]:hidden'; - const desktopBlockClasses = isTouchLayout ? 'hidden' : 'hidden min-[1540px]:block'; - const desktopFlexClasses = isTouchLayout ? 'hidden' : 'hidden min-[1540px]:flex'; + const compactFramePosition = (isToolbarOpen && isInRoom) ? 'bottom-[90px] min-[1700px]:bottom-0' : 'bottom-0'; + const mobileOnlyClasses = isTouchLayout ? '' : 'min-[1700px]:hidden'; + const desktopBlockClasses = isTouchLayout ? 'hidden' : 'hidden min-[1700px]:block'; + const desktopFlexClasses = isTouchLayout ? 'hidden' : 'hidden min-[1700px]:flex'; const leftNavVariants = useMemo(() => ({ hidden: { opacity: 0, x: isInRoom ? -10 : 0, y: isInRoom ? 0 : 8, pointerEvents: 'none' }, visible: { opacity: 1, x: 0, y: 0, pointerEvents: 'auto' } From acf870ff6aff17ec4975983490cabb2d648e6c48 Mon Sep 17 00:00:00 2001 From: duckietm Date: Wed, 27 May 2026 07:46:10 +0200 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=86=99=20Enable=20back=20the=20live?= =?UTF-8?q?=20previes=20of=20the=20floorplan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rooms/widgets/useFloorplanLiveSync.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/hooks/rooms/widgets/useFloorplanLiveSync.ts b/src/hooks/rooms/widgets/useFloorplanLiveSync.ts index 8c5610c..ed3ae72 100644 --- a/src/hooks/rooms/widgets/useFloorplanLiveSync.ts +++ b/src/hooks/rooms/widgets/useFloorplanLiveSync.ts @@ -121,9 +121,8 @@ export const useFloorplanLiveSync = (opts: UseFloorplanLiveSyncOptions): UseFloo const baselineRef = useRef(null); const lastAppliedRef = useRef(null); + const wasEnabledRef = useRef(false); - // Destructure first so the memo deps stay precise without - // triggering exhaustive-deps on `state` as a whole. const { tiles, door, thickness, wallHeight } = state; const currentPayload = useMemo(() => ({ tilemap: serializeTilemap(tiles), @@ -150,18 +149,22 @@ export const useFloorplanLiveSync = (opts: UseFloorplanLiveSyncOptions): UseFloo if(applyToRenderer(baseline, roomId)) lastAppliedRef.current = baseline; }, [ roomId ]); - // Apply the current payload to the renderer whenever it - // diverges from what's already in the room. Synchronous + no - // debounce — the renderer pipeline is fast enough that every - // brush stroke can land a paint. useEffect(() => { - if(!enabled) return; + if(!enabled) + { + wasEnabledRef.current = false; + return; + } + + if(!baselineRef.current) return; + + const isFirstEnable = !wasEnabledRef.current; + wasEnabledRef.current = true; const previous = lastAppliedRef.current; - if(previous && livePreviewPayloadsEqual(currentPayload, previous)) return; - if(!previous && !baselineRef.current) return; + if(!isFirstEnable && previous && livePreviewPayloadsEqual(currentPayload, previous)) return; if(applyToRenderer(currentPayload, roomId)) lastAppliedRef.current = currentPayload; }, [ enabled, currentPayload, roomId ]); From a52a4a024ad87c6a02ee10a261c943b9384e6929 Mon Sep 17 00:00:00 2001 From: duckietm Date: Wed, 27 May 2026 09:39:08 +0200 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=86=95=20Added=20Pickup=20furni=20to?= =?UTF-8?q?=20the=20floorplan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../floorplan-editor/FloorplanEditorView.tsx | 25 ++++-- .../rooms/widgets/useFloorplanLiveSync.ts | 81 +++++++------------ 2 files changed, 49 insertions(+), 57 deletions(-) diff --git a/src/components/floorplan-editor/FloorplanEditorView.tsx b/src/components/floorplan-editor/FloorplanEditorView.tsx index 07e721e..8094d1b 100644 --- a/src/components/floorplan-editor/FloorplanEditorView.tsx +++ b/src/components/floorplan-editor/FloorplanEditorView.tsx @@ -1,6 +1,6 @@ import { AddLinkEventTracker, convertNumbersForSaving, convertSettingToNumber, FloorHeightMapEvent, GetRoomEntryTileMessageComposer, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomEntryTileMessageEvent, RoomVisualizationSettingsEvent, UpdateFloorPropertiesMessageComposer } from '@nitrots/nitro-renderer'; import { FC, useEffect, useMemo, useRef, useState } from 'react'; -import { FaBolt, FaCaretLeft, FaCaretRight } from 'react-icons/fa'; +import { FaBolt, FaBoxOpen, FaCaretLeft, FaCaretRight } from 'react-icons/fa'; import { LocalizeText, SendMessageComposer } from '../../api'; import { Button, ButtonGroup, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; import { useMessageEvent, useNitroEvent } from '../../hooks'; @@ -29,6 +29,7 @@ export const FloorplanEditorView: FC = () => const [ importExportVisible, setImportExportVisible ] = useState(false); const [ liveSync, setLiveSync ] = useState(true); const [ panMode, setPanMode ] = useState(false); + const [ autoPickup, setAutoPickup ] = useState(false); const { state, dispatch, loadFromServer, undo, redo, canUndo, canRedo } = useFloorplanReducer(); const originalRef = useRef<{ tilemap: string; @@ -41,7 +42,7 @@ export const FloorplanEditorView: FC = () => const area = useMemo(() => areaCount(state.tiles), [ state.tiles ]); - const { setBaseline, revert: revertLivePreview } = useFloorplanLiveSync({ enabled: liveSync && isVisible, state }); + const { setBaseline, mergeBaseline, revert: revertLivePreview } = useFloorplanLiveSync({ enabled: liveSync && isVisible, state }); useNitroEvent(RoomEngineEvent.DISPOSED, () => setIsVisible(false)); @@ -64,6 +65,7 @@ export const FloorplanEditorView: FC = () => }; dispatch({ type: 'SET_DOOR', x: parser.x, y: parser.y, source: 'remote' }); dispatch({ type: 'SET_DOOR_DIR', dir: ((parser.direction | 0) & 7) as EntryDir, source: 'remote' }); + mergeBaseline({ doorX: parser.x, doorY: parser.y, doorDir: (parser.direction | 0) & 7 }); }); useMessageEvent(FloorHeightMapEvent, event => @@ -110,6 +112,7 @@ export const FloorplanEditorView: FC = () => wallHeight: originalRef.current?.wallHeight ?? -1 }; dispatch({ type: 'SET_THICKNESS', wall, floor, source: 'remote' }); + mergeBaseline({ thicknessWall: wall, thicknessFloor: floor }); }); useEffect(() => @@ -173,7 +176,8 @@ export const FloorplanEditorView: FC = () => state.door.dir, convertNumbersForSaving(state.thickness.wall), convertNumbersForSaving(state.thickness.floor), - state.wallHeight - 1 + state.wallHeight - 1, + autoPickup )); }; @@ -224,7 +228,17 @@ export const FloorplanEditorView: FC = () => setAutoPickup(v => !v) } + title="On save: pick up furniture blocking the new floor plan and return it to its owner's inventory" + > + + { autoPickup ? 'Pick up blocking furni ON' : 'Pick up blocking furni OFF' } + + setLiveSync(v => !v) } title="Local in-room preview while drawing (does not save to server)" > @@ -256,7 +270,8 @@ export const FloorplanEditorView: FC = () => state.door.dir, convertNumbersForSaving(state.thickness.wall), convertNumbersForSaving(state.thickness.floor), - state.wallHeight - 1 + state.wallHeight - 1, + autoPickup )); } } onRevertText={ () => originalRef.current?.tilemap ?? serializeTilemap(state.tiles) } diff --git a/src/hooks/rooms/widgets/useFloorplanLiveSync.ts b/src/hooks/rooms/widgets/useFloorplanLiveSync.ts index ed3ae72..9dbfdfa 100644 --- a/src/hooks/rooms/widgets/useFloorplanLiveSync.ts +++ b/src/hooks/rooms/widgets/useFloorplanLiveSync.ts @@ -1,44 +1,18 @@ import { GetRoomEngine, GetRoomMessageHandler } from '@nitrots/nitro-renderer'; import { useCallback, useEffect, useMemo, useRef } from 'react'; -import { serializeTilemap } from '../../../components/floorplan-editor/state/encoding'; +import { parseTilemap, serializeTilemap } from '../../../components/floorplan-editor/state/encoding'; import { FloorplanState } from '../../../components/floorplan-editor/state/types'; import { useActiveRoomSessionSnapshot } from '../../session/useSessionSnapshots'; -/** - * Client-side live preview for the floor-plan editor. - * - * Every tile / door / thickness / wallHeight change in the editor - * is applied IMMEDIATELY to the 3D room behind the editor card - * via the renderer's local `RoomMessageHandler.applyFloorModelLocally` - * (added in the renderer's `feat/floorplan-live-preview` branch). - * Nothing is sent to the server until the user explicitly clicks - * Save — at that point `FloorplanEditorView` fires the - * `UpdateFloorPropertiesMessageComposer` directly. - * - * Closing the editor without saving leaves the live preview - * in place visually. To restore the pre-edit room, call `revert` - * — it re-applies the baseline payload locally. The next - * `FloorHeightMapEvent` from the server (e.g. on room re-enter) - * also wins and overwrites whatever preview is in place. - * - * Thickness changes additionally call - * `RoomEngine.updateRoomInstancePlaneThickness` for zero-latency - * wall/floor depth feedback (the full geometry rebuild that - * `applyFloorModelLocally` performs already reflects the new - * thickness in its plane data, but the dedicated thickness - * setter is cheaper and updates instantly as a slider is dragged). - */ +const normalizeTilemap = (raw: string): string => serializeTilemap(parseTilemap(raw)); export type LivePreviewPayload = { - /** Newline-or-CR-separated tilemap (the renderer parser accepts \r). */ tilemap: string; doorX: number; doorY: number; doorDir: number; - /** Editor-space (0..3). */ thicknessWall: number; thicknessFloor: number; - /** Editor-space (1..N). Server space is `wallHeight - 1`. */ wallHeight: number; }; @@ -48,17 +22,8 @@ export type UseFloorplanLiveSyncOptions = { }; export type UseFloorplanLiveSyncApi = { - /** - * Mark a payload as "currently shown in the room" so subsequent - * state diffs are computed against it. Editors call this on - * every server-driven snapshot push (FloorHeightMapEvent, - * RoomVisualizationSettingsEvent, …). - */ setBaseline: (payload: LivePreviewPayload) => void; - /** - * Restore the in-room preview to the recorded baseline. - * Use when the user closes the editor without saving. - */ + mergeBaseline: (partial: Partial) => void; revert: () => void; }; @@ -121,7 +86,6 @@ export const useFloorplanLiveSync = (opts: UseFloorplanLiveSyncOptions): UseFloo const baselineRef = useRef(null); const lastAppliedRef = useRef(null); - const wasEnabledRef = useRef(false); const { tiles, door, thickness, wallHeight } = state; const currentPayload = useMemo(() => ({ @@ -136,8 +100,29 @@ export const useFloorplanLiveSync = (opts: UseFloorplanLiveSyncOptions): UseFloo const setBaseline = useCallback((payload: LivePreviewPayload) => { - baselineRef.current = payload; - lastAppliedRef.current = payload; + const normalized: LivePreviewPayload = { + ...payload, + tilemap: normalizeTilemap(payload.tilemap) + }; + + baselineRef.current = normalized; + lastAppliedRef.current = normalized; + }, []); + + const mergeBaseline = useCallback((partial: Partial) => + { + const previous = baselineRef.current; + + if(!previous) return; + + const next: LivePreviewPayload = { + ...previous, + ...partial, + tilemap: partial.tilemap !== undefined ? normalizeTilemap(partial.tilemap) : previous.tilemap + }; + + baselineRef.current = next; + lastAppliedRef.current = next; }, []); const revert = useCallback(() => @@ -151,23 +136,15 @@ export const useFloorplanLiveSync = (opts: UseFloorplanLiveSyncOptions): UseFloo useEffect(() => { - if(!enabled) - { - wasEnabledRef.current = false; - return; - } - + if(!enabled) return; if(!baselineRef.current) return; - const isFirstEnable = !wasEnabledRef.current; - wasEnabledRef.current = true; - const previous = lastAppliedRef.current; - if(!isFirstEnable && previous && livePreviewPayloadsEqual(currentPayload, previous)) return; + if(previous && livePreviewPayloadsEqual(currentPayload, previous)) return; if(applyToRenderer(currentPayload, roomId)) lastAppliedRef.current = currentPayload; }, [ enabled, currentPayload, roomId ]); - return { setBaseline, revert }; + return { setBaseline, mergeBaseline, revert }; }; From b1244cbd5afe7a47d410255a75fce75202425007 Mon Sep 17 00:00:00 2001 From: duckietm Date: Wed, 27 May 2026 13:42:11 +0200 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=86=99=20Fix=20BOTS=20in=20catalog=20?= =?UTF-8?q?and=20inventory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/layout/LayoutAvatarImageView.tsx | 50 +++++++++++++++---- .../page/common/CatalogGridOfferView.tsx | 2 +- .../page/layout/CatalogLayoutDefaultView.tsx | 4 +- .../views/bot/InventoryBotItemView.tsx | 4 +- .../inventory/views/bot/InventoryBotView.tsx | 2 +- 5 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/common/layout/LayoutAvatarImageView.tsx b/src/common/layout/LayoutAvatarImageView.tsx index 75233a8..bd640e2 100644 --- a/src/common/layout/LayoutAvatarImageView.tsx +++ b/src/common/layout/LayoutAvatarImageView.tsx @@ -12,35 +12,51 @@ export interface LayoutAvatarImageViewProps extends BaseProps headOnly?: boolean; direction?: number; scale?: number; + fit?: boolean; } export const LayoutAvatarImageView: FC = props => { - const { figure = '', gender = '', headOnly = false, direction = 0, scale = 1, classNames = [], style = {}, ...rest } = props; + const { figure = '', gender = '', headOnly = false, direction = 0, scale = 1, fit = false, classNames = [], style = {}, ...rest } = props; const [ avatarUrl, setAvatarUrl ] = useState(null); const [ isReady, setIsReady ] = useState(false); const isDisposed = useRef(false); - // Request id bumped on every prop change. The SDK can call - // resetFigure asynchronously when server-side figure data lands; - // if props change in quick succession the older callback could - // otherwise overwrite the newer image. The closure captures the - // id and bails when stale. const requestIdRef = useRef(0); const getClassNames = useMemo(() => { - const newClassNames: string[] = [ 'avatar-image relative w-[90px] h-[130px] bg-no-repeat left-[-2px] pointer-events-none' ]; + let newClassNames: string[]; + + if(fit) + { + newClassNames = [ 'avatar-image absolute inset-0 pointer-events-none' ]; + } + else if(headOnly) + { + newClassNames = [ 'avatar-image absolute inset-0 bg-no-repeat pointer-events-none' ]; + } + else + { + newClassNames = [ 'avatar-image relative w-[90px] h-[130px] bg-no-repeat left-[-2px] pointer-events-none' ]; + } if(classNames.length) newClassNames.push(...classNames); return newClassNames; - }, [ classNames ]); + }, [ classNames, headOnly, fit ]); const getStyle = useMemo(() => { let newStyle: CSSProperties = {}; - if(avatarUrl && avatarUrl.length) newStyle.backgroundImage = `url('${ avatarUrl }')`; + if(!fit && avatarUrl && avatarUrl.length) newStyle.backgroundImage = `url('${ avatarUrl }')`; + + if(headOnly && !fit) + { + newStyle.backgroundSize = '130px auto'; + newStyle.backgroundPosition = '51% 40%'; + newStyle.imageRendering = 'pixelated'; + } if(scale !== 1) { @@ -52,7 +68,7 @@ export const LayoutAvatarImageView: FC = props => if(Object.keys(style).length) newStyle = { ...newStyle, ...style }; return newStyle; - }, [ avatarUrl, scale, style ]); + }, [ avatarUrl, scale, style, headOnly, fit ]); useEffect(() => { @@ -116,5 +132,17 @@ export const LayoutAvatarImageView: FC = props => }; }, []); - return ; + return ( + + { fit && avatarUrl && avatarUrl.length > 0 && ( + + ) } + + ); }; diff --git a/src/components/catalog/views/page/common/CatalogGridOfferView.tsx b/src/components/catalog/views/page/common/CatalogGridOfferView.tsx index 936b8dc..6fa45ba 100644 --- a/src/components/catalog/views/page/common/CatalogGridOfferView.tsx +++ b/src/components/catalog/views/page/common/CatalogGridOfferView.tsx @@ -76,7 +76,7 @@ export const CatalogGridOfferView: FC = props => { iconUrl && !(offer.product.productType === ProductTypeEnum.ROBOT) &&
} { (offer.product.productType === ProductTypeEnum.ROBOT) && - } + }
diff --git a/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx index dd55268..3490e60 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx @@ -88,7 +88,6 @@ export const CatalogLayoutDefaultView: FC = props =>
} - { /* Welcome/description card */ } { !currentOffer &&
{ !!page.localization.getImage(1) && @@ -96,11 +95,10 @@ export const CatalogLayoutDefaultView: FC = props =>
} - { /* Item grid */ }
{ GetConfigurationValue('catalog.headers') && } - +
); diff --git a/src/components/inventory/views/bot/InventoryBotItemView.tsx b/src/components/inventory/views/bot/InventoryBotItemView.tsx index 9a084cf..4a48485 100644 --- a/src/components/inventory/views/bot/InventoryBotItemView.tsx +++ b/src/components/inventory/views/bot/InventoryBotItemView.tsx @@ -38,8 +38,8 @@ export const InventoryBotItemView: FC - + + { children } ); diff --git a/src/components/inventory/views/bot/InventoryBotView.tsx b/src/components/inventory/views/bot/InventoryBotView.tsx index 87b8adf..2882ea0 100644 --- a/src/components/inventory/views/bot/InventoryBotView.tsx +++ b/src/components/inventory/views/bot/InventoryBotView.tsx @@ -68,7 +68,7 @@ export const InventoryBotView: FC<{
- columnCount={ 6 } + columnCount={ 4 } itemRender={ item => } items={ botItems } />
From 00fbdc6f6dfc95751872e50e207546a7000852c4 Mon Sep 17 00:00:00 2001 From: duckietm Date: Wed, 27 May 2026 15:37:09 +0200 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=86=99=20Small=20update=20toolbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/toolbar/ToolbarView.tsx | 88 +++++++++++++------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index 8bec8e3..6bcc9a3 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -216,14 +216,6 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => CreateLinkEvent('catalog/toggle/normal') } className="tb-icon" /> - - CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" /> - - - CreateLinkEvent('inventory/toggle') } className="tb-icon" /> - { (getFullCount > 0) && - } - { isMeExpanded && @@ -237,7 +229,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => } @@ -245,11 +237,19 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => setMeExpanded(value => !value); event.stopPropagation(); } }> - + { (getTotalUnseen > 0) && } + + CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" /> + + + CreateLinkEvent('inventory/toggle') } className="tb-icon" /> + { (getFullCount > 0) && + } + { (isInRoom && showToolbarButton) && @@ -268,14 +268,14 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => { (openTicketsCount > 0) && } } - { isMod && - - CreateLinkEvent('furni-editor/toggle') } className="tb-icon" /> - } { (isHk && hkEnabled) && CreateLinkEvent('housekeeping/toggle') } className="tb-icon" /> } + { isMod && + + CreateLinkEvent('furni-editor/toggle') } className="tb-icon" /> + } = props => CreateLinkEvent('catalog/toggle/normal') } className="tb-icon" /> + + + { isMeExpanded && + + + } + + + { + setMeExpanded(value => !value); + event.stopPropagation(); + } }> + + + { (getTotalUnseen > 0) && + } + CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" /> @@ -333,32 +359,6 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => } - - - { isMeExpanded && - - - } - - - { - setMeExpanded(value => !value); - event.stopPropagation(); - } }> - - - { (getTotalUnseen > 0) && - } - @@ -380,14 +380,14 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => { (openTicketsCount > 0) && } } - { isMod && - - CreateLinkEvent('furni-editor/toggle') } className="tb-icon" /> - } { (isHk && hkEnabled) && CreateLinkEvent('housekeeping/toggle') } className="tb-icon" /> } + { isMod && + + CreateLinkEvent('furni-editor/toggle') } className="tb-icon" /> + } CreateLinkEvent('friends/toggle') } className="tb-icon" /> { (requests.length > 0) &&