From c9e746171422fdd1f640ca20647c2490282795fa Mon Sep 17 00:00:00 2001 From: Life Date: Sat, 4 Apr 2026 14:32:33 +0200 Subject: [PATCH] Dynamic badge slots from config, double-click remove, direct wear from toast Read user.badges.max.slots from config instead of hardcoded 5. InfoStand layout adapts: 5 slots shows group badge, 6 slots replaces group with 6th badge. Double-click on InfoStand badge removes it. Badge received toast now directly equips the badge via toggleBadge and closes. --- .../views/badge/InventoryBadgeView.tsx | 4 +- .../NotificationBadgeReceivedBubbleView.tsx | 14 +++- .../infostand/InfoStandBadgeSlotView.tsx | 12 +++- .../infostand/InfoStandWidgetUserView.tsx | 70 +++++++++---------- src/hooks/notification/useNotification.ts | 2 +- 5 files changed, 57 insertions(+), 45 deletions(-) diff --git a/src/components/inventory/views/badge/InventoryBadgeView.tsx b/src/components/inventory/views/badge/InventoryBadgeView.tsx index ea8228f..f1fad00 100644 --- a/src/components/inventory/views/badge/InventoryBadgeView.tsx +++ b/src/components/inventory/views/badge/InventoryBadgeView.tsx @@ -1,5 +1,5 @@ import { DeleteBadgeMessageComposer } from '@nitrots/nitro-renderer'; -import { FC, useCallback, useEffect, useState } from 'react'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { FaTrashAlt } from 'react-icons/fa'; import { GetConfigurationValue, LocalizeBadgeName, LocalizeText, SendMessageComposer, UnseenItemCategory } from '../../../../api'; import { LayoutBadgeImageView } from '../../../../common'; @@ -89,7 +89,7 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props = const [ isDragOverInventory, setIsDragOverInventory ] = useState(false); const [ isDraggingFromActive, setIsDraggingFromActive ] = useState(false); - const maxSlots = 5; + const maxSlots = useMemo(() => GetConfigurationValue('user.badges.max.slots', 5), []); const displayCodes = (filteredBadgeCodes !== null ? filteredBadgeCodes : badgeCodes); const attemptDeleteBadge = () => diff --git a/src/components/notification-center/views/bubble-layouts/NotificationBadgeReceivedBubbleView.tsx b/src/components/notification-center/views/bubble-layouts/NotificationBadgeReceivedBubbleView.tsx index 380c121..0969c8e 100644 --- a/src/components/notification-center/views/bubble-layouts/NotificationBadgeReceivedBubbleView.tsx +++ b/src/components/notification-center/views/bubble-layouts/NotificationBadgeReceivedBubbleView.tsx @@ -1,6 +1,7 @@ import { FC } from 'react'; -import { LocalizeText, NotificationBubbleItem, OpenUrl } from '../../../../api'; +import { LocalizeText, NotificationBubbleItem } from '../../../../api'; import { Flex, LayoutNotificationBubbleView, LayoutNotificationBubbleViewProps, Text } from '../../../../common'; +import { useInventoryBadges } from '../../../../hooks'; export interface NotificationBadgeReceivedBubbleViewProps extends LayoutNotificationBubbleViewProps { @@ -10,9 +11,16 @@ export interface NotificationBadgeReceivedBubbleViewProps extends LayoutNotifica export const NotificationBadgeReceivedBubbleView: FC = props => { const { item = null, onClose = null, ...rest } = props; + const { toggleBadge = null } = useInventoryBadges(); + + const handleWear = () => + { + if(item.linkUrl) toggleBadge(item.linkUrl); + onClose(); + }; return ( - + { item.iconUrl && } @@ -26,7 +34,7 @@ export const NotificationBadgeReceivedBubbleView: FC { OpenUrl(item.linkUrl); onClose(); } }> + onClick={ handleWear }> { LocalizeText('inventory.badges.wearbadge') } diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx index 9bda07f..267f535 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx @@ -79,7 +79,7 @@ const BadgeMiniPicker: FC<{ export const InfoStandBadgeSlotView: FC = ({ slotIndex, badgeCode: badgeCodeFromProps, isOwnUser }) => { - const { activeBadgeCodes = [], setBadgeAtSlot = null, swapBadges = null, requestBadges = null } = useInventoryBadges(); + const { activeBadgeCodes = [], setBadgeAtSlot = null, swapBadges = null, removeBadge = null, requestBadges = null } = useInventoryBadges(); const [ isDragOver, setIsDragOver ] = useState(false); const [ isDragging, setIsDragging ] = useState(false); const [ justDropped, setJustDropped ] = useState(false); @@ -157,6 +157,13 @@ export const InfoStandBadgeSlotView: FC = ({ slotIndex, setShowPicker(true); }, [ isOwnUser, badgeCode ]); + const handleDoubleClick = useCallback(() => + { + if(!isOwnUser || !badgeCode) return; + + removeBadge(badgeCode); + }, [ isOwnUser, badgeCode, removeBadge ]); + const handlePickerSelect = useCallback((code: string) => { setBadgeAtSlot(code, slotIndex); @@ -180,7 +187,8 @@ export const InfoStandBadgeSlotView: FC = ({ slotIndex, onDragOver={ onDragOver } onDragStart={ onDragStart } onDrop={ onDrop } - onClick={ handleSlotClick }> + onClick={ handleSlotClick } + onDoubleClick={ handleDoubleClick }> { badgeCode ? : isOwnUser && } diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx index 62aa0b2..91ced43 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx @@ -1,5 +1,6 @@ +import React from 'react'; import { GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomSessionFavoriteGroupUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserFigureUpdateEvent, UserRelationshipsComposer } from '@nitrots/nitro-renderer'; -import { Dispatch, FC, FocusEvent, KeyboardEvent, SetStateAction, useCallback, useEffect, useState } from 'react'; +import { Dispatch, FC, FocusEvent, KeyboardEvent, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react'; import { FaPencilAlt, FaTimes } from 'react-icons/fa'; import { AvatarInfoUser, CloneObject, GetConfigurationValue, GetGroupInformation, GetUserProfile, LocalizeText, SendMessageComposer } from '../../../../../api'; import { Base, Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, Text, UserProfileIconView } from '../../../../../common'; @@ -173,43 +174,38 @@ export const InfoStandWidgetUserView: FC = props = /> )} - { GetConfigurationValue('user.badges.group.slot.enabled', true) - ? ( - <> -
- - 0} onClick={event => GetGroupInformation(avatarInfo.groupId)}> - {avatarInfo.groupId > 0 && - } - -
- - - + { (() => { + const maxSlots = GetConfigurationValue('user.badges.max.slots', 5); + const isOwnUser = avatarInfo.type === AvatarInfoUser.OWN_USER; + const showGroup = maxSlots <= 5; + + const items: React.ReactNode[] = []; + items.push(); + + if(showGroup) { + items.push( + 0} onClick={event => GetGroupInformation(avatarInfo.groupId)}> + {avatarInfo.groupId > 0 && } - - - - - - ) - : ( - <> - - - - - - - - - - - - - - ) - } + ); + } else { + items.push(); + } + + const startIdx = showGroup ? 1 : 2; + for(let i = startIdx; i < maxSlots; i++) { + items.push(); + } + + const rows: React.ReactNode[][] = []; + for(let i = 0; i < items.length; i += 2) { + rows.push(items.slice(i, i + 2)); + } + + return rows.map((row, idx) => ( + {row} + )); + })() }

diff --git a/src/hooks/notification/useNotification.ts b/src/hooks/notification/useNotification.ts index 8fea817..3195133 100644 --- a/src/hooks/notification/useNotification.ts +++ b/src/hooks/notification/useNotification.ts @@ -223,7 +223,7 @@ const useNotificationState = () => const badgeName = LocalizeBadgeName(parser.badgeCode); const badgeImage = GetSessionDataManager().getBadgeUrl(parser.badgeCode); - showSingleBubble(badgeName, NotificationBubbleType.BADGE_RECEIVED, badgeImage, 'inventory/toggle'); + showSingleBubble(badgeName, NotificationBubbleType.BADGE_RECEIVED, badgeImage, parser.badgeCode); }); useMessageEvent(ClubGiftNotificationEvent, event =>