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.
This commit is contained in:
Life
2026-04-04 14:32:33 +02:00
parent 020db83870
commit c9e7461714
5 changed files with 57 additions and 45 deletions
@@ -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<number>('user.badges.max.slots', 5), []);
const displayCodes = (filteredBadgeCodes !== null ? filteredBadgeCodes : badgeCodes);
const attemptDeleteBadge = () =>
@@ -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<NotificationBadgeReceivedBubbleViewProps> = props =>
{
const { item = null, onClose = null, ...rest } = props;
const { toggleBadge = null } = useInventoryBadges();
const handleWear = () =>
{
if(item.linkUrl) toggleBadge(item.linkUrl);
onClose();
};
return (
<LayoutNotificationBubbleView className="flex-col" fadesOut={ false } onClose={ onClose } { ...rest }>
<LayoutNotificationBubbleView className="flex-col" onClose={ onClose } { ...rest }>
<Flex alignItems="center" gap={ 2 } className="mb-2">
<Flex center className="w-[50px] h-[50px] shrink-0">
{ item.iconUrl && <img alt="" className="no-select" src={ item.iconUrl } /> }
@@ -26,7 +34,7 @@ export const NotificationBadgeReceivedBubbleView: FC<NotificationBadgeReceivedBu
<button
className="btn btn-success w-full btn-sm"
type="button"
onClick={ () => { OpenUrl(item.linkUrl); onClose(); } }>
onClick={ handleWear }>
{ LocalizeText('inventory.badges.wearbadge') }
</button>
<span className="underline cursor-pointer text-nowrap" onClick={ onClose }>
@@ -79,7 +79,7 @@ const BadgeMiniPicker: FC<{
export const InfoStandBadgeSlotView: FC<InfoStandBadgeSlotProps> = ({ 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<InfoStandBadgeSlotProps> = ({ 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<InfoStandBadgeSlotProps> = ({ slotIndex,
onDragOver={ onDragOver }
onDragStart={ onDragStart }
onDrop={ onDrop }
onClick={ handleSlotClick }>
onClick={ handleSlotClick }
onDoubleClick={ handleDoubleClick }>
{ badgeCode
? <LayoutBadgeImageView badgeCode={ badgeCode } showInfo={ true } />
: isOwnUser && <FaPlus className="text-white/30 text-[10px]" /> }
@@ -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<InfoStandWidgetUserViewProps> = props =
/>
)}
<Column grow alignItems="center" gap={0}>
{ GetConfigurationValue<boolean>('user.badges.group.slot.enabled', true)
? (
<>
<div className="flex gap-1">
<InfoStandBadgeSlotView slotIndex={0} badgeCode={avatarInfo.badges[0]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
<Flex center className="relative w-[40px] h-[40px] bg-no-repeat bg-center" pointer={avatarInfo.groupId > 0} onClick={event => GetGroupInformation(avatarInfo.groupId)}>
{avatarInfo.groupId > 0 &&
<LayoutBadgeImageView badgeCode={avatarInfo.groupBadgeId} customTitle={avatarInfo.groupName} isGroup={true} showInfo={true} />}
</Flex>
</div>
<Flex center gap={1}>
<InfoStandBadgeSlotView slotIndex={1} badgeCode={avatarInfo.badges[1]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
<InfoStandBadgeSlotView slotIndex={2} badgeCode={avatarInfo.badges[2]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
{ (() => {
const maxSlots = GetConfigurationValue<number>('user.badges.max.slots', 5);
const isOwnUser = avatarInfo.type === AvatarInfoUser.OWN_USER;
const showGroup = maxSlots <= 5;
const items: React.ReactNode[] = [];
items.push(<InfoStandBadgeSlotView key={0} slotIndex={0} badgeCode={avatarInfo.badges[0]} isOwnUser={isOwnUser} />);
if(showGroup) {
items.push(
<Flex key="group" center className="relative w-[40px] h-[40px] bg-no-repeat bg-center" pointer={avatarInfo.groupId > 0} onClick={event => GetGroupInformation(avatarInfo.groupId)}>
{avatarInfo.groupId > 0 && <LayoutBadgeImageView badgeCode={avatarInfo.groupBadgeId} customTitle={avatarInfo.groupName} isGroup={true} showInfo={true} />}
</Flex>
<Flex center gap={1}>
<InfoStandBadgeSlotView slotIndex={3} badgeCode={avatarInfo.badges[3]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
<InfoStandBadgeSlotView slotIndex={4} badgeCode={avatarInfo.badges[4]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
</Flex>
</>
)
: (
<>
<Flex center gap={1}>
<InfoStandBadgeSlotView slotIndex={0} badgeCode={avatarInfo.badges[0]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
<InfoStandBadgeSlotView slotIndex={1} badgeCode={avatarInfo.badges[1]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
</Flex>
<Flex center gap={1}>
<InfoStandBadgeSlotView slotIndex={2} badgeCode={avatarInfo.badges[2]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
<InfoStandBadgeSlotView slotIndex={3} badgeCode={avatarInfo.badges[3]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
</Flex>
<Flex center gap={1}>
<InfoStandBadgeSlotView slotIndex={4} badgeCode={avatarInfo.badges[4]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
<InfoStandBadgeSlotView slotIndex={5} badgeCode={avatarInfo.badges[5]} isOwnUser={avatarInfo.type === AvatarInfoUser.OWN_USER} />
</Flex>
</>
)
}
);
} else {
items.push(<InfoStandBadgeSlotView key="slot1" slotIndex={1} badgeCode={avatarInfo.badges[1]} isOwnUser={isOwnUser} />);
}
const startIdx = showGroup ? 1 : 2;
for(let i = startIdx; i < maxSlots; i++) {
items.push(<InfoStandBadgeSlotView key={i} slotIndex={i} badgeCode={avatarInfo.badges[i]} isOwnUser={isOwnUser} />);
}
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) => (
<Flex key={idx} center gap={1}>{row}</Flex>
));
})() }
</Column>
</div>
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
+1 -1
View File
@@ -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>(ClubGiftNotificationEvent, event =>