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:
simoleo89
2026-05-11 20:37:21 +02:00
parent bb1238a5e5
commit 559d860a7b
6 changed files with 125 additions and 79 deletions
@@ -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:
return <InfoStandWidgetBotView avatarInfo={ (avatarInfo as AvatarInfoUser) } onClose={ () => setAvatarInfo(null) } />;
case AvatarInfoRentableBot.RENTABLE_BOT: