mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 15:36:18 +00:00
Pilot: move InfoStand event listeners to useAvatarInfoWidget owner
InfoStandWidgetUserView previously subscribed to three room-session events (RSUBE_BADGES, USER_FIGURE, FAVOURITE_GROUP_UPDATE) and pushed the result back to its parent via a setAvatarInfo prop, with each handler running CloneObject(prev) before patching one field. Three issues with that shape: - CloneObject was deep-cloning the whole AvatarInfoUser shape blindly with no class-prototype awareness; - the three listeners raced on shallow merges across the same prev reference in StrictMode dev; - the subscriptions lived outside the state owner, forcing a prop callback barrier per event. The subscriptions are now in useAvatarInfoWidget — the actual owner of avatarInfo — and call three pure reducers extracted to src/hooks/rooms/widgets/avatarInfo.reducers.ts (applyUserBadgesUpdate, applyUserFigureUpdate, applyFavouriteGroupUpdate). Each reducer returns the same reference when the event doesn't apply so React bail-outs work. The clone now constructs a fresh AvatarInfoUser preserving prototype. dedupeBadges is extracted to its own pure module under src/api/avatar/ so Vitest can cover it without pulling in the renderer. InfoStandWidgetUserView loses the setAvatarInfo prop (parent updated) and the CloneObject import.
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Strips duplicate badge codes from a server-supplied badge array,
|
||||||
|
* preserving slot indices: a duplicate is replaced by an empty string
|
||||||
|
* rather than shifted out, so badge[i] still corresponds to slot i.
|
||||||
|
*
|
||||||
|
* Empty / falsy entries are normalized to '' (some servers emit null
|
||||||
|
* inside the array for unused slots).
|
||||||
|
*/
|
||||||
|
export const dedupeBadges = (badges: ReadonlyArray<string>): string[] =>
|
||||||
|
{
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
|
return badges.map(code =>
|
||||||
|
{
|
||||||
|
if(!code || seen.has(code)) return '';
|
||||||
|
|
||||||
|
seen.add(code);
|
||||||
|
|
||||||
|
return code;
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -3,5 +3,6 @@ export * from './AvatarEditorColorSorter';
|
|||||||
export * from './AvatarEditorPartSorter';
|
export * from './AvatarEditorPartSorter';
|
||||||
export * from './AvatarEditorThumbnailsHelper';
|
export * from './AvatarEditorThumbnailsHelper';
|
||||||
export * from './BuildPurchasableClothingFigure';
|
export * from './BuildPurchasableClothingFigure';
|
||||||
|
export * from './dedupeBadges';
|
||||||
export * from './IAvatarEditorCategory';
|
export * from './IAvatarEditorCategory';
|
||||||
export * from './IAvatarEditorCategoryPartItem';
|
export * from './IAvatarEditorCategoryPartItem';
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ export const AvatarInfoWidgetView: FC<{}> = props =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <InfoStandWidgetUserView avatarInfo={ (avatarInfo as AvatarInfoUser) } setAvatarInfo={ setAvatarInfo } onClose={ () => setAvatarInfo(null) } />;
|
return <InfoStandWidgetUserView avatarInfo={ (avatarInfo as AvatarInfoUser) } onClose={ () => setAvatarInfo(null) } />;
|
||||||
case AvatarInfoUser.BOT:
|
case AvatarInfoUser.BOT:
|
||||||
return <InfoStandWidgetBotView avatarInfo={ (avatarInfo as AvatarInfoUser) } onClose={ () => setAvatarInfo(null) } />;
|
return <InfoStandWidgetBotView avatarInfo={ (avatarInfo as AvatarInfoUser) } onClose={ () => setAvatarInfo(null) } />;
|
||||||
case AvatarInfoRentableBot.RENTABLE_BOT:
|
case AvatarInfoRentableBot.RENTABLE_BOT:
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomSessionFavoriteGroupUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserFigureUpdateEvent, UserRelationshipsComposer } from '@nitrots/nitro-renderer';
|
import { GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, UserRelationshipsComposer } from '@nitrots/nitro-renderer';
|
||||||
import { Dispatch, FC, FocusEvent, KeyboardEvent, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react';
|
import { FC, FocusEvent, KeyboardEvent, useCallback, useEffect, useState } from 'react';
|
||||||
import { FaPencilAlt, FaTimes } from 'react-icons/fa';
|
import { FaPencilAlt, FaTimes } from 'react-icons/fa';
|
||||||
import { AvatarInfoUser, CloneObject, GetConfigurationValue, GetGroupInformation, GetUserProfile, LocalizeText, SendMessageComposer } from '../../../../../api';
|
import { AvatarInfoUser, GetConfigurationValue, GetGroupInformation, GetUserProfile, LocalizeText, SendMessageComposer } from '../../../../../api';
|
||||||
import { Base, Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, Text, UserIdentityView, UserProfileIconView } from '../../../../../common';
|
import { Base, Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, Text, UserIdentityView, UserProfileIconView } from '../../../../../common';
|
||||||
import { useMessageEvent, useNitroEvent, useRoom } from '../../../../../hooks';
|
import { useMessageEvent, useRoom } from '../../../../../hooks';
|
||||||
import { InfoStandBadgeSlotView } from './InfoStandBadgeSlotView';
|
import { InfoStandBadgeSlotView } from './InfoStandBadgeSlotView';
|
||||||
import { InfoStandWidgetUserRelationshipsView } from './InfoStandWidgetUserRelationshipsView';
|
import { InfoStandWidgetUserRelationshipsView } from './InfoStandWidgetUserRelationshipsView';
|
||||||
import { InfoStandWidgetUserTagsView } from './InfoStandWidgetUserTagsView';
|
import { InfoStandWidgetUserTagsView } from './InfoStandWidgetUserTagsView';
|
||||||
@@ -12,13 +12,12 @@ import { BackgroundsView } from '../../../../backgrounds/BackgroundsView';
|
|||||||
|
|
||||||
interface InfoStandWidgetUserViewProps {
|
interface InfoStandWidgetUserViewProps {
|
||||||
avatarInfo: AvatarInfoUser;
|
avatarInfo: AvatarInfoUser;
|
||||||
setAvatarInfo: Dispatch<SetStateAction<AvatarInfoUser>>;
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =>
|
export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =>
|
||||||
{
|
{
|
||||||
const { avatarInfo = null, setAvatarInfo = null, onClose = null } = props;
|
const { avatarInfo = null, onClose = null } = props;
|
||||||
const [motto, setMotto] = useState<string>(null);
|
const [motto, setMotto] = useState<string>(null);
|
||||||
const [isEditingMotto, setIsEditingMotto] = useState(false);
|
const [isEditingMotto, setIsEditingMotto] = useState(false);
|
||||||
const [relationships, setRelationships] = useState<RelationshipStatusInfoMessageParser>(null);
|
const [relationships, setRelationships] = useState<RelationshipStatusInfoMessageParser>(null);
|
||||||
@@ -65,77 +64,6 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useNitroEvent<RoomSessionUserBadgesEvent>(RoomSessionUserBadgesEvent.RSUBE_BADGES, event =>
|
|
||||||
{
|
|
||||||
if (!avatarInfo || avatarInfo.webID !== event.userId) return;
|
|
||||||
|
|
||||||
// Deduplicate badges from server
|
|
||||||
const seen = new Set<string>();
|
|
||||||
const dedupedBadges = event.badges.map(code =>
|
|
||||||
{
|
|
||||||
if (!code || seen.has(code)) return '';
|
|
||||||
seen.add(code);
|
|
||||||
return code;
|
|
||||||
});
|
|
||||||
|
|
||||||
const oldBadges = avatarInfo.badges.join('');
|
|
||||||
|
|
||||||
if (oldBadges === dedupedBadges.join('')) return;
|
|
||||||
|
|
||||||
setAvatarInfo(prevValue =>
|
|
||||||
{
|
|
||||||
if (!prevValue) return prevValue;
|
|
||||||
|
|
||||||
const newValue = CloneObject(prevValue);
|
|
||||||
newValue.badges = dedupedBadges;
|
|
||||||
return newValue;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
useNitroEvent<RoomSessionUserFigureUpdateEvent>(RoomSessionUserFigureUpdateEvent.USER_FIGURE, event =>
|
|
||||||
{
|
|
||||||
if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return;
|
|
||||||
|
|
||||||
setAvatarInfo(prevValue =>
|
|
||||||
{
|
|
||||||
if (!prevValue) return prevValue;
|
|
||||||
|
|
||||||
const newValue = CloneObject(prevValue);
|
|
||||||
newValue.figure = event.figure;
|
|
||||||
newValue.motto = event.customInfo;
|
|
||||||
newValue.achievementScore = event.activityPoints;
|
|
||||||
newValue.nickIcon = event.nickIcon;
|
|
||||||
newValue.prefixText = event.prefixText;
|
|
||||||
newValue.prefixColor = event.prefixColor;
|
|
||||||
newValue.prefixIcon = event.prefixIcon;
|
|
||||||
newValue.prefixEffect = event.prefixEffect;
|
|
||||||
newValue.displayOrder = event.displayOrder;
|
|
||||||
newValue.backgroundId = event.backgroundId;
|
|
||||||
newValue.standId = event.standId;
|
|
||||||
newValue.overlayId = event.overlayId;
|
|
||||||
newValue.cardBackgroundId = event.cardBackgroundId ?? 0;
|
|
||||||
return newValue;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
useNitroEvent<RoomSessionFavoriteGroupUpdateEvent>(RoomSessionFavoriteGroupUpdateEvent.FAVOURITE_GROUP_UPDATE, event =>
|
|
||||||
{
|
|
||||||
if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return;
|
|
||||||
|
|
||||||
setAvatarInfo(prevValue =>
|
|
||||||
{
|
|
||||||
if (!prevValue) return prevValue;
|
|
||||||
|
|
||||||
const newValue = CloneObject(prevValue);
|
|
||||||
const clearGroup = (event.status === -1) || (event.habboGroupId <= 0);
|
|
||||||
|
|
||||||
newValue.groupId = clearGroup ? -1 : event.habboGroupId;
|
|
||||||
newValue.groupName = clearGroup ? null : event.habboGroupName;
|
|
||||||
newValue.groupBadgeId = clearGroup ? null : GetSessionDataManager().getGroupBadge(event.habboGroupId);
|
|
||||||
return newValue;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
useMessageEvent<RelationshipStatusInfoEvent>(RelationshipStatusInfoEvent, event =>
|
useMessageEvent<RelationshipStatusInfoEvent>(RelationshipStatusInfoEvent, event =>
|
||||||
{
|
{
|
||||||
const parser = event.getParser();
|
const parser = event.getParser();
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { RoomSessionFavoriteGroupUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserFigureUpdateEvent } from '@nitrots/nitro-renderer';
|
||||||
|
import { AvatarInfoUser, dedupeBadges, IAvatarInfo } from '../../../api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure reducers consumed by useAvatarInfoWidget to update the inspected
|
||||||
|
* AvatarInfoUser when room-session events fire. Exported standalone for
|
||||||
|
* Vitest coverage — no React, no renderer dispatcher access.
|
||||||
|
*
|
||||||
|
* Each reducer returns the same reference if the event doesn't apply
|
||||||
|
* (state unchanged) so React bail-outs work and consumers don't re-render
|
||||||
|
* uselessly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const cloneAvatarInfoUser = (state: AvatarInfoUser): AvatarInfoUser =>
|
||||||
|
{
|
||||||
|
const clone = new AvatarInfoUser(state.type);
|
||||||
|
|
||||||
|
Object.assign(clone, state);
|
||||||
|
|
||||||
|
return clone;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const applyUserBadgesUpdate = (state: IAvatarInfo | null, event: RoomSessionUserBadgesEvent): IAvatarInfo | null =>
|
||||||
|
{
|
||||||
|
if(!(state instanceof AvatarInfoUser)) return state;
|
||||||
|
if(state.webID !== event.userId) return state;
|
||||||
|
|
||||||
|
const dedupedBadges = dedupeBadges(event.badges);
|
||||||
|
|
||||||
|
if(state.badges.join('') === dedupedBadges.join('')) return state;
|
||||||
|
|
||||||
|
const next = cloneAvatarInfoUser(state);
|
||||||
|
|
||||||
|
next.badges = dedupedBadges;
|
||||||
|
|
||||||
|
return next;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const applyUserFigureUpdate = (state: IAvatarInfo | null, event: RoomSessionUserFigureUpdateEvent): IAvatarInfo | null =>
|
||||||
|
{
|
||||||
|
if(!(state instanceof AvatarInfoUser)) return state;
|
||||||
|
if(state.roomIndex !== event.roomIndex) return state;
|
||||||
|
|
||||||
|
const next = cloneAvatarInfoUser(state);
|
||||||
|
|
||||||
|
next.figure = event.figure;
|
||||||
|
next.motto = event.customInfo;
|
||||||
|
next.achievementScore = event.activityPoints;
|
||||||
|
next.nickIcon = event.nickIcon;
|
||||||
|
next.prefixText = event.prefixText;
|
||||||
|
next.prefixColor = event.prefixColor;
|
||||||
|
next.prefixIcon = event.prefixIcon;
|
||||||
|
next.prefixEffect = event.prefixEffect;
|
||||||
|
next.displayOrder = event.displayOrder;
|
||||||
|
next.backgroundId = event.backgroundId;
|
||||||
|
next.standId = event.standId;
|
||||||
|
next.overlayId = event.overlayId;
|
||||||
|
next.cardBackgroundId = event.cardBackgroundId ?? 0;
|
||||||
|
|
||||||
|
return next;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const applyFavouriteGroupUpdate = (
|
||||||
|
state: IAvatarInfo | null,
|
||||||
|
event: RoomSessionFavoriteGroupUpdateEvent,
|
||||||
|
resolveGroupBadge: (groupId: number) => string
|
||||||
|
): IAvatarInfo | null =>
|
||||||
|
{
|
||||||
|
if(!(state instanceof AvatarInfoUser)) return state;
|
||||||
|
if(state.roomIndex !== event.roomIndex) return state;
|
||||||
|
|
||||||
|
const clearGroup = (event.status === -1) || (event.habboGroupId <= 0);
|
||||||
|
const next = cloneAvatarInfoUser(state);
|
||||||
|
|
||||||
|
next.groupId = clearGroup ? -1 : event.habboGroupId;
|
||||||
|
next.groupName = clearGroup ? null : event.habboGroupName;
|
||||||
|
next.groupBadgeId = clearGroup ? null : resolveGroupBadge(event.habboGroupId);
|
||||||
|
|
||||||
|
return next;
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { GetRoomEngine, GetSessionDataManager, RoomEngineObjectEvent, RoomEngineUseProductEvent, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomSessionPetInfoUpdateEvent, RoomSessionPetStatusUpdateEvent, RoomSessionUserDataUpdateEvent } from '@nitrots/nitro-renderer';
|
import { GetRoomEngine, GetSessionDataManager, RoomEngineObjectEvent, RoomEngineUseProductEvent, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomSessionFavoriteGroupUpdateEvent, RoomSessionPetInfoUpdateEvent, RoomSessionPetStatusUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserDataUpdateEvent, RoomSessionUserFigureUpdateEvent } from '@nitrots/nitro-renderer';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { AvatarInfoFurni, AvatarInfoName, AvatarInfoPet, AvatarInfoRentableBot, AvatarInfoUser, AvatarInfoUtilities, CanManipulateFurniture, FurniCategory, IAvatarInfo, IsOwnerOfFurniture, RoomWidgetUpdateRoomObjectEvent, UseProductItem } from '../../../api';
|
import { AvatarInfoFurni, AvatarInfoName, AvatarInfoPet, AvatarInfoRentableBot, AvatarInfoUser, AvatarInfoUtilities, CanManipulateFurniture, FurniCategory, IAvatarInfo, IsOwnerOfFurniture, RoomWidgetUpdateRoomObjectEvent, UseProductItem } from '../../../api';
|
||||||
import { useNitroEvent, useUiEvent } from '../../events';
|
import { useNitroEvent, useUiEvent } from '../../events';
|
||||||
@@ -6,6 +6,7 @@ import { useFriends } from '../../friends';
|
|||||||
import { useWired } from '../../wired';
|
import { useWired } from '../../wired';
|
||||||
import { useObjectDeselectedEvent, useObjectRollOutEvent, useObjectRollOverEvent, useObjectSelectedEvent } from '../engine';
|
import { useObjectDeselectedEvent, useObjectRollOutEvent, useObjectRollOverEvent, useObjectSelectedEvent } from '../engine';
|
||||||
import { useRoom } from '../useRoom';
|
import { useRoom } from '../useRoom';
|
||||||
|
import { applyFavouriteGroupUpdate, applyUserBadgesUpdate, applyUserFigureUpdate } from './avatarInfo.reducers';
|
||||||
|
|
||||||
const useAvatarInfoWidgetState = () =>
|
const useAvatarInfoWidgetState = () =>
|
||||||
{
|
{
|
||||||
@@ -296,6 +297,21 @@ const useAvatarInfoWidgetState = () =>
|
|||||||
setIsDecorating(true);
|
setIsDecorating(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useNitroEvent<RoomSessionUserBadgesEvent>(RoomSessionUserBadgesEvent.RSUBE_BADGES, event =>
|
||||||
|
{
|
||||||
|
setAvatarInfo(prev => applyUserBadgesUpdate(prev, event));
|
||||||
|
});
|
||||||
|
|
||||||
|
useNitroEvent<RoomSessionUserFigureUpdateEvent>(RoomSessionUserFigureUpdateEvent.USER_FIGURE, event =>
|
||||||
|
{
|
||||||
|
setAvatarInfo(prev => applyUserFigureUpdate(prev, event));
|
||||||
|
});
|
||||||
|
|
||||||
|
useNitroEvent<RoomSessionFavoriteGroupUpdateEvent>(RoomSessionFavoriteGroupUpdateEvent.FAVOURITE_GROUP_UPDATE, event =>
|
||||||
|
{
|
||||||
|
setAvatarInfo(prev => applyFavouriteGroupUpdate(prev, event, groupId => GetSessionDataManager().getGroupBadge(groupId)));
|
||||||
|
});
|
||||||
|
|
||||||
useObjectSelectedEvent(event =>
|
useObjectSelectedEvent(event =>
|
||||||
{
|
{
|
||||||
getObjectInfo(event.id, event.category);
|
getObjectInfo(event.id, event.category);
|
||||||
|
|||||||
Reference in New Issue
Block a user