revert(hooks): roll back the three snapshot-consumer migrations to pre-71a0eee state

The migrations of useSessionInfo, useChatWidget.ownUserId and the
AvatarInfo Ignore/Unignore menu to the new useSessionSnapshots hooks
were correct in code but produce a persistent runtime error in the
user's deployment:

  TypeError: (intermediate value)() is undefined
      ToolbarView ToolbarView.tsx:46

The error fires from React's render loop on the first paint —
ToolbarView is the first mounted consumer of useSessionInfo, which is
why it carries the boundary message. Two attempted fixes did not
resolve it on the user's side:
- 790ad2b: vite alias forcing @nitrots/nitro-renderer to source index.ts
- c35a2d4: defensive typeof guards on every Manager method call inside
  useSessionSnapshots (so a missing method degrades to a frozen default
  rather than calling undefined)

Both are correct defenses but the error persists, meaning the failure
mode is upstream of those guards. Rather than burn more cycles
remote-debugging, roll back the three consumer migrations:

- useSessionInfo: restored to the pre-71a0eee shape — 5 useState
  fields driven by useMessageEvent<UserInfoEvent, FigureUpdateEvent,
  UserSettingsEvent>. The five consumers (ToolbarView, HcCenterView,
  ChatInputView, AvatarInfoPetTrainingPanelView, InfoStandWidgetPetView,
  AvatarInfo{Avatar,Pet,OwnPet}View) get the same destructured shape
  they had before this session.
- useChatWidget.ownUserId: restored to `GetSessionDataManager()?.userId`
  (synchronous, captured at mount). Loses the session-change reactivity
  but matches the previous, working behaviour.
- AvatarInfoWidgetAvatarView Ignore/Unignore: restored to
  `avatarInfo.isIgnored` (captured by AvatarInfoUtilities at click
  time, not reactive). Loses the live-toggle if the user is
  ignored/unignored while the popup is open — known small regression,
  worth it for stability.

Kept intact:
- The useSessionSnapshots.ts hook file itself, with defensive guards,
  so the API stays available for any future opt-in consumer.
- 790ad2b vite alias for the umbrella, still useful as defence in
  depth for future migrations.
- All the other non-snapshot modernizations from this session
  (usePetPackageWidget reducer, useWordQuizWidget bug fix,
  useChatCommandSelector Zustand store, useAvatarInfoWidget typed
  globalThis accessor).

Verification: yarn typecheck clean, yarn test 207/207, yarn build green.
The toolbar should boot without the error now — the call chain on the
first paint no longer touches the new useExternalSnapshot / snapshot
getter path.
This commit is contained in:
simoleo89
2026-05-18 22:16:48 +02:00
parent c35a2d4b4f
commit e142efd793
3 changed files with 41 additions and 35 deletions
@@ -3,7 +3,7 @@ import { FC, useEffect, useMemo, useState } from 'react';
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
import { AvatarInfoUser, DispatchUiEvent, GetOwnRoomObject, GetUserProfile, LocalizeText, MessengerFriend, ReportType, RoomWidgetUpdateChatInputContentEvent, SendMessageComposer } from '../../../../../api';
import { Flex } from '../../../../../common';
import { useFriends, useHelp, useIsUserIgnored, useRoom, useSessionInfo, useWiredTools } from '../../../../../hooks';
import { useFriends, useHelp, useRoom, useSessionInfo, useWiredTools } from '../../../../../hooks';
import { ContextMenuHeaderView } from '../../context-menu/ContextMenuHeaderView';
import { ContextMenuListItemView } from '../../context-menu/ContextMenuListItemView';
import { ContextMenuView } from '../../context-menu/ContextMenuView';
@@ -31,10 +31,6 @@ export const AvatarInfoWidgetAvatarView: FC<AvatarInfoWidgetAvatarViewProps> = p
const { roomSession = null, isHandItemBlocked = false } = useRoom();
const { userRespectRemaining = 0, respectUser = null } = useSessionInfo();
const { openInspectionForUser, showInspectButton } = useWiredTools();
// Reactive: the menu auto-flips Ignore <-> Unignore if the state
// changes while the popup is open, instead of being frozen to the
// snapshot AvatarInfoUtilities took at click time.
const isIgnored = useIsUserIgnored(avatarInfo.name);
const isShowGiveRights = useMemo(() =>
{
@@ -235,11 +231,11 @@ export const AvatarInfoWidgetAvatarView: FC<AvatarInfoWidgetAvatarViewProps> = p
{ LocalizeText('infostand.link.relationship') }
<FaChevronRight className="right fa-icon" />
</ContextMenuListItemView> }
{ !isIgnored &&
{ !avatarInfo.isIgnored &&
<ContextMenuListItemView onClick={ event => processAction('ignore') }>
{ LocalizeText('infostand.button.ignore') }
</ContextMenuListItemView> }
{ isIgnored &&
{ avatarInfo.isIgnored &&
<ContextMenuListItemView onClick={ event => processAction('unignore') }>
{ LocalizeText('infostand.button.unignore') }
</ContextMenuListItemView> }
+2 -7
View File
@@ -1,8 +1,7 @@
import { GetGuestRoomResultEvent, GetRoomEngine, PetFigureData, RoomChatSettings, RoomChatSettingsEvent, RoomDragEvent, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomSessionChatEvent, RoomUserData, SystemChatStyleEnum } from '@nitrots/nitro-renderer';
import { GetGuestRoomResultEvent, GetRoomEngine, GetSessionDataManager, PetFigureData, RoomChatSettings, RoomChatSettingsEvent, RoomDragEvent, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomSessionChatEvent, RoomUserData, SystemChatStyleEnum } from '@nitrots/nitro-renderer';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ChatBubbleMessage, ChatBubbleUtilities, ChatEntryType, ChatHistoryCurrentDate, GetConfigurationValue, GetRoomObjectScreenLocation, IRoomChatSettings, LocalizeText, PlaySound, RoomChatFormatter } from '../../../api';
import { useMessageEvent, useNitroEvent } from '../../events';
import { useUserDataSnapshot } from '../../session/useSessionSnapshots';
import { useTranslation } from '../../translation';
import { useRoom } from '../useRoom';
import { useChatHistory } from './../../chat-history';
@@ -23,11 +22,7 @@ const useChatWidgetState = () =>
const { addChatEntry, updateChatEntry } = useChatHistory();
const { settings, translateIncoming, consumeOutgoingTranslation } = useTranslation();
const isDisposed = useRef(false);
// Reactive: a session change (re-login without reload) immediately
// updates the outgoing-translation owner check below. Old code read
// GetSessionDataManager()?.userId at hook mount and would have stayed
// stuck on the previous session's id.
const ownUserId = (useUserDataSnapshot().userId || -1);
const ownUserId = (GetSessionDataManager()?.userId || -1);
const applyTranslationToBubble = useCallback((chatMessage: ChatBubbleMessage, originalText: string, translatedText: string, detectedLanguage: string, targetLanguage: string) =>
{
+36 -21
View File
@@ -1,21 +1,16 @@
import { GetSessionDataManager, RoomUnitChatStyleComposer, UserSettingsEvent } from '@nitrots/nitro-renderer';
import { FigureUpdateEvent, GetSessionDataManager, RoomUnitChatStyleComposer, UserInfoDataParser, UserInfoEvent, UserSettingsEvent } from '@nitrots/nitro-renderer';
import { useState } from 'react';
import { useBetween } from 'use-between';
import { SendMessageComposer } from '../../api';
import { useMessageEvent } from '../events';
import { useUserDataSnapshot } from './useSessionSnapshots';
const useSessionInfoState = () =>
{
// figure / respectsLeft / respectsPetLeft come from the renderer's
// referentially-stable IUserDataSnapshot. The snapshot is invalidated
// (and re-derived on next read) inside SessionDataManager whenever
// the underlying state changes: UserInfoEvent + FigureUpdateEvent +
// giveRespect/givePetRespect all call invalidateUserDataSnapshot().
// So we no longer need to mirror those fields into local useState.
const userData = useUserDataSnapshot();
const [ userInfo, setUserInfo ] = useState<UserInfoDataParser>(null);
const [ userFigure, setUserFigure ] = useState<string>(null);
const [ chatStyleId, setChatStyleId ] = useState<number>(0);
const [ userRespectRemaining, setUserRespectRemaining ] = useState<number>(0);
const [ petRespectRemaining, setPetRespectRemaining ] = useState<number>(0);
const updateChatStyleId = (styleId: number) =>
{
@@ -24,8 +19,36 @@ const useSessionInfoState = () =>
SendMessageComposer(new RoomUnitChatStyleComposer(styleId));
};
const respectUser = (userId: number) => GetSessionDataManager().giveRespect(userId);
const respectPet = (petId: number) => GetSessionDataManager().givePetRespect(petId);
const respectUser = (userId: number) =>
{
GetSessionDataManager().giveRespect(userId);
setUserRespectRemaining(GetSessionDataManager().respectsLeft);
};
const respectPet = (petId: number) =>
{
GetSessionDataManager().givePetRespect(petId);
setPetRespectRemaining(GetSessionDataManager().respectsPetLeft);
};
useMessageEvent<UserInfoEvent>(UserInfoEvent, event =>
{
const parser = event.getParser();
setUserInfo(parser.userInfo);
setUserFigure(parser.userInfo.figure);
setUserRespectRemaining(parser.userInfo.respectsRemaining);
setPetRespectRemaining(parser.userInfo.respectsPetRemaining);
});
useMessageEvent<FigureUpdateEvent>(FigureUpdateEvent, event =>
{
const parser = event.getParser();
setUserFigure(parser.figure);
});
useMessageEvent<UserSettingsEvent>(UserSettingsEvent, event =>
{
@@ -34,15 +57,7 @@ const useSessionInfoState = () =>
setChatStyleId(parser.chatType);
});
return {
userFigure: userData.figure,
chatStyleId,
userRespectRemaining: userData.respectsLeft,
petRespectRemaining: userData.respectsPetLeft,
respectUser,
respectPet,
updateChatStyleId
};
return { userInfo, userFigure, chatStyleId, userRespectRemaining, petRespectRemaining, respectUser, respectPet, updateChatStyleId };
};
export const useSessionInfo = () => useBetween(useSessionInfoState);