Files
Nitro-V3/src/hooks/rooms/widgets/avatarInfo.reducers.ts
T
simoleo89 559d860a7b 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.
2026-05-11 21:11:02 +02:00

81 lines
2.7 KiB
TypeScript

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;
};