Files
Nitro-V3/src/components/room/widgets/avatar-info/AvatarInfoPetTrainingPanelView.tsx
T
simoleo89 81656e7b19 Fix two logic bugs found while refactoring + document the open ones
These are the bugs surfaced during the structural work that are simple
enough to fix in isolation. Larger ones (race conditions that need
session-token tracking, async-fetch ordering) are deferred and documented
in docs/ARCHITECTURE.md "Known logic bugs" — the repo has Issues
disabled, so the doc is the issue board.

== Fix: room history wiped on every tab close

src/components/room/widgets/room-tools/RoomToolsWidgetView.tsx had a
useEffect that registered a `beforeunload` handler calling
`window.localStorage.removeItem('nitro.room.history')`. The whole point
of localStorage is to persist across sessions; wiping it on tab close is
either a leftover debug call or a misunderstanding of the API.

Removed the handler. History now persists across browser sessions, which
matches user expectations. If "session-only" was the intent, the right
primitive is `sessionStorage` (not localStorage + cleanup) — left as a
note in the doc.

== Fix: AvatarInfoPetTrainingPanelView null-pointer on session change

src/components/room/widgets/avatar-info/AvatarInfoPetTrainingPanelView.tsx
read `roomSession.userDataManager.getPetData(parser.petId)` without
guarding for `roomSession` being null. The PetTrainingPanelMessageEvent
can arrive during a room transition when `roomSession` is briefly null,
crashing the widget. Added `?.` chain on both `roomSession` and
`userDataManager`.

== Doc: known logic bugs section

Two open issues documented for follow-up:
- MainView.tsx CREATED/ENDED race — needs session-token tracking, fits
  cleanly into the future useNitroEventReducer companion to proposal #1.
- LayoutFurniImageView / LayoutAvatarImageView async fetch ordering —
  needs request-id refs, or solves itself once React Query (proposal #2)
  is enabled and the image fetch becomes a query keyed on props.

Plus a "recently fixed" subsection that records the four bugs already
addressed in this branch (doorbell close button, doorbell optimistic
remove, room history wipe, pet panel null-pointer) so the next reader
knows what changed and why.

== Verification

- yarn eslint on the two modified files: same error count before and
  after (5 pre-existing set-state-in-effect on RoomToolsWidgetView,
  none introduced).
- yarn tsc on the two modified files: clean.

https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
2026-05-11 16:31:52 +00:00

60 lines
2.8 KiB
TypeScript

import { IRoomUserData, PetTrainingMessageParser, PetTrainingPanelMessageEvent } from '@nitrots/nitro-renderer';
import { FC, useState } from 'react';
import { LocalizeText } from '../../../../api';
import { Button, Column, Flex, Grid, LayoutPetImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useMessageEvent, useRoom, useSessionInfo } from '../../../../hooks';
export const AvatarInfoPetTrainingPanelView: FC<{}> = props =>
{
const [ petData, setPetData ] = useState<IRoomUserData>(null);
const [ petTrainInformation, setPetTrainInformation ] = useState<PetTrainingMessageParser>(null);
const { chatStyleId = 0 } = useSessionInfo();
const { roomSession = null } = useRoom();
useMessageEvent<PetTrainingPanelMessageEvent>(PetTrainingPanelMessageEvent, event =>
{
const parser = event.getParser();
if(!parser) return;
const roomPetData = roomSession?.userDataManager?.getPetData(parser.petId);
if(!roomPetData) return;
setPetData(roomPetData);
setPetTrainInformation(parser);
});
const processPetAction = (petName: string, commandName: string) =>
{
if(!petName || !commandName) return;
roomSession?.sendChatMessage(`${ petName } ${ commandName }`, chatStyleId);
};
if(!petData || !petTrainInformation) return null;
return (
<NitroCardView className="user-settings-window no-resize" theme="primary-slim" uniqueKey="user-settings">
<NitroCardHeaderView headerText={ LocalizeText('widgets.pet.commands.title') } onCloseClick={ () => setPetTrainInformation(null) } />
<NitroCardContentView className="text-black">
<Flex alignItems="center" gap={ 2 } justifyContent="center">
<Grid columnCount={ 2 }>
<Column fullWidth className="body-image pet p-1" overflow="hidden">
<LayoutPetImageView direction={ 2 } figure={ petData.figure } posture={ 'std' } />
</Column>
<Text small wrap variant="black">{ petData.name }</Text>
</Grid>
</Flex>
<Grid columnCount={ 2 }>
{
(petTrainInformation.commands && petTrainInformation.commands.length > 0) && petTrainInformation.commands.map((command, index) =>
<Button key={ index } disabled={ !petTrainInformation.enabledCommands.includes(command) } onClick={ () => processPetAction(petData.name, LocalizeText(`pet.command.${ command }`)) }>{ LocalizeText(`pet.command.${ command }`) }</Button>
)
}
</Grid>
</NitroCardContentView>
</NitroCardView>
);
};